business: add privacy-aware user projection
(cherry picked from commit a636192ef22ee69d792b0ca7db1c6be963be9cb2)
This commit is contained in:
parent
14d220c971
commit
8ff3343ae0
29 changed files with 2239 additions and 112 deletions
|
|
@ -8,14 +8,37 @@ import (
|
|||
"sort"
|
||||
"unicode/utf8"
|
||||
|
||||
"telesrv/internal/app/userprojection"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// Service 提供会话列表查询。
|
||||
type Service struct {
|
||||
dialogs store.DialogStore
|
||||
channels store.ChannelStore
|
||||
dialogs store.DialogStore
|
||||
channels store.ChannelStore
|
||||
contacts store.ContactStore
|
||||
photos userprojection.ProfilePhotoProvider
|
||||
privacy userprojection.PrivacyEvaluator
|
||||
projector *userprojection.Projector
|
||||
}
|
||||
|
||||
// Option adjusts optional dialogs service dependencies.
|
||||
type Option func(*Service)
|
||||
|
||||
// WithContactStore enables viewer-specific user projection for dialog users.
|
||||
func WithContactStore(c store.ContactStore) Option {
|
||||
return func(s *Service) { s.contacts = c }
|
||||
}
|
||||
|
||||
// WithPhotoProvider enables current profile photo enrichment for dialog users.
|
||||
func WithPhotoProvider(p userprojection.ProfilePhotoProvider) Option {
|
||||
return func(s *Service) { s.photos = p }
|
||||
}
|
||||
|
||||
// WithPrivacyEvaluator enables viewer-specific privacy projection for dialog users.
|
||||
func WithPrivacyEvaluator(p userprojection.PrivacyEvaluator) Option {
|
||||
return func(s *Service) { s.privacy = p }
|
||||
}
|
||||
|
||||
// NewService 创建 dialogs 服务。
|
||||
|
|
@ -24,9 +47,33 @@ func NewService(dialogs store.DialogStore, channels ...store.ChannelStore) *Serv
|
|||
if len(channels) > 0 {
|
||||
s.channels = channels[0]
|
||||
}
|
||||
s.rebuildProjector()
|
||||
return s
|
||||
}
|
||||
|
||||
// Configure applies optional dependencies after construction.
|
||||
func (s *Service) Configure(opts ...Option) *Service {
|
||||
if s == nil {
|
||||
return s
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
s.rebuildProjector()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Service) rebuildProjector() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.projector = userprojection.New(
|
||||
userprojection.WithContactStore(s.contacts),
|
||||
userprojection.WithPhotoProvider(s.photos),
|
||||
userprojection.WithPrivacyEvaluator(s.privacy),
|
||||
)
|
||||
}
|
||||
|
||||
// GetDialogs 返回当前登录账号的会话摘要。未登录或无持久化实现时按空账号处理。
|
||||
func (s *Service) GetDialogs(ctx context.Context, userID int64, filter domain.DialogFilter) (domain.DialogList, error) {
|
||||
if s == nil || userID == 0 {
|
||||
|
|
@ -81,6 +128,9 @@ func (s *Service) GetDialogs(ctx context.Context, userID int64, filter domain.Di
|
|||
if err := s.attachDrafts(ctx, userID, &out); err != nil {
|
||||
return domain.DialogList{}, err
|
||||
}
|
||||
if err := s.projectDialogUsers(ctx, userID, &out); err != nil {
|
||||
return domain.DialogList{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
|
@ -124,6 +174,9 @@ func (s *Service) GetPeerDialogs(ctx context.Context, userID int64, peers []doma
|
|||
if err := s.attachDrafts(ctx, userID, &out); err != nil {
|
||||
return domain.DialogList{}, err
|
||||
}
|
||||
if err := s.projectDialogUsers(ctx, userID, &out); err != nil {
|
||||
return domain.DialogList{}, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
|
@ -439,6 +492,18 @@ func (s *Service) attachDrafts(ctx context.Context, userID int64, list *domain.D
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) projectDialogUsers(ctx context.Context, userID int64, list *domain.DialogList) error {
|
||||
if s == nil || s.projector == nil || list == nil || len(list.Users) == 0 {
|
||||
return nil
|
||||
}
|
||||
users, err := s.projector.ForViewer(ctx, userID, list.Users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list.Users = users
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateDraft(draft domain.DialogDraft) error {
|
||||
if err := validateDraftKey(draft.Peer, draft.TopMessageID); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -62,6 +62,64 @@ func TestGetDialogsIncludesChannelReadOutboxAfterOfflineRead(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetDialogsProjectsUsersWithCurrentProfilePhoto(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
const peerID int64 = 1002
|
||||
dialogStore := memory.NewDialogStore()
|
||||
if err := dialogStore.SaveList(ctx, ownerID, domain.DialogList{
|
||||
Dialogs: []domain.Dialog{{
|
||||
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID},
|
||||
TopMessage: 1,
|
||||
TopMessageDate: 20,
|
||||
}},
|
||||
Users: []domain.User{{
|
||||
ID: peerID,
|
||||
AccessHash: 22,
|
||||
Phone: "15550000002",
|
||||
FirstName: "Alice A",
|
||||
}},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveList: %v", err)
|
||||
}
|
||||
contacts := memory.NewContactStore()
|
||||
if _, err := contacts.Upsert(ctx, ownerID, domain.ContactInput{
|
||||
ContactUserID: peerID,
|
||||
Phone: "1111",
|
||||
FirstName: "Alice",
|
||||
LastName: "Saved",
|
||||
}); err != nil {
|
||||
t.Fatalf("upsert contact: %v", err)
|
||||
}
|
||||
dialogs := NewService(dialogStore).Configure(
|
||||
WithContactStore(contacts),
|
||||
WithPhotoProvider(dialogProfilePhotos{
|
||||
peerID: {PhotoID: 9201, DCID: 2, Stripped: []byte{7, 8}},
|
||||
}),
|
||||
)
|
||||
|
||||
list, err := dialogs.GetDialogs(ctx, ownerID, domain.DialogFilter{Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("GetDialogs: %v", err)
|
||||
}
|
||||
peer := findDialogUser(t, list.Users, peerID)
|
||||
if peer.PhotoID != 9201 || peer.PhotoDCID != 2 || string(peer.PhotoStripped) != string([]byte{7, 8}) {
|
||||
t.Fatalf("dialog user photo = id %d dc %d stripped %v, want 9201/2/[7 8]", peer.PhotoID, peer.PhotoDCID, peer.PhotoStripped)
|
||||
}
|
||||
if !peer.Contact || peer.FirstName != "Alice" || peer.LastName != "Saved" || peer.Phone != "1111" {
|
||||
t.Fatalf("dialog user projection = %+v, want contact view", peer)
|
||||
}
|
||||
|
||||
peerList, err := dialogs.GetPeerDialogs(ctx, ownerID, []domain.Peer{{Type: domain.PeerTypeUser, ID: peerID}})
|
||||
if err != nil {
|
||||
t.Fatalf("GetPeerDialogs: %v", err)
|
||||
}
|
||||
peer = findDialogUser(t, peerList.Users, peerID)
|
||||
if peer.PhotoID != 9201 || peer.PhotoDCID != 2 {
|
||||
t.Fatalf("peer dialog user photo = id %d dc %d, want 9201/2", peer.PhotoID, peer.PhotoDCID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDialogSettingsPersistThroughUnifiedDialogService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
channelStore := memory.NewChannelStore()
|
||||
|
|
@ -280,3 +338,26 @@ func findChannelDialog(t *testing.T, list domain.DialogList, channelID int64) do
|
|||
t.Fatalf("channel dialog %d not found in %+v", channelID, list.Dialogs)
|
||||
return domain.Dialog{}
|
||||
}
|
||||
|
||||
func findDialogUser(t *testing.T, users []domain.User, userID int64) domain.User {
|
||||
t.Helper()
|
||||
for _, user := range users {
|
||||
if user.ID == userID {
|
||||
return user
|
||||
}
|
||||
}
|
||||
t.Fatalf("user %d not found in %+v", userID, users)
|
||||
return domain.User{}
|
||||
}
|
||||
|
||||
type dialogProfilePhotos map[int64]domain.ProfilePhotoRef
|
||||
|
||||
func (p dialogProfilePhotos) CurrentProfilePhotos(_ context.Context, _ domain.PeerType, ids []int64) (map[int64]domain.ProfilePhotoRef, error) {
|
||||
out := make(map[int64]domain.ProfilePhotoRef, len(ids))
|
||||
for _, id := range ids {
|
||||
if ref, ok := p[id]; ok {
|
||||
out[id] = ref
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue