From 5401c9a364a5855a29fc25d4bc2c72c09805cd4b Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:49:41 +0800 Subject: [PATCH] fix(messages): sync Android pinned message search --- .../rpc/channels_multi_pin_android_rpc_test.go | 9 +++++++-- internal/rpc/messages_history.go | 11 ++++++++--- internal/rpc/messages_history_rpc_test.go | 3 +++ internal/rpc/messages_reactions_test.go | 17 +++++++++++++++++ 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/internal/rpc/channels_multi_pin_android_rpc_test.go b/internal/rpc/channels_multi_pin_android_rpc_test.go index e36f7071..b23fc117 100644 --- a/internal/rpc/channels_multi_pin_android_rpc_test.go +++ b/internal/rpc/channels_multi_pin_android_rpc_test.go @@ -97,12 +97,17 @@ func TestChannelMultiPinAndroidOpenAndJump(t *testing.T) { } // ① 打开聊天:MediaDataController.loadPinnedMessages → messages.search filterPinned。 - searchEnc := dispatch(&tg.MessagesSearchRequest{ + androidPinnedSearch := &tg.MessagesSearchRequest{ Peer: peer, Q: "", Filter: &tg.InputMessagesFilterPinned{}, Limit: 40, - }) + } + // DrKLO initializes saved_reaction to an empty non-nil ArrayList and its + // serializer consequently emits flags.3 + Vector length 0 on every + // messages.search, including channel filterPinned. + androidPinnedSearch.SetSavedReaction([]tg.ReactionClass{}) + searchEnc := dispatch(androidPinnedSearch) channelMessages, ok := searchEnc.(*tg.MessagesChannelMessages) if !ok { t.Fatalf("pinned search response = %T, want messages.channelMessages", searchEnc) diff --git a/internal/rpc/messages_history.go b/internal/rpc/messages_history.go index e1bffd3c..c0002968 100644 --- a/internal/rpc/messages_history.go +++ b/internal/rpc/messages_history.go @@ -804,8 +804,13 @@ func (r *Router) messageFilterFromSearchRequest(ctx context.Context, userID int6 filter.Peer = peer } savedReactions, hasSavedReactions := req.GetSavedReaction() + // An empty optional vector carries no reaction-filtering semantics. Some TL + // clients emit flags.3 with a zero-length vector on ordinary peer searches. + // Keep the wire presence intact at the TL edge, but only apply Saved + // Messages scope and reaction validation when the vector has values. + hasSavedReactionFilter := hasSavedReactions && len(savedReactions) > 0 savedPeerInput, hasSavedPeer := req.GetSavedPeerID() - if hasSavedReactions || hasSavedPeer { + if hasSavedReactionFilter || hasSavedPeer { if !filter.HasPeer || filter.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: userID}) { return domain.MessageFilter{}, peerIDInvalidErr() @@ -821,8 +826,8 @@ func (r *Router) messageFilterFromSearchRequest(ctx context.Context, userID int6 } filter.SavedPeer = savedPeer } - if hasSavedReactions { - if len(savedReactions) == 0 || len(savedReactions) > maxReactionVector { + if hasSavedReactionFilter { + if len(savedReactions) > maxReactionVector { return domain.MessageFilter{}, reactionInvalidErr() } seen := make(map[string]struct{}, len(savedReactions)) diff --git a/internal/rpc/messages_history_rpc_test.go b/internal/rpc/messages_history_rpc_test.go index ebcf66dd..5443ba8f 100644 --- a/internal/rpc/messages_history_rpc_test.go +++ b/internal/rpc/messages_history_rpc_test.go @@ -225,6 +225,9 @@ func TestMessagesSearchChannelPeerReturnsSingleCopyMessages(t *testing.T) { Filter: &tg.InputMessagesFilterPhotos{}, Limit: 0, } + // Match DrKLO's ordinary messages.search wire shape: flags.3 is present + // even though the saved_reaction vector is empty. + mediaCountReq.SetSavedReaction([]tg.ReactionClass{}) in.Reset() if err := mediaCountReq.Encode(&in); err != nil { t.Fatalf("encode shared media count search: %v", err) diff --git a/internal/rpc/messages_reactions_test.go b/internal/rpc/messages_reactions_test.go index d102a582..d47d2afb 100644 --- a/internal/rpc/messages_reactions_test.go +++ b/internal/rpc/messages_reactions_test.go @@ -234,6 +234,23 @@ func TestMessageFilterFromSearchRequestParsesSavedTagsAndPeer(t *testing.T) { if _, err := r.messageFilterFromSearchRequest(WithUserID(context.Background(), userID), userID, req); !tgerr.Is(err, "PEER_ID_INVALID") { t.Fatalf("non-self saved search err = %v, want PEER_ID_INVALID", err) } + + emptyTagReq := &tg.MessagesSearchRequest{ + Peer: &tg.InputPeerUser{UserID: userID + 1, AccessHash: 1}, + Q: "ordinary", + Filter: &tg.InputMessagesFilterEmpty{}, + Limit: 20, + } + emptyTagReq.SetSavedReaction([]tg.ReactionClass{}) + ordinary, err := r.messageFilterFromSearchRequest(WithUserID(context.Background(), userID), userID, emptyTagReq) + if err != nil { + t.Fatalf("empty saved reaction on ordinary peer search: %v", err) + } + if !ordinary.HasPeer || + ordinary.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: userID + 1}) || + len(ordinary.SavedReactions) != 0 { + t.Fatalf("ordinary peer filter with empty saved reaction = %+v", ordinary) + } } func TestMessagesGetDefaultTagReactionsReturnsHashableCatalog(t *testing.T) {