fix: sync protocol and discussion stability fixes
This commit is contained in:
parent
9f73dc20da
commit
aa21bd04e1
43 changed files with 7258 additions and 503 deletions
|
|
@ -377,7 +377,20 @@ WHERE c.id = ANY($2::bigint[]) AND NOT c.deleted`, viewerUserID, ids)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
linkedGuests, err := s.listLinkedDiscussionGuests(ctx, s.db, viewerUserID, remaining)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, channel := range channels {
|
||||
if member, ok := linkedGuests[channel.ID]; ok {
|
||||
views[channel.ID] = domain.ChannelView{
|
||||
Channel: channel,
|
||||
Self: member,
|
||||
Dialog: previewChannelDialog(viewerUserID, channel, member),
|
||||
SelfBoostsApplied: 0,
|
||||
}
|
||||
continue
|
||||
}
|
||||
if member, _, ok, err := s.monoforumAdminPreview(ctx, s.db, viewerUserID, channel); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
|
|
|
|||
|
|
@ -96,6 +96,72 @@ func (s *ChannelStore) getLinkedDiscussionGuest(ctx context.Context, db sqlcgen.
|
|||
return guest, true, nil
|
||||
}
|
||||
|
||||
// listLinkedDiscussionGuests is the bounded batch equivalent of
|
||||
// getLinkedDiscussionGuest. GetChannels uses it after loading the requested
|
||||
// channel rows so messages.getPeerDialogs can materialize linked discussion
|
||||
// histories without an N+1 query per requested peer.
|
||||
//
|
||||
// The target membership predicate deliberately excludes active, kicked,
|
||||
// banned and view-messages-banned rows. Active members were projected by the
|
||||
// primary GetChannels query; explicit target denial must always win over the
|
||||
// source broadcast membership. Returned members are transient and are never
|
||||
// persisted to channel_members/channel_dialogs.
|
||||
func (s *ChannelStore) listLinkedDiscussionGuests(ctx context.Context, db sqlcgen.DBTX, viewerUserID int64, targetIDs []int64) (map[int64]domain.ChannelMember, error) {
|
||||
out := make(map[int64]domain.ChannelMember)
|
||||
if viewerUserID == 0 || len(targetIDs) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT target.id
|
||||
FROM channels target
|
||||
JOIN channels source
|
||||
ON source.id = target.linked_chat_id
|
||||
AND NOT source.deleted
|
||||
AND source.broadcast
|
||||
AND source.linked_chat_id = target.id
|
||||
JOIN channel_members source_member
|
||||
ON source_member.channel_id = source.id
|
||||
AND source_member.user_id = $1
|
||||
AND source_member.status = 'active'
|
||||
AND NOT COALESCE((source_member.banned_rights->>'ViewMessages')::boolean, false)
|
||||
LEFT JOIN channel_members target_member
|
||||
ON target_member.channel_id = target.id
|
||||
AND target_member.user_id = $1
|
||||
WHERE target.id = ANY($2::bigint[])
|
||||
AND NOT target.deleted
|
||||
AND target.megagroup
|
||||
AND NOT target.broadcast
|
||||
AND (
|
||||
target_member.user_id IS NULL
|
||||
OR (
|
||||
target_member.status NOT IN ('active', 'banned', 'kicked')
|
||||
AND NOT COALESCE((target_member.banned_rights->>'ViewMessages')::boolean, false)
|
||||
)
|
||||
)
|
||||
ORDER BY target.id`, viewerUserID, targetIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list linked discussion guests: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var channelID int64
|
||||
if err := rows.Scan(&channelID); err != nil {
|
||||
return nil, fmt.Errorf("scan linked discussion guest: %w", err)
|
||||
}
|
||||
out[channelID] = domain.ChannelMember{
|
||||
ChannelID: channelID,
|
||||
UserID: viewerUserID,
|
||||
Status: domain.ChannelMemberLeft,
|
||||
Role: domain.ChannelRoleMember,
|
||||
Guest: true,
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate linked discussion guests: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) getPublicPreviewMember(ctx context.Context, db sqlcgen.DBTX, viewerUserID int64, ch domain.Channel) (domain.ChannelMember, error) {
|
||||
member, err := s.getChannelMember(ctx, db, ch.ID, viewerUserID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package postgres
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
|
@ -12,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func (s *ChannelStore) GetParticipants(ctx context.Context, viewerUserID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) {
|
||||
channel, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
channel, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelParticipantList{}, err
|
||||
}
|
||||
|
|
@ -170,14 +171,21 @@ WHERE channel_id = $1
|
|||
}
|
||||
|
||||
func (s *ChannelStore) GetParticipant(ctx context.Context, viewerUserID, channelID, participantUserID int64) (domain.ChannelMember, error) {
|
||||
_, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
_, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelMember{}, err
|
||||
}
|
||||
if viewerUserID == participantUserID && viewer.Guest {
|
||||
return viewer, nil
|
||||
return domain.ChannelMember{}, domain.ErrUserNotParticipant
|
||||
}
|
||||
return s.getChannelMember(ctx, s.db, channelID, participantUserID)
|
||||
member, err := s.getChannelMember(ctx, s.db, channelID, participantUserID)
|
||||
if errors.Is(err, domain.ErrChannelPrivate) {
|
||||
return domain.ChannelMember{}, domain.ErrUserNotParticipant
|
||||
}
|
||||
if err == nil && participantUserID == viewerUserID && member.Status == domain.ChannelMemberLeft {
|
||||
return domain.ChannelMember{}, domain.ErrUserNotParticipant
|
||||
}
|
||||
return member, err
|
||||
}
|
||||
|
||||
func (s *ChannelStore) ListActiveChannelMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
|
||||
|
|
@ -204,7 +212,7 @@ func (s *ChannelStore) ListActiveChannelMemberIDs(ctx context.Context, viewerUse
|
|||
}
|
||||
|
||||
func (s *ChannelStore) ListActiveChannelMembers(ctx context.Context, viewerUserID, channelID int64, limit int) (domain.Channel, domain.ChannelMember, []domain.ChannelMember, error) {
|
||||
channel, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
channel, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, nil, err
|
||||
}
|
||||
|
|
@ -237,7 +245,7 @@ LIMIT $2`, channelID, limit)
|
|||
}
|
||||
|
||||
func (s *ChannelStore) ListActiveChannelBotMembers(ctx context.Context, viewerUserID, channelID int64, offset, limit int) (domain.ChannelParticipantList, error) {
|
||||
channel, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
channel, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelParticipantList{}, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,10 +66,18 @@ func (s *ChannelStore) sendChannelMessageOnce(ctx context.Context, req domain.Se
|
|||
channel, member, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
|
||||
if errors.Is(err, domain.ErrChannelPrivate) {
|
||||
if candidate, candidateErr := s.channelByID(ctx, tx, req.ChannelID); candidateErr == nil {
|
||||
var guest bool
|
||||
member, guest, err = s.getLinkedDiscussionGuest(ctx, tx, req.UserID, candidate)
|
||||
if guest {
|
||||
guestMember, guest, guestErr := s.getLinkedDiscussionGuest(ctx, tx, req.UserID, candidate)
|
||||
switch {
|
||||
case guestErr != nil:
|
||||
err = guestErr
|
||||
case guest:
|
||||
channel = candidate
|
||||
member = guestMember
|
||||
err = nil
|
||||
default:
|
||||
// A clean "not a linked guest" result is not authorization.
|
||||
// Preserve the original private-member error and fail closed.
|
||||
err = domain.ErrChannelPrivate
|
||||
}
|
||||
} else {
|
||||
err = candidateErr
|
||||
|
|
|
|||
|
|
@ -0,0 +1,263 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
appdialogs "telesrv/internal/app/dialogs"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestPublicChannelAndMegagroupPreviewPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{AccessHash: 941, Phone: "+1941" + suffix + "01", FirstName: "PreviewOwner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
viewer, err := users.Create(ctx, domain.User{AccessHash: 942, Phone: "+1942" + suffix + "02", FirstName: "PreviewViewer"})
|
||||
if err != nil {
|
||||
t.Fatalf("create viewer: %v", err)
|
||||
}
|
||||
channels := NewChannelStore(pool)
|
||||
var channelIDs []int64
|
||||
t.Cleanup(func() {
|
||||
if len(channelIDs) != 0 {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
|
||||
}
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, viewer.ID})
|
||||
})
|
||||
|
||||
for i, tc := range []struct {
|
||||
name string
|
||||
broadcast bool
|
||||
}{
|
||||
{name: "broadcast", broadcast: true},
|
||||
{name: "megagroup"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
created, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Public Preview " + tc.name + " " + suffix,
|
||||
Broadcast: tc.broadcast,
|
||||
Megagroup: !tc.broadcast,
|
||||
Date: 1700009400 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create channel: %v", err)
|
||||
}
|
||||
channelIDs = append(channelIDs, created.Channel.ID)
|
||||
public, err := channels.UpdateUsername(ctx, domain.UpdateChannelUsernameRequest{
|
||||
UserID: owner.ID, ChannelID: created.Channel.ID, Username: "pub" + tc.name + suffix,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("make public: %v", err)
|
||||
}
|
||||
sent, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: owner.ID, ChannelID: public.ID, RandomID: int64(94100 + i), Message: "public history", Date: 1700009410 + i,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send public message: %v", err)
|
||||
}
|
||||
history, err := channels.ListChannelHistory(ctx, viewer.ID, domain.ChannelHistoryFilter{ChannelID: public.ID, Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("public preview history: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, message := range history.Messages {
|
||||
if message.ID == sent.Message.ID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found || history.Self.Status != domain.ChannelMemberLeft {
|
||||
t.Fatalf("preview history = %+v self=%+v", history.Messages, history.Self)
|
||||
}
|
||||
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)
|
||||
}
|
||||
var memberExists bool
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
|
||||
)`, public.ID, viewer.ID).Scan(&memberExists); err != nil {
|
||||
t.Fatalf("check preview member row: %v", err)
|
||||
}
|
||||
if memberExists {
|
||||
t.Fatal("public preview persisted a channel member row")
|
||||
}
|
||||
if _, err := channels.JoinChannel(ctx, public.ID, viewer.ID, 1700009420+i); err != nil {
|
||||
t.Fatalf("join public peer: %v", err)
|
||||
}
|
||||
if _, err := channels.LeaveChannel(ctx, public.ID, viewer.ID, 1700009430+i); err != nil {
|
||||
t.Fatalf("leave public peer: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if _, err := channels.ListChannelHistory(ctx, viewer.ID, domain.ChannelHistoryFilter{ChannelID: public.ID, Limit: 20}); err != nil {
|
||||
t.Fatalf("public history after leave: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID, Title: "Private Preview " + suffix, Megagroup: true, Date: 1700009450,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create private group: %v", err)
|
||||
}
|
||||
channelIDs = append(channelIDs, private.Channel.ID)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkedDiscussionGuestPeerDialogProjectionPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{AccessHash: 951, Phone: "+1951" + suffix + "01", FirstName: "DiscussionOwner"})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
subscriber, err := users.Create(ctx, domain.User{AccessHash: 952, Phone: "+1952" + suffix + "02", FirstName: "DiscussionSubscriber"})
|
||||
if err != nil {
|
||||
t.Fatalf("create subscriber: %v", err)
|
||||
}
|
||||
outsider, err := users.Create(ctx, domain.User{AccessHash: 953, Phone: "+1953" + suffix + "03", FirstName: "DiscussionOutsider"})
|
||||
if err != nil {
|
||||
t.Fatalf("create outsider: %v", err)
|
||||
}
|
||||
channels := NewChannelStore(pool,
|
||||
WithChannelRowCache(NewChannelRowCache(32)),
|
||||
WithChannelMemberCache(NewChannelMemberCache(64)))
|
||||
var channelIDs []int64
|
||||
t.Cleanup(func() {
|
||||
if len(channelIDs) != 0 {
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM channels WHERE id = ANY($1::bigint[])", channelIDs)
|
||||
}
|
||||
_, _ = pool.Exec(ctx, "DELETE FROM users WHERE id = ANY($1::bigint[])", []int64{owner.ID, subscriber.ID, outsider.ID})
|
||||
})
|
||||
|
||||
broadcast, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID, Title: "Peer Dialog Source " + suffix, Broadcast: true, Date: 1700009500,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create broadcast: %v", err)
|
||||
}
|
||||
channelIDs = append(channelIDs, broadcast.Channel.ID)
|
||||
group, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID, Title: "Peer Dialog Group " + suffix, Megagroup: true, Date: 1700009501,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create discussion group: %v", err)
|
||||
}
|
||||
channelIDs = append(channelIDs, group.Channel.ID)
|
||||
if _, err := channels.SetDiscussionGroup(ctx, owner.ID, broadcast.Channel.ID, group.Channel.ID); err != nil {
|
||||
t.Fatalf("set discussion group: %v", err)
|
||||
}
|
||||
if _, err := channels.InviteToChannel(ctx, broadcast.Channel.ID, owner.ID, []int64{subscriber.ID}, 1700009502); err != nil {
|
||||
t.Fatalf("invite broadcast subscriber: %v", err)
|
||||
}
|
||||
post, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: owner.ID, ChannelID: broadcast.Channel.ID, RandomID: 95101, Message: "peer dialog root", Date: 1700009503,
|
||||
})
|
||||
if err != nil || post.Discussion == nil {
|
||||
t.Fatalf("send linked post = %+v err %v", post, err)
|
||||
}
|
||||
|
||||
views, err := channels.GetChannels(ctx, subscriber.ID, []int64{group.Channel.ID})
|
||||
if err != nil || len(views) != 1 {
|
||||
t.Fatalf("batch linked guest views = %+v err %v, want one", views, err)
|
||||
}
|
||||
view := views[0]
|
||||
if !view.Self.Guest || view.Self.Status != domain.ChannelMemberLeft || view.Dialog.TopMessageID == 0 || view.Channel.Pts == 0 {
|
||||
t.Fatalf("batch linked guest view = %+v dialog=%+v channel=%+v", view.Self, view.Dialog, view.Channel)
|
||||
}
|
||||
dialogs := appdialogs.NewService(nil, channels)
|
||||
peerDialogs, err := dialogs.GetPeerDialogs(ctx, subscriber.ID, []domain.Peer{{Type: domain.PeerTypeChannel, ID: group.Channel.ID}})
|
||||
if err != nil {
|
||||
t.Fatalf("get linked guest peer dialogs: %v", err)
|
||||
}
|
||||
if len(peerDialogs.Dialogs) != 1 || len(peerDialogs.Channels) != 1 || len(peerDialogs.ChannelMessages) == 0 {
|
||||
t.Fatalf("linked guest peer dialogs = %+v, want transient dialog/channel/top message", peerDialogs)
|
||||
}
|
||||
if peerDialogs.Dialogs[0].TopMessage == 0 || peerDialogs.Dialogs[0].Pts != view.Channel.Pts {
|
||||
t.Fatalf("linked guest dialog = %+v, want top message and pts %d", peerDialogs.Dialogs[0], view.Channel.Pts)
|
||||
}
|
||||
directReplies, err := channels.ListChannelReplies(ctx, subscriber.ID, domain.ChannelRepliesFilter{
|
||||
ChannelID: group.Channel.ID, RootMessageID: post.Discussion.Message.ID, Limit: 20,
|
||||
})
|
||||
if err != nil || !directReplies.Self.Guest || directReplies.Self.Status != domain.ChannelMemberLeft || directReplies.Channel.ID != group.Channel.ID {
|
||||
t.Fatalf("direct linked guest replies = %+v err %v, want guest self for group", directReplies, err)
|
||||
}
|
||||
viaBroadcastReplies, err := channels.ListChannelReplies(ctx, subscriber.ID, domain.ChannelRepliesFilter{
|
||||
ChannelID: broadcast.Channel.ID, RootMessageID: post.Message.ID, Limit: 20,
|
||||
})
|
||||
if err != nil || !viaBroadcastReplies.Self.Guest || viaBroadcastReplies.Self.Status != domain.ChannelMemberLeft || viaBroadcastReplies.Channel.ID != group.Channel.ID {
|
||||
t.Fatalf("broadcast linked guest replies = %+v err %v, want guest self for target group", viaBroadcastReplies, err)
|
||||
}
|
||||
|
||||
outsiderViews, err := channels.GetChannels(ctx, outsider.ID, []int64{group.Channel.ID})
|
||||
if err != nil || len(outsiderViews) != 0 {
|
||||
t.Fatalf("private discussion outsider views = %+v err %v, want empty", outsiderViews, err)
|
||||
}
|
||||
outsiderDialogs, err := dialogs.GetPeerDialogs(ctx, outsider.ID, []domain.Peer{{Type: domain.PeerTypeChannel, ID: group.Channel.ID}})
|
||||
if err != nil || len(outsiderDialogs.Dialogs) != 0 {
|
||||
t.Fatalf("private discussion outsider dialogs = %+v err %v, want empty", outsiderDialogs, err)
|
||||
}
|
||||
if _, err := channels.InviteToChannel(ctx, broadcast.Channel.ID, owner.ID, []int64{outsider.ID}, 1700009504); err != nil {
|
||||
t.Fatalf("invite second broadcast subscriber: %v", err)
|
||||
}
|
||||
if _, err := channels.EditChannelBanned(ctx, domain.EditChannelBannedRequest{
|
||||
UserID: owner.ID,
|
||||
ChannelID: group.Channel.ID,
|
||||
Participant: domain.Peer{Type: domain.PeerTypeUser, ID: outsider.ID},
|
||||
BannedRights: domain.ChannelBannedRights{
|
||||
ViewMessages: true,
|
||||
UntilDate: 2147483647,
|
||||
},
|
||||
Date: 1700009505,
|
||||
}); err != nil {
|
||||
t.Fatalf("ban linked subscriber from target group: %v", err)
|
||||
}
|
||||
bannedViews, err := channels.GetChannels(ctx, outsider.ID, []int64{group.Channel.ID})
|
||||
if err != nil || len(bannedViews) != 1 || !bannedViews[0].Forbidden || bannedViews[0].Self.Guest {
|
||||
t.Fatalf("target-banned linked subscriber views = %+v err %v, want forbidden non-guest", bannedViews, err)
|
||||
}
|
||||
bannedDialogs, err := dialogs.GetPeerDialogs(ctx, outsider.ID, []domain.Peer{{Type: domain.PeerTypeChannel, ID: group.Channel.ID}})
|
||||
if !errors.Is(err, domain.ErrChannelUserBanned) || len(bannedDialogs.Dialogs) != 0 {
|
||||
t.Fatalf("target-banned linked subscriber dialogs = %+v err %v, want ErrChannelUserBanned without dialog", bannedDialogs, err)
|
||||
}
|
||||
if _, err := channels.ListChannelReplies(ctx, outsider.ID, domain.ChannelRepliesFilter{
|
||||
ChannelID: group.Channel.ID, RootMessageID: post.Discussion.Message.ID, Limit: 20,
|
||||
}); !errors.Is(err, domain.ErrChannelUserBanned) {
|
||||
t.Fatalf("target-banned direct replies err = %v, want ErrChannelUserBanned", err)
|
||||
}
|
||||
if _, err := channels.ListChannelReplies(ctx, outsider.ID, domain.ChannelRepliesFilter{
|
||||
ChannelID: broadcast.Channel.ID, RootMessageID: post.Message.ID, Limit: 20,
|
||||
}); !errors.Is(err, domain.ErrChannelUserBanned) {
|
||||
t.Fatalf("target-banned broadcast replies err = %v, want ErrChannelUserBanned", err)
|
||||
}
|
||||
|
||||
var memberExists, dialogExists bool
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM channel_members WHERE channel_id = $1 AND user_id = $2
|
||||
)`, group.Channel.ID, subscriber.ID).Scan(&memberExists); err != nil {
|
||||
t.Fatalf("check transient guest member: %v", err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT EXISTS (
|
||||
SELECT 1 FROM channel_dialogs WHERE channel_id = $1 AND user_id = $2
|
||||
)`, group.Channel.ID, subscriber.ID).Scan(&dialogExists); err != nil {
|
||||
t.Fatalf("check transient guest dialog: %v", err)
|
||||
}
|
||||
if memberExists || dialogExists {
|
||||
t.Fatalf("transient guest persisted state: member=%v dialog=%v", memberExists, dialogExists)
|
||||
}
|
||||
}
|
||||
|
|
@ -595,28 +595,27 @@ func (s *ChannelStore) ListChannelReplies(ctx context.Context, viewerUserID int6
|
|||
return domain.ChannelHistory{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
target := source
|
||||
availableMinID := member.AvailableMinID
|
||||
targetMember := member
|
||||
availableMinID := targetMember.AvailableMinID
|
||||
extraChannels := []domain.Channel(nil)
|
||||
rootID := root.ID
|
||||
if source.Broadcast {
|
||||
if root.Discussion == nil || root.Discussion.ChannelID == 0 || root.Discussion.MessageID == 0 {
|
||||
return domain.ChannelHistory{Channel: source}, nil
|
||||
return domain.ChannelHistory{Channel: source, Self: member}, nil
|
||||
}
|
||||
linked, err := getChannelByID(ctx, s.db, root.Discussion.ChannelID)
|
||||
linked, linkedMember, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, root.Discussion.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelHistory{Channel: source}, nil
|
||||
return domain.ChannelHistory{}, err
|
||||
}
|
||||
target = linked
|
||||
targetMember = linkedMember
|
||||
rootID = root.Discussion.MessageID
|
||||
availableMinID = 0
|
||||
if linkedMember, err := s.getChannelMember(ctx, s.db, linked.ID, viewerUserID); err == nil && validateChannelMemberVisible(linkedMember) == nil {
|
||||
availableMinID = linkedMember.AvailableMinID
|
||||
}
|
||||
availableMinID = targetMember.AvailableMinID
|
||||
extraChannels = append(extraChannels, source)
|
||||
}
|
||||
targetRoot, err := s.getChannelMessage(ctx, s.db, target.ID, rootID)
|
||||
if err != nil || targetRoot.Deleted || targetRoot.ID <= availableMinID {
|
||||
return domain.ChannelHistory{Channel: target, Channels: extraChannels}, nil
|
||||
return domain.ChannelHistory{Channel: target, Self: targetMember, Channels: extraChannels}, nil
|
||||
}
|
||||
limit := filter.Limit
|
||||
if limit <= 0 || limit > domain.MaxChannelRepliesLimit {
|
||||
|
|
@ -646,7 +645,7 @@ func (s *ChannelStore) ListChannelReplies(ctx context.Context, viewerUserID int6
|
|||
return domain.ChannelHistory{}, err
|
||||
}
|
||||
}
|
||||
return domain.ChannelHistory{Channel: target, Channels: extraChannels, Topics: topics, Messages: messages, Count: count}, nil
|
||||
return domain.ChannelHistory{Channel: target, Self: targetMember, Channels: extraChannels, Topics: topics, Messages: messages, Count: count}, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) getForumTopic(ctx context.Context, db sqlcgen.DBTX, channelID int64, topicID int) (domain.ChannelForumTopic, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue