From 3fa0c6f1e425bd6d36b1c0f587277bedd8c6be83 Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:06:39 +0800 Subject: [PATCH] fix(rpc): sync verification icon envelope coverage Follow up gramsrv#27 with the remaining peer-bearing messages.Messages boundaries and route-level wire regression coverage from the telesrv canonical implementation. Origin-PR: gramsrv#27 Origin-Head: f0bd6c9699f1eb97fbbfd71358a6057e50ff97d8 Origin-Merge: 7713847b2ab40be1b52f879be6868eea94c6c471 Canonical: f1ba406015a2d5e37802b963757a5cac8ee3eb0e Co-authored-by: epilepticseizureee --- internal/rpc/bot_verification_flags_test.go | 47 +++++++++++++++++++ internal/rpc/channels_messages.go | 4 +- internal/rpc/channels_messages_rpc_test.go | 15 +++++- .../rpc/channels_passive_stubs_rpc_test.go | 11 +++++ internal/rpc/chat_automation_rpc_test.go | 34 +++++++++++++- internal/rpc/messages_compat.go | 7 ++- internal/rpc/messages_history.go | 14 ++++-- internal/rpc/messages_monoforum.go | 6 ++- internal/rpc/messages_monoforum_rpc_test.go | 16 ++++++- internal/rpc/messages_polls.go | 6 ++- internal/rpc/messages_quick_replies.go | 1 + .../rpc/messages_saved_dialogs_rpc_test.go | 11 +++++ 12 files changed, 156 insertions(+), 16 deletions(-) diff --git a/internal/rpc/bot_verification_flags_test.go b/internal/rpc/bot_verification_flags_test.go index 01083ac3..5a02a755 100644 --- a/internal/rpc/bot_verification_flags_test.go +++ b/internal/rpc/bot_verification_flags_test.go @@ -65,6 +65,53 @@ func assertFlagBitDelta(t *testing.T, label string, before, after bin.Fields, wa } } +func assertMessagesEnvelopeBotVerificationIcon(t *testing.T, out tg.MessagesMessagesClass, peer domain.Peer, want int64) { + t.Helper() + var users []tg.UserClass + var chats []tg.ChatClass + switch value := out.(type) { + case *tg.MessagesMessages: + users, chats = value.Users, value.Chats + case *tg.MessagesMessagesSlice: + users, chats = value.Users, value.Chats + case *tg.MessagesChannelMessages: + users, chats = value.Users, value.Chats + default: + t.Fatalf("messages envelope = %T, want peer-bearing messages.Messages", out) + } + switch peer.Type { + case domain.PeerTypeUser: + for _, item := range users { + user, ok := item.(*tg.User) + if !ok || user.ID != peer.ID { + continue + } + wire := &tg.User{} + tlRoundTrip(t, user, wire) + if icon, ok := wire.GetBotVerificationIcon(); !ok || icon != want { + t.Fatalf("user %d bot_verification_icon = %d, ok=%v, want %d", peer.ID, icon, ok, want) + } + return + } + case domain.PeerTypeChannel: + for _, item := range chats { + channel, ok := item.(*tg.Channel) + if !ok || channel.ID != peer.ID { + continue + } + wire := &tg.Channel{} + tlRoundTrip(t, channel, wire) + if icon, ok := wire.GetBotVerificationIcon(); !ok || icon != want { + t.Fatalf("channel %d bot_verification_icon = %d, ok=%v, want %d", peer.ID, icon, ok, want) + } + return + } + default: + t.Fatalf("unsupported verification peer %+v", peer) + } + t.Fatalf("messages envelope %T does not carry peer %+v", out, peer) +} + // TestBotVerificationConstructorIDs pins the two constructor ids the feature // serialises. A drift here silently reshapes every payload below. func TestBotVerificationConstructorIDs(t *testing.T) { diff --git a/internal/rpc/channels_messages.go b/internal/rpc/channels_messages.go index 41e1455f..f73e8e1d 100644 --- a/internal/rpc/channels_messages.go +++ b/internal/rpc/channels_messages.go @@ -100,7 +100,9 @@ func (r *Router) onChannelsSearchPosts(ctx context.Context, req *tg.ChannelsSear return nil, channelInvalidErr(err) } history = r.enrichChannelHistory(ctx, userID, history) - return tgChannelSearchPostsMessages(userID, history), nil + result := tgChannelSearchPostsMessages(userID, history) + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } func validateChannelSearchPostsRequest(req *tg.ChannelsSearchPostsRequest) error { diff --git a/internal/rpc/channels_messages_rpc_test.go b/internal/rpc/channels_messages_rpc_test.go index 22b0894d..838c7cf8 100644 --- a/internal/rpc/channels_messages_rpc_test.go +++ b/internal/rpc/channels_messages_rpc_test.go @@ -835,9 +835,11 @@ func TestChannelsSearchPostsReturnsPublicPostsWithSeekPaging(t *testing.T) { viewer, _ := userStore.Create(ctx, domain.User{AccessHash: 91002, Phone: "15550091002", FirstName: "Viewer"}) channelStore := memory.NewChannelStore() channelService := appchannels.NewService(channelStore) + verify := newFakeBotVerifications() r := New(Config{}, Deps{ - Users: appusers.NewService(userStore), - Channels: channelService, + Users: appusers.NewService(userStore), + Channels: channelService, + BotVerifications: verify, }, zaptest.NewLogger(t), clock.System) public, err := channelService.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{ Title: "Public Search", @@ -847,6 +849,14 @@ func TestChannelsSearchPostsReturnsPublicPostsWithSeekPaging(t *testing.T) { if err != nil { t.Fatalf("create public channel: %v", err) } + const searchPostsIcon = int64(8800021) + searchPostsPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: public.Channel.ID} + verify.marks[searchPostsPeer] = domain.CustomVerification{ + VerifierBotID: 777000123, + Peer: searchPostsPeer, + IconDocumentID: searchPostsIcon, + Description: "Verified search result", + } if _, err := channelService.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{ UserID: owner.ID, ChannelID: public.Channel.ID, @@ -920,6 +930,7 @@ func TestChannelsSearchPostsReturnsPublicPostsWithSeekPaging(t *testing.T) { if peer, ok := first.PeerID.(*tg.PeerChannel); !ok || peer.ChannelID != public.Channel.ID { t.Fatalf("first result peer = %#v, want public channel %d", first.PeerID, public.Channel.ID) } + assertMessagesEnvelopeBotVerificationIcon(t, got, searchPostsPeer, searchPostsIcon) page2 := &tg.ChannelsSearchPostsRequest{ OffsetRate: slice.NextRate, diff --git a/internal/rpc/channels_passive_stubs_rpc_test.go b/internal/rpc/channels_passive_stubs_rpc_test.go index 26e6a145..af98c012 100644 --- a/internal/rpc/channels_passive_stubs_rpc_test.go +++ b/internal/rpc/channels_passive_stubs_rpc_test.go @@ -961,6 +961,16 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) { }); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") { t.Fatalf("messages.deletePollAnswer err = %v, want MESSAGE_ID_INVALID without poll store", err) } + verify := newFakeBotVerifications() + r.deps.BotVerifications = verify + const unreadPollIcon = int64(8800024) + unreadPollPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID} + verify.marks[unreadPollPeer] = domain.CustomVerification{ + VerifierBotID: 777000123, + Peer: unreadPollPeer, + IconDocumentID: unreadPollIcon, + Description: "Verified poll peer", + } unreadPollVotes, err := r.onMessagesGetUnreadPollVotes(ownerCtx, &tg.MessagesGetUnreadPollVotesRequest{ Peer: inputPeerChannel(channel), Limit: 10, @@ -968,6 +978,7 @@ func TestTDesktopPassiveChannelStubs(t *testing.T) { if err != nil { t.Fatalf("messages.getUnreadPollVotes: %v", err) } + assertMessagesEnvelopeBotVerificationIcon(t, unreadPollVotes, unreadPollPeer, unreadPollIcon) if len(unreadPollVotes.(*tg.MessagesMessages).Messages) != 0 { t.Fatalf("messages.getUnreadPollVotes = %+v, want empty messages", unreadPollVotes) } diff --git a/internal/rpc/chat_automation_rpc_test.go b/internal/rpc/chat_automation_rpc_test.go index 93a40b83..ba5ca95d 100644 --- a/internal/rpc/chat_automation_rpc_test.go +++ b/internal/rpc/chat_automation_rpc_test.go @@ -10,14 +10,35 @@ import ( "go.uber.org/zap/zaptest" accountapp "telesrv/internal/app/account" + appusers "telesrv/internal/app/users" "telesrv/internal/domain" "telesrv/internal/store/memory" ) func TestQuickReplyRPCSaveListAndDeleteMessage(t *testing.T) { - const userID int64 = 1000000001 + const userID int64 = domain.UserIDSequenceBase ctx := WithSessionID(WithAuthKeyID(WithUserID(context.Background(), userID), [8]byte{1}), 77) r, updates := newChatAutomationTestRouter(t) + verify := newFakeBotVerifications() + r.deps.BotVerifications = verify + userStore := memory.NewUserStore() + self, err := userStore.Create(context.Background(), domain.User{ + AccessHash: 7000001, + Phone: "15550007001", + FirstName: "Quick", + }) + if err != nil || self.ID != userID { + t.Fatalf("create quick-reply user = %+v err %v, want id %d", self, err, userID) + } + r.deps.Users = appusers.NewService(userStore) + const quickReplyIcon = int64(8800023) + quickReplyPeer := domain.Peer{Type: domain.PeerTypeUser, ID: userID} + verify.marks[quickReplyPeer] = domain.CustomVerification{ + VerifierBotID: 777000123, + Peer: quickReplyPeer, + IconDocumentID: quickReplyIcon, + Description: "Verified quick-reply peer", + } got, err := r.onMessagesSendMessage(ctx, &tg.MessagesSendMessageRequest{ Peer: &tg.InputPeerSelf{}, @@ -68,6 +89,17 @@ func TestQuickReplyRPCSaveListAndDeleteMessage(t *testing.T) { t.Fatalf("quick replies = %#v", list) } + quickReplyMessages, err := r.onMessagesGetQuickReplyMessages(ctx, &tg.MessagesGetQuickReplyMessagesRequest{ + ShortcutID: shortcutID, + }) + if err != nil { + t.Fatalf("onMessagesGetQuickReplyMessages: %v", err) + } + assertMessagesEnvelopeBotVerificationIcon(t, quickReplyMessages, quickReplyPeer, quickReplyIcon) + if verify.batchCalls != 0 || verify.peerCalls != 1 { + t.Fatalf("quick-reply verification reads = batch %d peer %d, want 0/1 for one peer", verify.batchCalls, verify.peerCalls) + } + deleted, err := r.onMessagesDeleteQuickReplyMessages(ctx, &tg.MessagesDeleteQuickReplyMessagesRequest{ ShortcutID: shortcutID, ID: []int{messageID}, diff --git a/internal/rpc/messages_compat.go b/internal/rpc/messages_compat.go index 919b0948..03993183 100644 --- a/internal/rpc/messages_compat.go +++ b/internal/rpc/messages_compat.go @@ -42,11 +42,13 @@ func (r *Router) onMessagesGetSavedHistory(ctx context.Context, req *tg.Messages if req.Hash != 0 { return &tg.MessagesMessagesNotModified{Count: 0}, nil } - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: []tg.MessageClass{}, Chats: r.savedHistoryChats(ctx, userID, hasParent, parentPeer, req.Peer), Users: []tg.UserClass{}, - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } // parent_peer = monoforum:返回该订阅者(req.Peer)在频道私信内的历史。 return r.monoforumSavedHistory(ctx, userID, mono, savedPeer, req.Limit, req.OffsetID) @@ -83,6 +85,7 @@ func (r *Router) onMessagesGetSavedHistory(ctx context.Context, req *tg.Messages m.Chats = mergeTGChats(m.Chats, chats) } } + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } diff --git a/internal/rpc/messages_history.go b/internal/rpc/messages_history.go index 6a70bbef..aa685d01 100644 --- a/internal/rpc/messages_history.go +++ b/internal/rpc/messages_history.go @@ -298,11 +298,13 @@ func (r *Router) onMessagesGetReplies(ctx context.Context, req *tg.MessagesGetRe } return r.tgChannelHistoryMessages(ctx, userID, r.enrichChannelHistory(ctx, userID, replies)), nil } - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: []tg.MessageClass{}, Chats: r.chatsForInputPeer(ctx, userID, req.Peer), Users: []tg.UserClass{}, - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } func (r *Router) onMessagesGetDiscussionMessage(ctx context.Context, req *tg.MessagesGetDiscussionMessageRequest) (*tg.MessagesDiscussionMessage, error) { @@ -596,7 +598,9 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea return nil, err } if emptyCommunitySearch { - return appendCommunitySearchChat(&tg.MessagesMessages{}, communityView), nil + result := appendCommunitySearchChat(&tg.MessagesMessages{}, communityView) + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } var private domain.MessageList if !req.BroadcastsOnly && !req.GroupsOnly && r.deps.Messages != nil { @@ -623,7 +627,9 @@ func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSea } } if req.UsersOnly || r.deps.Channels == nil { - return appendCommunitySearchChat(tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), communityView), nil + result := appendCommunitySearchChat(tgMessagesMessages(userID, r.enrichMessageList(ctx, userID, limitMessageList(private, limit))), communityView) + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } channelHistory, err := r.deps.Channels.SearchJoinedMessages(ctx, userID, domain.ChannelGlobalSearchRequest{ Query: query, diff --git a/internal/rpc/messages_monoforum.go b/internal/rpc/messages_monoforum.go index 8943da19..55e5b0f2 100644 --- a/internal/rpc/messages_monoforum.go +++ b/internal/rpc/messages_monoforum.go @@ -119,12 +119,14 @@ func (r *Router) monoforumSavedHistory(ctx context.Context, userID int64, mono d messages = append(messages, item) } } - return &tg.MessagesMessagesSlice{ + result := &tg.MessagesMessagesSlice{ Count: hist.Count, Messages: messages, Chats: r.monoforumChats(ctx, userID, mono), Users: r.monoforumSubscriberUsers(ctx, userID, nil, hist.Messages), - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } // monoforumChats 投影客户端 materialize monoforum 私信所需的频道:monoforum 自身直接投影, diff --git a/internal/rpc/messages_monoforum_rpc_test.go b/internal/rpc/messages_monoforum_rpc_test.go index 3480ffad..1f741c03 100644 --- a/internal/rpc/messages_monoforum_rpc_test.go +++ b/internal/rpc/messages_monoforum_rpc_test.go @@ -34,9 +34,11 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { channelStore := memory.NewChannelStore() channelSvc := appchannels.NewService(channelStore) + verify := newFakeBotVerifications() r := New(Config{}, Deps{ - Users: appusers.NewService(userStore), - Channels: channelSvc, + Users: appusers.NewService(userStore), + Channels: channelSvc, + BotVerifications: verify, }, zaptest.NewLogger(t), clock.System) created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{Title: "DM Broadcast", Broadcast: true, Date: 1000}) @@ -65,6 +67,14 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { } monoInput := &tg.InputPeerChannel{ChannelID: monoID, AccessHash: mono.AccessHash} parentInput := &tg.InputPeerChannel{ChannelID: created.Channel.ID, AccessHash: created.Channel.AccessHash} + const monoforumIcon = int64(8800025) + monoforumPeer := domain.Peer{Type: domain.PeerTypeChannel, ID: monoID} + verify.marks[monoforumPeer] = domain.CustomVerification{ + VerifierBotID: 777000123, + Peer: monoforumPeer, + IconDocumentID: monoforumIcon, + Description: "Verified monoforum peer", + } // TDesktop 点 Direct Messages 入口会先按 monoforum peer 拉普通 channel history。 // 主历史只应返回 monoforum 自身的 service messages,不能混入 saved_peer 子会话消息。 @@ -83,6 +93,7 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { if len(mainHistory.Messages) != 1 { t.Fatalf("main monoforum history = %d msgs, want only the creation service", len(mainHistory.Messages)) } + assertMessagesEnvelopeBotVerificationIcon(t, mainHistory, monoforumPeer, monoforumIcon) service, ok := mainHistory.Messages[0].(*tg.MessageService) if !ok { t.Fatalf("main monoforum message = %T, want MessageService", mainHistory.Messages[0]) @@ -167,6 +178,7 @@ func TestMonoforumSavedDialogsAndHistory(t *testing.T) { if err != nil { t.Fatalf("getSavedHistory(monoforum): %v", err) } + assertMessagesEnvelopeBotVerificationIcon(t, hres, monoforumPeer, monoforumIcon) var gotMsgs []tg.MessageClass switch m := hres.(type) { case *tg.MessagesMessages: diff --git a/internal/rpc/messages_polls.go b/internal/rpc/messages_polls.go index 946ae15d..6f4b5fbb 100644 --- a/internal/rpc/messages_polls.go +++ b/internal/rpc/messages_polls.go @@ -265,12 +265,14 @@ func (r *Router) onMessagesGetUnreadPollVotes(ctx context.Context, req *tg.Messa if topMsgID, ok := req.GetTopMsgID(); ok && (topMsgID < 0 || topMsgID > domain.MaxMessageBoxID) { return nil, messageIDInvalidErr() } - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: []tg.MessageClass{}, Topics: []tg.ForumTopicClass{}, Chats: r.chatsForInputPeer(ctx, userID, req.Peer), Users: []tg.UserClass{}, - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } func (r *Router) onMessagesReadPollVotes(ctx context.Context, req *tg.MessagesReadPollVotesRequest) (*tg.MessagesAffectedHistory, error) { diff --git a/internal/rpc/messages_quick_replies.go b/internal/rpc/messages_quick_replies.go index 23d8ce66..3c2574fe 100644 --- a/internal/rpc/messages_quick_replies.go +++ b/internal/rpc/messages_quick_replies.go @@ -141,6 +141,7 @@ func (r *Router) onMessagesGetQuickReplyMessages(ctx context.Context, req *tg.Me } out := tgMessagesQuickReplyMessages(list) out.Users = r.quickReplyUsers(ctx, userID) + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } diff --git a/internal/rpc/messages_saved_dialogs_rpc_test.go b/internal/rpc/messages_saved_dialogs_rpc_test.go index 6b549d30..10f273bb 100644 --- a/internal/rpc/messages_saved_dialogs_rpc_test.go +++ b/internal/rpc/messages_saved_dialogs_rpc_test.go @@ -447,6 +447,16 @@ func TestMessagesGetSavedDialogsByID(t *testing.T) { func TestMessagesGetSavedHistoryFiltersBySavedPeer(t *testing.T) { ctx := context.Background() r, _, alice, bob, noteID, fwdID := savedDialogsFixture(t) + verify := newFakeBotVerifications() + r.deps.BotVerifications = verify + const savedHistoryIcon = int64(8800022) + bobPeer := domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID} + verify.marks[bobPeer] = domain.CustomVerification{ + VerifierBotID: 777000123, + Peer: bobPeer, + IconDocumentID: savedHistoryIcon, + Description: "Verified saved peer", + } bobHistory, err := r.onMessagesGetSavedHistory(WithUserID(ctx, alice.ID), &tg.MessagesGetSavedHistoryRequest{ Peer: &tg.InputPeerUser{UserID: bob.ID, AccessHash: bob.AccessHash}, @@ -455,6 +465,7 @@ func TestMessagesGetSavedHistoryFiltersBySavedPeer(t *testing.T) { if err != nil { t.Fatalf("getSavedHistory bob: %v", err) } + assertMessagesEnvelopeBotVerificationIcon(t, bobHistory, bobPeer, savedHistoryIcon) bobMsgs := savedHistoryMessages(t, bobHistory) if len(bobMsgs) != 1 || bobMsgs[0].ID != fwdID { t.Fatalf("bob saved history = %+v, want forwarded message only", bobMsgs)