From 08c763b68312371ec5f08691672de62c3caf4107 Mon Sep 17 00:00:00 2001 From: A Date: Mon, 20 Jul 2026 16:50:37 +0800 Subject: [PATCH] fix: sync legacy community projection Sync telesrv 07644e0 (fix(communities): hide aggregates from legacy layers). --- internal/rpc/communities.go | 13 +++++++++++++ internal/rpc/communities_rpc_test.go | 25 ++++++++++++++++++++++--- internal/rpc/dialogs_pinned.go | 22 +++++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/internal/rpc/communities.go b/internal/rpc/communities.go index 65221d10..ec6434b4 100644 --- a/internal/rpc/communities.go +++ b/internal/rpc/communities.go @@ -9,6 +9,8 @@ import ( "telesrv/internal/domain" ) +const communitiesLayer = 228 + func communityErr(err error) error { switch { case err == nil: @@ -160,6 +162,17 @@ func (r *Router) communityMutationUpdates(ctx context.Context, userID int64, vie } func (r *Router) withCommunityDialogList(ctx context.Context, userID int64, filter domain.DialogFilter, list domain.DialogList) (domain.DialogList, error) { + if LayerFrom(ctx) < communitiesLayer { + return list, nil + } + return r.withCollapsedCommunityDialogs(ctx, userID, filter, list) +} + +// withCollapsedCommunityDialogs applies the account-level Community dialog +// state without a wire-layer visibility decision. Business invariants such as +// the shared pinned limit use this path; RPC response construction must use +// withCommunityDialogList instead. +func (r *Router) withCollapsedCommunityDialogs(ctx context.Context, userID int64, filter domain.DialogFilter, list domain.DialogList) (domain.DialogList, error) { if r.deps.Communities == nil || (filter.HasFolderID && filter.FolderID != domain.DialogMainFolderID) { return list, nil } diff --git a/internal/rpc/communities_rpc_test.go b/internal/rpc/communities_rpc_test.go index ca073d9c..f0bfaa60 100644 --- a/internal/rpc/communities_rpc_test.go +++ b/internal/rpc/communities_rpc_test.go @@ -39,7 +39,7 @@ func communityRPCChannel(t *testing.T, service *appchannels.Service, creator dom } func TestCommunityDialogsSharePinnedLimit(t *testing.T) { - ctx := context.Background() + ctx := WithLayer(context.Background(), communitiesLayer) users := memory.NewUserStore() owner, err := users.Create(ctx, domain.User{AccessHash: 711, Phone: "15552000011", FirstName: "Pin Owner"}) if err != nil { @@ -102,6 +102,21 @@ func TestCommunityDialogsSharePinnedLimit(t *testing.T) { if len(order) != domain.MaxPinnedDialogsMainFolder || order[0] != (domain.Peer{Type: domain.PeerTypeChannel, ID: ordinary.ID}) { t.Fatalf("combined pinned order = %+v (dialogs=%+v communities=%+v count=%d), want ordinary dialog promoted above Communities", order, pinned.Dialogs, pinned.Communities, pinned.Count) } + legacyPinned, err := r.pinnedDialogsList(WithLayer(ctx, 227), owner.ID, domain.DialogMainFolderID) + if err != nil { + t.Fatal(err) + } + if len(legacyPinned.Communities) != 0 || len(legacyPinned.Dialogs) != 1 || legacyPinned.Count != 1 { + t.Fatalf("Layer 227 pinned dialogs = %+v, want only the ordinary pinned dialog", legacyPinned) + } + legacyOrdinary := communityRPCChannel(t, channelService, owner, "Legacy Ordinary Pinned Channel") + legacyToggle := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeer{Peer: &tg.InputPeerChannel{ + ChannelID: legacyOrdinary.ID, AccessHash: legacyOrdinary.AccessHash, + }}} + legacyToggle.SetPinned(true) + if ok, err := r.onMessagesToggleDialogPin(WithLayer(WithUserID(ctx, owner.ID), 227), legacyToggle); err == nil || ok || !tgerr.Is(err, "PINNED_DIALOGS_TOO_MUCH") { + t.Fatalf("Layer 227 pin beyond shared account limit = %v, %v", ok, err) + } overLimit := &tg.MessagesToggleDialogPinRequest{Peer: &tg.InputDialogPeerCommunity{Community: inputs[len(inputs)-1]}} overLimit.SetPinned(true) if ok, err := r.onMessagesToggleDialogPin(WithUserID(ctx, owner.ID), overLimit); err == nil || ok || !tgerr.Is(err, "PINNED_DIALOGS_TOO_MUCH") { @@ -116,7 +131,7 @@ func TestCommunityDialogsSharePinnedLimit(t *testing.T) { } func TestCommunitiesRPCLayer228Lifecycle(t *testing.T) { - ctx := context.Background() + ctx := WithLayer(context.Background(), communitiesLayer) userStore := memory.NewUserStore() owner, _ := userStore.Create(ctx, domain.User{AccessHash: 701, Phone: "15552000001", FirstName: "Owner"}) member, _ := userStore.Create(ctx, domain.User{AccessHash: 702, Phone: "15552000002", FirstName: "Member"}) @@ -186,8 +201,12 @@ func TestCommunitiesRPCLayer228Lifecycle(t *testing.T) { if len(collapsed.Chats) == 0 || !collapsed.Chats[0].(*tg.Community).CollapsedInDialogs { t.Fatalf("collapsed updates = %+v", collapsed.Chats) } + legacyList, err := r.withCommunityDialogList(WithLayer(ctx, 227), owner.ID, domain.DialogFilter{}, domain.DialogList{Count: 7}) + if err != nil || len(legacyList.Communities) != 0 || legacyList.Count != 7 { + t.Fatalf("Layer 227 community dialog projection = %+v err=%v, want unchanged list", legacyList, err) + } list, err := r.withCommunityDialogList(ctx, owner.ID, domain.DialogFilter{}, domain.DialogList{}) - if err != nil || len(list.Communities) != 1 { + if err != nil || len(list.Communities) != 1 || list.Count != 1 { t.Fatalf("community dialog list = %+v err=%v", list, err) } dialogs := tgMessagesDialogs(owner.ID, list).(*tg.MessagesDialogs) diff --git a/internal/rpc/dialogs_pinned.go b/internal/rpc/dialogs_pinned.go index 62236267..811438ec 100644 --- a/internal/rpc/dialogs_pinned.go +++ b/internal/rpc/dialogs_pinned.go @@ -9,6 +9,14 @@ import ( ) func (r *Router) pinnedDialogsList(ctx context.Context, userID int64, folderID int) (domain.DialogList, error) { + list, err := r.pinnedDialogsBaseList(ctx, userID, folderID) + if err != nil { + return domain.DialogList{}, err + } + return r.withCommunityDialogList(ctx, userID, domain.DialogFilter{PinnedOnly: true, HasFolderID: true, FolderID: folderID}, list) +} + +func (r *Router) pinnedDialogsBaseList(ctx context.Context, userID int64, folderID int) (domain.DialogList, error) { if r == nil { return domain.DialogList{}, nil } @@ -28,11 +36,19 @@ func (r *Router) pinnedDialogsList(ctx context.Context, userID int64, folderID i return domain.DialogList{}, err } if list, ok := value.(domain.DialogList); ok { - return r.withCommunityDialogList(ctx, userID, domain.DialogFilter{PinnedOnly: true, HasFolderID: true, FolderID: folderID}, list) + return list, nil } return domain.DialogList{}, nil } +func (r *Router) combinedPinnedDialogsList(ctx context.Context, userID int64, folderID int) (domain.DialogList, error) { + list, err := r.pinnedDialogsBaseList(ctx, userID, folderID) + if err != nil { + return domain.DialogList{}, err + } + return r.withCollapsedCommunityDialogs(ctx, userID, domain.DialogFilter{PinnedOnly: true, HasFolderID: true, FolderID: folderID}, list) +} + // combinedPinnedDialogPeers merges ordinary dialogs and collapsed Communities // by their shared server order. The two persistence implementations deliberately // store their own rows, but Layer 228 exposes one messages.getPinnedDialogs list. @@ -74,7 +90,7 @@ func combinedPinnedDialogPeers(list domain.DialogList) []domain.Peer { } func (r *Router) ensureCombinedPinCapacity(ctx context.Context, userID int64, folderID int, peer domain.Peer) error { - list, err := r.pinnedDialogsList(ctx, userID, folderID) + list, err := r.combinedPinnedDialogsList(ctx, userID, folderID) if err != nil { return err } @@ -94,7 +110,7 @@ func (r *Router) ensureCombinedPinCapacity(ctx context.Context, userID int64, fo // dialogs and Communities. It is called after the underlying row is pinned so // both stores can project the same mixed order without owning each other's data. func (r *Router) promoteCombinedPinnedDialog(ctx context.Context, userID int64, folderID int, peer domain.Peer) error { - list, err := r.pinnedDialogsList(ctx, userID, folderID) + list, err := r.combinedPinnedDialogsList(ctx, userID, folderID) if err != nil { return err }