From cec4361d8bbd62bcd15d67a22cab6e8ad1dee940 Mon Sep 17 00:00:00 2001 From: iamxvbaba <28732408+iamxvbaba@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:16:23 +0800 Subject: [PATCH] fix(rpc): sync peer verification badge persistence --- internal/rpc/bot_verification_flags_test.go | 65 +++++++++++++++++++++ internal/rpc/messages_dialogs.go | 9 ++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/internal/rpc/bot_verification_flags_test.go b/internal/rpc/bot_verification_flags_test.go index 2516aa08..12855218 100644 --- a/internal/rpc/bot_verification_flags_test.go +++ b/internal/rpc/bot_verification_flags_test.go @@ -246,6 +246,71 @@ func TestUserAndUserFullCarryBotVerificationOnLayer228Bits(t *testing.T) { } } +// TestPeerSettingsUserPreservesBotVerificationOnLayer228Bit covers the chat-open +// race behind a disappearing badge. Official clients merge messages.peerSettings +// users into the same peer cache as users.getFullUser; therefore the auxiliary +// User must carry the same flags2.14 value regardless of which RPC arrives last. +func TestPeerSettingsUserPreservesBotVerificationOnLayer228Bit(t *testing.T) { + f := newBotVerificationFixture(t, newFakeBotVerifications()) + f.enableVerifier(f.bot.ID, 8800011, true) + ownerCtx := WithUserID(context.Background(), f.owner.ID) + peer := &tg.InputPeerUser{UserID: f.target.ID, AccessHash: f.target.AccessHash} + + peerSettingsUser := func() *tg.User { + t.Helper() + out, err := f.router.onMessagesGetPeerSettings(ownerCtx, peer) + if err != nil { + t.Fatalf("get peer settings: %v", err) + } + if len(out.Users) != 1 { + t.Fatalf("get peer settings returned %d users, want 1", len(out.Users)) + } + wire := &tg.User{} + tlRoundTrip(t, out.Users[0].(*tg.User), wire) + return wire + } + + plain := peerSettingsUser() + if _, ok := plain.GetBotVerificationIcon(); ok { + t.Fatalf("unmarked peer settings user carries an icon: %+v", plain) + } + + if ok, err := f.router.onBotsSetCustomVerification(ownerCtx, + setCustomVerificationRequest(inputPeerUser(f.target), inputUser(f.bot), true, "Stable in chat cache")); err != nil || !ok { + t.Fatalf("grant = %v,%v, want true,nil", ok, err) + } + + // Exercise both possible response orders. Neither fullUser -> peerSettings nor + // peerSettings -> fullUser may turn the marked peer back into an unmarked one. + markedFull, _ := getFullUserProjection(t, f.router, f.owner.ID, f.target) + fullMark, ok := markedFull.GetBotVerification() + if !ok || fullMark.Icon != 8800011 { + t.Fatalf("userFull bot_verification = %+v, ok=%v, want icon 8800011", fullMark, ok) + } + marked := peerSettingsUser() + icon, ok := marked.GetBotVerificationIcon() + if !ok || icon != 8800011 { + t.Fatalf("peer settings user bot_verification_icon = %d, ok=%v, want 8800011", icon, ok) + } + assertFlagBitDelta(t, "peerSettings.user", plain.Flags2, marked.Flags2, 14) + + marked = peerSettingsUser() + markedFull, _ = getFullUserProjection(t, f.router, f.owner.ID, f.target) + icon, ok = marked.GetBotVerificationIcon() + fullMark, fullOK := markedFull.GetBotVerification() + if !ok || icon != 8800011 || !fullOK || fullMark.Icon != icon { + t.Fatalf("reverse order drift: peer icon=%d ok=%v full=%+v ok=%v", icon, ok, fullMark, fullOK) + } + + if ok, err := f.router.onBotsSetCustomVerification(ownerCtx, + setCustomVerificationRequest(inputPeerUser(f.target), inputUser(f.bot), false, "")); err != nil || !ok { + t.Fatalf("revoke = %v,%v, want true,nil", ok, err) + } + if revoked := peerSettingsUser(); revoked.Flags2.Has(14) { + t.Fatalf("revoked peer settings user still carries flags2.14: %032b", uint32(revoked.Flags2)) + } +} + // TestBotInfoCarriesVerifierSettingsOnFlags9 pins botInfo#4d8a0299 // verifier_settings:flags.9 inside the verifier bot's own userFull, and the operator // kill switch: a disabled verifier stops advertising itself. diff --git a/internal/rpc/messages_dialogs.go b/internal/rpc/messages_dialogs.go index 4c69995d..a4e3359e 100644 --- a/internal/rpc/messages_dialogs.go +++ b/internal/rpc/messages_dialogs.go @@ -534,9 +534,16 @@ func (r *Router) onMessagesGetPeerSettings(ctx context.Context, input tg.InputPe } r.peerSettingsProjectionCache.StoreIfEpoch(userID, peer, settings, loadEpoch) } + users := r.peerSettingsUsers(ctx, userID, input) + // messages.getPeerSettings is requested while a chat is opened and official + // clients feed the returned peer objects into the same cache as getDialogs and + // getFullUser. Keep these auxiliary objects on the common response-boundary + // projection path: a full User without bot_verification_icon is authoritative + // to TDesktop/Android/TWeb and would clear a badge learned from another RPC. + r.applyPeerReadModels(ctx, userID, users, nil) return &tg.MessagesPeerSettings{ Settings: tgPeerSettings(settings), - Users: r.peerSettingsUsers(ctx, userID, input), + Users: users, }, nil }