owpengram-server/internal/rpc/photos.go
2026-06-04 01:37:39 +08:00

250 lines
7.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/domain"
)
// registerPhotos 注册 photos.* RPC handler头像上传 / 切换 / 查询 / 删除)。
func (r *Router) registerPhotos(d *tg.ServerDispatcher) {
d.OnPhotosUploadProfilePhoto(r.onPhotosUploadProfilePhoto)
d.OnPhotosUpdateProfilePhoto(r.onPhotosUpdateProfilePhoto)
d.OnPhotosGetUserPhotos(r.onPhotosGetUserPhotos)
d.OnPhotosDeletePhotos(r.onPhotosDeletePhotos)
}
func (r *Router) onPhotosUploadProfilePhoto(ctx context.Context, req *tg.PhotosUploadProfilePhotoRequest) (*tg.PhotosPhoto, error) {
if r.deps.Files == nil {
return nil, notImplementedErr()
}
userID, ok, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if !ok || userID == 0 {
return nil, photoInvalidErr()
}
file, hasFile := req.GetFile()
if !hasFile {
// 仅 fallback / 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()))
if err != nil {
return nil, photoUploadErr(err)
}
return r.photosPhotoForSelf(ctx, userID, photo), nil
}
func (r *Router) onPhotosUpdateProfilePhoto(ctx context.Context, req *tg.PhotosUpdateProfilePhotoRequest) (*tg.PhotosPhoto, error) {
if r.deps.Files == nil {
return nil, notImplementedErr()
}
userID, ok, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if !ok || userID == 0 {
return nil, photoInvalidErr()
}
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()))
if err != nil {
return nil, internalErr()
}
if !found {
return nil, photoInvalidErr()
}
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})
}
return r.photosPhotoForSelf(ctx, userID, domain.Photo{}), nil
}
}
func (r *Router) onPhotosGetUserPhotos(ctx context.Context, req *tg.PhotosGetUserPhotosRequest) (tg.PhotosPhotosClass, error) {
if r.deps.Files == nil {
return &tg.PhotosPhotos{}, nil
}
currentUserID, ok, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if !ok || currentUserID == 0 {
return nil, userIDInvalidErr()
}
target, found, err := r.userFromInput(ctx, currentUserID, req.UserID)
if err != nil {
return nil, internalErr()
}
if !found {
return nil, userIDInvalidErr()
}
offset := req.Offset
if offset < 0 {
offset = 0
}
limit := req.Limit
if limit <= 0 || limit > 100 {
limit = 100
}
photos, total, err := r.deps.Files.GetProfilePhotos(ctx, domain.PeerTypeUser, target.ID, offset, limit, req.MaxID)
if err != nil {
return nil, internalErr()
}
tgPhotos := make([]tg.PhotoClass, 0, len(photos))
for _, p := range photos {
tgPhotos = append(tgPhotos, tgPhoto(p))
}
users := []tg.UserClass{r.tgUser(target)}
if total > len(photos)+offset {
return &tg.PhotosPhotosSlice{Count: total, Photos: tgPhotos, Users: users}, nil
}
return &tg.PhotosPhotos{Photos: tgPhotos, Users: users}, nil
}
func (r *Router) onPhotosDeletePhotos(ctx context.Context, id []tg.InputPhotoClass) ([]int64, error) {
if r.deps.Files == nil {
return []int64{}, nil
}
userID, ok, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if !ok || userID == 0 {
return nil, photoInvalidErr()
}
ids := make([]int64, 0, len(id))
for _, in := range id {
if photo, isPhoto := in.(*tg.InputPhoto); isPhoto && photo.ID != 0 {
ids = append(ids, photo.ID)
}
}
if len(ids) == 0 {
return []int64{}, nil
}
if _, err := r.deps.Files.DeleteProfilePhotos(ctx, domain.PeerTypeUser, userID, ids); err != nil {
return nil, internalErr()
}
return ids, nil
}
// photosPhotoForSelf 组装 photos.photo 响应(新照片 + 带头像的 self user并在头像变更后
// 向该账号其它在线设备推送,使其即时刷新头像。仅由 uploadProfilePhoto / updateProfilePhoto
// 等头像变更路径调用(只读路径不得使用,否则会误触发推送)。
func (r *Router) photosPhotoForSelf(ctx context.Context, userID int64, photo domain.Photo) *tg.PhotosPhoto {
out := &tg.PhotosPhoto{Photo: tgPhoto(photo), Users: []tg.UserClass{}}
if r.deps.Users == nil {
return out
}
self, err := r.deps.Users.Self(ctx, userID)
if err != nil {
return out
}
out.Users = append(out.Users, r.tgSelfUser(self))
r.pushSelfPhotoUpdate(ctx, self)
return out
}
// pushSelfPhotoUpdate 向该账号其它在线设备推送头像变更。updateUserName 不含 photo 无法刷新
// 头像updateUser 只是「该 user 变了」的信号TDesktop 仅当 peer 已 full-loaded 时才
// forceFull 重拉);最可靠是在 Updates.Users 带上含新 userProfilePhoto 的完整 self user
// TDesktop 经 processUser→setPhoto→peerUpdated(Photo) 即时刷新。当前设备同时经 RPC 返回更新,
// 重复推送对 TDesktop 幂等setUserpicChecked 比对 photo_id相同则 no-op
func (r *Router) pushSelfPhotoUpdate(ctx context.Context, self domain.User) {
if self.ID == 0 {
return
}
r.pushUserUpdates(ctx, self.ID, &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateUser{UserID: self.ID}},
Users: []tg.UserClass{r.tgSelfUser(self)},
Date: int(r.clock.Now().Unix()),
})
}
// uploadedFileRef 把 tg.InputFile / InputFileBig 转成 domain.UploadedFileRef。
func uploadedFileRef(ownerUserID int64, file tg.InputFileClass) (domain.UploadedFileRef, bool) {
switch f := file.(type) {
case *tg.InputFile:
if f.ID == 0 || f.Parts <= 0 {
return domain.UploadedFileRef{}, false
}
return domain.UploadedFileRef{OwnerUserID: ownerUserID, FileID: f.ID, Parts: f.Parts, Name: f.Name, MD5: f.MD5Checksum}, true
case *tg.InputFileBig:
if f.ID == 0 || f.Parts <= 0 {
return domain.UploadedFileRef{}, false
}
return domain.UploadedFileRef{OwnerUserID: ownerUserID, FileID: f.ID, Parts: f.Parts, Name: f.Name, Big: true}, true
default:
return domain.UploadedFileRef{}, false
}
}
// resolveInputChatPhoto 把 tg.InputChatPhoto 解析为 *domain.Photonil=清除头像)。
// 支持新上传InputChatUploadedPhoto与引用已有照片InputChatPhoto{InputPhoto})。
func (r *Router) resolveInputChatPhoto(ctx context.Context, userID int64, input tg.InputChatPhotoClass) (*domain.Photo, error) {
switch in := input.(type) {
case *tg.InputChatPhotoEmpty:
return nil, nil
case *tg.InputChatUploadedPhoto:
file, ok := in.GetFile()
if !ok {
return nil, photoInvalidErr()
}
if r.deps.Files == nil {
return nil, photoInvalidErr()
}
ref, ok := uploadedFileRef(userID, file)
if !ok {
return nil, fileReferenceInvalidErr()
}
// 频道/群头像用 avatar 尺寸('a'/'c'),匹配 InputPeerPhotoFileLocation 下载路径。
photo, err := r.deps.Files.CreateAvatarFromUpload(ctx, ref)
if err != nil {
return nil, photoUploadErr(err)
}
return &photo, nil
case *tg.InputChatPhoto:
switch id := in.ID.(type) {
case *tg.InputPhoto:
if r.deps.Files == nil {
return nil, photoInvalidErr()
}
photo, found, err := r.deps.Files.GetPhoto(ctx, id.ID)
if err != nil {
return nil, internalErr()
}
if !found {
return nil, photoInvalidErr()
}
return &photo, nil
default:
return nil, nil // InputPhotoEmpty → 清除
}
default:
return nil, photoInvalidErr()
}
}
func photoUploadErr(err error) error {
switch {
case errors.Is(err, domain.ErrFilePartsInvalid):
return filePartsInvalidErr()
case errors.Is(err, domain.ErrPhotoInvalid):
return photoInvalidErr()
default:
return internalErr()
}
}