disabling marksbot when third-party verifications if turned off
This commit is contained in:
parent
2491088e81
commit
818c9a58a3
7 changed files with 158 additions and 1 deletions
19
cmd/telesrv-admin/filter_bots_test.go
Normal file
19
cmd/telesrv-admin/filter_bots_test.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFilterOutBot(t *testing.T) {
|
||||||
|
rows := []BotRow{{ID: 1}, {ID: 2}, {ID: 3}}
|
||||||
|
out := filterOutBot(rows, 2)
|
||||||
|
if len(out) != 2 || out[0].ID != 1 || out[1].ID != 3 {
|
||||||
|
t.Fatalf("filterOutBot = %+v, want [1, 3]", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterOutBotNoMatch(t *testing.T) {
|
||||||
|
rows := []BotRow{{ID: 1}, {ID: 3}}
|
||||||
|
out := filterOutBot(rows, 2)
|
||||||
|
if len(out) != 2 || out[0].ID != 1 || out[1].ID != 3 {
|
||||||
|
t.Fatalf("filterOutBot (no match) = %+v, want unchanged [1, 3]", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -858,6 +858,19 @@ func (s *server) handleSetChannelAvatarAPI(w http.ResponseWriter, r *http.Reques
|
||||||
writeCommandResultAPI(w, result, err)
|
writeCommandResultAPI(w, result, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterOutBot drops the given bot id from a row slice in place, preserving
|
||||||
|
// order. Used to keep a hidden built-in bot out of admin listings without
|
||||||
|
// touching the underlying SQL projection.
|
||||||
|
func filterOutBot(rows []BotRow, excludeID int64) []BotRow {
|
||||||
|
out := rows[:0]
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.ID != excludeID {
|
||||||
|
out = append(out, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (s *server) handleBotsAPI(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleBotsAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.read == nil {
|
if s.read == nil {
|
||||||
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
writeAPIError(w, http.StatusServiceUnavailable, "read store is not configured")
|
||||||
|
|
@ -878,6 +891,12 @@ func (s *server) handleBotsAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// @marksbot is not fully finished (see requireThirdPartyVerificationVisible):
|
||||||
|
// while third-party verification is hidden, it must not be discoverable in
|
||||||
|
// the bot list either, not just unreachable at /api/botverification/*.
|
||||||
|
if s.cfg.HideThirdPartyVerification {
|
||||||
|
rows = filterOutBot(rows, domain.VerifierBotUserID)
|
||||||
|
}
|
||||||
nextBeforeID := int64(0)
|
nextBeforeID := int64(0)
|
||||||
if hasMore && len(rows) > 0 {
|
if hasMore && len(rows) > 0 {
|
||||||
nextBeforeID = rows[len(rows)-1].ID
|
nextBeforeID = rows[len(rows)-1].ID
|
||||||
|
|
@ -908,6 +927,10 @@ func (s *server) handleBotDetailAPI(w http.ResponseWriter, r *http.Request) {
|
||||||
writeAPIError(w, http.StatusBadRequest, "invalid id")
|
writeAPIError(w, http.StatusBadRequest, "invalid id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if s.cfg.HideThirdPartyVerification && botID == domain.VerifierBotUserID {
|
||||||
|
writeAPIError(w, http.StatusNotFound, "bot not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
detail, err := s.read.BotDetail(r.Context(), botID)
|
detail, err := s.read.BotDetail(r.Context(), botID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
writeAPIError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
|
|
||||||
|
|
@ -909,6 +909,7 @@ func run(logger *zap.Logger) error {
|
||||||
contacts.WithPrivacyEvaluator(privacyService),
|
contacts.WithPrivacyEvaluator(privacyService),
|
||||||
contacts.WithAccountFreezeProvider(adminService),
|
contacts.WithAccountFreezeProvider(adminService),
|
||||||
contacts.WithReadModelVersions(readModelVersionStore),
|
contacts.WithReadModelVersions(readModelVersionStore),
|
||||||
|
contacts.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification),
|
||||||
)
|
)
|
||||||
if seeded, err := langPackService.SeedDirectory(ctx, cfg.LangPackSeedDir); err != nil {
|
if seeded, err := langPackService.SeedDirectory(ctx, cfg.LangPackSeedDir); err != nil {
|
||||||
return fmt.Errorf("seed langpack: %w", err)
|
return fmt.Errorf("seed langpack: %w", err)
|
||||||
|
|
@ -1128,7 +1129,7 @@ func run(logger *zap.Logger) error {
|
||||||
passkeyapp.WithAllowedOrigins(cfg.PasskeyAllowedOrigins))
|
passkeyapp.WithAllowedOrigins(cfg.PasskeyAllowedOrigins))
|
||||||
// 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。
|
// 自定义云主题(Create a New Theme):主题目录与每用户已安装列表均持久化到 postgres。
|
||||||
themeService := themesapp.NewService(postgres.NewThemeStore(pool))
|
themeService := themesapp.NewService(postgres.NewThemeStore(pool))
|
||||||
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(adminService))
|
usersService := users.NewService(userStore, users.WithBaseUserCache(userCache), users.WithContactStore(contactStore), users.WithPhotoProvider(cachedPhotos), users.WithPrivacyEvaluator(privacyService), users.WithAccountFreezeProvider(adminService), users.WithHideThirdPartyVerification(cfg.HideThirdPartyVerification))
|
||||||
privacyService.ConfigureReadModels(usersService, channelStore)
|
privacyService.ConfigureReadModels(usersService, channelStore)
|
||||||
aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...)
|
aiComposeService := aiapp.NewService(aiComposeStore, newAIComposeOptions(cfg, rateLimiter, usersService.PremiumActive, logger)...)
|
||||||
botsService.SetAIChatGenerator(aiComposeService)
|
botsService.SetAIChatGenerator(aiComposeService)
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ type Service struct {
|
||||||
projector *userprojection.Projector
|
projector *userprojection.Projector
|
||||||
versions store.ReadModelVersionStore
|
versions store.ReadModelVersionStore
|
||||||
cache *contactListReadModelCache
|
cache *contactListReadModelCache
|
||||||
|
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
|
// while true, Search never returns @marksbot (domain.VerifierBotUserID),
|
||||||
|
// so the account is unreachable for a client that doesn't already know it.
|
||||||
|
hideThirdPartyVerification bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option adjusts optional contacts service dependencies.
|
// Option adjusts optional contacts service dependencies.
|
||||||
|
|
@ -55,6 +59,12 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
|
||||||
return func(s *Service) { s.freezes = p }
|
return func(s *Service) { s.freezes = p }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
|
// while true, Search filters @marksbot out of every result set.
|
||||||
|
func WithHideThirdPartyVerification(hidden bool) Option {
|
||||||
|
return func(s *Service) { s.hideThirdPartyVerification = hidden }
|
||||||
|
}
|
||||||
|
|
||||||
// WithReadModelVersions enables durable hash-token fast paths for NotModified RPCs.
|
// WithReadModelVersions enables durable hash-token fast paths for NotModified RPCs.
|
||||||
func WithReadModelVersions(v store.ReadModelVersionStore) Option {
|
func WithReadModelVersions(v store.ReadModelVersionStore) Option {
|
||||||
return func(s *Service) { s.versions = v }
|
return func(s *Service) { s.versions = v }
|
||||||
|
|
@ -317,6 +327,18 @@ func (s *Service) ImportContacts(ctx context.Context, userID int64, inputs []dom
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterOutUserID drops the given user id from a result slice in place,
|
||||||
|
// preserving order.
|
||||||
|
func filterOutUserID(users []domain.User, excludeID int64) []domain.User {
|
||||||
|
out := users[:0]
|
||||||
|
for _, u := range users {
|
||||||
|
if u.ID != excludeID {
|
||||||
|
out = append(out, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Search(ctx context.Context, userID int64, query string, limit int) (domain.UserSearchResult, error) {
|
func (s *Service) Search(ctx context.Context, userID int64, query string, limit int) (domain.UserSearchResult, error) {
|
||||||
if s == nil || s.users == nil || userID == 0 {
|
if s == nil || s.users == nil || userID == 0 {
|
||||||
return domain.UserSearchResult{}, nil
|
return domain.UserSearchResult{}, nil
|
||||||
|
|
@ -338,6 +360,10 @@ func (s *Service) Search(ctx context.Context, userID int64, query string, limit
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.UserSearchResult{}, err
|
return domain.UserSearchResult{}, err
|
||||||
}
|
}
|
||||||
|
if s.hideThirdPartyVerification {
|
||||||
|
res.MyResults = filterOutUserID(res.MyResults, domain.VerifierBotUserID)
|
||||||
|
res.Results = filterOutUserID(res.Results, domain.VerifierBotUserID)
|
||||||
|
}
|
||||||
if s.privacy != nil && phoneQuery != "" && len(res.MyResults)+len(res.Results) > 0 {
|
if s.privacy != nil && phoneQuery != "" && len(res.MyResults)+len(res.Results) > 0 {
|
||||||
targetIDs := make([]int64, 0, len(res.MyResults)+len(res.Results))
|
targetIDs := make([]int64, 0, len(res.MyResults)+len(res.Results))
|
||||||
for _, target := range res.MyResults {
|
for _, target := range res.MyResults {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
privacyapp "telesrv/internal/app/privacy"
|
privacyapp "telesrv/internal/app/privacy"
|
||||||
|
|
@ -644,6 +645,46 @@ func TestAcceptContactRequiresExistingContactRequest(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marksbotSearchStore wraps memory.UserStore to fold domain.VerifierBotUser()
|
||||||
|
// into a matching Search result, since memory.UserStore.Create always assigns
|
||||||
|
// an id from its own auto-increment sequence and can never produce the fixed
|
||||||
|
// domain.VerifierBotUserID a real deployment seeds it under.
|
||||||
|
type marksbotSearchStore struct {
|
||||||
|
*memory.UserStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *marksbotSearchStore) Search(ctx context.Context, currentUserID int64, query, phoneQuery string, limit int) (domain.UserSearchResult, error) {
|
||||||
|
res, err := s.UserStore.Search(ctx, currentUserID, query, phoneQuery, limit)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
if strings.Contains(strings.ToLower(query), "marks") {
|
||||||
|
res.Results = append(res.Results, domain.VerifierBotUser())
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearchHidesMarksbotWhenThirdPartyVerificationHidden(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
users := &marksbotSearchStore{UserStore: memory.NewUserStore()}
|
||||||
|
viewer, err := users.Create(ctx, domain.User{Phone: "15550000201", FirstName: "Viewer"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create viewer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible := NewService(memory.NewContactStore(), users).Configure(WithHideThirdPartyVerification(false))
|
||||||
|
found, err := visible.Search(ctx, viewer.ID, "marksbot", 10)
|
||||||
|
if err != nil || len(found.Results) != 1 || found.Results[0].ID != domain.VerifierBotUserID {
|
||||||
|
t.Fatalf("Search (visible) = %+v err=%v, want @marksbot", found, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden := NewService(memory.NewContactStore(), users).Configure(WithHideThirdPartyVerification(true))
|
||||||
|
found, err = hidden.Search(ctx, viewer.ID, "marksbot", 10)
|
||||||
|
if err != nil || len(found.Results) != 0 {
|
||||||
|
t.Fatalf("Search (hidden) = %+v err=%v, want no results", found, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSearchFindsOnlyActiveCollectibleUsernames(t *testing.T) {
|
func TestSearchFindsOnlyActiveCollectibleUsernames(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
users := memory.NewUserStore()
|
users := memory.NewUserStore()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ type Service struct {
|
||||||
privacy userprojection.PrivacyEvaluator
|
privacy userprojection.PrivacyEvaluator
|
||||||
freezes userprojection.AccountFreezeProvider
|
freezes userprojection.AccountFreezeProvider
|
||||||
projector *userprojection.Projector
|
projector *userprojection.Projector
|
||||||
|
// hideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
|
// while true, ResolveUsername never resolves @marksbot
|
||||||
|
// (domain.VerifierBotUserID), so a client cannot discover it by username.
|
||||||
|
hideThirdPartyVerification bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type usernameAvailabilityStore interface {
|
type usernameAvailabilityStore interface {
|
||||||
|
|
@ -64,6 +68,12 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
|
||||||
return func(s *Service) { s.freezes = p }
|
return func(s *Service) { s.freezes = p }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithHideThirdPartyVerification mirrors config.HideThirdPartyVerification:
|
||||||
|
// while true, ResolveUsername treats @marksbot as not found.
|
||||||
|
func WithHideThirdPartyVerification(hidden bool) Option {
|
||||||
|
return func(s *Service) { s.hideThirdPartyVerification = hidden }
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
minUsernameLen = 5
|
minUsernameLen = 5
|
||||||
maxUsernameLen = 32
|
maxUsernameLen = 32
|
||||||
|
|
@ -617,6 +627,9 @@ func (s *Service) ResolveUsername(ctx context.Context, currentUserID int64, user
|
||||||
if err != nil || !found {
|
if err != nil || !found {
|
||||||
return u, found, err
|
return u, found, err
|
||||||
}
|
}
|
||||||
|
if s.hideThirdPartyVerification && u.ID == domain.VerifierBotUserID {
|
||||||
|
return domain.User{}, false, nil
|
||||||
|
}
|
||||||
s.putCachedUsers(ctx, u)
|
s.putCachedUsers(ctx, u)
|
||||||
u, err = s.projectOne(ctx, currentUserID, u)
|
u, err = s.projectOne(ctx, currentUserID, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,40 @@ func TestServiceUsernameLifecycle(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// marksbotOverrideStore wraps memory.UserStore to serve domain.VerifierBotUser()
|
||||||
|
// for a fixed username lookup, since memory.UserStore.Create always assigns an
|
||||||
|
// id from its own auto-increment sequence and can never produce the fixed
|
||||||
|
// domain.VerifierBotUserID a real deployment seeds it under.
|
||||||
|
type marksbotOverrideStore struct {
|
||||||
|
*memory.UserStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *marksbotOverrideStore) ByUsername(ctx context.Context, username string) (domain.User, bool, error) {
|
||||||
|
if strings.EqualFold(username, "marksbot") {
|
||||||
|
return domain.VerifierBotUser(), true, nil
|
||||||
|
}
|
||||||
|
return s.UserStore.ByUsername(ctx, username)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveUsernameHidesMarksbotWhenThirdPartyVerificationHidden(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
store := &marksbotOverrideStore{UserStore: memory.NewUserStore()}
|
||||||
|
viewer, err := store.Create(ctx, domain.User{AccessHash: 1, Phone: "15550002001", FirstName: "Viewer"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create viewer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
visible := NewService(store, WithHideThirdPartyVerification(false))
|
||||||
|
if u, found, err := visible.ResolveUsername(ctx, viewer.ID, "marksbot"); err != nil || !found || u.ID != domain.VerifierBotUserID {
|
||||||
|
t.Fatalf("ResolveUsername (visible) = user %+v found %v err %v, want @marksbot", u, found, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden := NewService(store, WithHideThirdPartyVerification(true))
|
||||||
|
if _, found, err := hidden.ResolveUsername(ctx, viewer.ID, "marksbot"); err != nil || found {
|
||||||
|
t.Fatalf("ResolveUsername (hidden) found=%v err=%v, want not found", found, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestResolvePhoneHonorsAddedByPhone(t *testing.T) {
|
func TestResolvePhoneHonorsAddedByPhone(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
users := memory.NewUserStore()
|
users := memory.NewUserStore()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue