diff --git a/internal/rpc/bot_verification_flags_test.go b/internal/rpc/bot_verification_flags_test.go index 12855218..01083ac3 100644 --- a/internal/rpc/bot_verification_flags_test.go +++ b/internal/rpc/bot_verification_flags_test.go @@ -679,3 +679,106 @@ func TestUsersGetUsersResolvesBotVerificationInOneBatch(t *testing.T) { verify.batchCalls, verify.peerCalls) } } + +// TestOpeningChatMessageLookupsKeepBotVerificationIcons covers the supplemental +// message lookups official clients issue while opening a chat. These responses +// update the same peer cache as messages.getDialogs, so returning an unstamped +// user/channel here makes a visible badge disappear until the dialogs response is +// loaded again. +func TestOpeningChatMessageLookupsKeepBotVerificationIcons(t *testing.T) { + t.Run("private messages.getMessages", func(t *testing.T) { + const ( + viewerID = int64(1000000001) + targetID = int64(1000000002) + iconID = int64(8800010) + ) + verify := newFakeBotVerifications() + peer := domain.Peer{Type: domain.PeerTypeUser, ID: targetID} + verify.marks[peer] = domain.CustomVerification{ + VerifierBotID: 777000123, + Peer: peer, + IconDocumentID: iconID, + Description: "Verified by Acme Trust", + } + r := New(Config{}, Deps{ + Messages: &captureMessages{list: domain.MessageList{ + Messages: []domain.Message{{ + ID: 7, + OwnerUserID: viewerID, + Peer: peer, + From: peer, + Date: 1700000000, + Body: "reply source", + }}, + Count: 1, + }}, + Users: mapUsersService{users: map[int64]domain.User{ + viewerID: {ID: viewerID, FirstName: "Viewer"}, + targetID: {ID: targetID, FirstName: "Target"}, + }}, + BotVerifications: verify, + }, zaptest.NewLogger(t), clock.System) + + result, err := r.onMessagesGetMessages( + WithUserID(context.Background(), viewerID), + []tg.InputMessageClass{&tg.InputMessageID{ID: 7}}, + ) + if err != nil { + t.Fatalf("messages.getMessages: %v", err) + } + box := result.(*tg.MessagesMessages) + if len(box.Users) != 1 { + t.Fatalf("users = %d, want target user", len(box.Users)) + } + user := &tg.User{} + tlRoundTrip(t, box.Users[0].(*tg.User), user) + if icon, ok := user.GetBotVerificationIcon(); !ok || icon != iconID { + t.Fatalf("opening-chat user bot_verification_icon = %d, ok=%v, want %d", icon, ok, iconID) + } + }) + + t.Run("channel channels.getMessages", func(t *testing.T) { + const iconID = int64(8800011) + f := newBotVerificationFixture(t, newFakeBotVerifications()) + group := f.botVerificationGroup(t, "Verified Group") + sent, err := f.router.onMessagesSendMessage( + WithUserID(context.Background(), f.owner.ID), + &tg.MessagesSendMessageRequest{ + Peer: &tg.InputPeerChannel{ChannelID: group.ID, AccessHash: group.AccessHash}, + Message: "pinned source", + RandomID: 8800011, + }, + ) + if err != nil { + t.Fatalf("send channel message: %v", err) + } + messageID := sent.(*tg.Updates).Updates[1].(*tg.UpdateNewChannelMessage).Message.(*tg.Message).ID + peer := domain.Peer{Type: domain.PeerTypeChannel, ID: group.ID} + f.verify.marks[peer] = domain.CustomVerification{ + VerifierBotID: f.bot.ID, + Peer: peer, + IconDocumentID: iconID, + Description: "Verified by Acme Trust", + } + + result, err := f.router.onChannelsGetMessages( + WithUserID(context.Background(), f.owner.ID), + &tg.ChannelsGetMessagesRequest{ + Channel: &tg.InputChannel{ChannelID: group.ID, AccessHash: group.AccessHash}, + ID: []tg.InputMessageClass{&tg.InputMessageID{ID: messageID}}, + }, + ) + if err != nil { + t.Fatalf("channels.getMessages: %v", err) + } + box := result.(*tg.MessagesMessages) + if len(box.Chats) != 1 { + t.Fatalf("chats = %d, want target channel", len(box.Chats)) + } + channel := &tg.Channel{} + tlRoundTrip(t, box.Chats[0].(*tg.Channel), channel) + if icon, ok := channel.GetBotVerificationIcon(); !ok || icon != iconID { + t.Fatalf("opening-chat channel bot_verification_icon = %d, ok=%v, want %d", icon, ok, iconID) + } + }) +} diff --git a/internal/rpc/channels_messages.go b/internal/rpc/channels_messages.go index 67c10c45..41e1455f 100644 --- a/internal/rpc/channels_messages.go +++ b/internal/rpc/channels_messages.go @@ -293,11 +293,13 @@ func (r *Router) onChannelsGetMessages(ctx context.Context, req *tg.ChannelsGetM messages = append(messages, &tg.MessageEmpty{ID: id}) } } - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: messages, Chats: tgChannels(userID, []domain.Channel{history.Channel}), Users: r.tgUsersForViewer(userID, history.Users), // viewer 补拉自己的消息(含置顶)须带 self - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } func (r *Router) onChannelsDeleteMessages(ctx context.Context, req *tg.ChannelsDeleteMessagesRequest) (*tg.MessagesAffectedMessages, error) { diff --git a/internal/rpc/messages_history.go b/internal/rpc/messages_history.go index c0002968..6a70bbef 100644 --- a/internal/rpc/messages_history.go +++ b/internal/rpc/messages_history.go @@ -490,11 +490,13 @@ func (r *Router) onMessagesGetMessages(ctx context.Context, ids []tg.InputMessag out = append(out, tgMessage(msg)) } chats := r.chatsForMessageUpdates(ctx, userID, found) - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: out, Users: r.usersForMessageUpdates(ctx, userID, found), Chats: chats, - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } // onMessagesGetRichMessage 返回单条消息的完整富文本(Layer 227 richMessage)。消息列表 @@ -535,11 +537,13 @@ func (r *Router) onMessagesGetRichMessage(ctx context.Context, req *tg.MessagesG if len(out) == 0 { out = append(out, &tg.MessageEmpty{ID: req.ID}) } - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: out, Users: r.usersForMessageUpdates(ctx, userID, found), Chats: r.chatsForMessageUpdates(ctx, userID, found), - }, nil + } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } func (r *Router) onMessagesSearchGlobal(ctx context.Context, req *tg.MessagesSearchGlobalRequest) (tg.MessagesMessagesClass, error) { diff --git a/internal/rpc/messages_reactions_lists.go b/internal/rpc/messages_reactions_lists.go index 0f3250c9..dbe0d15e 100644 --- a/internal/rpc/messages_reactions_lists.go +++ b/internal/rpc/messages_reactions_lists.go @@ -301,7 +301,7 @@ func (r *Router) onMessagesGetUnreadReactions(ctx context.Context, req *tg.Messa out.Messages = append(out.Messages, item) } } - r.applyStoryMaxIDsToMessages(ctx, userID, out) + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } out := &tg.MessagesMessages{ @@ -310,7 +310,7 @@ func (r *Router) onMessagesGetUnreadReactions(ctx context.Context, req *tg.Messa Chats: r.chatsForInputPeer(ctx, userID, req.Peer), Users: []tg.UserClass{}, } - r.applyStoryMaxIDsToMessages(ctx, userID, out) + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } diff --git a/internal/rpc/messages_read.go b/internal/rpc/messages_read.go index 02249d3e..cbd60e71 100644 --- a/internal/rpc/messages_read.go +++ b/internal/rpc/messages_read.go @@ -130,7 +130,7 @@ func (r *Router) onMessagesGetUnreadMentions(ctx context.Context, req *tg.Messag Chats: r.chatsForInputPeer(ctx, userID, req.Peer), Users: []tg.UserClass{}, } - r.applyStoryMaxIDsToMessages(ctx, userID, out) + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } diff --git a/internal/rpc/messages_register.go b/internal/rpc/messages_register.go index 38520986..6cd84ab0 100644 --- a/internal/rpc/messages_register.go +++ b/internal/rpc/messages_register.go @@ -726,7 +726,7 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) { Chats: []tg.ChatClass{tgChannelChatForView(userID, view)}, Users: []tg.UserClass{}, } - r.applyStoryMaxIDsToMessages(ctx, userID, out) + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } if searchFilterNeedsMediaStore(req.Filter) { @@ -746,7 +746,7 @@ func (r *Router) registerMessages(d *tlprofile.Dispatcher) { Chats: []tg.ChatClass{tgChannelChatForView(userID, view)}, Users: []tg.UserClass{}, } - r.applyStoryMaxIDsToMessages(ctx, userID, out) + r.applyPeerReadModelsToMessages(ctx, userID, out) return out, nil } if err := r.validateInputPeerChannelAccess(ctx, userID, req.Peer, filter.Peer.ID); err != nil { diff --git a/internal/rpc/messages_scheduled.go b/internal/rpc/messages_scheduled.go index 0eb6ea37..e347d7ae 100644 --- a/internal/rpc/messages_scheduled.go +++ b/internal/rpc/messages_scheduled.go @@ -20,7 +20,9 @@ func (r *Router) onMessagesGetScheduledMessages(ctx context.Context, req *tg.Mes } scheduledSvc, ok := r.deps.Messages.(scheduledMessagesService) if r.deps.Messages == nil || !ok { - return &tg.MessagesMessages{Chats: r.chatsForMessageUpdate(ctx, userID, domain.Message{Peer: peer})}, nil + result := &tg.MessagesMessages{Chats: r.chatsForMessageUpdate(ctx, userID, domain.Message{Peer: peer})} + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } list, err := scheduledSvc.GetScheduledMessages(ctx, userID, domain.ScheduledMessageFilter{ OwnerUserID: userID, @@ -48,7 +50,9 @@ func (r *Router) onMessagesGetScheduledHistory(ctx context.Context, req *tg.Mess if req.Hash != 0 { return &tg.MessagesMessagesNotModified{Count: 0}, nil } - return &tg.MessagesMessages{Chats: r.chatsForMessageUpdate(ctx, userID, domain.Message{Peer: peer})}, nil + result := &tg.MessagesMessages{Chats: r.chatsForMessageUpdate(ctx, userID, domain.Message{Peer: peer})} + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result, nil } list, err := scheduledSvc.ListScheduledMessages(ctx, userID, domain.ScheduledMessageFilter{ OwnerUserID: userID, @@ -284,11 +288,13 @@ func (r *Router) tgScheduledMessages(ctx context.Context, userID int64, peer dom if len(chats) == 0 && peer.Type == domain.PeerTypeChannel { chats = r.chatsForMessageUpdate(ctx, userID, domain.Message{Peer: peer}) } - return &tg.MessagesMessages{ + result := &tg.MessagesMessages{ Messages: out, Chats: chats, Users: r.usersForMessageUpdates(ctx, userID, messages), } + r.applyPeerReadModelsToMessages(ctx, userID, result) + return result } func (r *Router) tgNewScheduledMessageUpdates(ctx context.Context, userID int64, msg domain.ScheduledMessage, randomID int64, date int) *tg.Updates { diff --git a/internal/rpc/story_peer_projection.go b/internal/rpc/story_peer_projection.go index 72508df0..eb57bc09 100644 --- a/internal/rpc/story_peer_projection.go +++ b/internal/rpc/story_peer_projection.go @@ -113,13 +113,13 @@ func (r *Router) tgResolvedChannelPeerWithStories(ctx context.Context, viewerUse func (r *Router) tgGlobalChannelMessages(ctx context.Context, viewerUserID int64, history domain.ChannelHistory) tg.MessagesMessagesClass { out := tgGlobalChannelMessages(viewerUserID, history) - r.applyStoryMaxIDsToMessages(ctx, viewerUserID, out) + r.applyPeerReadModelsToMessages(ctx, viewerUserID, out) return out } func (r *Router) tgMessagesMessages(ctx context.Context, viewerUserID int64, list domain.MessageList) tg.MessagesMessagesClass { out := tgMessagesMessages(viewerUserID, list) - r.applyStoryMaxIDsToMessages(ctx, viewerUserID, out) + r.applyPeerReadModelsToMessages(ctx, viewerUserID, out) return out } @@ -135,7 +135,7 @@ func (r *Router) tgChannelHistoryMessages(ctx context.Context, viewerUserID int6 value.Chats = replaceTGChat(value.Chats, linked) } } - r.applyStoryMaxIDsToMessages(ctx, viewerUserID, out) + r.applyPeerReadModelsToMessages(ctx, viewerUserID, out) return out } @@ -174,11 +174,15 @@ func (r *Router) applyStoryMaxIDsToMessageReactionsList(ctx context.Context, vie func (r *Router) tgGlobalSearchMessages(ctx context.Context, viewerUserID int64, limit int, private domain.MessageList, channel domain.ChannelHistory) tg.MessagesMessagesClass { out := tgGlobalSearchMessages(viewerUserID, limit, private, channel) - r.applyStoryMaxIDsToMessages(ctx, viewerUserID, out) + r.applyPeerReadModelsToMessages(ctx, viewerUserID, out) return out } -func (r *Router) applyStoryMaxIDsToMessages(ctx context.Context, viewerUserID int64, out tg.MessagesMessagesClass) { +// applyPeerReadModelsToMessages stamps every user/channel carried by a messages +// envelope. Supplemental lookups such as messages.getMessages and +// channels.getMessages update the same client-side peer cache as getDialogs, so +// they must use the same response-boundary overlays as history and search. +func (r *Router) applyPeerReadModelsToMessages(ctx context.Context, viewerUserID int64, out tg.MessagesMessagesClass) { switch v := out.(type) { case *tg.MessagesMessages: r.applyPeerReadModels(ctx, viewerUserID, v.Users, v.Chats)