protocol: expose privacy and profile photo RPCs

(cherry picked from commit 77b033c8bf8c0a76ff0d7065e2192cbe55d3a3b6)
This commit is contained in:
A 2026-06-08 01:07:38 +08:00
parent 8ff3343ae0
commit 75a8861ec9
9 changed files with 476 additions and 26 deletions

View file

@ -35,9 +35,8 @@ func (r *Router) registerAccount(d *tg.ServerDispatcher) {
d.OnAccountUpdateNotifySettings(func(ctx context.Context, req *tg.AccountUpdateNotifySettingsRequest) (bool, error) {
return true, nil
})
d.OnAccountGetPrivacy(func(ctx context.Context, key tg.InputPrivacyKeyClass) (*tg.AccountPrivacyRules, error) {
return tdesktop.PrivacyRules(key), nil
})
d.OnAccountGetPrivacy(r.onAccountGetPrivacy)
d.OnAccountSetPrivacy(r.onAccountSetPrivacy)
d.OnAccountGetAuthorizations(func(ctx context.Context) (*tg.AccountAuthorizations, error) {
return tdesktop.Authorizations(), nil
})
@ -80,6 +79,60 @@ func (r *Router) registerAccount(d *tg.ServerDispatcher) {
d.OnAccountUpdateStatus(r.onAccountUpdateStatus)
}
func (r *Router) onAccountGetPrivacy(ctx context.Context, key tg.InputPrivacyKeyClass) (*tg.AccountPrivacyRules, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
domainKey, ok := domainPrivacyKeyFromInput(key)
if !ok {
return nil, privacyKeyInvalidErr()
}
if r.deps.Privacy == nil {
return tdesktop.PrivacyRules(key), nil
}
rules, err := r.deps.Privacy.GetRules(ctx, userID, domainKey)
if err != nil {
return nil, privacyErr(err)
}
return r.tgAccountPrivacyRules(ctx, userID, rules)
}
func (r *Router) onAccountSetPrivacy(ctx context.Context, req *tg.AccountSetPrivacyRequest) (*tg.AccountPrivacyRules, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
domainKey, ok := domainPrivacyKeyFromInput(req.Key)
if !ok {
return nil, privacyKeyInvalidErr()
}
rules, err := r.domainPrivacyRulesFromInput(ctx, userID, req.Rules)
if err != nil {
return nil, err
}
if r.deps.Privacy == nil {
return &tg.AccountPrivacyRules{Rules: tgPrivacyRules(rules), Users: []tg.UserClass{}, Chats: []tg.ChatClass{}}, nil
}
saved, err := r.deps.Privacy.SetRules(ctx, userID, domainKey, rules)
if err != nil {
return nil, privacyErr(err)
}
out, err := r.tgAccountPrivacyRules(ctx, userID, saved)
if err != nil {
return nil, err
}
r.pushUserUpdates(ctx, userID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdatePrivacy{
Key: tgPrivacyKey(saved.Key),
Rules: tgPrivacyRules(saved.Rules),
}},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
})
return out, nil
}
func (r *Router) onAccountGetAccountTTL(ctx context.Context) (*tg.AccountDaysTTL, error) {
if _, _, err := r.currentUserID(ctx); err != nil {
return nil, internalErr()
@ -100,6 +153,218 @@ func (r *Router) onAccountUpdateStatus(ctx context.Context, offline bool) (bool,
return true, nil
}
func (r *Router) tgAccountPrivacyRules(ctx context.Context, viewerUserID int64, rules domain.PrivacyRules) (*tg.AccountPrivacyRules, error) {
userIDs := privacyRuleUserIDs(rules.Rules)
users := []domain.User{}
if r.deps.Users != nil && len(userIDs) > 0 {
var err error
users, err = r.deps.Users.ByIDs(ctx, viewerUserID, userIDs)
if err != nil {
return nil, internalErr()
}
}
return &tg.AccountPrivacyRules{
Rules: tgPrivacyRules(rules.Rules),
Users: tgUsers(users),
Chats: []tg.ChatClass{},
}, nil
}
func (r *Router) domainPrivacyRulesFromInput(ctx context.Context, userID int64, in []tg.InputPrivacyRuleClass) ([]domain.PrivacyRule, error) {
out := make([]domain.PrivacyRule, 0, len(in))
for _, rule := range in {
switch v := rule.(type) {
case *tg.InputPrivacyValueAllowContacts:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowContacts})
case *tg.InputPrivacyValueAllowAll:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowAll})
case *tg.InputPrivacyValueAllowUsers:
ids, err := r.privacyUserIDsFromInput(ctx, userID, v.Users)
if err != nil {
return nil, err
}
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowUsers, UserIDs: ids})
case *tg.InputPrivacyValueDisallowContacts:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleDisallowContacts})
case *tg.InputPrivacyValueDisallowAll:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleDisallowAll})
case *tg.InputPrivacyValueDisallowUsers:
ids, err := r.privacyUserIDsFromInput(ctx, userID, v.Users)
if err != nil {
return nil, err
}
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleDisallowUsers, UserIDs: ids})
case *tg.InputPrivacyValueAllowChatParticipants:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowChatParticipants, ChatIDs: append([]int64(nil), v.Chats...)})
case *tg.InputPrivacyValueDisallowChatParticipants:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleDisallowChatParticipants, ChatIDs: append([]int64(nil), v.Chats...)})
case *tg.InputPrivacyValueAllowCloseFriends:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowCloseFriends})
case *tg.InputPrivacyValueAllowPremium:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowPremium})
case *tg.InputPrivacyValueAllowBots:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleAllowBots})
case *tg.InputPrivacyValueDisallowBots:
out = append(out, domain.PrivacyRule{Kind: domain.PrivacyRuleDisallowBots})
default:
return nil, privacyValueInvalidErr()
}
}
return out, nil
}
func (r *Router) privacyUserIDsFromInput(ctx context.Context, currentUserID int64, inputs []tg.InputUserClass) ([]int64, error) {
out := make([]int64, 0, len(inputs))
seen := make(map[int64]struct{}, len(inputs))
for _, input := range inputs {
u, found, err := r.userFromInput(ctx, currentUserID, input)
if err != nil {
return nil, internalErr()
}
if !found || u.ID == 0 {
return nil, userIDInvalidErr()
}
if _, ok := seen[u.ID]; ok {
continue
}
seen[u.ID] = struct{}{}
out = append(out, u.ID)
}
return out, nil
}
func domainPrivacyKeyFromInput(key tg.InputPrivacyKeyClass) (domain.PrivacyKey, bool) {
switch key.(type) {
case *tg.InputPrivacyKeyStatusTimestamp:
return domain.PrivacyKeyStatusTimestamp, true
case *tg.InputPrivacyKeyChatInvite:
return domain.PrivacyKeyChatInvite, true
case *tg.InputPrivacyKeyPhoneCall:
return domain.PrivacyKeyPhoneCall, true
case *tg.InputPrivacyKeyPhoneP2P:
return domain.PrivacyKeyPhoneP2P, true
case *tg.InputPrivacyKeyForwards:
return domain.PrivacyKeyForwards, true
case *tg.InputPrivacyKeyProfilePhoto:
return domain.PrivacyKeyProfilePhoto, true
case *tg.InputPrivacyKeyPhoneNumber:
return domain.PrivacyKeyPhoneNumber, true
case *tg.InputPrivacyKeyAddedByPhone:
return domain.PrivacyKeyAddedByPhone, true
case *tg.InputPrivacyKeyVoiceMessages:
return domain.PrivacyKeyVoiceMessages, true
case *tg.InputPrivacyKeyAbout:
return domain.PrivacyKeyAbout, true
case *tg.InputPrivacyKeyBirthday:
return domain.PrivacyKeyBirthday, true
case *tg.InputPrivacyKeyStarGiftsAutoSave:
return domain.PrivacyKeyStarGiftsAutoSave, true
case *tg.InputPrivacyKeyNoPaidMessages:
return domain.PrivacyKeyNoPaidMessages, true
case *tg.InputPrivacyKeySavedMusic:
return domain.PrivacyKeySavedMusic, true
default:
return "", false
}
}
func tgPrivacyKey(key domain.PrivacyKey) tg.PrivacyKeyClass {
switch key {
case domain.PrivacyKeyStatusTimestamp:
return &tg.PrivacyKeyStatusTimestamp{}
case domain.PrivacyKeyChatInvite:
return &tg.PrivacyKeyChatInvite{}
case domain.PrivacyKeyPhoneCall:
return &tg.PrivacyKeyPhoneCall{}
case domain.PrivacyKeyPhoneP2P:
return &tg.PrivacyKeyPhoneP2P{}
case domain.PrivacyKeyForwards:
return &tg.PrivacyKeyForwards{}
case domain.PrivacyKeyProfilePhoto:
return &tg.PrivacyKeyProfilePhoto{}
case domain.PrivacyKeyPhoneNumber:
return &tg.PrivacyKeyPhoneNumber{}
case domain.PrivacyKeyAddedByPhone:
return &tg.PrivacyKeyAddedByPhone{}
case domain.PrivacyKeyVoiceMessages:
return &tg.PrivacyKeyVoiceMessages{}
case domain.PrivacyKeyAbout:
return &tg.PrivacyKeyAbout{}
case domain.PrivacyKeyBirthday:
return &tg.PrivacyKeyBirthday{}
case domain.PrivacyKeyStarGiftsAutoSave:
return &tg.PrivacyKeyStarGiftsAutoSave{}
case domain.PrivacyKeyNoPaidMessages:
return &tg.PrivacyKeyNoPaidMessages{}
case domain.PrivacyKeySavedMusic:
return &tg.PrivacyKeySavedMusic{}
default:
return &tg.PrivacyKeyStatusTimestamp{}
}
}
func tgPrivacyRules(rules []domain.PrivacyRule) []tg.PrivacyRuleClass {
out := make([]tg.PrivacyRuleClass, 0, len(rules))
for _, rule := range rules {
switch rule.Kind {
case domain.PrivacyRuleAllowContacts:
out = append(out, &tg.PrivacyValueAllowContacts{})
case domain.PrivacyRuleAllowAll:
out = append(out, &tg.PrivacyValueAllowAll{})
case domain.PrivacyRuleAllowUsers:
out = append(out, &tg.PrivacyValueAllowUsers{Users: append([]int64(nil), rule.UserIDs...)})
case domain.PrivacyRuleDisallowContacts:
out = append(out, &tg.PrivacyValueDisallowContacts{})
case domain.PrivacyRuleDisallowAll:
out = append(out, &tg.PrivacyValueDisallowAll{})
case domain.PrivacyRuleDisallowUsers:
out = append(out, &tg.PrivacyValueDisallowUsers{Users: append([]int64(nil), rule.UserIDs...)})
case domain.PrivacyRuleAllowChatParticipants:
out = append(out, &tg.PrivacyValueAllowChatParticipants{Chats: append([]int64(nil), rule.ChatIDs...)})
case domain.PrivacyRuleDisallowChatParticipants:
out = append(out, &tg.PrivacyValueDisallowChatParticipants{Chats: append([]int64(nil), rule.ChatIDs...)})
case domain.PrivacyRuleAllowCloseFriends:
out = append(out, &tg.PrivacyValueAllowCloseFriends{})
case domain.PrivacyRuleAllowPremium:
out = append(out, &tg.PrivacyValueAllowPremium{})
case domain.PrivacyRuleAllowBots:
out = append(out, &tg.PrivacyValueAllowBots{})
case domain.PrivacyRuleDisallowBots:
out = append(out, &tg.PrivacyValueDisallowBots{})
}
}
return out
}
func privacyRuleUserIDs(rules []domain.PrivacyRule) []int64 {
seen := map[int64]struct{}{}
out := make([]int64, 0)
for _, rule := range rules {
for _, id := range rule.UserIDs {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
out = append(out, id)
}
}
return out
}
func privacyErr(err error) error {
switch {
case errors.Is(err, domain.ErrPrivacyKeyInvalid):
return privacyKeyInvalidErr()
case errors.Is(err, domain.ErrPrivacyRuleInvalid):
return privacyValueInvalidErr()
default:
return internalErr()
}
}
type accountReactionSettingsService interface {
GetReactionSettings(ctx context.Context, userID int64) (domain.AccountReactionSettings, error)
SetReactionsNotifySettings(ctx context.Context, userID int64, settings domain.ReactionsNotifySettings) (domain.AccountReactionSettings, error)

View file

@ -112,7 +112,7 @@ func tgUserProfilePhoto(u domain.User) tg.UserProfilePhotoClass {
if u.PhotoID == 0 {
return nil
}
photo := &tg.UserProfilePhoto{PhotoID: u.PhotoID, DCID: u.PhotoDCID}
photo := &tg.UserProfilePhoto{PhotoID: u.PhotoID, DCID: u.PhotoDCID, Personal: u.PhotoPersonal}
if len(u.PhotoStripped) > 0 {
photo.SetStrippedThumb(u.PhotoStripped)
}

View file

@ -29,3 +29,35 @@ func TestTGMessagesMessagesMarksViewerSelfAndKeepsProjectedPhone(t *testing.T) {
t.Fatalf("peer user = %+v ok=%v, want projected non-self without phone", full.Users[1], ok)
}
}
func TestTGMessagesDialogsIncludesUserProfilePhoto(t *testing.T) {
const viewerID int64 = 1001
const peerID int64 = 1002
res := tgMessagesDialogs(viewerID, domain.DialogList{
Dialogs: []domain.Dialog{{
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: peerID},
TopMessage: 1,
TopMessageDate: 10,
}},
Users: []domain.User{{
ID: peerID,
AccessHash: 22,
FirstName: "Alice A",
PhotoID: 9301,
PhotoDCID: 2,
PhotoStripped: []byte{9, 10},
}},
})
full, ok := res.(*tg.MessagesDialogs)
if !ok {
t.Fatalf("result = %T, want *tg.MessagesDialogs", res)
}
peer, ok := full.Users[0].(*tg.User)
if !ok {
t.Fatalf("user = %T, want *tg.User", full.Users[0])
}
photo, ok := peer.Photo.(*tg.UserProfilePhoto)
if !ok || photo.PhotoID != 9301 || photo.DCID != 2 || string(photo.StrippedThumb) != string([]byte{9, 10}) {
t.Fatalf("photo = %+v ok=%v, want userProfilePhoto 9301/2/[9 10]", peer.Photo, ok)
}
}

View file

@ -96,6 +96,10 @@ func pollAnswerInvalidErr() error { return tgerr.New(400, "POLL_ANSWER_INVALID")
func reactionInvalidErr() error { return tgerr.New(400, "REACTION_INVALID") }
func privacyKeyInvalidErr() error { return tgerr.New(400, "PRIVACY_KEY_INVALID") }
func privacyValueInvalidErr() error { return tgerr.New(400, "PRIVACY_VALUE_INVALID") }
func todoItemsEmptyErr() error { return tgerr.New(400, "TODO_ITEMS_EMPTY") }
func todoNotModifiedErr() error { return tgerr.New(400, "TODO_NOT_MODIFIED") }

View file

@ -13,6 +13,7 @@ import (
func (r *Router) registerPhotos(d *tg.ServerDispatcher) {
d.OnPhotosUploadProfilePhoto(r.onPhotosUploadProfilePhoto)
d.OnPhotosUpdateProfilePhoto(r.onPhotosUpdateProfilePhoto)
d.OnPhotosUploadContactProfilePhoto(r.onPhotosUploadContactProfilePhoto)
d.OnPhotosGetUserPhotos(r.onPhotosGetUserPhotos)
d.OnPhotosDeletePhotos(r.onPhotosDeletePhotos)
}
@ -28,16 +29,23 @@ func (r *Router) onPhotosUploadProfilePhoto(ctx context.Context, req *tg.PhotosU
if !ok || userID == 0 {
return nil, photoInvalidErr()
}
if bot, hasBot := req.GetBot(); hasBot && bot != nil {
return nil, inputConstructorInvalidErr()
}
file, hasFile := req.GetFile()
if !hasFile {
// 仅 fallback / video / emoji markup 等本阶段不支持的头像变体。
// video / emoji markup 等头像变体本阶段不支持。
return nil, photoInvalidErr()
}
ref, ok := uploadedFileRef(userID, file)
if !ok {
return nil, fileReferenceInvalidErr()
}
photo, err := r.deps.Files.UploadProfilePhoto(ctx, domain.PeerTypeUser, userID, ref, int(r.clock.Now().Unix()))
kind := domain.ProfilePhotoKindProfile
if req.GetFallback() {
kind = domain.ProfilePhotoKindFallback
}
photo, err := r.deps.Files.UploadProfilePhotoKind(ctx, domain.PeerTypeUser, userID, kind, ref, int(r.clock.Now().Unix()))
if err != nil {
return nil, photoUploadErr(err)
}
@ -55,9 +63,16 @@ func (r *Router) onPhotosUpdateProfilePhoto(ctx context.Context, req *tg.PhotosU
if !ok || userID == 0 {
return nil, photoInvalidErr()
}
if bot, hasBot := req.GetBot(); hasBot && bot != nil {
return nil, inputConstructorInvalidErr()
}
kind := domain.ProfilePhotoKindProfile
if req.GetFallback() {
kind = domain.ProfilePhotoKindFallback
}
switch in := req.ID.(type) {
case *tg.InputPhoto:
photo, found, err := r.deps.Files.SetCurrentProfilePhoto(ctx, domain.PeerTypeUser, userID, in.ID, int(r.clock.Now().Unix()))
photo, found, err := r.deps.Files.SetCurrentProfilePhotoKind(ctx, domain.PeerTypeUser, userID, kind, in.ID, int(r.clock.Now().Unix()))
if err != nil {
return nil, internalErr()
}
@ -67,13 +82,60 @@ func (r *Router) onPhotosUpdateProfilePhoto(ctx context.Context, req *tg.PhotosU
return r.photosPhotoForSelf(ctx, userID, photo), nil
default:
// InputPhotoEmpty移除当前头像停用现有当前照片
if cur, found, err := r.deps.Files.CurrentProfilePhoto(ctx, domain.PeerTypeUser, userID); err == nil && found {
_, _ = r.deps.Files.DeleteProfilePhotos(ctx, domain.PeerTypeUser, userID, []int64{cur.ID})
if cur, found, err := r.deps.Files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, userID, kind); err == nil && found {
_, _ = r.deps.Files.DeleteProfilePhotosKind(ctx, domain.PeerTypeUser, userID, kind, []int64{cur.ID})
}
return r.photosPhotoForSelf(ctx, userID, domain.Photo{}), nil
}
}
func (r *Router) onPhotosUploadContactProfilePhoto(ctx context.Context, req *tg.PhotosUploadContactProfilePhotoRequest) (*tg.PhotosPhoto, error) {
if r.deps.Files == nil || r.deps.Contacts == nil {
return nil, notImplementedErr()
}
userID, ok, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if !ok || userID == 0 {
return nil, photoInvalidErr()
}
target, found, err := r.userFromInput(ctx, userID, req.UserID)
if err != nil {
return nil, internalErr()
}
if !found || target.ID == 0 || target.ID == userID {
return nil, userIDInvalidErr()
}
file, hasFile := req.GetFile()
if !hasFile {
if req.GetSave() {
if _, err := r.deps.Contacts.ClearPersonalPhoto(ctx, userID, target.ID, int(r.clock.Now().Unix())); err != nil {
return nil, contactErr(err)
}
return r.photosPhotoForUser(ctx, userID, target.ID, domain.Photo{}), nil
}
return nil, photoInvalidErr()
}
ref, ok := uploadedFileRef(userID, file)
if !ok {
return nil, fileReferenceInvalidErr()
}
photo, err := r.deps.Files.CreateAvatarFromUpload(ctx, ref)
if err != nil {
return nil, photoUploadErr(err)
}
if req.GetSuggest() {
// TODO: add private MessageActionSuggestProfilePhoto once private service-message
// actions exist in the domain message model.
return r.photosPhotoForUser(ctx, userID, target.ID, photo), nil
}
if _, err := r.deps.Contacts.SetPersonalPhoto(ctx, userID, target.ID, photo, int(r.clock.Now().Unix())); err != nil {
return nil, contactErr(err)
}
return r.photosPhotoForUser(ctx, userID, target.ID, photo), nil
}
func (r *Router) onPhotosGetUserPhotos(ctx context.Context, req *tg.PhotosGetUserPhotosRequest) (tg.PhotosPhotosClass, error) {
if r.deps.Files == nil {
return &tg.PhotosPhotos{}, nil
@ -158,6 +220,19 @@ func (r *Router) photosPhotoForSelf(ctx context.Context, userID int64, photo dom
return out
}
func (r *Router) photosPhotoForUser(ctx context.Context, viewerUserID, targetUserID int64, photo domain.Photo) *tg.PhotosPhoto {
out := &tg.PhotosPhoto{Photo: tgPhoto(photo), Users: []tg.UserClass{}}
if r.deps.Users == nil {
return out
}
user, found, err := r.deps.Users.ByID(ctx, viewerUserID, targetUserID)
if err != nil || !found {
return out
}
out.Users = append(out.Users, r.tgUser(user))
return out
}
// pushSelfPhotoUpdate 向该账号其它在线设备推送头像变更。updateUserName 不含 photo 无法刷新
// 头像updateUser 只是「该 user 变了」的信号TDesktop 仅当 peer 已 full-loaded 时才
// forceFull 重拉);最可靠是在 Updates.Users 带上含新 userProfilePhoto 的完整 self user

View file

@ -87,12 +87,25 @@ func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) (
if _, ok := id.(*tg.InputUserSelf); ok {
user = r.tgSelfUser(u)
}
about := u.About
if r.deps.Privacy != nil && u.ID != currentUserID {
allowed, err := r.deps.Privacy.CanSee(ctx, u.ID, currentUserID, domain.PrivacyKeyAbout)
if err != nil {
return nil, internalErr()
}
if !allowed {
about = ""
}
}
full := tg.UserFull{
ID: u.ID,
About: u.About,
About: about,
Settings: tg.PeerSettings{},
NotifySettings: *tdesktop.NotifySettings(),
}
if err := r.fillUserFullPhotos(ctx, currentUserID, u.ID, &full); err != nil {
return nil, err
}
if r.deps.Channels != nil && u.ID != currentUserID {
common, err := r.deps.Channels.CommonChannels(ctx, currentUserID, domain.CommonChannelsRequest{
UserID: currentUserID,
@ -137,6 +150,62 @@ func (r *Router) onUsersGetSavedMusicByID(ctx context.Context, req *tg.UsersGetS
}, nil
}
func (r *Router) fillUserFullPhotos(ctx context.Context, viewerUserID, ownerUserID int64, full *tg.UserFull) error {
if r.deps.Files == nil || full == nil || ownerUserID == 0 {
return nil
}
if viewerUserID == ownerUserID {
if photo, found, err := r.deps.Files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, ownerUserID, domain.ProfilePhotoKindProfile); err != nil {
return internalErr()
} else if found {
full.SetProfilePhoto(tgPhoto(photo))
}
if photo, found, err := r.deps.Files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, ownerUserID, domain.ProfilePhotoKindFallback); err != nil {
return internalErr()
} else if found {
full.SetFallbackPhoto(tgPhoto(photo))
}
return nil
}
if r.deps.Contacts != nil {
refs, err := r.deps.Contacts.PersonalPhotos(ctx, viewerUserID, []int64{ownerUserID})
if err != nil {
return internalErr()
}
if ref, ok := refs[ownerUserID]; ok && ref.PhotoID != 0 {
photo, found, err := r.deps.Files.GetPhoto(ctx, ref.PhotoID)
if err != nil {
return internalErr()
}
if found {
full.SetPersonalPhoto(tgPhoto(photo))
}
}
}
profileAllowed := true
if r.deps.Privacy != nil {
var err error
profileAllowed, err = r.deps.Privacy.CanSee(ctx, ownerUserID, viewerUserID, domain.PrivacyKeyProfilePhoto)
if err != nil {
return internalErr()
}
}
if profileAllowed {
if photo, found, err := r.deps.Files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, ownerUserID, domain.ProfilePhotoKindProfile); err != nil {
return internalErr()
} else if found {
full.SetProfilePhoto(tgPhoto(photo))
}
return nil
}
if photo, found, err := r.deps.Files.CurrentProfilePhotoKind(ctx, domain.PeerTypeUser, ownerUserID, domain.ProfilePhotoKindFallback); err != nil {
return internalErr()
} else if found {
full.SetFallbackPhoto(tgPhoto(photo))
}
return nil
}
func emptyUserFull() *tg.UsersUserFull {
return &tg.UsersUserFull{
FullUser: tg.UserFull{