disabling marksbot when third-party verifications if turned off

This commit is contained in:
onysd 2026-08-06 15:45:32 +03:00
parent 2491088e81
commit 818c9a58a3
7 changed files with 158 additions and 1 deletions

View file

@ -36,6 +36,10 @@ type Service struct {
projector *userprojection.Projector
versions store.ReadModelVersionStore
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.
@ -55,6 +59,12 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
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.
func WithReadModelVersions(v store.ReadModelVersionStore) Option {
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
}
// 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) {
if s == nil || s.users == nil || userID == 0 {
return domain.UserSearchResult{}, nil
@ -338,6 +360,10 @@ func (s *Service) Search(ctx context.Context, userID int64, query string, limit
if err != nil {
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 {
targetIDs := make([]int64, 0, len(res.MyResults)+len(res.Results))
for _, target := range res.MyResults {

View file

@ -4,6 +4,7 @@ import (
"context"
"errors"
"reflect"
"strings"
"testing"
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) {
ctx := context.Background()
users := memory.NewUserStore()

View file

@ -27,6 +27,10 @@ type Service struct {
privacy userprojection.PrivacyEvaluator
freezes userprojection.AccountFreezeProvider
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 {
@ -64,6 +68,12 @@ func WithAccountFreezeProvider(p userprojection.AccountFreezeProvider) Option {
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 (
minUsernameLen = 5
maxUsernameLen = 32
@ -617,6 +627,9 @@ func (s *Service) ResolveUsername(ctx context.Context, currentUserID int64, user
if err != nil || !found {
return u, found, err
}
if s.hideThirdPartyVerification && u.ID == domain.VerifierBotUserID {
return domain.User{}, false, nil
}
s.putCachedUsers(ctx, u)
u, err = s.projectOne(ctx, currentUserID, u)
if err != nil {

View file

@ -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) {
ctx := context.Background()
users := memory.NewUserStore()