fix: sync contact phone privacy disclosure
This commit is contained in:
parent
0e2fcdf9c8
commit
e1a95c7318
19 changed files with 789 additions and 50 deletions
|
|
@ -22,6 +22,7 @@ const maxCloseFriendsCount = 5000
|
|||
|
||||
type phonePrivacyService interface {
|
||||
userprojection.PrivacyEvaluator
|
||||
userprojection.BatchPrivacyEvaluator
|
||||
AddAllowUser(ctx context.Context, ownerUserID int64, key domain.PrivacyKey, targetUserID int64) (domain.PrivacyRules, bool, error)
|
||||
}
|
||||
|
||||
|
|
@ -123,19 +124,17 @@ func (s *Service) AddContact(ctx context.Context, userID int64, input domain.Con
|
|||
return domain.Contact{}, ErrContactNameEmpty
|
||||
}
|
||||
// Android 的 contacts.addContact 会提交带 "+" 前缀的号码(TDesktop 传纯数字或空),
|
||||
// 归一成纯数字;无数字时落空串,走下方 target.Phone 回填。
|
||||
// 归一成纯数字。空串表示客户端只按 user id 添加联系人,必须原样保留;
|
||||
// TL 明确允许省略号码,服务端不得从 target 全局资料反向补出隐私号码。
|
||||
input.Phone = digitsOnly(input.Phone)
|
||||
if s.users != nil {
|
||||
target, found, err := s.users.ByID(ctx, input.ContactUserID)
|
||||
_, found, err := s.users.ByID(ctx, input.ContactUserID)
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.Contact{}, ErrContactIDInvalid
|
||||
}
|
||||
if input.Phone == "" {
|
||||
input.Phone = target.Phone
|
||||
}
|
||||
}
|
||||
contact, err := s.contacts.Upsert(ctx, userID, input)
|
||||
if err != nil {
|
||||
|
|
@ -235,6 +234,30 @@ func (s *Service) ImportContacts(ctx context.Context, userID int64, inputs []dom
|
|||
if err != nil {
|
||||
return domain.ImportContactsResult{}, err
|
||||
}
|
||||
if s.privacy != nil && len(targets) > 0 {
|
||||
targetIDs := make([]int64, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.ID != 0 && target.ID != userID {
|
||||
targetIDs = append(targetIDs, target.ID)
|
||||
}
|
||||
}
|
||||
visibility, err := s.privacy.CanSeeBatch(
|
||||
ctx,
|
||||
targetIDs,
|
||||
userID,
|
||||
[]domain.PrivacyKey{domain.PrivacyKeyAddedByPhone},
|
||||
)
|
||||
if err != nil {
|
||||
return domain.ImportContactsResult{}, err
|
||||
}
|
||||
allowed := targets[:0]
|
||||
for _, target := range targets {
|
||||
if visibility[target.ID][domain.PrivacyKeyAddedByPhone] {
|
||||
allowed = append(allowed, target)
|
||||
}
|
||||
}
|
||||
targets = allowed
|
||||
}
|
||||
byPhone := make(map[string]domain.User, len(targets))
|
||||
for _, target := range targets {
|
||||
if target.Phone != "" {
|
||||
|
|
@ -310,10 +333,52 @@ func (s *Service) Search(ctx context.Context, userID int64, query string, limit
|
|||
if limit <= 0 || limit > maxSearchLimit {
|
||||
limit = maxSearchLimit
|
||||
}
|
||||
res, err := s.users.Search(ctx, userID, query, normalizePhone(query), limit)
|
||||
phoneQuery := ""
|
||||
if isPhoneSearchQuery(query) {
|
||||
phoneQuery = normalizePhone(query)
|
||||
}
|
||||
res, err := s.users.Search(ctx, userID, query, phoneQuery, limit)
|
||||
if err != nil {
|
||||
return domain.UserSearchResult{}, err
|
||||
}
|
||||
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 {
|
||||
if target.ID != 0 && target.ID != userID {
|
||||
targetIDs = append(targetIDs, target.ID)
|
||||
}
|
||||
}
|
||||
for _, target := range res.Results {
|
||||
if target.ID != 0 && target.ID != userID {
|
||||
targetIDs = append(targetIDs, target.ID)
|
||||
}
|
||||
}
|
||||
visibility, err := s.privacy.CanSeeBatch(
|
||||
ctx,
|
||||
targetIDs,
|
||||
userID,
|
||||
[]domain.PrivacyKey{domain.PrivacyKeyAddedByPhone},
|
||||
)
|
||||
if err != nil {
|
||||
return domain.UserSearchResult{}, err
|
||||
}
|
||||
knownContacts := map[int64]domain.Contact{}
|
||||
if s.contacts != nil && len(targetIDs) > 0 {
|
||||
knownContacts, err = s.contacts.GetMany(ctx, userID, targetIDs)
|
||||
if err != nil {
|
||||
return domain.UserSearchResult{}, err
|
||||
}
|
||||
}
|
||||
allowed := func(target domain.User) bool {
|
||||
if visibility[target.ID][domain.PrivacyKeyAddedByPhone] {
|
||||
return true
|
||||
}
|
||||
contact, found := knownContacts[target.ID]
|
||||
return found && contact.Phone != "" && strings.HasPrefix(contact.Phone, phoneQuery)
|
||||
}
|
||||
res.MyResults = filterSearchUsers(res.MyResults, allowed)
|
||||
res.Results = filterSearchUsers(res.Results, allowed)
|
||||
}
|
||||
return s.projectSearchResult(ctx, userID, res)
|
||||
}
|
||||
|
||||
|
|
@ -452,11 +517,14 @@ func (s *Service) peerCanSeeCurrentUserPhone(ctx context.Context, ownerUserID, v
|
|||
if s.contacts == nil {
|
||||
return false, nil
|
||||
}
|
||||
_, found, err := s.contacts.Get(ctx, viewerUserID, ownerUserID)
|
||||
contact, found, err := s.contacts.Get(ctx, viewerUserID, ownerUserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return found, nil
|
||||
// Merely adding the owner by user id does not mean the viewer knows the
|
||||
// owner's phone. Only a non-empty owner-scoped contact phone can suppress the
|
||||
// "share my phone" prompt when PhoneNumber privacy itself denies visibility.
|
||||
return found && contact.Phone != "", nil
|
||||
}
|
||||
|
||||
// BlockContact adds peer to the current user's blocklist.
|
||||
|
|
@ -506,7 +574,22 @@ func (s *Service) GetBlocked(ctx context.Context, userID int64, offset, limit in
|
|||
if limit <= 0 || limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
return s.contacts.ListBlocked(ctx, userID, offset, limit)
|
||||
list, err := s.contacts.ListBlocked(ctx, userID, offset, limit)
|
||||
if err != nil || len(list.Blocked) == 0 || s.projector == nil {
|
||||
return list, err
|
||||
}
|
||||
users := make([]domain.User, len(list.Blocked))
|
||||
for i := range list.Blocked {
|
||||
users[i] = list.Blocked[i].User
|
||||
}
|
||||
projected, err := s.projector.ForViewer(ctx, userID, users)
|
||||
if err != nil {
|
||||
return domain.BlockedContactList{}, err
|
||||
}
|
||||
for i := range list.Blocked {
|
||||
list.Blocked[i].User = projected[i]
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *Service) ContactIDs(ctx context.Context, userID int64, hash int64) ([]int, bool, error) {
|
||||
|
|
@ -590,6 +673,34 @@ func normalizePhone(phone string) string {
|
|||
return phone
|
||||
}
|
||||
|
||||
func isPhoneSearchQuery(query string) bool {
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return false
|
||||
}
|
||||
hasDigit := false
|
||||
for _, r := range query {
|
||||
switch {
|
||||
case r >= '0' && r <= '9':
|
||||
hasDigit = true
|
||||
case r == '+', r == ' ', r == '-', r == '(', r == ')':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return hasDigit
|
||||
}
|
||||
|
||||
func filterSearchUsers(users []domain.User, keep func(domain.User) bool) []domain.User {
|
||||
out := users[:0]
|
||||
for _, user := range users {
|
||||
if keep(user) {
|
||||
out = append(out, user)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func normalizeCloseFriendIDs(userID int64, ids []int64) []int64 {
|
||||
out := make([]int64, 0, len(ids))
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue