business: fix contact projection and phone sharing
(cherry picked from commit c0a0e5b52240ed415d3b43ba77659821887bf50b)
This commit is contained in:
parent
d84fa6e126
commit
860e581d06
18 changed files with 703 additions and 36 deletions
|
|
@ -11,8 +11,9 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrContactIDInvalid = errors.New("contact id invalid")
|
||||
ErrContactNameEmpty = errors.New("contact name empty")
|
||||
ErrContactIDInvalid = errors.New("contact id invalid")
|
||||
ErrContactNameEmpty = errors.New("contact name empty")
|
||||
ErrContactReqMissing = errors.New("contact request missing")
|
||||
)
|
||||
|
||||
const maxSearchLimit = 50
|
||||
|
|
@ -112,6 +113,54 @@ func (s *Service) AddContact(ctx context.Context, userID int64, input domain.Con
|
|||
return contact, nil
|
||||
}
|
||||
|
||||
// AcceptContact shares the current user's phone/profile with an existing one-way contact.
|
||||
func (s *Service) AcceptContact(ctx context.Context, userID, contactUserID int64) (domain.Contact, error) {
|
||||
if s == nil || s.contacts == nil || s.users == nil || userID == 0 || contactUserID == 0 || contactUserID == userID {
|
||||
return domain.Contact{}, ErrContactIDInvalid
|
||||
}
|
||||
ownerContact, found, err := s.contacts.Get(ctx, userID, contactUserID)
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.Contact{}, ErrContactReqMissing
|
||||
}
|
||||
self, found, err := s.users.ByID(ctx, userID)
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.Contact{}, ErrContactIDInvalid
|
||||
}
|
||||
target, found, err := s.users.ByID(ctx, contactUserID)
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.Contact{}, ErrContactIDInvalid
|
||||
}
|
||||
if ownerContact.Mutual {
|
||||
return ownerContact, nil
|
||||
}
|
||||
_, err = s.contacts.Upsert(ctx, contactUserID, domain.ContactInput{
|
||||
ContactUserID: userID,
|
||||
Phone: self.Phone,
|
||||
FirstName: self.FirstName,
|
||||
LastName: self.LastName,
|
||||
})
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
contact, found, err := s.contacts.Get(ctx, userID, target.ID)
|
||||
if err != nil {
|
||||
return domain.Contact{}, err
|
||||
}
|
||||
if !found {
|
||||
return domain.Contact{}, ErrContactReqMissing
|
||||
}
|
||||
return contact, nil
|
||||
}
|
||||
|
||||
func (s *Service) ImportContacts(ctx context.Context, userID int64, inputs []domain.ContactInput) (domain.ImportContactsResult, error) {
|
||||
if s == nil || s.contacts == nil || s.users == nil || userID == 0 || len(inputs) == 0 {
|
||||
return domain.ImportContactsResult{}, nil
|
||||
|
|
@ -225,7 +274,7 @@ func (s *Service) GetPeerSettings(ctx context.Context, userID int64, peer domain
|
|||
if s == nil || s.contacts == nil || userID == 0 || peer.Type != domain.PeerTypeUser || peer.ID == 0 || peer.ID == userID {
|
||||
return domain.PeerSettings{}, nil
|
||||
}
|
||||
_, found, err := s.contacts.Get(ctx, userID, peer.ID)
|
||||
contact, found, err := s.contacts.Get(ctx, userID, peer.ID)
|
||||
if err != nil {
|
||||
return domain.PeerSettings{}, err
|
||||
}
|
||||
|
|
@ -236,7 +285,7 @@ func (s *Service) GetPeerSettings(ctx context.Context, userID int64, peer domain
|
|||
return domain.PeerSettings{
|
||||
AddContact: !found,
|
||||
BlockContact: !blocked,
|
||||
ShareContact: found,
|
||||
ShareContact: found && !contact.Mutual,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package contacts
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -42,3 +43,90 @@ func TestImportContactsBatchesPhonesAndDedupesUpserts(t *testing.T) {
|
|||
t.Fatalf("contact first name = %q, want final input", res.Contacts[0].FirstName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptContactSharesPhoneAndClearsShareContact(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
contactsStore := memory.NewContactStore()
|
||||
alice, err := users.Create(ctx, domain.User{Phone: "15550000001", FirstName: "Alice", LastName: "A"})
|
||||
if err != nil {
|
||||
t.Fatalf("create alice: %v", err)
|
||||
}
|
||||
bob, err := users.Create(ctx, domain.User{Phone: "15550000002", FirstName: "Bob", LastName: "B"})
|
||||
if err != nil {
|
||||
t.Fatalf("create bob: %v", err)
|
||||
}
|
||||
svc := NewService(contactsStore, users)
|
||||
|
||||
if _, err := svc.AddContact(ctx, alice.ID, domain.ContactInput{
|
||||
ContactUserID: bob.ID,
|
||||
Phone: bob.Phone,
|
||||
FirstName: "Bobby",
|
||||
LastName: "Remark",
|
||||
}); err != nil {
|
||||
t.Fatalf("alice add bob: %v", err)
|
||||
}
|
||||
settings, err := svc.GetPeerSettings(ctx, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("alice peer settings before accept: %v", err)
|
||||
}
|
||||
if !settings.ShareContact {
|
||||
t.Fatalf("alice settings before accept = %+v, want share contact", settings)
|
||||
}
|
||||
|
||||
contact, err := svc.AcceptContact(ctx, alice.ID, bob.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("AcceptContact: %v", err)
|
||||
}
|
||||
if !contact.Mutual || !contact.User.Mutual {
|
||||
t.Fatalf("accepted contact = %+v, want mutual", contact)
|
||||
}
|
||||
aliceSettings, err := svc.GetPeerSettings(ctx, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("alice peer settings after accept: %v", err)
|
||||
}
|
||||
if aliceSettings.ShareContact || aliceSettings.AddContact {
|
||||
t.Fatalf("alice settings after accept = %+v, want no share/add", aliceSettings)
|
||||
}
|
||||
bobSettings, err := svc.GetPeerSettings(ctx, bob.ID, domain.Peer{Type: domain.PeerTypeUser, ID: alice.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("bob peer settings after accept: %v", err)
|
||||
}
|
||||
if bobSettings.ShareContact || bobSettings.AddContact {
|
||||
t.Fatalf("bob settings after accept = %+v, want no share/add", bobSettings)
|
||||
}
|
||||
reverse, found, err := contactsStore.Get(ctx, bob.ID, alice.ID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("bob contact alice found=%v err=%v", found, err)
|
||||
}
|
||||
if reverse.Phone != alice.Phone || reverse.FirstName != alice.FirstName || reverse.LastName != alice.LastName || !reverse.Mutual {
|
||||
t.Fatalf("bob contact alice = %+v, want alice phone/name and mutual", reverse)
|
||||
}
|
||||
|
||||
repeated, err := svc.AcceptContact(ctx, alice.ID, bob.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("AcceptContact repeat: %v", err)
|
||||
}
|
||||
if !repeated.Mutual {
|
||||
t.Fatalf("repeated accept = %+v, want mutual", repeated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptContactRequiresExistingContactRequest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
contactsStore := memory.NewContactStore()
|
||||
alice, err := users.Create(ctx, domain.User{Phone: "15550000001", FirstName: "Alice"})
|
||||
if err != nil {
|
||||
t.Fatalf("create alice: %v", err)
|
||||
}
|
||||
bob, err := users.Create(ctx, domain.User{Phone: "15550000002", FirstName: "Bob"})
|
||||
if err != nil {
|
||||
t.Fatalf("create bob: %v", err)
|
||||
}
|
||||
svc := NewService(contactsStore, users)
|
||||
|
||||
if _, err := svc.AcceptContact(ctx, alice.ID, bob.ID); !errors.Is(err, ErrContactReqMissing) {
|
||||
t.Fatalf("AcceptContact without contact err = %v, want ErrContactReqMissing", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package messages
|
|||
import (
|
||||
"context"
|
||||
|
||||
"telesrv/internal/app/userprojection"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
|
@ -11,11 +12,24 @@ import (
|
|||
type Service struct {
|
||||
messages store.MessageStore
|
||||
dialogs store.DialogStore
|
||||
contacts store.ContactStore
|
||||
}
|
||||
|
||||
// Option adjusts optional message service dependencies.
|
||||
type Option func(*Service)
|
||||
|
||||
// WithContactStore enables viewer-specific user projection for message history.
|
||||
func WithContactStore(c store.ContactStore) Option {
|
||||
return func(s *Service) { s.contacts = c }
|
||||
}
|
||||
|
||||
// NewService 创建 messages 服务。
|
||||
func NewService(messages store.MessageStore, dialogs store.DialogStore) *Service {
|
||||
return &Service{messages: messages, dialogs: dialogs}
|
||||
func NewService(messages store.MessageStore, dialogs store.DialogStore, opts ...Option) *Service {
|
||||
s := &Service{messages: messages, dialogs: dialogs}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SendPrivateText 发送一条私聊文本消息。
|
||||
|
|
@ -45,7 +59,11 @@ func (s *Service) GetMessages(ctx context.Context, userID int64, ids []int) (dom
|
|||
if s == nil || s.messages == nil || userID == 0 || len(ids) == 0 {
|
||||
return domain.MessageList{}, nil
|
||||
}
|
||||
return s.messages.GetByIDs(ctx, userID, ids)
|
||||
list, err := s.messages.GetByIDs(ctx, userID, ids)
|
||||
if err != nil {
|
||||
return domain.MessageList{}, err
|
||||
}
|
||||
return s.projectMessageUsers(ctx, userID, list)
|
||||
}
|
||||
|
||||
// GetHistory 返回当前账号某个 peer 的历史消息。
|
||||
|
|
@ -176,5 +194,18 @@ func (s *Service) list(ctx context.Context, userID int64, filter domain.MessageF
|
|||
if s == nil || s.messages == nil || userID == 0 {
|
||||
return domain.MessageList{}, nil
|
||||
}
|
||||
return s.messages.ListByUser(ctx, userID, filter)
|
||||
list, err := s.messages.ListByUser(ctx, userID, filter)
|
||||
if err != nil {
|
||||
return domain.MessageList{}, err
|
||||
}
|
||||
return s.projectMessageUsers(ctx, userID, list)
|
||||
}
|
||||
|
||||
func (s *Service) projectMessageUsers(ctx context.Context, userID int64, list domain.MessageList) (domain.MessageList, error) {
|
||||
users, err := userprojection.ForViewer(ctx, s.contacts, userID, list.Users)
|
||||
if err != nil {
|
||||
return domain.MessageList{}, err
|
||||
}
|
||||
list.Users = users
|
||||
return list, nil
|
||||
}
|
||||
|
|
|
|||
117
internal/app/messages/service_test.go
Normal file
117
internal/app/messages/service_test.go
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package messages
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func TestServiceProjectsMessageUsersForViewerContacts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
const friendID int64 = 1002
|
||||
const strangerID int64 = 1003
|
||||
contacts := memory.NewContactStore()
|
||||
if _, err := contacts.Upsert(ctx, ownerID, domain.ContactInput{
|
||||
ContactUserID: friendID,
|
||||
Phone: "15550000002",
|
||||
FirstName: "Remark",
|
||||
LastName: "Friend",
|
||||
}); err != nil {
|
||||
t.Fatalf("upsert contact: %v", err)
|
||||
}
|
||||
store := projectionMessageStore{list: domain.MessageList{
|
||||
Users: []domain.User{
|
||||
{ID: ownerID, Phone: "15550000001", FirstName: "Owner"},
|
||||
{ID: friendID, AccessHash: 22, Phone: "15550000002", FirstName: "Public", LastName: "Name"},
|
||||
{ID: strangerID, AccessHash: 33, Phone: "15550000003", FirstName: "Stranger"},
|
||||
},
|
||||
}}
|
||||
svc := NewService(store, nil, WithContactStore(contacts))
|
||||
|
||||
list, err := svc.GetHistory(ctx, ownerID, domain.MessageFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("GetHistory: %v", err)
|
||||
}
|
||||
friend := findUser(t, list.Users, friendID)
|
||||
if !friend.Contact || friend.FirstName != "Remark" || friend.LastName != "Friend" || friend.Phone != "15550000002" {
|
||||
t.Fatalf("friend projection = %+v, want contact remark and phone", friend)
|
||||
}
|
||||
stranger := findUser(t, list.Users, strangerID)
|
||||
if stranger.Contact || stranger.Phone != "" || stranger.FirstName != "Stranger" {
|
||||
t.Fatalf("stranger projection = %+v, want non-contact with hidden phone", stranger)
|
||||
}
|
||||
self := findUser(t, list.Users, ownerID)
|
||||
if self.Phone != "15550000001" {
|
||||
t.Fatalf("self phone = %q, want preserved", self.Phone)
|
||||
}
|
||||
}
|
||||
|
||||
func findUser(t *testing.T, users []domain.User, id int64) domain.User {
|
||||
t.Helper()
|
||||
for _, user := range users {
|
||||
if user.ID == id {
|
||||
return user
|
||||
}
|
||||
}
|
||||
t.Fatalf("user %d not found in %+v", id, users)
|
||||
return domain.User{}
|
||||
}
|
||||
|
||||
type projectionMessageStore struct {
|
||||
list domain.MessageList
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) Create(context.Context, domain.Message) (domain.Message, error) {
|
||||
return domain.Message{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) SendPrivateText(context.Context, domain.SendPrivateTextRequest) (domain.SendPrivateTextResult, error) {
|
||||
return domain.SendPrivateTextResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) ForwardPrivateMessages(context.Context, domain.ForwardPrivateMessagesRequest) (domain.ForwardPrivateMessagesResult, error) {
|
||||
return domain.ForwardPrivateMessagesResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) ReadHistory(context.Context, domain.ReadHistoryRequest) (domain.ReadHistoryResult, error) {
|
||||
return domain.ReadHistoryResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) ReadMessageContents(context.Context, domain.ReadMessageContentsRequest) (domain.ReadMessageContentsResult, error) {
|
||||
return domain.ReadMessageContentsResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) GetOutboxReadDate(context.Context, domain.OutboxReadDateRequest) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) SetMessageReactions(context.Context, domain.SetPrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
|
||||
return domain.PrivateMessageReactionsResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) GetMessageReactions(context.Context, domain.PrivateMessageReactionsRequest) (domain.PrivateMessageReactionsResult, error) {
|
||||
return domain.PrivateMessageReactionsResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) EditMessage(context.Context, domain.EditMessageRequest) (domain.EditMessageResult, error) {
|
||||
return domain.EditMessageResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) DeleteMessages(context.Context, domain.DeleteMessagesRequest) (domain.DeleteMessagesResult, error) {
|
||||
return domain.DeleteMessagesResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) DeleteHistory(context.Context, domain.DeleteHistoryRequest) (domain.DeleteMessagesResult, error) {
|
||||
return domain.DeleteMessagesResult{}, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) GetByIDs(context.Context, int64, []int) (domain.MessageList, error) {
|
||||
return s.list, nil
|
||||
}
|
||||
|
||||
func (s projectionMessageStore) ListByUser(context.Context, int64, domain.MessageFilter) (domain.MessageList, error) {
|
||||
return s.list, nil
|
||||
}
|
||||
75
internal/app/userprojection/projection.go
Normal file
75
internal/app/userprojection/projection.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package userprojection
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// ForViewer applies the owner-specific user view that Telegram clients expect.
|
||||
// In particular, phone is visible for self and contacts; non-contacts should not
|
||||
// receive a phone field because TDesktop will prefer it over the public name.
|
||||
func ForViewer(ctx context.Context, contacts store.ContactStore, viewerUserID int64, users []domain.User) ([]domain.User, error) {
|
||||
if contacts == nil || viewerUserID == 0 || len(users) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
out := make([]domain.User, len(users))
|
||||
copy(out, users)
|
||||
cache := make(map[int64]domain.User, len(users))
|
||||
for i := range out {
|
||||
u := out[i]
|
||||
if u.ID == 0 || u.ID == viewerUserID || u.ID == domain.OfficialSystemUserID {
|
||||
continue
|
||||
}
|
||||
if projected, ok := cache[u.ID]; ok {
|
||||
out[i] = projected
|
||||
continue
|
||||
}
|
||||
projected, err := projectOne(ctx, contacts, viewerUserID, u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache[u.ID] = projected
|
||||
out[i] = projected
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// One applies ForViewer to a single user.
|
||||
func One(ctx context.Context, contacts store.ContactStore, viewerUserID int64, user domain.User) (domain.User, error) {
|
||||
projected, err := ForViewer(ctx, contacts, viewerUserID, []domain.User{user})
|
||||
if err != nil || len(projected) == 0 {
|
||||
return domain.User{}, err
|
||||
}
|
||||
return projected[0], nil
|
||||
}
|
||||
|
||||
func projectOne(ctx context.Context, contacts store.ContactStore, viewerUserID int64, user domain.User) (domain.User, error) {
|
||||
contact, found, err := contacts.Get(ctx, viewerUserID, user.ID)
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
if !found {
|
||||
user.Phone = ""
|
||||
user.Contact = false
|
||||
user.Mutual = false
|
||||
return user, nil
|
||||
}
|
||||
projected := user
|
||||
projected.Contact = true
|
||||
projected.Mutual = contact.Mutual || contact.User.Mutual
|
||||
if contact.User.Phone != "" {
|
||||
projected.Phone = contact.User.Phone
|
||||
} else {
|
||||
projected.Phone = contact.Phone
|
||||
}
|
||||
if contact.User.FirstName != "" || contact.User.LastName != "" {
|
||||
projected.FirstName = contact.User.FirstName
|
||||
projected.LastName = contact.User.LastName
|
||||
} else if contact.FirstName != "" || contact.LastName != "" {
|
||||
projected.FirstName = contact.FirstName
|
||||
projected.LastName = contact.LastName
|
||||
}
|
||||
return projected, nil
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"telesrv/internal/app/userprojection"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
|
@ -20,8 +21,9 @@ type ProfilePhotoProvider interface {
|
|||
|
||||
// Service 提供用户查询。
|
||||
type Service struct {
|
||||
users store.UserStore
|
||||
photos ProfilePhotoProvider
|
||||
users store.UserStore
|
||||
contacts store.ContactStore
|
||||
photos ProfilePhotoProvider
|
||||
}
|
||||
|
||||
// Option 调整用户服务可选依赖。
|
||||
|
|
@ -32,6 +34,11 @@ func WithPhotoProvider(p ProfilePhotoProvider) Option {
|
|||
return func(s *Service) { s.photos = p }
|
||||
}
|
||||
|
||||
// WithContactStore enables viewer-specific contact name/phone projection.
|
||||
func WithContactStore(c store.ContactStore) Option {
|
||||
return func(s *Service) { s.contacts = c }
|
||||
}
|
||||
|
||||
const (
|
||||
minUsernameLen = 5
|
||||
maxUsernameLen = 32
|
||||
|
|
@ -85,7 +92,12 @@ func (s *Service) ByID(ctx context.Context, currentUserID, userID int64) (domain
|
|||
if !found {
|
||||
return u, false, nil
|
||||
}
|
||||
return s.enrichOne(ctx, u), true, nil
|
||||
u = s.enrichOne(ctx, u)
|
||||
u, err = userprojection.One(ctx, s.contacts, currentUserID, u)
|
||||
if err != nil {
|
||||
return domain.User{}, false, err
|
||||
}
|
||||
return u, true, nil
|
||||
}
|
||||
|
||||
// ByIDs 批量返回指定用户。调用方必须已登录;缺失用户不会出现在结果中。
|
||||
|
|
@ -115,7 +127,8 @@ func (s *Service) ByIDs(ctx context.Context, currentUserID int64, userIDs []int6
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.enrich(ctx, users), nil
|
||||
users = s.enrich(ctx, users)
|
||||
return userprojection.ForViewer(ctx, s.contacts, currentUserID, users)
|
||||
}
|
||||
|
||||
// enrich 批量把当前头像富化到用户列表(best-effort:失败不影响用户查询)。
|
||||
|
|
@ -248,7 +261,12 @@ func (s *Service) ResolveUsername(ctx context.Context, currentUserID int64, user
|
|||
if err != nil || !found {
|
||||
return u, found, err
|
||||
}
|
||||
return s.enrichOne(ctx, u), true, nil
|
||||
u = s.enrichOne(ctx, u)
|
||||
u, err = userprojection.One(ctx, s.contacts, currentUserID, u)
|
||||
if err != nil {
|
||||
return domain.User{}, false, err
|
||||
}
|
||||
return u, true, nil
|
||||
}
|
||||
|
||||
// ResolvePhone 解析手机号到用户;当前阶段默认允许手机号深链解析,隐私规则后续接 account privacy。
|
||||
|
|
@ -264,7 +282,12 @@ func (s *Service) ResolvePhone(ctx context.Context, currentUserID int64, phone s
|
|||
if err != nil || !found {
|
||||
return u, found, err
|
||||
}
|
||||
return s.enrichOne(ctx, u), true, nil
|
||||
u = s.enrichOne(ctx, u)
|
||||
u, err = userprojection.One(ctx, s.contacts, currentUserID, u)
|
||||
if err != nil {
|
||||
return domain.User{}, false, err
|
||||
}
|
||||
return u, true, nil
|
||||
}
|
||||
|
||||
func normalizeUsername(username string) string {
|
||||
|
|
|
|||
|
|
@ -117,6 +117,48 @@ func TestServiceByIDDoesNotReloadSelf(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestServiceProjectsUsersForViewerContacts(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
userStore := memory.NewUserStore()
|
||||
contacts := memory.NewContactStore()
|
||||
owner, err := userStore.Create(ctx, domain.User{AccessHash: 1, Phone: "15550000001", FirstName: "Owner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
friend, err := userStore.Create(ctx, domain.User{AccessHash: 2, Phone: "15550000002", FirstName: "Public", LastName: "Name"})
|
||||
if err != nil {
|
||||
t.Fatalf("create friend: %v", err)
|
||||
}
|
||||
stranger, err := userStore.Create(ctx, domain.User{AccessHash: 3, Phone: "15550000003", FirstName: "Stranger"})
|
||||
if err != nil {
|
||||
t.Fatalf("create stranger: %v", err)
|
||||
}
|
||||
if _, err := contacts.Upsert(ctx, owner.ID, domain.ContactInput{
|
||||
ContactUserID: friend.ID,
|
||||
Phone: "15550000002",
|
||||
FirstName: "Remark",
|
||||
LastName: "Friend",
|
||||
}); err != nil {
|
||||
t.Fatalf("upsert contact: %v", err)
|
||||
}
|
||||
svc := NewService(userStore, WithContactStore(contacts))
|
||||
|
||||
contactUser, found, err := svc.ByID(ctx, owner.ID, friend.ID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("ByID contact found=%v err=%v", found, err)
|
||||
}
|
||||
if !contactUser.Contact || contactUser.FirstName != "Remark" || contactUser.LastName != "Friend" || contactUser.Phone != "15550000002" {
|
||||
t.Fatalf("projected contact = %+v, want contact remark and phone", contactUser)
|
||||
}
|
||||
nonContact, found, err := svc.ByID(ctx, owner.ID, stranger.ID)
|
||||
if err != nil || !found {
|
||||
t.Fatalf("ByID non-contact found=%v err=%v", found, err)
|
||||
}
|
||||
if nonContact.Contact || nonContact.Phone != "" || nonContact.FirstName != "Stranger" {
|
||||
t.Fatalf("projected non-contact = %+v, want name with hidden phone", nonContact)
|
||||
}
|
||||
}
|
||||
|
||||
type countingUserStore struct {
|
||||
*memory.UserStore
|
||||
byIDCalls int
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue