owpengram-server/internal/rpc/users.go
A 75a8861ec9 protocol: expose privacy and profile photo RPCs
(cherry picked from commit 77b033c8bf8c0a76ff0d7065e2192cbe55d3a3b6)
2026-06-14 00:20:04 +08:00

256 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"telesrv/internal/app/users"
"telesrv/internal/compat/tdesktop"
"telesrv/internal/domain"
)
const maxSavedMusicLimit = 100
// registerUsers 注册 users.* RPC handler。
func (r *Router) registerUsers(d *tg.ServerDispatcher) {
d.OnUsersGetUsers(r.onUsersGetUsers)
d.OnUsersGetFullUser(r.onUsersGetFullUser)
d.OnUsersGetSavedMusic(r.onUsersGetSavedMusic)
d.OnUsersGetSavedMusicByID(r.onUsersGetSavedMusicByID)
}
// onUsersGetUsers 处理 users.getUsers支持 self 和已知 user peer含 777000 官方账号)。
func (r *Router) onUsersGetUsers(ctx context.Context, ids []tg.InputUserClass) ([]tg.UserClass, error) {
currentUserID, authorized, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
out := make([]tg.UserClass, 0, len(ids))
for _, in := range ids {
if r.deps.Users == nil {
continue
}
switch v := in.(type) {
case *tg.InputUserSelf:
if !authorized {
continue
}
u, err := r.deps.Users.Self(ctx, currentUserID)
if err != nil {
if errors.Is(err, users.ErrNotAuthorized) {
continue // 未登录getUsers 尽力而为,跳过 self
}
return nil, internalErr()
}
out = append(out, r.tgSelfUser(u))
case *tg.InputUser:
if !authorized {
continue
}
u, found, err := r.deps.Users.ByID(ctx, currentUserID, v.UserID)
if err != nil {
if errors.Is(err, users.ErrNotAuthorized) {
continue
}
return nil, internalErr()
}
if !found || (v.AccessHash != 0 && v.AccessHash != u.AccessHash) {
continue
}
out = append(out, r.tgUser(u))
}
}
return out, nil
}
func (r *Router) onUsersGetFullUser(ctx context.Context, id tg.InputUserClass) (*tg.UsersUserFull, error) {
if r.deps.Users == nil {
return emptyUserFull(), nil
}
currentUserID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
u, found, err := r.userFromInput(ctx, currentUserID, id)
if err != nil {
if errors.Is(err, users.ErrNotAuthorized) {
return emptyUserFull(), nil
}
return nil, internalErr()
}
if !found {
return emptyUserFull(), nil
}
user := r.tgUser(u)
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: 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,
TargetUserID: u.ID,
Limit: 1,
CountOnly: true,
})
if err != nil {
return nil, internalErr()
}
full.CommonChatsCount = common.Count
}
return &tg.UsersUserFull{
FullUser: full,
Users: []tg.UserClass{user},
}, nil
}
func (r *Router) onUsersGetSavedMusic(ctx context.Context, req *tg.UsersGetSavedMusicRequest) (tg.UsersSavedMusicClass, error) {
if req == nil || req.Offset < 0 || req.Limit < 0 || req.Limit > maxSavedMusicLimit {
return nil, limitInvalidErr()
}
if err := r.validateInputUser(ctx, req.ID); err != nil {
return nil, err
}
return &tg.UsersSavedMusic{
Count: 0,
Documents: []tg.DocumentClass{},
}, nil
}
func (r *Router) onUsersGetSavedMusicByID(ctx context.Context, req *tg.UsersGetSavedMusicByIDRequest) (tg.UsersSavedMusicClass, error) {
if req == nil || len(req.Documents) > maxSavedMusicLimit {
return nil, limitInvalidErr()
}
if err := r.validateInputUser(ctx, req.ID); err != nil {
return nil, err
}
return &tg.UsersSavedMusic{
Count: 0,
Documents: []tg.DocumentClass{},
}, 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{
Settings: tg.PeerSettings{},
NotifySettings: *tdesktop.NotifySettings(),
},
}
}
func (r *Router) userFromInput(ctx context.Context, currentUserID int64, id tg.InputUserClass) (domain.User, bool, error) {
switch v := id.(type) {
case *tg.InputUserSelf:
u, err := r.deps.Users.Self(ctx, currentUserID)
return u, err == nil, err
case *tg.InputUser:
u, found, err := r.deps.Users.ByID(ctx, currentUserID, v.UserID)
if err != nil || !found {
return domain.User{}, found, err
}
if v.AccessHash != 0 && v.AccessHash != u.AccessHash {
return domain.User{}, false, nil
}
return u, true, nil
default:
return domain.User{}, false, nil
}
}
func (r *Router) validateInputUser(ctx context.Context, id tg.InputUserClass) error {
if r.deps.Users == nil {
return nil
}
currentUserID, _, err := r.currentUserID(ctx)
if err != nil {
return internalErr()
}
_, found, err := r.userFromInput(ctx, currentUserID, id)
if err != nil {
if errors.Is(err, users.ErrNotAuthorized) {
return userIDInvalidErr()
}
return internalErr()
}
if !found {
return userIDInvalidErr()
}
return nil
}