Merge PR #27: preserve verification icons in message envelopes

Reviewed against current telesrv/main as PASS_WITH_FOLLOW_UP. Preserve the contributor commit; maintainer canonicalization will complete the bounded response-boundary coverage and tests.
This commit is contained in:
yialdd 2026-07-30 13:45:56 +08:00 committed by GitHub
commit 7713847b2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 138 additions and 19 deletions

View file

@ -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)
}
})
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)