fix: sync public channel preview updates
This commit is contained in:
parent
2289a31f46
commit
b4aaf57d6b
27 changed files with 877 additions and 128 deletions
|
|
@ -183,6 +183,10 @@ type ChannelStore interface {
|
|||
ListActiveChannelMembers(ctx context.Context, viewerUserID, channelID int64, limit int) (domain.Channel, domain.ChannelMember, []domain.ChannelMember, error)
|
||||
ListChannelInviteAdminMemberIDs(ctx context.Context, channelID int64, limit int) ([]int64, error)
|
||||
FilterActiveChannelMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)
|
||||
// FilterChannelMessageAudienceIDs authoritatively intersects a bounded online
|
||||
// candidate set with users allowed to receive channel message-box updates:
|
||||
// active members plus non-banned public-channel preview subscribers.
|
||||
FilterChannelMessageAudienceIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error)
|
||||
MaxChannelPts(ctx context.Context, channelID int64) (int, error)
|
||||
// MaxChannelPtsBatch returns existing channel watermarks with one bounded store round trip.
|
||||
// Missing/deleted ids are omitted so a stale process-local membership key cannot poison the
|
||||
|
|
|
|||
|
|
@ -892,6 +892,42 @@ func (s *ChannelStore) FilterActiveChannelMemberIDs(_ context.Context, channelID
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) FilterChannelMessageAudienceIDs(_ context.Context, channelID int64, userIDs []int64) ([]int64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if channelID == 0 || len(userIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
channel, ok := s.channels[channelID]
|
||||
if !ok || channel.Deleted {
|
||||
return nil, nil
|
||||
}
|
||||
public := publicPreviewableChannel(channel)
|
||||
members := s.members[channelID]
|
||||
out := make([]int64, 0, len(userIDs))
|
||||
seen := make(map[int64]struct{}, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
if userID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[userID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[userID] = struct{}{}
|
||||
member, found := members[userID]
|
||||
if member.BannedRights.ViewMessages ||
|
||||
member.Status == domain.ChannelMemberKicked ||
|
||||
member.Status == domain.ChannelMemberBanned {
|
||||
continue
|
||||
}
|
||||
if member.Status == domain.ChannelMemberActive || public && (!found || member.Status == domain.ChannelMemberLeft) {
|
||||
out = append(out, userID)
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListActiveChannelMembers(_ context.Context, viewerUserID, channelID int64, limit int) (domain.Channel, domain.ChannelMember, []domain.ChannelMember, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
|
|
|||
|
|
@ -31,16 +31,6 @@ 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
|
||||
}
|
||||
checkpoint := s.channelUpdateCheckpointLocked(req.ChannelID, channel)
|
||||
if req.Pts < checkpoint.RetainedThroughPts || channel.Pts-req.Pts > limit {
|
||||
messages := make([]domain.ChannelMessage, 0, domain.MaxChannelDifferenceTooLongMessages)
|
||||
|
|
@ -81,10 +71,15 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann
|
|||
}
|
||||
}
|
||||
}
|
||||
scanned := 0
|
||||
for _, event := range s.events[req.ChannelID] {
|
||||
if event.Pts <= req.Pts {
|
||||
continue
|
||||
}
|
||||
if scanned >= limit {
|
||||
break
|
||||
}
|
||||
scanned++
|
||||
lastPts = event.Pts
|
||||
visible, ok := domain.FilterChannelUpdateEventForAvailableMinID(cloneChannelEvent(event), member.AvailableMinID)
|
||||
if !ok {
|
||||
|
|
@ -106,7 +101,7 @@ func (s *ChannelStore) ListChannelDifference(_ context.Context, req domain.Chann
|
|||
Channel: channel,
|
||||
Self: member,
|
||||
Pts: maxInt(lastPts, req.Pts),
|
||||
Final: true,
|
||||
Final: lastPts >= channel.Pts,
|
||||
Timeout: 30,
|
||||
Dialog: dialog,
|
||||
}, nil
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ func TestChannelStoreDifferenceStartsAtMemberAvailableMinPts(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChannelStorePublicPreviewDifferenceSkipsNonMemberMessages(t *testing.T) {
|
||||
func TestChannelStorePublicPreviewDifferenceReplaysVisibleMessages(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
|
|
@ -183,12 +183,57 @@ func TestChannelStorePublicPreviewDifferenceSkipsNonMemberMessages(t *testing.T)
|
|||
if err != nil {
|
||||
t.Fatalf("list public preview difference: %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 durable public preview message at pts %d", diff, sent.Event.Pts)
|
||||
}
|
||||
if diff.NewMessages[0].ID != sent.Message.ID || diff.NewMessages[0].Body != sent.Message.Body {
|
||||
t.Fatalf("preview diff message = %+v, want sent message %+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)
|
||||
}
|
||||
edited, err := channels.EditChannelMessage(ctx, domain.EditChannelMessageRequest{
|
||||
UserID: owner.ID, ChannelID: channelID, ID: sent.Message.ID,
|
||||
Message: "public preview edited", EditDate: 1700000372,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("edit public preview message: %v", err)
|
||||
}
|
||||
pinned, err := channels.UpdatePinnedMessage(ctx, domain.UpdateChannelPinnedMessageRequest{
|
||||
UserID: owner.ID, ChannelID: channelID, MessageID: sent.Message.ID,
|
||||
Pinned: true, Date: 1700000373,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("pin public preview message: %v", err)
|
||||
}
|
||||
deleted, err := channels.DeleteChannelMessages(ctx, domain.DeleteChannelMessagesRequest{
|
||||
UserID: owner.ID, ChannelID: channelID, IDs: []int{sent.Message.ID}, Date: 1700000374,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("delete public preview message: %v", err)
|
||||
}
|
||||
mutations, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
||||
UserID: viewer.ID, ChannelID: channelID, Pts: sent.Event.Pts, Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list public preview mutations: %v", err)
|
||||
}
|
||||
if !mutations.Final || mutations.Pts != deleted.Event.Pts || len(mutations.NewMessages) != 0 || len(mutations.OtherUpdates) != 3 {
|
||||
t.Fatalf("public preview mutations = %+v, want edit/pin/delete through pts %d", mutations, deleted.Event.Pts)
|
||||
}
|
||||
wantTypes := []domain.ChannelUpdateEventType{
|
||||
domain.ChannelUpdateEditMessage,
|
||||
domain.ChannelUpdatePinnedMessages,
|
||||
domain.ChannelUpdateDeleteMessages,
|
||||
}
|
||||
for i, want := range wantTypes {
|
||||
if mutations.OtherUpdates[i].Type != want {
|
||||
t.Fatalf("public preview mutation[%d] = %+v, want %s", i, mutations.OtherUpdates[i], want)
|
||||
}
|
||||
}
|
||||
if edited.Event.Pts >= pinned.Event.Pts || pinned.Event.Pts >= deleted.Event.Pts {
|
||||
t.Fatalf("public preview mutation pts = edit %d pin %d delete %d, want strictly increasing", edited.Event.Pts, pinned.Event.Pts, deleted.Event.Pts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreDifferenceUsesDurableMessageSnapshots(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -360,3 +360,52 @@ ORDER BY user_id`, channelID, candidates[start:end])
|
|||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) FilterChannelMessageAudienceIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error) {
|
||||
if channelID == 0 || len(userIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
candidates := uniqueChannelUserIDs(userIDs, 0)
|
||||
if len(candidates) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
out := make([]int64, 0, len(candidates))
|
||||
for start := 0; start < len(candidates); start += channelMemberFilterBatch {
|
||||
end := start + channelMemberFilterBatch
|
||||
if end > len(candidates) {
|
||||
end = len(candidates)
|
||||
}
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT candidate.user_id
|
||||
FROM channels c
|
||||
CROSS JOIN unnest($2::bigint[]) AS candidate(user_id)
|
||||
LEFT JOIN channel_members m
|
||||
ON m.channel_id = c.id AND m.user_id = candidate.user_id
|
||||
WHERE c.id = $1
|
||||
AND NOT c.deleted
|
||||
AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
|
||||
AND COALESCE(m.status, '') NOT IN ('kicked', 'banned')
|
||||
AND (
|
||||
m.status = 'active'
|
||||
OR (COALESCE(c.username, '') <> '' AND COALESCE(m.status, 'left') = 'left')
|
||||
)
|
||||
ORDER BY candidate.user_id`, channelID, candidates[start:end])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("filter channel message audience: %w", err)
|
||||
}
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, userID)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rows.Close()
|
||||
return nil, err
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,12 +75,42 @@ func TestPublicChannelAndMegagroupPreviewPostgres(t *testing.T) {
|
|||
if !found || history.Self.Status != domain.ChannelMemberLeft {
|
||||
t.Fatalf("preview history = %+v self=%+v", history.Messages, history.Self)
|
||||
}
|
||||
audience, err := channels.FilterChannelMessageAudienceIDs(ctx, public.ID, []int64{viewer.ID, owner.ID, viewer.ID})
|
||||
if err != nil {
|
||||
t.Fatalf("filter public message audience: %v", err)
|
||||
}
|
||||
if len(audience) != 2 || audience[0] != owner.ID || audience[1] != viewer.ID {
|
||||
t.Fatalf("public message audience = %v, want owner/member and viewer/subscriber", audience)
|
||||
}
|
||||
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
||||
UserID: viewer.ID, ChannelID: public.ID, Pts: created.Event.Pts, Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("public preview difference: %v", err)
|
||||
}
|
||||
if !diff.Final || diff.Pts != sent.Event.Pts || len(diff.NewMessages) != 1 || diff.NewMessages[0].ID != sent.Message.ID {
|
||||
t.Fatalf("public preview difference = %+v, want sent message through pts %d", diff, sent.Event.Pts)
|
||||
}
|
||||
if _, err := channels.GetParticipants(ctx, viewer.ID, public.ID, domain.ChannelParticipantsFilter{Kind: domain.ChannelParticipantsRecent}, 0, 20); err != nil {
|
||||
t.Fatalf("public preview participants: %v", err)
|
||||
}
|
||||
if _, err := channels.GetParticipant(ctx, viewer.ID, public.ID, viewer.ID); !errors.Is(err, domain.ErrUserNotParticipant) {
|
||||
t.Fatalf("public preview self participant err = %v, want ErrUserNotParticipant", err)
|
||||
}
|
||||
dialogs := appdialogs.NewService(nil, channels)
|
||||
peerDialogs, err := dialogs.GetPeerDialogs(ctx, viewer.ID, []domain.Peer{{Type: domain.PeerTypeChannel, ID: public.ID}})
|
||||
if err != nil {
|
||||
t.Fatalf("public preview peer dialogs: %v", err)
|
||||
}
|
||||
if len(peerDialogs.Dialogs) != 1 || len(peerDialogs.ChannelMessages) != 0 || len(peerDialogs.Channels) != 1 {
|
||||
t.Fatalf("public preview peer dialogs = %+v, want one zero-top bootstrap", peerDialogs)
|
||||
}
|
||||
previewDialog := peerDialogs.Dialogs[0]
|
||||
if previewDialog.TopMessage != 0 || !previewDialog.ChannelLeft ||
|
||||
previewDialog.ReadInboxMaxID != 0 || previewDialog.ReadOutboxMaxID != 0 ||
|
||||
previewDialog.Pts != sent.Event.Pts {
|
||||
t.Fatalf("public preview bootstrap dialog = %+v", previewDialog)
|
||||
}
|
||||
var memberExists bool
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
|
||||
|
|
@ -96,6 +126,28 @@ SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
|
|||
if _, err := channels.LeaveChannel(ctx, public.ID, viewer.ID, 1700009430+i); err != nil {
|
||||
t.Fatalf("leave public peer: %v", err)
|
||||
}
|
||||
filtered, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{
|
||||
UserID: viewer.ID, ChannelID: public.ID, Pts: sent.Event.Pts, Limit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("public difference across participant events: %v", err)
|
||||
}
|
||||
if tc.broadcast {
|
||||
if !filtered.Final || filtered.Pts != sent.Event.Pts || len(filtered.Events) != 0 ||
|
||||
len(filtered.NewMessages) != 0 || len(filtered.OtherUpdates) != 0 {
|
||||
t.Fatalf("broadcast difference after transient participant changes = %+v, want unchanged PTS", filtered)
|
||||
}
|
||||
} else {
|
||||
if !filtered.Final || filtered.Pts <= sent.Event.Pts || len(filtered.NewMessages) != 2 ||
|
||||
len(filtered.OtherUpdates) != 0 {
|
||||
t.Fatalf("megagroup join/leave difference = %+v, want two real service messages", filtered)
|
||||
}
|
||||
for _, message := range filtered.NewMessages {
|
||||
if message.Action == nil {
|
||||
t.Fatalf("megagroup join/leave difference message = %+v, want service action", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := channels.GetParticipant(ctx, viewer.ID, public.ID, viewer.ID); !errors.Is(err, domain.ErrUserNotParticipant) {
|
||||
t.Fatalf("left self participant err = %v, want ErrUserNotParticipant", err)
|
||||
}
|
||||
|
|
@ -112,6 +164,9 @@ SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
|
|||
t.Fatalf("create private group: %v", err)
|
||||
}
|
||||
channelIDs = append(channelIDs, private.Channel.ID)
|
||||
if audience, err := channels.FilterChannelMessageAudienceIDs(ctx, private.Channel.ID, []int64{viewer.ID}); err != nil || len(audience) != 0 {
|
||||
t.Fatalf("private message audience = %v err %v, want empty", audience, err)
|
||||
}
|
||||
if _, err := channels.ListChannelHistory(ctx, viewer.ID, domain.ChannelHistoryFilter{ChannelID: private.Channel.ID, Limit: 20}); !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
t.Fatalf("private preview history err = %v, want ErrChannelPrivate", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,6 @@ 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
|
||||
}
|
||||
checkpoint, err := getChannelUpdateCheckpoint(ctx, s.db, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelDifference{}, err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue