From 35e5d38f4d8d8dc20c53946693badc7850ee62b8 Mon Sep 17 00:00:00 2001 From: A Date: Fri, 3 Jul 2026 13:26:33 +0800 Subject: [PATCH] fix: sync latest channel and contact fixes --- internal/app/channels/service_test.go | 4 +- internal/app/contacts/service.go | 26 +++++- internal/app/contacts/service_test.go | 89 +++++++++++++++++++ internal/app/dialogs/service.go | 3 + internal/app/dialogs/service_test.go | 54 +++-------- .../rpc/channels_public_preview_rpc_test.go | 37 ++------ internal/rpc/contacts_users_rpc_test.go | 79 ++++++++++++++++ internal/store/memory/channel_dialogs.go | 18 +++- internal/store/memory/channel_updates.go | 10 +++ .../channel_difference_integration_test.go | 6 +- internal/store/postgres/channel_updates.go | 10 +++ 11 files changed, 255 insertions(+), 81 deletions(-) diff --git a/internal/app/channels/service_test.go b/internal/app/channels/service_test.go index 80c140d4..b907f70d 100644 --- a/internal/app/channels/service_test.go +++ b/internal/app/channels/service_test.go @@ -2748,8 +2748,8 @@ func TestPublicChannelPreviewAllowsNonMemberHistory(t *testing.T) { if err != nil { t.Fatalf("non-member GetDifference public preview: %v", err) } - if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.NewMessages) != 1 || diff.NewMessages[0].Body != "public preview post" { - t.Fatalf("preview diff = %+v, want public preview post at current pts", diff) + if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.Events) != 0 || len(diff.NewMessages) != 0 || len(diff.OtherUpdates) != 0 { + t.Fatalf("preview diff = %+v, want empty public preview difference at current pts", diff) } if diff.Dialog.UnreadCount != 0 || diff.Dialog.ReadInboxMaxID < sent.Message.ID { t.Fatalf("preview diff dialog = %+v, want read-only public preview dialog", diff.Dialog) diff --git a/internal/app/contacts/service.go b/internal/app/contacts/service.go index 01bc7ede..2fe8b5de 100644 --- a/internal/app/contacts/service.go +++ b/internal/app/contacts/service.go @@ -421,20 +421,38 @@ func (s *Service) GetPeerSettings(ctx context.Context, userID int64, peer domain return domain.PeerSettings{}, err } shareContact := found && !contact.Mutual + needContactsException := false if s.privacy != nil { - peerCanSeePhone, err := s.privacy.CanSee(ctx, userID, peer.ID, domain.PrivacyKeyPhoneNumber) + peerCanSeePhone, err := s.peerCanSeeCurrentUserPhone(ctx, userID, peer.ID) if err != nil { return domain.PeerSettings{}, err } + needContactsException = !peerCanSeePhone shareContact = found && !peerCanSeePhone } return domain.PeerSettings{ - AddContact: !found, - BlockContact: !blocked, - ShareContact: shareContact, + AddContact: !found, + BlockContact: !blocked, + ShareContact: shareContact, + NeedContactsException: needContactsException, }, nil } +func (s *Service) peerCanSeeCurrentUserPhone(ctx context.Context, ownerUserID, viewerUserID int64) (bool, error) { + allowed, err := s.privacy.CanSee(ctx, ownerUserID, viewerUserID, domain.PrivacyKeyPhoneNumber) + if err != nil || allowed { + return allowed, err + } + if s.contacts == nil { + return false, nil + } + _, found, err := s.contacts.Get(ctx, viewerUserID, ownerUserID) + if err != nil { + return false, err + } + return found, nil +} + // BlockContact adds peer to the current user's blocklist. func (s *Service) BlockContact(ctx context.Context, userID, peerUserID int64, date int) (bool, error) { if s == nil || s.contacts == nil || userID == 0 || peerUserID == 0 || peerUserID == userID { diff --git a/internal/app/contacts/service_test.go b/internal/app/contacts/service_test.go index 6a3b919c..ee7a911a 100644 --- a/internal/app/contacts/service_test.go +++ b/internal/app/contacts/service_test.go @@ -6,6 +6,7 @@ import ( "reflect" "testing" + privacyapp "telesrv/internal/app/privacy" "telesrv/internal/domain" "telesrv/internal/store" "telesrv/internal/store/memory" @@ -391,6 +392,94 @@ func TestAddContactNormalizesPhoneToDigits(t *testing.T) { } } +func TestAddContactPhonePrivacyExceptionPeerSettings(t *testing.T) { + ctx := context.Background() + users := memory.NewUserStore() + contactsStore := memory.NewContactStore() + privacySvc := privacyapp.NewService(memory.NewPrivacyStore(), contactsStore) + alice, err := users.Create(ctx, domain.User{Phone: "15550000101", FirstName: "Alice", LastName: "A"}) + if err != nil { + t.Fatalf("create alice: %v", err) + } + bob, err := users.Create(ctx, domain.User{Phone: "15550000102", FirstName: "Bob", LastName: "B"}) + if err != nil { + t.Fatalf("create bob: %v", err) + } + carol, err := users.Create(ctx, domain.User{Phone: "15550000103", FirstName: "Carol", LastName: "C"}) + if err != nil { + t.Fatalf("create carol: %v", err) + } + dave, err := users.Create(ctx, domain.User{Phone: "15550000104", FirstName: "Dave", LastName: "D"}) + if err != nil { + t.Fatalf("create dave: %v", err) + } + svc := NewService(contactsStore, users).Configure(WithPrivacyEvaluator(privacySvc)) + + bobSettings, err := svc.GetPeerSettings(ctx, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID}) + if err != nil { + t.Fatalf("bob peer settings before add: %v", err) + } + if !bobSettings.AddContact || bobSettings.ShareContact || !bobSettings.NeedContactsException { + t.Fatalf("bob settings before add = %+v, want add + need exception only", bobSettings) + } + if _, err := svc.AddContact(ctx, alice.ID, domain.ContactInput{ + ContactUserID: bob.ID, + Phone: bob.Phone, + FirstName: "Bobby", + }); err != nil { + t.Fatalf("alice add bob: %v", err) + } + bobSettings, err = svc.GetPeerSettings(ctx, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: bob.ID}) + if err != nil { + t.Fatalf("bob peer settings after add without exception: %v", err) + } + if bobSettings.AddContact || !bobSettings.ShareContact || !bobSettings.NeedContactsException { + t.Fatalf("bob settings after add without exception = %+v, want share + need exception", bobSettings) + } + if allowed, err := privacySvc.CanSee(ctx, alice.ID, bob.ID, domain.PrivacyKeyPhoneNumber); err != nil { + t.Fatalf("bob can see alice phone: %v", err) + } else if allowed { + t.Fatalf("bob can see alice phone = true, want false before exception") + } + + if _, err := svc.AddContact(ctx, alice.ID, domain.ContactInput{ + ContactUserID: carol.ID, + Phone: carol.Phone, + FirstName: "Carol", + AddPhonePrivacyException: true, + }); err != nil { + t.Fatalf("alice add carol with exception: %v", err) + } + carolSettings, err := svc.GetPeerSettings(ctx, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: carol.ID}) + if err != nil { + t.Fatalf("carol peer settings after exception: %v", err) + } + if carolSettings.AddContact || carolSettings.ShareContact || carolSettings.NeedContactsException { + t.Fatalf("carol settings after exception = %+v, want no add/share/need exception", carolSettings) + } + if allowed, err := privacySvc.CanSee(ctx, alice.ID, carol.ID, domain.PrivacyKeyPhoneNumber); err != nil { + t.Fatalf("carol can see alice phone: %v", err) + } else if !allowed { + t.Fatalf("carol can see alice phone = false, want true after exception") + } + + if _, err := contactsStore.Upsert(ctx, dave.ID, domain.ContactInput{ + ContactUserID: alice.ID, + Phone: alice.Phone, + FirstName: alice.FirstName, + LastName: alice.LastName, + }); err != nil { + t.Fatalf("dave add alice: %v", err) + } + daveSettings, err := svc.GetPeerSettings(ctx, alice.ID, domain.Peer{Type: domain.PeerTypeUser, ID: dave.ID}) + if err != nil { + t.Fatalf("dave peer settings before alice add: %v", err) + } + if !daveSettings.AddContact || daveSettings.ShareContact || daveSettings.NeedContactsException { + t.Fatalf("dave settings with reverse contact = %+v, want add only", daveSettings) + } +} + func TestAcceptContactRequiresExistingContactRequest(t *testing.T) { ctx := context.Background() users := memory.NewUserStore() diff --git a/internal/app/dialogs/service.go b/internal/app/dialogs/service.go index a4ff8208..224d4631 100644 --- a/internal/app/dialogs/service.go +++ b/internal/app/dialogs/service.go @@ -336,6 +336,9 @@ func (s *Service) appendMissingChannelPeerPreviews(ctx context.Context, userID i if !ok || view.Forbidden { continue } + if view.Self.Status != domain.ChannelMemberActive { + continue + } history, err := s.channels.ListChannelHistory(ctx, userID, domain.ChannelHistoryFilter{ ChannelID: channelID, Limit: 1, diff --git a/internal/app/dialogs/service_test.go b/internal/app/dialogs/service_test.go index 5f51dd3a..dffb055f 100644 --- a/internal/app/dialogs/service_test.go +++ b/internal/app/dialogs/service_test.go @@ -695,7 +695,7 @@ func TestGetPeerDialogsRejectsHugeVector(t *testing.T) { } } -func TestGetPeerDialogsIncludesPublicChannelPreviewForNonMember(t *testing.T) { +func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) { ctx := context.Background() channelStore := memory.NewChannelStore() channels := appchannels.NewService(channelStore) @@ -716,13 +716,12 @@ func TestGetPeerDialogsIncludesPublicChannelPreviewForNonMember(t *testing.T) { }); err != nil { t.Fatalf("UpdateUsername public: %v", err) } - sent, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{ + if _, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{ ChannelID: public.Channel.ID, RandomID: 99, Message: "public peer dialog top", Date: 1700002010, - }) - if err != nil { + }); err != nil { t.Fatalf("SendMessage public: %v", err) } private, err := channels.CreateChannel(ctx, 1001, domain.CreateChannelRequest{ @@ -741,28 +740,12 @@ func TestGetPeerDialogsIncludesPublicChannelPreviewForNonMember(t *testing.T) { if err != nil { t.Fatalf("GetPeerDialogs public preview: %v", err) } - if len(list.Dialogs) != 1 { - t.Fatalf("dialogs = %+v, want only public preview dialog", list.Dialogs) - } - dialog := findChannelDialog(t, list, public.Channel.ID) - if dialog.TopMessage != sent.Message.ID || dialog.TopMessageDate != sent.Message.Date { - t.Fatalf("preview dialog top = id %d date %d, want %d/%d", dialog.TopMessage, dialog.TopMessageDate, sent.Message.ID, sent.Message.Date) - } - if !dialog.ChannelLeft { - t.Fatalf("preview dialog ChannelLeft = false, want read-only left preview") - } - if dialog.UnreadCount != 0 || dialog.ReadInboxMaxID < sent.Message.ID || dialog.ReadOutboxMaxID < sent.Message.ID { - t.Fatalf("preview dialog read/unread = %+v, want read through top and no unread", dialog) - } - if len(list.ChannelMessages) != 1 || list.ChannelMessages[0].Body != "public peer dialog top" { - t.Fatalf("channel messages = %+v, want public top message", list.ChannelMessages) - } - if len(list.Channels) != 1 || list.Channels[0].ID != public.Channel.ID { - t.Fatalf("channels = %+v, want public channel shell", list.Channels) + if len(list.Dialogs) != 0 || len(list.ChannelMessages) != 0 || len(list.Channels) != 0 || list.Count != 0 { + t.Fatalf("peer dialogs = %+v, want no materialized public preview dialog", list) } } -func TestGetPeerDialogsBatchesMissingChannelPreviews(t *testing.T) { +func TestGetPeerDialogsBatchesMissingChannelVisibilityChecks(t *testing.T) { ctx := context.Background() channelStore := &countingDialogChannelStore{ChannelStore: memory.NewChannelStore()} channels := appchannels.NewService(channelStore) @@ -783,13 +766,12 @@ func TestGetPeerDialogsBatchesMissingChannelPreviews(t *testing.T) { }); err != nil { t.Fatalf("UpdateUsername first: %v", err) } - firstMsg, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{ + if _, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{ ChannelID: first.Channel.ID, RandomID: 101, Message: "first public preview", Date: 1700002110, - }) - if err != nil { + }); err != nil { t.Fatalf("SendMessage first: %v", err) } @@ -808,13 +790,12 @@ func TestGetPeerDialogsBatchesMissingChannelPreviews(t *testing.T) { }); err != nil { t.Fatalf("UpdateUsername second: %v", err) } - secondMsg, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{ + if _, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{ ChannelID: second.Channel.ID, RandomID: 102, Message: "second public preview", Date: 1700002130, - }) - if err != nil { + }); err != nil { t.Fatalf("SendMessage second: %v", err) } @@ -839,19 +820,10 @@ func TestGetPeerDialogsBatchesMissingChannelPreviews(t *testing.T) { t.Fatalf("GetPeerDialogs batch previews: %v", err) } if channelStore.getChannelsCalls != 1 || channelStore.getChannelCalls != 0 { - t.Fatalf("preview channel calls: GetChannels=%d GetChannel=%d, want one batch call only", channelStore.getChannelsCalls, channelStore.getChannelCalls) + t.Fatalf("visibility channel calls: GetChannels=%d GetChannel=%d, want one batch call only", channelStore.getChannelsCalls, channelStore.getChannelCalls) } - if len(list.Dialogs) != 2 { - t.Fatalf("dialogs = %+v, want two public previews", list.Dialogs) - } - if got := findChannelDialog(t, list, first.Channel.ID); got.TopMessage != firstMsg.Message.ID { - t.Fatalf("first preview top = %d, want %d", got.TopMessage, firstMsg.Message.ID) - } - if got := findChannelDialog(t, list, second.Channel.ID); got.TopMessage != secondMsg.Message.ID { - t.Fatalf("second preview top = %d, want %d", got.TopMessage, secondMsg.Message.ID) - } - if len(list.ChannelMessages) != 2 { - t.Fatalf("channel messages = %+v, want two top messages", list.ChannelMessages) + if len(list.Dialogs) != 0 || len(list.ChannelMessages) != 0 || len(list.Channels) != 0 || list.Count != 0 { + t.Fatalf("peer dialogs = %+v, want no public preview dialogs", list) } } diff --git a/internal/rpc/channels_public_preview_rpc_test.go b/internal/rpc/channels_public_preview_rpc_test.go index 70c5ddab..3792e5ce 100644 --- a/internal/rpc/channels_public_preview_rpc_test.go +++ b/internal/rpc/channels_public_preview_rpc_test.go @@ -135,20 +135,9 @@ func TestPublicChannelPreviewRPCsAllowNonMember(t *testing.T) { if err != nil { t.Fatalf("non-member getChannelDifference public preview: %v", err) } - fullDiff, ok := diff.(*tg.UpdatesChannelDifference) - if !ok || !fullDiff.Final || fullDiff.Pts != sent.Event.Pts || len(fullDiff.NewMessages) != 1 { - t.Fatalf("channel difference = %T %+v, want one public preview message at current pts", diff, diff) - } - diffMsg, ok := fullDiff.NewMessages[0].(*tg.Message) - if !ok || diffMsg.Message != "public preview rpc post" { - t.Fatalf("channel difference message = %T %+v, want public preview rpc post", fullDiff.NewMessages[0], fullDiff.NewMessages[0]) - } - if len(fullDiff.Chats) != 1 { - t.Fatalf("channel difference chats = %d, want public channel chat", len(fullDiff.Chats)) - } - diffChat, ok := fullDiff.Chats[0].(*tg.Channel) - if !ok || !diffChat.Left || diffChat.ID != public.Channel.ID { - t.Fatalf("channel difference chat = %T %+v, want left public channel", fullDiff.Chats[0], fullDiff.Chats[0]) + emptyDiff, ok := diff.(*tg.UpdatesChannelDifferenceEmpty) + if !ok || !emptyDiff.Final || emptyDiff.Pts != sent.Event.Pts { + t.Fatalf("channel difference = %T %+v, want empty public preview difference at current pts", diff, diff) } domainPeers, err := r.dialogPeersFromInput(WithUserID(ctx, viewer.ID), viewer.ID, []tg.InputDialogPeerClass{&tg.InputDialogPeer{Peer: peer}}) @@ -162,8 +151,8 @@ func TestPublicChannelPreviewRPCsAllowNonMember(t *testing.T) { if err != nil { t.Fatalf("dialog service public preview: %v", err) } - if len(directPeerDialogs.Dialogs) != 1 || len(directPeerDialogs.ChannelMessages) != 1 || len(directPeerDialogs.Channels) != 1 { - t.Fatalf("direct peer dialogs = %+v, want one dialog/message/channel", directPeerDialogs) + if len(directPeerDialogs.Dialogs) != 0 || len(directPeerDialogs.ChannelMessages) != 0 || len(directPeerDialogs.Channels) != 0 { + t.Fatalf("direct peer dialogs = %+v, want no public preview dialog/message/channel", directPeerDialogs) } peerDialogsReq := &tg.MessagesGetPeerDialogsRequest{ @@ -181,20 +170,8 @@ func TestPublicChannelPreviewRPCsAllowNonMember(t *testing.T) { if !ok { t.Fatalf("getPeerDialogs response = %T, want peer dialogs", peerDialogsEnc) } - if len(peerDialogs.Dialogs) != 1 || len(peerDialogs.Messages) != 1 || len(peerDialogs.Chats) != 1 { - t.Fatalf("peer dialogs = %+v, want one dialog/message/channel", peerDialogs) - } - tgDialog, ok := peerDialogs.Dialogs[0].(*tg.Dialog) - if !ok || tgDialog.TopMessage <= 0 || tgDialog.UnreadCount != 0 { - t.Fatalf("peer dialog = %T %+v, want read-only public preview dialog", peerDialogs.Dialogs[0], peerDialogs.Dialogs[0]) - } - tgMessage, ok := peerDialogs.Messages[0].(*tg.Message) - if !ok || tgMessage.Message != "public preview rpc post" { - t.Fatalf("peer dialog message = %T %+v, want public preview rpc post", peerDialogs.Messages[0], peerDialogs.Messages[0]) - } - peerDialogChat, ok := peerDialogs.Chats[0].(*tg.Channel) - if !ok || !peerDialogChat.Left || peerDialogChat.ID != public.Channel.ID { - t.Fatalf("peer dialog chat = %T %+v, want left public channel", peerDialogs.Chats[0], peerDialogs.Chats[0]) + if len(peerDialogs.Dialogs) != 0 || len(peerDialogs.Messages) != 0 || len(peerDialogs.Chats) != 0 { + t.Fatalf("peer dialogs = %+v, want no public preview dialog/message/channel", peerDialogs) } if _, err := channelService.JoinChannel(ctx, viewer.ID, public.Channel.ID, 1700010120); err != nil { diff --git a/internal/rpc/contacts_users_rpc_test.go b/internal/rpc/contacts_users_rpc_test.go index 0e663d9a..0f6e625c 100644 --- a/internal/rpc/contacts_users_rpc_test.go +++ b/internal/rpc/contacts_users_rpc_test.go @@ -10,6 +10,7 @@ import ( "strings" appchannels "telesrv/internal/app/channels" appcontacts "telesrv/internal/app/contacts" + appprivacy "telesrv/internal/app/privacy" appstories "telesrv/internal/app/stories" appupdates "telesrv/internal/app/updates" appusers "telesrv/internal/app/users" @@ -1057,6 +1058,84 @@ func TestContactsAcceptContactReturnsSettingsAndReset(t *testing.T) { } } +func TestContactsAddContactPhonePrivacyExceptionUpdatesPeerSettings(t *testing.T) { + ctx := context.Background() + userStore := memory.NewUserStore() + contactsStore := memory.NewContactStore() + privacySvc := appprivacy.NewService(memory.NewPrivacyStore(), contactsStore) + alice, err := userStore.Create(ctx, domain.User{AccessHash: 31, Phone: "2001", FirstName: "Alice", LastName: "A"}) + if err != nil { + t.Fatalf("create alice: %v", err) + } + bob, err := userStore.Create(ctx, domain.User{AccessHash: 32, Phone: "2002", FirstName: "Bob", LastName: "B"}) + if err != nil { + t.Fatalf("create bob: %v", err) + } + carol, err := userStore.Create(ctx, domain.User{AccessHash: 33, Phone: "2003", FirstName: "Carol", LastName: "C"}) + if err != nil { + t.Fatalf("create carol: %v", err) + } + contactsSvc := appcontacts.NewService(contactsStore, userStore).Configure(appcontacts.WithPrivacyEvaluator(privacySvc)) + usersSvc := appusers.NewService(userStore, appusers.WithContactStore(contactsStore), appusers.WithPrivacyEvaluator(privacySvc)) + r := New(Config{}, Deps{ + Contacts: contactsSvc, + Users: usersSvc, + }, zaptest.NewLogger(t), fixedClock{now: time.Unix(1700000410, 0)}) + + withoutException, err := r.onContactsAddContact(WithUserID(ctx, alice.ID), &tg.ContactsAddContactRequest{ + ID: &tg.InputUser{UserID: bob.ID, AccessHash: bob.AccessHash}, + Phone: bob.Phone, + FirstName: "Bobby", + }) + if err != nil { + t.Fatalf("contacts.addContact without exception: %v", err) + } + bobSettings := firstPeerSettingsUpdate(t, withoutException) + if bobSettings.Settings.AddContact || !bobSettings.Settings.ShareContact || !bobSettings.Settings.NeedContactsException { + t.Fatalf("bob peer settings = %+v, want share + need exception", bobSettings.Settings) + } + if allowed, err := privacySvc.CanSee(ctx, alice.ID, bob.ID, domain.PrivacyKeyPhoneNumber); err != nil { + t.Fatalf("bob can see alice phone: %v", err) + } else if allowed { + t.Fatalf("bob can see alice phone = true, want false before exception") + } + + withException, err := r.onContactsAddContact(WithUserID(ctx, alice.ID), &tg.ContactsAddContactRequest{ + AddPhonePrivacyException: true, + ID: &tg.InputUser{UserID: carol.ID, AccessHash: carol.AccessHash}, + Phone: carol.Phone, + FirstName: "Carol", + }) + if err != nil { + t.Fatalf("contacts.addContact with exception: %v", err) + } + carolSettings := firstPeerSettingsUpdate(t, withException) + if carolSettings.Settings.AddContact || carolSettings.Settings.ShareContact || carolSettings.Settings.NeedContactsException { + t.Fatalf("carol peer settings = %+v, want no add/share/need exception", carolSettings.Settings) + } + if allowed, err := privacySvc.CanSee(ctx, alice.ID, carol.ID, domain.PrivacyKeyPhoneNumber); err != nil { + t.Fatalf("carol can see alice phone: %v", err) + } else if !allowed { + t.Fatalf("carol can see alice phone = false, want true after exception") + } +} + +func firstPeerSettingsUpdate(t *testing.T, updates tg.UpdatesClass) *tg.UpdatePeerSettings { + t.Helper() + got, ok := updates.(*tg.Updates) + if !ok { + t.Fatalf("updates = %T, want *tg.Updates", updates) + } + if len(got.Updates) == 0 { + t.Fatalf("updates = %+v, want UpdatePeerSettings", got.Updates) + } + settings, ok := got.Updates[0].(*tg.UpdatePeerSettings) + if !ok { + t.Fatalf("update[0] = %T, want UpdatePeerSettings", got.Updates[0]) + } + return settings +} + func TestContactsStatusesUsesOnlineSessionFallback(t *testing.T) { ctx := context.Background() alice := domain.User{ID: 1000000001, AccessHash: 11, FirstName: "Alice"} diff --git a/internal/store/memory/channel_dialogs.go b/internal/store/memory/channel_dialogs.go index 5c8e682b..a1de0199 100644 --- a/internal/store/memory/channel_dialogs.go +++ b/internal/store/memory/channel_dialogs.go @@ -2,6 +2,7 @@ package memory import ( "context" + "errors" "sort" "telesrv/internal/domain" ) @@ -119,8 +120,23 @@ func (s *ChannelStore) GetChannelDialogs(_ context.Context, viewerUserID int64, continue } seen[channelID] = struct{}{} - channel, member, _, err := s.channelForViewerLocked(viewerUserID, channelID) + channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID) if err != nil { + if !errors.Is(err, domain.ErrChannelPrivate) && !errors.Is(err, domain.ErrChannelInvalid) { + continue + } + var ok bool + channel, ok = s.channels[channelID] + if !ok || channel.Deleted || !channel.Monoforum || channel.LinkedMonoforumID == 0 { + continue + } + parentMember, ok := s.members[channel.LinkedMonoforumID][viewerUserID] + if !ok || parentMember.Status != domain.ChannelMemberActive || !isChannelAdmin(parentMember) { + continue + } + member = syntheticMonoforumAdminMember(channel, parentMember) + } + if member.Status != domain.ChannelMemberActive { continue } dialog := channelDialogToDialog(s.dialogForMemberLocked(viewerUserID, channel, member), channel.Pts, member.Status) diff --git a/internal/store/memory/channel_updates.go b/internal/store/memory/channel_updates.go index d5470ff1..33e9900b 100644 --- a/internal/store/memory/channel_updates.go +++ b/internal/store/memory/channel_updates.go @@ -27,6 +27,16 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann if preview { dialog = previewChannelDialog(req.UserID, channel, member) } + if preview && member.Status != domain.ChannelMemberActive { + return domain.ChannelDifference{ + Channel: channel, + Self: member, + Pts: channel.Pts, + Final: true, + Timeout: 30, + Dialog: dialog, + }, nil + } if channel.Pts-req.Pts > limit { messages := make([]domain.ChannelMessage, 0, domain.MaxChannelDifferenceTooLongMessages) for i := len(s.messages[req.ChannelID]) - 1; i >= 0 && len(messages) < domain.MaxChannelDifferenceTooLongMessages; i-- { diff --git a/internal/store/postgres/channel_difference_integration_test.go b/internal/store/postgres/channel_difference_integration_test.go index 13bba9bc..fbda74c7 100644 --- a/internal/store/postgres/channel_difference_integration_test.go +++ b/internal/store/postgres/channel_difference_integration_test.go @@ -115,7 +115,7 @@ func TestChannelStoreDifferenceStartsAtMemberAvailableMinPts(t *testing.T) { } } -func TestChannelStorePublicPreviewDifferenceAllowsNonMember(t *testing.T) { +func TestChannelStorePublicPreviewDifferenceSkipsNonMemberMessages(t *testing.T) { pool := testPool(t) ctx := context.Background() suffix := randomSuffix(t) @@ -183,8 +183,8 @@ func TestChannelStorePublicPreviewDifferenceAllowsNonMember(t *testing.T) { if err != nil { t.Fatalf("list public preview difference: %v", err) } - if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.NewMessages) != 1 || diff.NewMessages[0].Body != "public preview difference" { - t.Fatalf("preview diff = %+v, want one public preview message at current pts", diff) + if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.Events) != 0 || len(diff.NewMessages) != 0 || len(diff.OtherUpdates) != 0 { + t.Fatalf("preview diff = %+v, want empty public preview difference at current pts", diff) } if diff.Dialog.UnreadCount != 0 || diff.Dialog.ReadInboxMaxID < sent.Message.ID { t.Fatalf("preview diff dialog = %+v, want read-only public preview dialog", diff.Dialog) diff --git a/internal/store/postgres/channel_updates.go b/internal/store/postgres/channel_updates.go index 7b5071c8..87ac7a52 100644 --- a/internal/store/postgres/channel_updates.go +++ b/internal/store/postgres/channel_updates.go @@ -27,6 +27,16 @@ func (s *ChannelStore) ListChannelDifference(ctx context.Context, req domain.Cha if limit <= 0 || limit > domain.MaxChannelDifferenceLimit { limit = domain.MaxChannelDifferenceLimit } + if preview && member.Status != domain.ChannelMemberActive { + return domain.ChannelDifference{ + Channel: channel, + Self: member, + Pts: channel.Pts, + Final: true, + Timeout: 30, + Dialog: previewChannelDialog(req.UserID, channel, member), + }, nil + } if channel.Pts-req.Pts > limit { args := []any{req.ChannelID} where := "channel_id = $1 AND NOT deleted"