package userprojection import ( "context" "time" "golang.org/x/sync/errgroup" "telesrv/internal/domain" "telesrv/internal/store" ) // ProfilePhotoProvider returns current profile photos for a batch of owners. type ProfilePhotoProvider interface { CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64) (map[int64]domain.ProfilePhotoRef, error) } // ProfilePhotoKindProvider returns current profile/fallback photos for a batch of owners. type ProfilePhotoKindProvider interface { CurrentProfilePhotosKind(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64, kind domain.ProfilePhotoKind) (map[int64]domain.ProfilePhotoRef, error) } // PrivacyEvaluator answers viewer-specific visibility for one user privacy key. type PrivacyEvaluator interface { CanSee(ctx context.Context, ownerUserID, viewerUserID int64, key domain.PrivacyKey) (bool, error) } // AccountFreezeProvider returns durable account freeze facts for a bounded // batch. The projector only exposes them to viewers other than the frozen user. type AccountFreezeProvider interface { AccountFreezes(ctx context.Context, userIDs []int64) (map[int64]domain.AccountFreeze, error) } // CollectiblePhoneProvider returns the live collectible number attached to each // user. It is batched because user vectors are projected on every hot RPC path. type CollectiblePhoneProvider interface { OwnedCollectiblePhones(ctx context.Context, userIDs []int64) (map[int64]domain.CollectiblePhone, error) } // BatchPrivacyEvaluator evaluates visibility of multiple owners to a single viewer in a // batch, eliminating the per-user 3×CanSee N+1 in projectBatch / fan-out projection. // Optional: an evaluator that implements it (privacy.Service) makes projectBatch prefer // batch prefetch, otherwise it falls back to per-CanSee. Results must be byte-equivalent to // per-CanSee. type BatchPrivacyEvaluator interface { CanSeeBatch(ctx context.Context, ownerUserIDs []int64, viewerUserID int64, keys []domain.PrivacyKey) (map[int64]map[domain.PrivacyKey]bool, error) } // MatrixPrivacyEvaluator evaluates the owners×viewers×keys visibility matrix in a batch // (one ListPrivacyRules + one GetMany per owner + in-memory Evaluate), letting ForViewers // reduce the privacy queries of cross-viewer fan-out projection from O(viewer) to O(owner). // Optional: privacy.Service implements it, otherwise ForViewers falls back to per-CanSee. // Results must be byte-equivalent to per-CanSee. type MatrixPrivacyEvaluator interface { CanSeeMatrix(ctx context.Context, ownerUserIDs, viewerUserIDs []int64, keys []domain.PrivacyKey) (map[int64]map[int64]map[domain.PrivacyKey]bool, error) } // privacyProjectionKeys are the privacy keys projectBatch projection uses (phone/status/photo). var privacyProjectionKeys = []domain.PrivacyKey{ domain.PrivacyKeyPhoneNumber, domain.PrivacyKeyStatusTimestamp, domain.PrivacyKeyProfilePhoto, } // Projector builds the current viewer's user view for RPC response payloads. // It intentionally stays in app/domain types; tg.* conversion remains in rpc. type Projector struct { contacts store.ContactStore photos ProfilePhotoProvider privacy PrivacyEvaluator freezes AccountFreezeProvider phones CollectiblePhoneProvider } // Option configures a Projector. type Option func(*Projector) // WithContactStore enables viewer-specific contact name/phone projection. func WithContactStore(c store.ContactStore) Option { return func(p *Projector) { p.contacts = c } } // WithPhotoProvider enables current profile photo enrichment. func WithPhotoProvider(photos ProfilePhotoProvider) Option { return func(p *Projector) { p.photos = photos } } // WithPrivacyEvaluator enables profile/photo/status privacy projection. func WithPrivacyEvaluator(privacy PrivacyEvaluator) Option { return func(p *Projector) { p.privacy = privacy } } // WithAccountFreezeProvider enables viewer-scoped frozen-account visibility. func WithAccountFreezeProvider(provider AccountFreezeProvider) Option { return func(p *Projector) { p.freezes = provider } } func WithCollectiblePhoneProvider(provider CollectiblePhoneProvider) Option { return func(p *Projector) { p.phones = provider } } // New creates a user projector. func New(opts ...Option) *Projector { p := &Projector{} for _, opt := range opts { opt(p) } return p } // ForViewer applies both current profile photos and owner-specific contact view. func (p *Projector) ForViewer(ctx context.Context, viewerUserID int64, users []domain.User) ([]domain.User, error) { users = sanitizeDeletedUsers(users) if p == nil { return users, nil } return projectBatch(ctx, p.contacts, p.photos, p.privacy, p.freezes, p.phones, viewerUserID, users) } // One applies ForViewer to a single user. func (p *Projector) One(ctx context.Context, viewerUserID int64, user domain.User) (domain.User, error) { if p == nil { return user, nil } projected, err := p.ForViewer(ctx, viewerUserID, []domain.User{user}) if err != nil || len(projected) == 0 { return domain.User{}, err } return projected[0], nil } // ForViewers projects the same set of owner users in a batch across multiple viewers // (fan-out templating). It compresses the cost of running ForViewer (=projectBatch) once per // viewer (O(viewer)×(photos+contacts+privacy) queries) into: // - one profile/fallback photo batch (reused across viewers) // - one viewer-owned contact projection (contact rename/phone override + personal photo overlay) // - O(owner) GetMany calls + one ListPrivacyRules (done inside CanSeeMatrix) // // It returns map[viewerID][]domain.User, each slice byte-equivalent to that viewer's // ForViewer(viewer, users). The caller's users are never modified (copied internally). func (p *Projector) ForViewers(ctx context.Context, viewerUserIDs []int64, users []domain.User) (map[int64][]domain.User, error) { users = sanitizeDeletedUsers(users) out := make(map[int64][]domain.User, len(viewerUserIDs)) if p == nil || len(users) == 0 { for _, v := range viewerUserIDs { out[v] = cloneUsers(users) } return out, nil } viewers := dedupNonZeroInt64(viewerUserIDs) if len(viewers) == 0 { for _, v := range viewerUserIDs { out[v] = cloneUsers(users) } return out, nil } ids := uniqueUserIDs(users) // The three prefetch groups are independent (shared photos, viewer-owned contact projection, privacy matrix), so they run concurrently in a single wave. var ( profileRefs map[int64]domain.ProfilePhotoRef fallbackRefs map[int64]domain.ProfilePhotoRef contactsByViewer map[int64]map[int64]domain.Contact personalRefsByViewer map[int64]map[int64]domain.ProfilePhotoRef matrix map[int64]map[int64]map[domain.PrivacyKey]bool freezes map[int64]domain.AccountFreeze collectiblePhones map[int64]domain.CollectiblePhone ) g, gctx := errgroup.WithContext(ctx) // 1) Shared photos: one profile/fallback batch, reused across all viewers. g.Go(func() error { var err error profileRefs, fallbackRefs, err = p.batchProfileFallbackPhotos(gctx, ids) return err }) // 2) Rename/phone override + personal photo: read the contact rows the viewer owns in a // batch, matching the same semantics as projectBatch's GetMany/PersonalPhotos(viewer, owners). g.Go(func() error { if p.contacts == nil || len(ids) == 0 || len(viewers) == 0 { return nil } batch, err := p.contacts.ContactProjectionForViewers(gctx, viewers, ids) if err != nil { return err } contactsByViewer = batch.Contacts personalRefsByViewer = batch.PersonalPhotos return err }) // 3) privacy visibility matrix: O(owner) queries; when nil (no MatrixPrivacyEvaluator), applyPrivacy falls back to per-CanSee. if me, ok := p.privacy.(MatrixPrivacyEvaluator); ok && p.privacy != nil { g.Go(func() error { var err error matrix, err = me.CanSeeMatrix(gctx, ids, viewers, privacyProjectionKeys) return err }) } if p.freezes != nil && len(ids) > 0 { g.Go(func() error { var err error freezes, err = p.freezes.AccountFreezes(gctx, ids) return err }) } if p.phones != nil && len(ids) > 0 { g.Go(func() error { var err error collectiblePhones, err = p.phones.OwnedCollectiblePhones(gctx, ids) return err }) } if err := g.Wait(); err != nil { return nil, err } // 4) Assemble per viewer, reusing the exact same apply* chain as projectBatch. for _, viewer := range viewers { personalRefs := personalRefsByViewer[viewer] projected := cloneUsers(users) cache := make(map[int64]domain.User, len(projected)) for i := range projected { u := projected[i] if u.ID == 0 { continue } if u.Deleted { projected[i] = u.DeletedTombstone() continue } if pj, ok := cache[u.ID]; ok { projected[i] = pj continue } pj := applyBasePhotos(u, profileRefs, fallbackRefs, personalRefs, viewer) if viewer != 0 && u.ID != viewer && u.ID != domain.OfficialSystemUserID && !u.Bot { contact, found := contactsByViewer[viewer][u.ID] pj = applyContactProjection(pj, contact, found) pj = applyCollectiblePhone(pj, collectiblePhones[u.ID]) var vis map[domain.PrivacyKey]bool if matrix != nil { vis = matrix[u.ID][viewer] } var perr error hasKnownContactPhone := found && contact.Phone != "" pj, perr = applyPrivacy(ctx, p.privacy, viewer, pj, hasKnownContactPhone, vis, profileRefs, fallbackRefs, personalRefs) if perr != nil { return nil, perr } } if viewer == 0 || u.ID == viewer || u.ID == domain.OfficialSystemUserID || u.Bot { pj = applyCollectiblePhone(pj, collectiblePhones[u.ID]) } pj = reapplyExclusiveCollectiblePhone(pj, collectiblePhones[u.ID]) pj = applyAccountFreezeProjection(pj, viewer, freezes[u.ID]) cache[u.ID] = pj projected[i] = pj } out[viewer] = projected } return out, nil } // batchProfileFallbackPhotos fetches owners' profile/fallback photos (same logic as // projectBatch). When photos is nil it returns empty maps (applyBasePhotos treats it as no // photo lookups). func (p *Projector) batchProfileFallbackPhotos(ctx context.Context, ids []int64) (profileRefs, fallbackRefs map[int64]domain.ProfilePhotoRef, err error) { profileRefs = map[int64]domain.ProfilePhotoRef{} fallbackRefs = map[int64]domain.ProfilePhotoRef{} if p.photos == nil || len(ids) == 0 { return profileRefs, fallbackRefs, nil } if kindPhotos, ok := p.photos.(ProfilePhotoKindProvider); ok { refs, err := kindPhotos.CurrentProfilePhotosKind(ctx, domain.PeerTypeUser, ids, domain.ProfilePhotoKindProfile) if err != nil { return nil, nil, err } profileRefs = refs refs, err = kindPhotos.CurrentProfilePhotosKind(ctx, domain.PeerTypeUser, ids, domain.ProfilePhotoKindFallback) if err != nil { return nil, nil, err } fallbackRefs = refs return profileRefs, fallbackRefs, nil } refs, err := p.photos.CurrentProfilePhotos(ctx, domain.PeerTypeUser, ids) if err != nil { return nil, nil, err } return refs, fallbackRefs, nil } func cloneUsers(users []domain.User) []domain.User { if len(users) == 0 { return nil } out := make([]domain.User, len(users)) copy(out, users) for i := range out { out[i].PhotoStripped = append([]byte(nil), out[i].PhotoStripped...) out[i].ContactNoteEntities = append([]domain.MessageEntity(nil), out[i].ContactNoteEntities...) out[i].RestrictionReasons = append([]domain.UserRestrictionReason(nil), out[i].RestrictionReasons...) } return out } func dedupNonZeroInt64(ids []int64) []int64 { seen := make(map[int64]struct{}, len(ids)) out := make([]int64, 0, len(ids)) for _, id := range ids { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out } // WithProfilePhotos enriches users with their current avatar from profile photo storage. // The lookup is best-effort: a storage error keeps the original user list. func WithProfilePhotos(ctx context.Context, photos ProfilePhotoProvider, users []domain.User) []domain.User { users = sanitizeDeletedUsers(users) if photos == nil || len(users) == 0 { return users } ids := make([]int64, 0, len(users)) seen := make(map[int64]struct{}, len(users)) for _, u := range users { if u.ID == 0 || u.Deleted { continue } if _, ok := seen[u.ID]; ok { continue } seen[u.ID] = struct{}{} ids = append(ids, u.ID) } if len(ids) == 0 { return users } refs, err := photos.CurrentProfilePhotos(ctx, domain.PeerTypeUser, ids) if err != nil || len(refs) == 0 { return users } out := cloneUsers(users) for i := range out { if ref, ok := refs[out[i].ID]; ok { applyPhotoRef(&out[i], ref) } } return out } // ForViewer applies the owner-specific user view that Telegram clients expect. // A contact relationship alone never grants phone visibility. A viewer may retain // an owner-scoped phone it explicitly supplied, while the target account phone is // governed by PhoneNumber privacy. func ForViewer(ctx context.Context, contacts store.ContactStore, viewerUserID int64, users []domain.User) ([]domain.User, error) { users = sanitizeDeletedUsers(users) if contacts == nil || viewerUserID == 0 || len(users) == 0 { return users, nil } out := cloneUsers(users) cache := make(map[int64]domain.User, len(users)) for i := range out { u := out[i] if u.ID == 0 || u.Deleted || u.ID == viewerUserID || u.ID == domain.OfficialSystemUserID || u.Bot { 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 projectBatch(ctx context.Context, contacts store.ContactStore, photos ProfilePhotoProvider, privacy PrivacyEvaluator, freezesProvider AccountFreezeProvider, phoneProvider CollectiblePhoneProvider, viewerUserID int64, users []domain.User) ([]domain.User, error) { if len(users) == 0 { return users, nil } out := cloneUsers(users) out = sanitizeDeletedUsers(out) ids := uniqueUserIDs(out) var ( profileRefs = map[int64]domain.ProfilePhotoRef{} fallbackRefs = map[int64]domain.ProfilePhotoRef{} personalRefs = map[int64]domain.ProfilePhotoRef{} contactsByID map[int64]domain.Contact visibility map[int64]map[domain.PrivacyKey]bool freezes map[int64]domain.AccountFreeze collectiblePhones map[int64]domain.CollectiblePhone ) // These prefetch queries are independent (photo profile/fallback, contact // GetMany/PersonalPhotos, privacy visibility); running them concurrently collapses ~6 // serial round-trips into one wave. Each goroutine writes only its own variable and the // assembly loop runs serially after Wait (pure memory, no queries), so there is no data race. g, gctx := errgroup.WithContext(ctx) if photos != nil && len(ids) > 0 { if kindPhotos, ok := photos.(ProfilePhotoKindProvider); ok { g.Go(func() error { refs, err := kindPhotos.CurrentProfilePhotosKind(gctx, domain.PeerTypeUser, ids, domain.ProfilePhotoKindProfile) if err != nil { return err } profileRefs = refs return nil }) g.Go(func() error { refs, err := kindPhotos.CurrentProfilePhotosKind(gctx, domain.PeerTypeUser, ids, domain.ProfilePhotoKindFallback) if err != nil { return err } fallbackRefs = refs return nil }) } else { g.Go(func() error { refs, err := photos.CurrentProfilePhotos(gctx, domain.PeerTypeUser, ids) if err != nil { return err } profileRefs = refs return nil }) } } if contacts != nil && viewerUserID != 0 && len(ids) > 0 { g.Go(func() error { m, err := contacts.GetMany(gctx, viewerUserID, ids) if err != nil { return err } contactsByID = m return nil }) g.Go(func() error { refs, err := contacts.PersonalPhotos(gctx, viewerUserID, ids) if err != nil { return err } personalRefs = refs return nil }) } // Batch-prefetch privacy visibility (if the evaluator supports it): reduces the per-user // 3×CanSee×2-row N+1 to one ListPrivacyRules + one GetReverseContacts + in-memory // Evaluate; when nil, applyPrivacy falls back to per-CanSee. g.Go(func() error { v, err := prefetchPrivacyVisibility(gctx, privacy, viewerUserID, out) if err != nil { return err } visibility = v return nil }) if freezesProvider != nil && len(ids) > 0 { g.Go(func() error { m, err := freezesProvider.AccountFreezes(gctx, ids) if err != nil { return err } freezes = m return nil }) } if phoneProvider != nil && len(ids) > 0 { g.Go(func() error { var err error collectiblePhones, err = phoneProvider.OwnedCollectiblePhones(gctx, ids) return err }) } if err := g.Wait(); err != nil { return nil, err } cache := make(map[int64]domain.User, len(out)) for i := range out { u := out[i] if u.ID == 0 { continue } if u.Deleted { out[i] = u.DeletedTombstone() continue } if projected, ok := cache[u.ID]; ok { out[i] = projected continue } projected := applyBasePhotos(u, profileRefs, fallbackRefs, personalRefs, viewerUserID) // Bots and the system account are exempt from contact/privacy projection: official // bots have no phone/last seen, and do not take part in contact rename or privacy trimming. if viewerUserID != 0 && u.ID != viewerUserID && u.ID != domain.OfficialSystemUserID && !u.Bot { contact, found := contactsByID[u.ID] projected = applyContactProjection(projected, contact, found) projected = applyCollectiblePhone(projected, collectiblePhones[u.ID]) hasKnownContactPhone := found && contact.Phone != "" var err error projected, err = applyPrivacy(ctx, privacy, viewerUserID, projected, hasKnownContactPhone, visibility[u.ID], profileRefs, fallbackRefs, personalRefs) if err != nil { return nil, err } } if viewerUserID == 0 || u.ID == viewerUserID || u.ID == domain.OfficialSystemUserID || u.Bot { projected = applyCollectiblePhone(projected, collectiblePhones[u.ID]) } projected = reapplyExclusiveCollectiblePhone(projected, collectiblePhones[u.ID]) projected = applyAccountFreezeProjection(projected, viewerUserID, freezes[u.ID]) cache[u.ID] = projected out[i] = projected } return out, nil } func applyCollectiblePhone(user domain.User, phone domain.CollectiblePhone) domain.User { if !user.Deleted && !user.Bot && phone.Owned() && phone.OwnerUserID == user.ID { user.Phone = phone.Phone } return user } func reapplyExclusiveCollectiblePhone(user domain.User, phone domain.CollectiblePhone) domain.User { if phone.AlwaysVisible() && !user.Deleted && !user.Bot && phone.OwnerUserID == user.ID { user.Phone = phone.Phone } return user } func applyAccountFreezeProjection(user domain.User, viewerUserID int64, freeze domain.AccountFreeze) domain.User { // Frozen accounts are presented to peers as deleted accounts: the client // renders "Deleted account" instead of the frozen persona. The owner and // unauthenticated/system views keep the live account. The tombstone carries // FrozenForViewer so the RPC boundary can attach the synthetic "frozen" // third-party mark alongside the deleted presentation. if freeze.Frozen && viewerUserID != 0 && user.ID != 0 && user.ID != viewerUserID && user.ID != domain.OfficialSystemUserID { user.Deleted = true tombstone := user.DeletedTombstone() tombstone.FrozenForViewer = true return tombstone } if freeze.Frozen { user.Status = domain.UserStatus{Kind: domain.UserStatusEmpty} user.LastSeenAt = 0 } user.RestrictionReasons = nil return user } func prefetchPrivacyVisibility(ctx context.Context, privacy PrivacyEvaluator, viewerUserID int64, users []domain.User) (map[int64]map[domain.PrivacyKey]bool, error) { if privacy == nil || viewerUserID == 0 { return nil, nil } batch, ok := privacy.(BatchPrivacyEvaluator) if !ok { return nil, nil } ids := make([]int64, 0, len(users)) seen := make(map[int64]struct{}, len(users)) for _, u := range users { if u.ID == 0 || u.Deleted || u.ID == viewerUserID || u.ID == domain.OfficialSystemUserID || u.Bot { continue } if _, ok := seen[u.ID]; ok { continue } seen[u.ID] = struct{}{} ids = append(ids, u.ID) } if len(ids) == 0 { return nil, nil } return batch.CanSeeBatch(ctx, ids, viewerUserID, privacyProjectionKeys) } func projectOne(ctx context.Context, contacts store.ContactStore, viewerUserID int64, user domain.User) (domain.User, error) { if user.Deleted { return user.DeletedTombstone(), nil } contact, found, err := contacts.Get(ctx, viewerUserID, user.ID) if err != nil { return domain.User{}, err } return applyContactProjection(user, contact, found), nil } func uniqueUserIDs(users []domain.User) []int64 { seen := make(map[int64]struct{}, len(users)) ids := make([]int64, 0, len(users)) for _, user := range users { if user.ID == 0 || user.Deleted { continue } if _, ok := seen[user.ID]; ok { continue } seen[user.ID] = struct{}{} ids = append(ids, user.ID) } return ids } func applyBasePhotos(user domain.User, profileRefs, fallbackRefs, personalRefs map[int64]domain.ProfilePhotoRef, viewerUserID int64) domain.User { if user.Deleted { return user.DeletedTombstone() } if !hasPhotoLookups(profileRefs, fallbackRefs, personalRefs) { return user } clearPhoto(&user) if viewerUserID != 0 && user.ID != viewerUserID { if ref, ok := personalRefs[user.ID]; ok && ref.PhotoID != 0 { ref.Personal = true applyPhotoRef(&user, ref) return user } } if ref, ok := profileRefs[user.ID]; ok && ref.PhotoID != 0 { applyPhotoRef(&user, ref) return user } if ref, ok := fallbackRefs[user.ID]; ok && ref.PhotoID != 0 { applyPhotoRef(&user, ref) } return user } func applyContactProjection(user domain.User, contact domain.Contact, found bool) domain.User { if user.Deleted { return user.DeletedTombstone() } if !found { // A contact relationship alone never grants phone visibility; the target // account phone is governed by PhoneNumber privacy (applyPrivacy decides // whether to strip it — never wipe here so AllowAll still surfaces it). user.Contact = false user.Mutual = false user.CloseFriend = false user.ContactNote = "" user.ContactNoteEntities = nil return user } user.Contact = true user.Mutual = contact.Mutual || contact.User.Mutual user.CloseFriend = contact.CloseFriend || contact.User.CloseFriend user.ContactNote = contact.Note user.ContactNoteEntities = append([]domain.MessageEntity(nil), contact.NoteEntities...) // contact.Phone is an owner-local fact supplied by this viewer. It may differ // from the target's current account phone and is safe to preserve because the // viewer already knew it. An empty contact.Phone must not replace or authorize // the target account phone carried by user.Phone. if contact.Phone != "" { user.Phone = contact.Phone } if contact.FirstName != "" || contact.LastName != "" { // FirstName uses NULLIF at the durable read boundary while LastName is an // explicit owner-local value. Preserve the base first name when only a // local last name exists; setting a local first name with an empty last // name intentionally clears the base last name. if contact.FirstName != "" { user.FirstName = contact.FirstName user.LastName = contact.LastName } else { user.LastName = contact.LastName } } else if contact.User.FirstName != "" || contact.User.LastName != "" { user.FirstName = contact.User.FirstName user.LastName = contact.User.LastName } return user } func applyPrivacy(ctx context.Context, privacy PrivacyEvaluator, viewerUserID int64, user domain.User, hasKnownContactPhone bool, vis map[domain.PrivacyKey]bool, profileRefs, fallbackRefs, personalRefs map[int64]domain.ProfilePhotoRef) (domain.User, error) { if user.Deleted { return user.DeletedTombstone(), nil } if privacy == nil { // Missing privacy wiring must fail closed for an account phone. The only // safe exception is an owner-scoped phone the viewer explicitly supplied. if !hasKnownContactPhone { user.Phone = "" } return user, nil } // vis is the batch-prefetch result (computed by projectBatch via one ListPrivacyRules + // GetReverseContacts); when nil it falls back to per-CanSee, and the two are equivalent. canSee := func(key domain.PrivacyKey) (bool, error) { if vis != nil { return vis[key], nil } return privacy.CanSee(ctx, user.ID, viewerUserID, key) } phoneAllowed, err := canSee(domain.PrivacyKeyPhoneNumber) if err != nil { return domain.User{}, err } if !phoneAllowed && !hasKnownContactPhone { user.Phone = "" } statusAllowed, err := canSee(domain.PrivacyKeyStatusTimestamp) if err != nil { return domain.User{}, err } if !statusAllowed { user.Status = domain.ApproximateUserStatus(user.LastSeenAt, int(time.Now().Unix())) user.LastSeenAt = 0 } if ref, ok := personalRefs[user.ID]; ok && ref.PhotoID != 0 { ref.Personal = true applyPhotoRef(&user, ref) return user, nil } if !hasPhotoLookups(profileRefs, fallbackRefs, personalRefs) && user.PhotoID == 0 { return user, nil } profileAllowed, err := canSee(domain.PrivacyKeyProfilePhoto) if err != nil { return domain.User{}, err } if profileAllowed { if ref, ok := profileRefs[user.ID]; ok && ref.PhotoID != 0 { applyPhotoRef(&user, ref) return user, nil } } if ref, ok := fallbackRefs[user.ID]; ok && ref.PhotoID != 0 { applyPhotoRef(&user, ref) return user, nil } clearPhoto(&user) return user, nil } func hasPhotoLookups(profileRefs, fallbackRefs, personalRefs map[int64]domain.ProfilePhotoRef) bool { return len(profileRefs) != 0 || len(fallbackRefs) != 0 || len(personalRefs) != 0 } func applyPhotoRef(user *domain.User, ref domain.ProfilePhotoRef) { user.PhotoID = ref.PhotoID user.PhotoDCID = ref.DCID user.PhotoStripped = append([]byte(nil), ref.Stripped...) user.PhotoPersonal = ref.Personal user.PhotoHasVideo = ref.HasVideo } func clearPhoto(user *domain.User) { user.PhotoID = 0 user.PhotoDCID = 0 user.PhotoStripped = nil user.PhotoPersonal = false user.PhotoHasVideo = false } func sanitizeDeletedUsers(users []domain.User) []domain.User { var out []domain.User for i, user := range users { if !user.Deleted { continue } if out == nil { out = make([]domain.User, len(users)) copy(out, users) } out[i] = user.DeletedTombstone() } if out != nil { return out } return users }