fix: sync public channel preview updates
This commit is contained in:
parent
2289a31f46
commit
b4aaf57d6b
27 changed files with 877 additions and 128 deletions
|
|
@ -2295,6 +2295,21 @@ func (s *Service) FilterActiveMemberIDs(ctx context.Context, channelID int64, us
|
|||
return s.channels.FilterActiveChannelMemberIDs(ctx, channelID, candidates)
|
||||
}
|
||||
|
||||
// FilterMessageAudienceIDs keeps active members and currently authorized
|
||||
// public-preview viewers from a bounded online candidate set. The store performs
|
||||
// one batched authoritative check per bounded chunk so runtime session indexes
|
||||
// never become an access-control source of truth.
|
||||
func (s *Service) FilterMessageAudienceIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error) {
|
||||
if s == nil || s.channels == nil || channelID == 0 {
|
||||
return nil, domain.ErrChannelInvalid
|
||||
}
|
||||
candidates := uniqueNonZero(userIDs)
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return s.channels.FilterChannelMessageAudienceIDs(ctx, channelID, candidates)
|
||||
}
|
||||
|
||||
// GetDifference returns channel-scoped pts difference.
|
||||
func (s *Service) GetDifference(ctx context.Context, userID int64, req domain.ChannelDifferenceRequest) (domain.ChannelDifference, error) {
|
||||
if s == nil || s.channels == nil || userID == 0 || req.ChannelID == 0 {
|
||||
|
|
|
|||
|
|
@ -3014,12 +3014,37 @@ 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.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.Final || diff.Pts != sent.Event.Pts || len(diff.Events) != 1 || len(diff.NewMessages) != 1 || len(diff.OtherUpdates) != 0 {
|
||||
t.Fatalf("preview diff = %+v, want one public preview message at current pts", diff)
|
||||
}
|
||||
if diff.NewMessages[0].ID != sent.Message.ID || diff.NewMessages[0].Body != sent.Message.Body {
|
||||
t.Fatalf("preview diff message = %+v, want sent public post %+v", diff.NewMessages[0], sent.Message)
|
||||
}
|
||||
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)
|
||||
}
|
||||
audience, err := service.FilterMessageAudienceIDs(ctx, public.ID, []int64{viewerID, ownerID, viewerID})
|
||||
if err != nil || len(audience) != 2 {
|
||||
t.Fatalf("public message audience = %v err %v, want owner and preview viewer", audience, err)
|
||||
}
|
||||
if _, err := service.JoinChannel(ctx, viewerID, public.ID, 21); err != nil {
|
||||
t.Fatalf("JoinChannel public preview viewer: %v", err)
|
||||
}
|
||||
if _, err := service.LeaveChannel(ctx, viewerID, public.ID, 22); err != nil {
|
||||
t.Fatalf("LeaveChannel public preview viewer: %v", err)
|
||||
}
|
||||
filtered, err := service.GetDifference(ctx, viewerID, domain.ChannelDifferenceRequest{
|
||||
ChannelID: public.ID,
|
||||
Pts: sent.Event.Pts,
|
||||
Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("preview difference across participant events: %v", err)
|
||||
}
|
||||
if !filtered.Final || filtered.Pts != sent.Event.Pts || len(filtered.Events) != 0 ||
|
||||
len(filtered.NewMessages) != 0 || len(filtered.OtherUpdates) != 0 {
|
||||
t.Fatalf("difference after transient participant changes = %+v, want unchanged PTS", filtered)
|
||||
}
|
||||
|
||||
private, err := service.CreateChannel(ctx, ownerID, domain.CreateChannelRequest{
|
||||
Title: "Private Preview",
|
||||
|
|
@ -3047,6 +3072,9 @@ func TestPublicChannelPreviewAllowsNonMemberHistory(t *testing.T) {
|
|||
if _, err := service.GetDifference(ctx, viewerID, domain.ChannelDifferenceRequest{ChannelID: public.ID, Pts: created.Event.Pts, Limit: 10}); !errors.Is(err, domain.ErrChannelUserBanned) {
|
||||
t.Fatalf("banned public preview GetDifference err = %v, want ErrChannelUserBanned", err)
|
||||
}
|
||||
if audience, err := service.FilterMessageAudienceIDs(ctx, public.ID, []int64{viewerID}); err != nil || len(audience) != 0 {
|
||||
t.Fatalf("banned public message audience = %v err %v, want empty", audience, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelDifferenceStartsAtMemberAvailableMinPts(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -342,11 +342,24 @@ func (s *Service) appendMissingChannelPeerPreviews(ctx context.Context, userID i
|
|||
if !ok || view.Forbidden {
|
||||
continue
|
||||
}
|
||||
if view.Self.Status == domain.ChannelMemberLeft && !view.Self.Guest {
|
||||
// A visible public preview still needs one transient dialog so a
|
||||
// client can finish bootstrapping the requested peer. Keep the top
|
||||
// message and read state at zero: the response is an admission
|
||||
// token, not a persisted/chat-list dialog snapshot. In particular,
|
||||
// clients that persist non-zero top dialogs will instead continue
|
||||
// with messages.getHistory, which is the authoritative preview
|
||||
// history path.
|
||||
out.Dialogs = append(out.Dialogs, publicChannelPreviewBootstrapDialog(view))
|
||||
out.Channels = append(out.Channels, view.Channel)
|
||||
out.Count++
|
||||
present[channelID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
// Linked discussion guests need a transient peer-dialog snapshot so
|
||||
// TDesktop can finish materializing the comments History after
|
||||
// requestSelf. ChannelLeft keeps the snapshot out of the main chat list,
|
||||
// and Guest guarantees this path never turns an ordinary public preview
|
||||
// into a dialog.
|
||||
// while Guest authorizes the target's real top-message snapshot.
|
||||
if view.Self.Status != domain.ChannelMemberActive && !view.Self.Guest {
|
||||
continue
|
||||
}
|
||||
|
|
@ -378,6 +391,14 @@ func (s *Service) appendMissingChannelPeerPreviews(ctx context.Context, userID i
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func publicChannelPreviewBootstrapDialog(view domain.ChannelView) domain.Dialog {
|
||||
return domain.Dialog{
|
||||
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: view.Channel.ID},
|
||||
ChannelLeft: true,
|
||||
Pts: view.Channel.Pts,
|
||||
}
|
||||
}
|
||||
|
||||
func isChannelPreviewAccessError(err error) bool {
|
||||
return errors.Is(err, domain.ErrChannelPrivate) ||
|
||||
errors.Is(err, domain.ErrChannelUserBanned) ||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ type countingDialogChannelStore struct {
|
|||
getChannelCalls int
|
||||
getChannelsCalls int
|
||||
getChannelDialogsCalls int
|
||||
listHistoryCalls int
|
||||
}
|
||||
|
||||
func (s *countingDialogChannelStore) GetChannel(ctx context.Context, viewerUserID, channelID int64) (domain.ChannelView, error) {
|
||||
|
|
@ -77,6 +78,11 @@ func (s *countingDialogChannelStore) GetChannelDialogs(ctx context.Context, view
|
|||
return s.ChannelStore.GetChannelDialogs(ctx, viewerUserID, channelIDs)
|
||||
}
|
||||
|
||||
func (s *countingDialogChannelStore) ListChannelHistory(ctx context.Context, viewerUserID int64, filter domain.ChannelHistoryFilter) (domain.ChannelHistory, error) {
|
||||
s.listHistoryCalls++
|
||||
return s.ChannelStore.ListChannelHistory(ctx, viewerUserID, filter)
|
||||
}
|
||||
|
||||
func TestGetDialogsHashUsesWarmStableHashCacheAndInvalidatesOnWrite(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
const ownerID int64 = 1001
|
||||
|
|
@ -695,7 +701,7 @@ func TestGetPeerDialogsRejectsHugeVector(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) {
|
||||
func TestGetPeerDialogsReturnsZeroTopBootstrapForPublicChannelPreview(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
channelStore := memory.NewChannelStore()
|
||||
channels := appchannels.NewService(channelStore)
|
||||
|
|
@ -716,12 +722,13 @@ func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) {
|
|||
}); err != nil {
|
||||
t.Fatalf("UpdateUsername public: %v", err)
|
||||
}
|
||||
if _, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{
|
||||
sent, err := channels.SendMessage(ctx, 1001, domain.SendChannelMessageRequest{
|
||||
ChannelID: public.Channel.ID,
|
||||
RandomID: 99,
|
||||
Message: "public peer dialog top",
|
||||
Date: 1700002010,
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SendMessage public: %v", err)
|
||||
}
|
||||
private, err := channels.CreateChannel(ctx, 1001, domain.CreateChannelRequest{
|
||||
|
|
@ -740,8 +747,19 @@ func TestGetPeerDialogsSkipsPublicChannelPreviewForNonMember(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("GetPeerDialogs public preview: %v", err)
|
||||
}
|
||||
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)
|
||||
if len(list.Dialogs) != 1 || len(list.ChannelMessages) != 0 || len(list.Channels) != 1 || list.Count != 1 {
|
||||
t.Fatalf("peer dialogs = %+v, want one zero-top public preview bootstrap", list)
|
||||
}
|
||||
dialog := list.Dialogs[0]
|
||||
if dialog.Peer.Type != domain.PeerTypeChannel || dialog.Peer.ID != public.Channel.ID ||
|
||||
!dialog.ChannelLeft || dialog.TopMessage != 0 || dialog.TopMessageDate != 0 ||
|
||||
dialog.ReadInboxMaxID != 0 || dialog.ReadOutboxMaxID != 0 ||
|
||||
dialog.UnreadCount != 0 || dialog.UnreadMentions != 0 ||
|
||||
dialog.UnreadReactions != 0 || dialog.Pts != sent.Event.Pts {
|
||||
t.Fatalf("public preview bootstrap dialog = %+v", dialog)
|
||||
}
|
||||
if list.Channels[0].ID != public.Channel.ID {
|
||||
t.Fatalf("public preview channels = %+v, want channel %d", list.Channels, public.Channel.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -810,6 +828,7 @@ func TestGetPeerDialogsBatchesMissingChannelVisibilityChecks(t *testing.T) {
|
|||
|
||||
channelStore.getChannelCalls = 0
|
||||
channelStore.getChannelsCalls = 0
|
||||
channelStore.listHistoryCalls = 0
|
||||
list, err := dialogs.GetPeerDialogs(ctx, 1002, []domain.Peer{
|
||||
{Type: domain.PeerTypeChannel, ID: first.Channel.ID},
|
||||
{Type: domain.PeerTypeChannel, ID: private.Channel.ID},
|
||||
|
|
@ -822,8 +841,16 @@ func TestGetPeerDialogsBatchesMissingChannelVisibilityChecks(t *testing.T) {
|
|||
if channelStore.getChannelsCalls != 1 || channelStore.getChannelCalls != 0 {
|
||||
t.Fatalf("visibility channel calls: GetChannels=%d GetChannel=%d, want one batch call only", channelStore.getChannelsCalls, channelStore.getChannelCalls)
|
||||
}
|
||||
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)
|
||||
if channelStore.listHistoryCalls != 0 {
|
||||
t.Fatalf("public preview history calls = %d, want zero", channelStore.listHistoryCalls)
|
||||
}
|
||||
if len(list.Dialogs) != 2 || len(list.ChannelMessages) != 0 || len(list.Channels) != 2 || list.Count != 2 {
|
||||
t.Fatalf("peer dialogs = %+v, want two deduplicated zero-top public previews", list)
|
||||
}
|
||||
for _, dialog := range list.Dialogs {
|
||||
if !dialog.ChannelLeft || dialog.TopMessage != 0 || dialog.ReadInboxMaxID != 0 || dialog.ReadOutboxMaxID != 0 {
|
||||
t.Fatalf("public preview bootstrap dialog = %+v", dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue