fix: sync contact phone privacy disclosure

This commit is contained in:
iamxvbaba 2026-07-24 11:56:58 +08:00
parent 0e2fcdf9c8
commit e1a95c7318
19 changed files with 789 additions and 50 deletions

View file

@ -563,7 +563,9 @@ func (s *Service) ResolveUsername(ctx context.Context, currentUserID int64, user
return u, true, nil
}
// ResolvePhone 解析手机号到用户;当前阶段默认允许手机号深链解析,隐私规则后续接 account privacy。
// ResolvePhone resolves a phone number only when the target's AddedByPhone
// privacy allows the current viewer. The evaluator is backed by owner-level
// privacy/contact snapshots in production, so this adds no per-rule SQL query.
func (s *Service) ResolvePhone(ctx context.Context, currentUserID int64, phone string) (domain.User, bool, error) {
if _, err := s.loadSelf(ctx, currentUserID); err != nil {
return domain.User{}, false, err
@ -577,6 +579,28 @@ func (s *Service) ResolvePhone(ctx context.Context, currentUserID int64, phone s
return u, found, err
}
s.putCachedUsers(ctx, u)
if s.privacy != nil && u.ID != currentUserID {
allowed := false
var err error
if batch, ok := s.privacy.(userprojection.BatchPrivacyEvaluator); ok {
visibility, batchErr := batch.CanSeeBatch(
ctx,
[]int64{u.ID},
currentUserID,
[]domain.PrivacyKey{domain.PrivacyKeyAddedByPhone},
)
err = batchErr
allowed = visibility[u.ID][domain.PrivacyKeyAddedByPhone]
} else {
allowed, err = s.privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyAddedByPhone)
}
if err != nil {
return domain.User{}, false, err
}
if !allowed {
return domain.User{}, false, domain.ErrPhoneNotOccupied
}
}
u, err = s.projectOne(ctx, currentUserID, u)
if err != nil {
return domain.User{}, false, err

View file

@ -6,6 +6,7 @@ import (
"strings"
"testing"
privacyapp "telesrv/internal/app/privacy"
"telesrv/internal/domain"
"telesrv/internal/store/memory"
)
@ -60,6 +61,42 @@ func TestServiceUsernameLifecycle(t *testing.T) {
}
}
func TestResolvePhoneHonorsAddedByPhone(t *testing.T) {
ctx := context.Background()
users := memory.NewUserStore()
contacts := memory.NewContactStore()
viewer, err := users.Create(ctx, domain.User{AccessHash: 1, Phone: "15550001001", FirstName: "Viewer"})
if err != nil {
t.Fatalf("create viewer: %v", err)
}
target, err := users.Create(ctx, domain.User{AccessHash: 2, Phone: "15550001002", FirstName: "Target"})
if err != nil {
t.Fatalf("create target: %v", err)
}
privacy := privacyapp.NewService(memory.NewPrivacyStore(), contacts)
if _, err := privacy.SetRules(ctx, target.ID, domain.PrivacyKeyAddedByPhone, []domain.PrivacyRule{{Kind: domain.PrivacyRuleAllowContacts}}); err != nil {
t.Fatalf("set AddedByPhone: %v", err)
}
svc := NewService(users,
WithContactStore(contacts),
WithPrivacyEvaluator(privacy),
)
if _, found, err := svc.ResolvePhone(ctx, viewer.ID, target.Phone); !errors.Is(err, domain.ErrPhoneNotOccupied) || found {
t.Fatalf("ResolvePhone stranger found=%v err=%v, want phone not occupied", found, err)
}
if _, err := contacts.Upsert(ctx, target.ID, domain.ContactInput{
ContactUserID: viewer.ID,
FirstName: viewer.FirstName,
}); err != nil {
t.Fatalf("target add viewer: %v", err)
}
got, found, err := svc.ResolvePhone(ctx, viewer.ID, target.Phone)
if err != nil || !found || got.ID != target.ID {
t.Fatalf("ResolvePhone contact = %+v found=%v err=%v, want target", got, found, err)
}
}
func TestServiceUpdateProfile(t *testing.T) {
ctx := context.Background()
store := memory.NewUserStore()