From 5e18bd3830c8ba93271282c7bc279741a1f68d0d Mon Sep 17 00:00:00 2001 From: onysd Date: Mon, 24 Aug 2026 22:42:40 +0300 Subject: [PATCH] fix for gif and emojies categories --- internal/app/files/emoji_group_icons_seed.go | 101 +++++++++++++++++++ internal/app/files/seed.go | 9 ++ internal/rpc/bots_inline.go | 13 ++- internal/rpc/messages_history.go | 42 ++++++++ internal/rpc/messages_register.go | 6 ++ internal/seed/catalog/emoji_groups.json | 22 ++-- 6 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 internal/app/files/emoji_group_icons_seed.go diff --git a/internal/app/files/emoji_group_icons_seed.go b/internal/app/files/emoji_group_icons_seed.go new file mode 100644 index 00000000..67d3b846 --- /dev/null +++ b/internal/app/files/emoji_group_icons_seed.go @@ -0,0 +1,101 @@ +package files + +import ( + "context" + "fmt" + + "telesrv/internal/domain" +) + +// emojiGroupIconSpec 描述 messages.getEmojiGroups 一个分类图标:把已 seed 的 +// DefaultSet_AnimatedEmoji 贴纸文档(SourceDocID,带 DocAttrSticker)在本地"复制"成一份 +// 带 DocAttrCustomEmoji 的影子文档(NewID),二进制内容(blob)原样复用,不重新下载/编码。 +// +// 背景:messages.getEmojiGroups 的 icon_emoji_id 语义上必须指向一份真正的自定义 emoji +// 文档——真实 Telegram 的这些 icon 文档始终带 DocumentAttributeCustomEmoji。我们最初直接 +// 复用 DefaultSet_AnimatedEmoji 里现成的贴纸文档 id(见 internal/seed/catalog/emoji_groups.json), +// 但那些文档只带 DocAttrSticker,不带 DocAttrCustomEmoji——Android 客户端渲染分类图标那一小格 +// 时找不到期望的自定义 emoji 元数据,一直转圈(桌面端似乎更宽松,不受影响)。 +// NewID 特意选用远小于真实 Telegram snowflake id(19 位)的 13 位数字,不会与任何真实/已 +// seed 的 document id 冲突。 +type emojiGroupIconSpec struct { + NewID int64 + SourceDocID int64 + Alt string +} + +var emojiGroupIconSpecs = []emojiGroupIconSpec{ + {NewID: 9000000000001, SourceDocID: 1258816259753929, Alt: "❤"}, // Love + {NewID: 9000000000002, SourceDocID: 5181593617004757506, Alt: "👍"}, // Approval + {NewID: 9000000000003, SourceDocID: 5181852277115192162, Alt: "👎"}, // Disapproval + {NewID: 9000000000004, SourceDocID: 4909118808687378938, Alt: "🎉"}, // Cheers + {NewID: 9000000000005, SourceDocID: 5129721261156467513, Alt: "😄"}, // Laughter + {NewID: 9000000000006, SourceDocID: 5129715527375127060, Alt: "😨"}, // Astonishment + {NewID: 9000000000007, SourceDocID: 5129804416018285169, Alt: "😔"}, // Sadness + {NewID: 9000000000008, SourceDocID: 5129791445217051348, Alt: "😡"}, // Anger + {NewID: 9000000000009, SourceDocID: 5129653108615414540, Alt: "😐"}, // Neutral + {NewID: 9000000000010, SourceDocID: 5127352539448082997, Alt: "🤔"}, // Doubt + {NewID: 9000000000011, SourceDocID: 5127684445930783362, Alt: "🤪"}, // Silly +} + +// seedEmojiGroupIcons 为 emojiGroupIconSpecs 中每一项确保存在一份带 DocAttrCustomEmoji +// 的影子文档。源贴纸未 seed(开发机没有该 sticker-seed 数据)时静默跳过该项而非报错。 +func (s *Service) seedEmojiGroupIcons(ctx context.Context, stats *SeedStats) error { + for _, spec := range emojiGroupIconSpecs { + existing, found, err := s.media.GetDocument(ctx, spec.NewID) + if err != nil { + return err + } + if found && existing.ID != 0 { + continue + } + src, found, err := s.media.GetDocument(ctx, spec.SourceDocID) + if err != nil { + return err + } + if !found || src.ID == 0 { + continue + } + blob, found, err := s.media.GetFileBlob(ctx, fmt.Sprintf("doc:%d", spec.SourceDocID)) + if err != nil { + return err + } + if !found { + continue + } + + attrs := []domain.DocumentAttribute{ + {Kind: domain.DocAttrCustomEmoji, Alt: spec.Alt}, + } + for _, a := range src.Attributes { + if a.Kind == domain.DocAttrImageSize { + attrs = append(attrs, a) + break + } + } + + doc := domain.Document{ + ID: spec.NewID, + AccessHash: src.AccessHash, + Date: src.Date, + MimeType: src.MimeType, + Size: src.Size, + DCID: src.DCID, + Attributes: attrs, + } + if err := s.media.PutDocument(ctx, doc); err != nil { + return err + } + if err := s.media.PutFileBlob(ctx, domain.FileBlob{ + LocationKey: fmt.Sprintf("doc:%d", spec.NewID), + Backend: blob.Backend, + ObjectKey: blob.ObjectKey, + Size: blob.Size, + MimeType: blob.MimeType, + }); err != nil { + return err + } + stats.Documents++ + } + return nil +} diff --git a/internal/app/files/seed.go b/internal/app/files/seed.go index bbbf3b82..8ca5d97e 100644 --- a/internal/app/files/seed.go +++ b/internal/app/files/seed.go @@ -111,6 +111,15 @@ func (s *Service) SeedMedia(ctx context.Context, root string, maxRegularSets int } s.logSeedPhase("effects", phaseStarted, phaseBefore, stats) + phaseStarted = time.Now() + phaseBefore = stats + // messages.getEmojiGroups 分类图标的影子自定义 emoji 文档,依赖上面 sticker sets 阶段 + // 已 seed 的源文档,故放在其后。 + if err := s.seedEmojiGroupIcons(ctx, &stats); err != nil { + return stats, fmt.Errorf("seed emoji group icons: %w", err) + } + s.logSeedPhase("emoji_group_icons", phaseStarted, phaseBefore, stats) + if stats.Reactions == 0 && stats.StickerSets == 0 && stats.Effects == 0 { stats.Skipped = true } diff --git a/internal/rpc/bots_inline.go b/internal/rpc/bots_inline.go index 2dc3d037..d6fad25c 100644 --- a/internal/rpc/bots_inline.go +++ b/internal/rpc/bots_inline.go @@ -78,9 +78,16 @@ func (r *Router) onMessagesGetInlineBotResults(ctx context.Context, req *tg.Mess return nil, err } } - peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer) - if err != nil { - return nil, err + // inputPeerEmpty 是合法输入:客户端在未绑定具体聊天的全局搜索场景(如 GIF 面板的 + // 类目/搜索,尚未选定发送目标)会这样发。之前这里无条件走 checkedDomainPeerFromInputPeer, + // 把它当无效 peer 拒绝(PEER_ID_INVALID),导致 Android 的 GIF 类目/搜索请求每次都 + // 400,客户端只能一直转圈——真实 Telegram 服务端接受这种"无聊天上下文"的查询。 + var peer domain.Peer + if _, empty := req.Peer.(*tg.InputPeerEmpty); !empty { + peer, err = r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer) + if err != nil { + return nil, err + } } // 内置(进程内)service bot 分支:@gif 没有 MTProto session、也没有 Bot API 消费者, // 走下面的「推 updateBotInlineQuery + 挂起 25s」必然超时,故同步问 responder。 diff --git a/internal/rpc/messages_history.go b/internal/rpc/messages_history.go index aa685d01..f7b71edb 100644 --- a/internal/rpc/messages_history.go +++ b/internal/rpc/messages_history.go @@ -4,6 +4,7 @@ import ( "context" "errors" "github.com/iamxvbaba/td/tg" + "github.com/iamxvbaba/td/tgerr" "sort" "telesrv/internal/domain" "unicode/utf8" @@ -25,6 +26,25 @@ func (r *Router) onMessagesSearchStickerSets(ctx context.Context, req *tg.Messag }, nil } +// onMessagesSearchEmojiStickerSets 之前完全未注册,Android 的 emoji 分类点击/搜索会 +// 无限重试这个方法(每次都拿 NOT_IMPLEMENTED,客户端不认为这是终态错误,一直重发), +// 表现为分类点击后结果区永远不出来。和 onMessagesSearchStickerSets 同策略,返回合法空结果。 +func (r *Router) onMessagesSearchEmojiStickerSets(ctx context.Context, req *tg.MessagesSearchEmojiStickerSetsRequest) (tg.MessagesFoundStickerSetsClass, error) { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + if utf8.RuneCountInString(req.Q) > maxStickerSearchQLength { + return nil, limitInvalidErr() + } + if req.Hash != 0 { + return &tg.MessagesFoundStickerSetsNotModified{}, nil + } + return &tg.MessagesFoundStickerSets{ + Hash: 0, + Sets: []tg.StickerSetCoveredClass{}, + }, nil +} + func (r *Router) onMessagesSearchStickers(ctx context.Context, req *tg.MessagesSearchStickersRequest) (tg.MessagesFoundStickersClass, error) { if _, _, err := r.currentUserID(ctx); err != nil { return nil, internalErr() @@ -52,6 +72,28 @@ func (r *Router) onMessagesSearchStickers(ctx context.Context, req *tg.MessagesS }, nil } +func emoticonEmptyErr() error { return tgerr.New(400, "EMOTICON_EMPTY") } + +// onMessagesSearchCustomEmoji 目前没有按 emoticon 反查自定义 emoji 的索引,故与 +// onMessagesSearchStickers/onMessagesSearchStickerSets 同策略:返回合法的空结果而非 +// NOT_IMPLEMENTED——之前完全未注册此方法时,Android 的 emoji 分类/搜索会拿到 +// notImplementedErr(),表现为一直转圈(客户端没有为这个具体错误重置搜索 UI 状态)。 +func (r *Router) onMessagesSearchCustomEmoji(ctx context.Context, req *tg.MessagesSearchCustomEmojiRequest) (tg.EmojiListClass, error) { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + if req.Emoticon == "" { + return nil, emoticonEmptyErr() + } + if utf8.RuneCountInString(req.Emoticon) > maxStickerSearchQLength { + return nil, limitInvalidErr() + } + if req.Hash != 0 { + return &tg.EmojiListNotModified{}, nil + } + return &tg.EmojiList{Hash: 0, DocumentID: []int64{}}, nil +} + func (r *Router) onMessagesGetSearchResultsCalendar(ctx context.Context, req *tg.MessagesGetSearchResultsCalendarRequest) (*tg.MessagesSearchResultsCalendar, error) { if req.OffsetID < 0 || req.OffsetID > domain.MaxMessageBoxID || req.OffsetDate < 0 { return nil, limitInvalidErr() diff --git a/internal/rpc/messages_register.go b/internal/rpc/messages_register.go index 8a1920e7..d57e2a8b 100644 --- a/internal/rpc/messages_register.go +++ b/internal/rpc/messages_register.go @@ -363,6 +363,12 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) { registerRPC[*tg.MessagesSearchStickersRequest](d, tlprofile.SemanticMethodMessagesSearchStickers, func(ctx context.Context, layerRequest *tg.MessagesSearchStickersRequest) (any, error) { return r.onMessagesSearchStickers(ctx, layerRequest) }) + registerRPC[*tg.MessagesSearchCustomEmojiRequest](d, tlprofile.SemanticMethodMessagesSearchCustomEmoji, func(ctx context.Context, layerRequest *tg.MessagesSearchCustomEmojiRequest) (any, error) { + return r.onMessagesSearchCustomEmoji(ctx, layerRequest) + }) + registerRPC[*tg.MessagesSearchEmojiStickerSetsRequest](d, tlprofile.SemanticMethodMessagesSearchEmojiStickerSets, func(ctx context.Context, layerRequest *tg.MessagesSearchEmojiStickerSetsRequest) (any, error) { + return r.onMessagesSearchEmojiStickerSets(ctx, layerRequest) + }) registerRPC[*tg.MessagesGetAttachMenuBotsRequest](d, tlprofile.SemanticMethodMessagesGetAttachMenuBots, func(ctx context.Context, layerRequest *tg.MessagesGetAttachMenuBotsRequest) (any, error) { return r.onMessagesGetAttachMenuBots(ctx, layerRequest. Hash) diff --git a/internal/seed/catalog/emoji_groups.json b/internal/seed/catalog/emoji_groups.json index 3fe443fd..eb983fa6 100644 --- a/internal/seed/catalog/emoji_groups.json +++ b/internal/seed/catalog/emoji_groups.json @@ -2,7 +2,7 @@ "groups": [ { "title": "Love", - "icon_emoji_id": 5355088357070218139, + "icon_emoji_id": 9000000000001, "emoticons": [ "❤", "😍", @@ -46,7 +46,7 @@ }, { "title": "Approval", - "icon_emoji_id": 5357465415310124060, + "icon_emoji_id": 9000000000002, "emoticons": [ "👍", "👏", @@ -62,7 +62,7 @@ }, { "title": "Disapproval", - "icon_emoji_id": 5357216259962315463, + "icon_emoji_id": 9000000000003, "emoticons": [ "👎", "😒", @@ -76,7 +76,7 @@ }, { "title": "Cheers", - "icon_emoji_id": 5357257143756005264, + "icon_emoji_id": 9000000000004, "emoticons": [ "🎉", "🥳", @@ -93,7 +93,7 @@ }, { "title": "Laughter", - "icon_emoji_id": 5354785574760753200, + "icon_emoji_id": 9000000000005, "emoticons": [ "😄", "😁", @@ -110,7 +110,7 @@ }, { "title": "Astonishment", - "icon_emoji_id": 5354978715145085779, + "icon_emoji_id": 9000000000006, "emoticons": [ "😨", "😦", @@ -128,7 +128,7 @@ }, { "title": "Sadness", - "icon_emoji_id": 5355181227148059369, + "icon_emoji_id": 9000000000007, "emoticons": [ "😔", "😪", @@ -152,7 +152,7 @@ }, { "title": "Anger", - "icon_emoji_id": 5355125405458113381, + "icon_emoji_id": 9000000000008, "emoticons": [ "😡", "🤬", @@ -165,7 +165,7 @@ }, { "title": "Neutral", - "icon_emoji_id": 5357252827313874903, + "icon_emoji_id": 9000000000009, "emoticons": [ "😐", "😑", @@ -180,7 +180,7 @@ }, { "title": "Doubt", - "icon_emoji_id": 5354848362887653592, + "icon_emoji_id": 9000000000010, "emoticons": [ "🤔", "🤨", @@ -195,7 +195,7 @@ }, { "title": "Silly", - "icon_emoji_id": 5354832952544993830, + "icon_emoji_id": 9000000000011, "emoticons": [ "🤪", "😜",