311 lines
10 KiB
Go
311 lines
10 KiB
Go
package rpc
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
|
||
"github.com/iamxvbaba/td/tg"
|
||
"go.uber.org/zap"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// 个人贴纸集合:收藏贴纸 / 最近贴纸 / 保存的 GIF。读写都经 stickerCollectionService
|
||
// (由 *app/account.Service 实现,类型断言自 r.deps.Account)。文档经 Files 解析为完整
|
||
// 文档对象,未接通服务时回落历史空 stub 行为。
|
||
|
||
type stickerCollectionService interface {
|
||
SaveStickerCollectionItem(ctx context.Context, userID int64, kind domain.StickerCollectionKind, documentID int64, unsave bool, now int) error
|
||
ListStickerCollection(ctx context.Context, userID int64, kind domain.StickerCollectionKind, limit int) ([]domain.StickerCollectionItem, error)
|
||
ClearStickerCollection(ctx context.Context, userID int64, kind domain.StickerCollectionKind) error
|
||
}
|
||
|
||
func (r *Router) stickerCollectionSvc() (stickerCollectionService, bool) {
|
||
svc, ok := r.deps.Account.(stickerCollectionService)
|
||
return svc, ok
|
||
}
|
||
|
||
// stickerDocumentFromInput 校验输入文档存在且为贴纸(faveSticker/saveRecentSticker)。
|
||
func (r *Router) stickerDocumentFromInput(ctx context.Context, input tg.InputDocumentClass, requireGif bool) (domain.Document, error) {
|
||
in, ok := input.(*tg.InputDocument)
|
||
if !ok || in.ID == 0 || r.deps.Files == nil {
|
||
if requireGif {
|
||
return domain.Document{}, gifIDInvalidErr()
|
||
}
|
||
return domain.Document{}, stickerInvalidErr()
|
||
}
|
||
doc, found, err := r.deps.Files.GetDocument(ctx, in.ID)
|
||
if err != nil {
|
||
return domain.Document{}, internalErr()
|
||
}
|
||
ok = found && doc.AccessHash == in.AccessHash
|
||
if ok && len(in.FileReference) > 0 && !bytes.Equal(in.FileReference, doc.FileReference) {
|
||
ok = false
|
||
}
|
||
if ok {
|
||
if requireGif {
|
||
ok = doc.IsGif()
|
||
} else {
|
||
ok = doc.IsSticker()
|
||
}
|
||
}
|
||
if !ok {
|
||
if requireGif {
|
||
return domain.Document{}, gifIDInvalidErr()
|
||
}
|
||
return domain.Document{}, stickerInvalidErr()
|
||
}
|
||
return doc, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesFaveSticker(ctx context.Context, req *tg.MessagesFaveStickerRequest) (bool, error) {
|
||
if req == nil {
|
||
return false, stickerInvalidErr()
|
||
}
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
doc, err := r.stickerDocumentFromInput(ctx, req.ID, false)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if svc, ok := r.stickerCollectionSvc(); ok {
|
||
if err := svc.SaveStickerCollectionItem(ctx, userID, domain.StickerCollectionFaved, doc.ID, req.Unfave, int(r.clock.Now().Unix())); err != nil {
|
||
return false, internalErr()
|
||
}
|
||
}
|
||
r.pushStickerCollectionUpdate(ctx, userID, &tg.UpdateFavedStickers{})
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesSaveRecentSticker(ctx context.Context, req *tg.MessagesSaveRecentStickerRequest) (bool, error) {
|
||
if req == nil {
|
||
return false, stickerInvalidErr()
|
||
}
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
doc, err := r.stickerDocumentFromInput(ctx, req.ID, false)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
kind := domain.StickerCollectionRecent
|
||
if req.Attached {
|
||
kind = domain.StickerCollectionRecentAttached
|
||
}
|
||
if svc, ok := r.stickerCollectionSvc(); ok {
|
||
if err := svc.SaveStickerCollectionItem(ctx, userID, kind, doc.ID, req.Unsave, int(r.clock.Now().Unix())); err != nil {
|
||
return false, internalErr()
|
||
}
|
||
}
|
||
r.pushStickerCollectionUpdate(ctx, userID, &tg.UpdateRecentStickers{})
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesSaveGif(ctx context.Context, req *tg.MessagesSaveGifRequest) (bool, error) {
|
||
if req == nil {
|
||
return false, gifIDInvalidErr()
|
||
}
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
doc, err := r.stickerDocumentFromInput(ctx, req.ID, true)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if svc, ok := r.stickerCollectionSvc(); ok {
|
||
if err := svc.SaveStickerCollectionItem(ctx, userID, domain.StickerCollectionGif, doc.ID, req.Unsave, int(r.clock.Now().Unix())); err != nil {
|
||
return false, internalErr()
|
||
}
|
||
}
|
||
r.pushStickerCollectionUpdate(ctx, userID, &tg.UpdateSavedGifs{})
|
||
return true, nil
|
||
}
|
||
|
||
// autoSaveSentGif mirrors official Telegram: sending a GIF adds it to the
|
||
// sender's Saved GIFs, and re-sending an already-saved one bumps it back to the
|
||
// front (SaveStickerCollectionItem's upsert refreshes used_at/order_key, so
|
||
// both cases are the same call).
|
||
//
|
||
// The server has to do this: clients never save on send. TDesktop only issues
|
||
// messages.saveGif for the explicit "Save GIF" toggle and its context-menu
|
||
// entry (api/api_toggling_media.cpp, history_view_context_menu.cpp), so without
|
||
// this a GIF picked out of @gif's catalog would send fine and still never show
|
||
// up in the user's own GIFs tab.
|
||
//
|
||
// Best-effort by design: the message is already sent and acknowledged by the
|
||
// time this runs, so a collection write failure is logged rather than turned
|
||
// into a send error the client would retry.
|
||
func (r *Router) autoSaveSentGif(ctx context.Context, userID int64, media *domain.MessageMedia) {
|
||
if media == nil || media.Kind != domain.MessageMediaKindDocument || media.Document == nil {
|
||
return
|
||
}
|
||
if !media.Document.IsGif() {
|
||
return
|
||
}
|
||
svc, ok := r.stickerCollectionSvc()
|
||
if !ok {
|
||
return
|
||
}
|
||
if err := svc.SaveStickerCollectionItem(ctx, userID, domain.StickerCollectionGif, media.Document.ID, false, int(r.clock.Now().Unix())); err != nil {
|
||
r.log.Warn("auto-save sent gif",
|
||
zap.Int64("user_id", userID), zap.Int64("document_id", media.Document.ID), zap.Error(err))
|
||
return
|
||
}
|
||
r.pushStickerCollectionUpdate(ctx, userID, &tg.UpdateSavedGifs{})
|
||
}
|
||
|
||
func (r *Router) onMessagesClearRecentStickers(ctx context.Context, req *tg.MessagesClearRecentStickersRequest) (bool, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
kind := domain.StickerCollectionRecent
|
||
if req != nil && req.Attached {
|
||
kind = domain.StickerCollectionRecentAttached
|
||
}
|
||
if svc, ok := r.stickerCollectionSvc(); ok {
|
||
if err := svc.ClearStickerCollection(ctx, userID, kind); err != nil {
|
||
return false, internalErr()
|
||
}
|
||
}
|
||
r.pushStickerCollectionUpdate(ctx, userID, &tg.UpdateRecentStickers{})
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesGetFavedStickers(ctx context.Context, hash int64) (tg.MessagesFavedStickersClass, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
docs, err := r.stickerCollectionDocuments(ctx, userID, domain.StickerCollectionFaved, nil)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
catalogHash := stickerDocumentsHash(docs)
|
||
if hash != 0 && hash == catalogHash {
|
||
return &tg.MessagesFavedStickersNotModified{}, nil
|
||
}
|
||
return &tg.MessagesFavedStickers{
|
||
Hash: catalogHash,
|
||
Packs: []tg.StickerPack{},
|
||
Stickers: tgDocuments(docs),
|
||
}, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesGetRecentStickers(ctx context.Context, req *tg.MessagesGetRecentStickersRequest) (tg.MessagesRecentStickersClass, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
kind := domain.StickerCollectionRecent
|
||
if req != nil && req.Attached {
|
||
kind = domain.StickerCollectionRecentAttached
|
||
}
|
||
var dates []int
|
||
docs, err := r.stickerCollectionDocuments(ctx, userID, kind, &dates)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
catalogHash := stickerDocumentsHash(docs)
|
||
if req != nil && req.Hash != 0 && req.Hash == catalogHash {
|
||
return &tg.MessagesRecentStickersNotModified{}, nil
|
||
}
|
||
return &tg.MessagesRecentStickers{
|
||
Hash: catalogHash,
|
||
Packs: []tg.StickerPack{},
|
||
Stickers: tgDocuments(docs),
|
||
Dates: dates,
|
||
}, nil
|
||
}
|
||
|
||
func (r *Router) onMessagesGetSavedGifs(ctx context.Context, hash int64) (tg.MessagesSavedGifsClass, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
docs, err := r.stickerCollectionDocuments(ctx, userID, domain.StickerCollectionGif, nil)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
catalogHash := stickerDocumentsHash(docs)
|
||
if hash != 0 && hash == catalogHash {
|
||
return &tg.MessagesSavedGifsNotModified{}, nil
|
||
}
|
||
return &tg.MessagesSavedGifs{Hash: catalogHash, Gifs: tgDocuments(docs)}, nil
|
||
}
|
||
|
||
// stickerCollectionDocuments 取某集合并解析为完整文档(最新在前,顺序与集合一致)。
|
||
// 集合引用缺失或类型错误属于坏数据并 fail-fast;若 datesOut 非 nil,按相同顺序填充 used_at。
|
||
func (r *Router) stickerCollectionDocuments(ctx context.Context, userID int64, kind domain.StickerCollectionKind, datesOut *[]int) ([]domain.Document, error) {
|
||
svc, ok := r.stickerCollectionSvc()
|
||
if !ok || r.deps.Files == nil {
|
||
if datesOut != nil {
|
||
*datesOut = []int{}
|
||
}
|
||
return nil, nil
|
||
}
|
||
items, err := svc.ListStickerCollection(ctx, userID, kind, domain.MaxStickerCollectionItems(kind))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(items) == 0 {
|
||
if datesOut != nil {
|
||
*datesOut = []int{}
|
||
}
|
||
return nil, nil
|
||
}
|
||
ids := make([]int64, 0, len(items))
|
||
dateByID := make(map[int64]int, len(items))
|
||
for _, it := range items {
|
||
ids = append(ids, it.DocumentID)
|
||
dateByID[it.DocumentID] = it.Date
|
||
}
|
||
resolved, err := r.deps.Files.GetDocuments(ctx, ids)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
byID := documentsByID(resolved)
|
||
docs := make([]domain.Document, 0, len(items))
|
||
dates := make([]int, 0, len(items))
|
||
for _, id := range ids { // 保持集合顺序(最新在前)
|
||
doc, ok := byID[id]
|
||
if !ok {
|
||
return nil, fmt.Errorf("sticker collection %s references missing document %d", kind, id)
|
||
}
|
||
if kind == domain.StickerCollectionGif && !doc.IsGif() {
|
||
return nil, fmt.Errorf("saved gif collection references non-GIFv document %d", id)
|
||
}
|
||
if kind != domain.StickerCollectionGif && !doc.IsSticker() {
|
||
return nil, fmt.Errorf("sticker collection %s references non-sticker document %d", kind, id)
|
||
}
|
||
docs = append(docs, doc)
|
||
dates = append(dates, dateByID[id])
|
||
}
|
||
if datesOut != nil {
|
||
*datesOut = dates
|
||
}
|
||
return docs, nil
|
||
}
|
||
|
||
func stickerDocumentsHash(docs []domain.Document) int64 {
|
||
values := make([]int64, 0, len(docs))
|
||
for _, d := range docs {
|
||
values = append(values, d.ID)
|
||
}
|
||
return int64(tdesktopCountHash(values))
|
||
}
|
||
|
||
// pushStickerCollectionUpdate 把 updateFaved/Recent/SavedGifs nudge 推给本人其它在线设备。
|
||
func (r *Router) pushStickerCollectionUpdate(ctx context.Context, userID int64, update tg.UpdateClass) {
|
||
r.pushUserUpdates(ctx, userID, &tg.Updates{
|
||
Updates: []tg.UpdateClass{update},
|
||
Users: []tg.UserClass{},
|
||
Chats: []tg.ChatClass{},
|
||
Date: int(r.clock.Now().Unix()),
|
||
})
|
||
}
|