fix: sync linked discussion guest support
This commit is contained in:
parent
5f7c0b9804
commit
9f73dc20da
27 changed files with 1004 additions and 30 deletions
|
|
@ -252,6 +252,38 @@ func (s *ChannelStore) GetChannel(ctx context.Context, viewerUserID, channelID i
|
|||
return domain.ChannelView{Channel: channel, Self: member, Dialog: dialog, SelfBoostsApplied: selfBoosts, ExportedInvite: exportedInvite}, nil
|
||||
}
|
||||
|
||||
// GetLinkedDiscussionChannel projects a private discussion peer through the
|
||||
// viewer's active membership in the source broadcast channel. This is a
|
||||
// peer-discovery boundary only; it never creates discussion-group membership.
|
||||
func (s *ChannelStore) GetLinkedDiscussionChannel(ctx context.Context, viewerUserID, sourceChannelID int64) (domain.ChannelView, error) {
|
||||
source, _, err := s.getChannelForMember(ctx, s.db, viewerUserID, sourceChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelView{}, err
|
||||
}
|
||||
if !source.Broadcast || source.LinkedChatID == 0 {
|
||||
return domain.ChannelView{}, domain.ErrChannelInvalid
|
||||
}
|
||||
linked, err := s.channelByID(ctx, s.db, source.LinkedChatID)
|
||||
if err != nil {
|
||||
return domain.ChannelView{}, err
|
||||
}
|
||||
if !linked.Megagroup || linked.Broadcast {
|
||||
return domain.ChannelView{}, domain.ErrChannelInvalid
|
||||
}
|
||||
self, guest, guestErr := s.getLinkedDiscussionGuest(ctx, s.db, viewerUserID, linked)
|
||||
if guestErr != nil {
|
||||
return domain.ChannelView{}, guestErr
|
||||
}
|
||||
if !guest {
|
||||
if member, memberErr := s.getChannelMember(ctx, s.db, linked.ID, viewerUserID); memberErr == nil {
|
||||
self = member
|
||||
} else {
|
||||
return domain.ChannelView{}, memberErr
|
||||
}
|
||||
}
|
||||
return domain.ChannelView{Channel: linked, Self: self}, nil
|
||||
}
|
||||
|
||||
// ResolveChannel 是 GetChannel 的轻量版:只做访问校验并返回 Channel(含 access_hash)+Self,
|
||||
// 跳过 dialog top message / 读态 / boost 求和这 3 条额外 PG 查询。供 inputPeerFor 等只需
|
||||
// access_hash / 频道标志的纯解析路径用——它们此前为拿一个 access_hash 付了完整 4 查询投影。
|
||||
|
|
|
|||
|
|
@ -0,0 +1,138 @@
|
|||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
// TestResolveDiscussionReadTargetPostgres locks the one-query readDiscussion
|
||||
// projection against the real schema. It verifies linked-root mapping and the
|
||||
// durable idempotent boundary without invoking the full discussion aggregate.
|
||||
func TestResolveDiscussionReadTargetPostgres(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
ctx := context.Background()
|
||||
suffix := randomSuffix(t)
|
||||
users := NewUserStore(pool)
|
||||
owner, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 991,
|
||||
Phone: "+1991" + suffix + "01",
|
||||
FirstName: "DiscussionReadOwner",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create owner: %v", err)
|
||||
}
|
||||
subscriber, err := users.Create(ctx, domain.User{
|
||||
AccessHash: 992,
|
||||
Phone: "+1992" + suffix + "02",
|
||||
FirstName: "DiscussionGuest",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create subscriber: %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})
|
||||
})
|
||||
broadcast, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: owner.ID,
|
||||
Title: "Discussion Read Source " + suffix,
|
||||
Broadcast: true,
|
||||
Date: 1700002900,
|
||||
})
|
||||
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: "Discussion Read Group " + suffix,
|
||||
Megagroup: true,
|
||||
Date: 1700002901,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create 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}, 1700002902); err != nil {
|
||||
t.Fatalf("invite broadcast subscriber: %v", err)
|
||||
}
|
||||
post, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: owner.ID,
|
||||
ChannelID: broadcast.Channel.ID,
|
||||
RandomID: 9912901,
|
||||
Message: "discussion read target",
|
||||
Date: 1700002902,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send post: %v", err)
|
||||
}
|
||||
if post.Discussion == nil {
|
||||
t.Fatal("send post discussion result is nil")
|
||||
}
|
||||
rootID := post.Discussion.Message.ID
|
||||
guestView, err := channels.GetChannel(ctx, subscriber.ID, group.Channel.ID)
|
||||
if err != nil || !guestView.Self.Guest || guestView.Self.Status != domain.ChannelMemberLeft {
|
||||
t.Fatalf("linked guest view = %+v err %v", guestView.Self, err)
|
||||
}
|
||||
guestTarget, err := channels.ResolveDiscussionReadTarget(ctx, subscriber.ID, broadcast.Channel.ID, post.Message.ID, rootID)
|
||||
if err != nil || !guestTarget.Guest {
|
||||
t.Fatalf("linked guest read target = %+v err %v", guestTarget, err)
|
||||
}
|
||||
directGuestTarget, err := channels.ResolveDiscussionReadTarget(ctx, subscriber.ID, group.Channel.ID, rootID, rootID)
|
||||
if err != nil || !directGuestTarget.Guest || directGuestTarget.ChannelID != group.Channel.ID {
|
||||
t.Fatalf("direct linked-group guest read target = %+v err %v", directGuestTarget, err)
|
||||
}
|
||||
if _, err := channels.ListActiveChannelBotMemberIDs(ctx, subscriber.ID, group.Channel.ID, 20); err != nil {
|
||||
t.Fatalf("linked guest bot-delivery preflight: %v", err)
|
||||
}
|
||||
if _, err := channels.ListActiveChannelBotMembers(ctx, subscriber.ID, group.Channel.ID, 0, 20); err != nil {
|
||||
t.Fatalf("linked guest bot participants: %v", err)
|
||||
}
|
||||
if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: subscriber.ID, ChannelID: group.Channel.ID, RandomID: 9912902,
|
||||
Message: "guest comment", Date: 1700002903,
|
||||
ReplyTo: &domain.MessageReply{MessageID: rootID, TopMessageID: rootID},
|
||||
}); err != nil {
|
||||
t.Fatalf("send linked guest comment: %v", err)
|
||||
}
|
||||
var persistedGuest 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(&persistedGuest); err != nil {
|
||||
t.Fatalf("check guest member row: %v", err)
|
||||
}
|
||||
if persistedGuest {
|
||||
t.Fatal("linked guest send persisted a discussion-group member row")
|
||||
}
|
||||
target, err := channels.ResolveDiscussionReadTarget(ctx, owner.ID, broadcast.Channel.ID, post.Message.ID, rootID)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve before read: %v", err)
|
||||
}
|
||||
if target.ChannelID != group.Channel.ID || target.RootID != rootID || target.AlreadyRead {
|
||||
t.Fatalf("target before read = %+v, want linked unread root", target)
|
||||
}
|
||||
read, err := channels.ReadChannelHistory(ctx, domain.ReadChannelHistoryRequest{
|
||||
UserID: owner.ID,
|
||||
ChannelID: group.Channel.ID,
|
||||
MaxID: rootID,
|
||||
Date: 1700002903,
|
||||
})
|
||||
if err != nil || !read.Changed {
|
||||
t.Fatalf("read linked group = %+v err %v, want changed", read, err)
|
||||
}
|
||||
target, err = channels.ResolveDiscussionReadTarget(ctx, owner.ID, broadcast.Channel.ID, post.Message.ID, rootID)
|
||||
if err != nil || !target.AlreadyRead {
|
||||
t.Fatalf("resolve after read = %+v err %v, want already read", target, err)
|
||||
}
|
||||
}
|
||||
|
|
@ -371,6 +371,11 @@ func (s *ChannelStore) getChannelForViewer(ctx context.Context, db sqlcgen.DBTX,
|
|||
if err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, err
|
||||
}
|
||||
if guest, ok, guestErr := s.getLinkedDiscussionGuest(ctx, db, viewerUserID, ch); guestErr != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, guestErr
|
||||
} else if ok {
|
||||
return ch, guest, true, nil
|
||||
}
|
||||
if member, _, ok, err := s.monoforumAdminPreview(ctx, db, viewerUserID, ch); err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, err
|
||||
} else if ok {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,77 @@ func (s *ChannelStore) getChannelForMember(ctx context.Context, db sqlcgen.DBTX,
|
|||
return ch, member, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) getChannelForMemberOrLinkedGuest(ctx context.Context, db sqlcgen.DBTX, viewerUserID, channelID int64) (domain.Channel, domain.ChannelMember, error) {
|
||||
channel, member, err := s.getChannelForMember(ctx, db, viewerUserID, channelID)
|
||||
if !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
return channel, member, err
|
||||
}
|
||||
target, targetErr := s.channelByID(ctx, db, channelID)
|
||||
if targetErr != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, targetErr
|
||||
}
|
||||
guest, allowed, guestErr := s.getLinkedDiscussionGuest(ctx, db, viewerUserID, target)
|
||||
if guestErr != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, guestErr
|
||||
}
|
||||
if !allowed {
|
||||
return domain.Channel{}, domain.ChannelMember{}, err
|
||||
}
|
||||
return target, guest, nil
|
||||
}
|
||||
|
||||
// getLinkedDiscussionGuest authorizes a private discussion group through an
|
||||
// active membership in its bidirectionally linked broadcast. The returned
|
||||
// member is computed only and must never be persisted. An explicit target ban
|
||||
// or kick takes precedence over the source-channel membership.
|
||||
func (s *ChannelStore) getLinkedDiscussionGuest(ctx context.Context, db sqlcgen.DBTX, viewerUserID int64, target domain.Channel) (domain.ChannelMember, bool, error) {
|
||||
if target.Broadcast || !target.Megagroup || target.LinkedChatID == 0 {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
existing, err := s.getChannelMember(ctx, db, target.ID, viewerUserID)
|
||||
if err == nil {
|
||||
if existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages {
|
||||
return domain.ChannelMember{}, false, domain.ErrChannelUserBanned
|
||||
}
|
||||
if existing.Status == domain.ChannelMemberActive {
|
||||
return existing, false, nil
|
||||
}
|
||||
} else if !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
return domain.ChannelMember{}, false, err
|
||||
}
|
||||
source, err := s.channelByID(ctx, db, target.LinkedChatID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrChannelInvalid) {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
return domain.ChannelMember{}, false, err
|
||||
}
|
||||
if !source.Broadcast || source.LinkedChatID != target.ID {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
sourceMember, err := s.getChannelMember(ctx, db, source.ID, viewerUserID)
|
||||
if err != nil {
|
||||
if errors.Is(err, domain.ErrChannelPrivate) {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
return domain.ChannelMember{}, false, err
|
||||
}
|
||||
if err := validateChannelMemberVisible(sourceMember); err != nil {
|
||||
return domain.ChannelMember{}, false, err
|
||||
}
|
||||
guest := domain.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
UserID: viewerUserID,
|
||||
Status: domain.ChannelMemberLeft,
|
||||
Role: domain.ChannelRoleMember,
|
||||
Guest: true,
|
||||
}
|
||||
if s.memberCacheActive(db) {
|
||||
s.memberCache.put(guest)
|
||||
}
|
||||
return guest, true, 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 {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,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.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
||||
channel, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelParticipantList{}, err
|
||||
}
|
||||
|
|
@ -170,9 +170,13 @@ WHERE channel_id = $1
|
|||
}
|
||||
|
||||
func (s *ChannelStore) GetParticipant(ctx context.Context, viewerUserID, channelID, participantUserID int64) (domain.ChannelMember, error) {
|
||||
if _, _, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID); err != nil {
|
||||
_, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelMember{}, err
|
||||
}
|
||||
if viewerUserID == participantUserID && viewer.Guest {
|
||||
return viewer, nil
|
||||
}
|
||||
return s.getChannelMember(ctx, s.db, channelID, participantUserID)
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +204,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.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
||||
channel, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, nil, err
|
||||
}
|
||||
|
|
@ -233,7 +237,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.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
||||
channel, viewer, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelParticipantList{}, err
|
||||
}
|
||||
|
|
@ -279,7 +283,7 @@ OFFSET $2 LIMIT $3`, channelID, offset, limit)
|
|||
}
|
||||
|
||||
func (s *ChannelStore) ListActiveChannelBotMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
|
||||
if _, _, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID); err != nil {
|
||||
if _, _, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout {
|
||||
|
|
|
|||
|
|
@ -565,6 +565,165 @@ func (s *ChannelStore) GetDiscussionMessage(ctx context.Context, viewerUserID, c
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// ResolveDiscussionReadTarget resolves source post -> linked discussion root
|
||||
// and checks the durable read boundary in one indexed query. It is the hot path
|
||||
// for messages.readDiscussion and must not load reply stats, reactions or peer
|
||||
// payloads used by messages.getDiscussionMessage.
|
||||
func (s *ChannelStore) ResolveDiscussionReadTarget(ctx context.Context, userID, sourceChannelID int64, sourceMessageID, readMaxID int) (domain.ChannelDiscussionReadTarget, error) {
|
||||
var out domain.ChannelDiscussionReadTarget
|
||||
var readInboxMaxID int
|
||||
var unreadMark bool
|
||||
var targetVisible bool
|
||||
var targetGuest bool
|
||||
var targetTopMessageID int
|
||||
err := s.db.QueryRow(ctx, `
|
||||
WITH source AS (
|
||||
SELECT c.broadcast,
|
||||
m.discussion_channel_id,
|
||||
m.discussion_message_id,
|
||||
(
|
||||
c.megagroup
|
||||
AND NOT c.broadcast
|
||||
AND c.linked_chat_id <> 0
|
||||
AND (viewer.user_id IS NULL OR viewer.status = 'left')
|
||||
AND NOT COALESCE((viewer.banned_rights->>'ViewMessages')::boolean, false)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM channels parent
|
||||
JOIN channel_members parent_member
|
||||
ON parent_member.channel_id = parent.id
|
||||
AND parent_member.user_id = $1
|
||||
AND parent_member.status = 'active'
|
||||
AND NOT COALESCE((parent_member.banned_rights->>'ViewMessages')::boolean, false)
|
||||
WHERE parent.id = c.linked_chat_id
|
||||
AND parent.broadcast
|
||||
AND NOT parent.deleted
|
||||
AND parent.linked_chat_id = c.id
|
||||
)
|
||||
) AS linked_guest
|
||||
FROM channels c
|
||||
LEFT JOIN channel_members viewer
|
||||
ON viewer.channel_id = c.id
|
||||
AND viewer.user_id = $1
|
||||
JOIN channel_messages m
|
||||
ON m.channel_id = c.id
|
||||
AND m.id = $3
|
||||
AND NOT m.deleted
|
||||
AND m.id > COALESCE(viewer.available_min_id, 0)
|
||||
WHERE c.id = $2 AND NOT c.deleted
|
||||
AND (
|
||||
(
|
||||
viewer.status = 'active'
|
||||
AND NOT COALESCE((viewer.banned_rights->>'ViewMessages')::boolean, false)
|
||||
)
|
||||
OR (
|
||||
c.megagroup
|
||||
AND NOT c.broadcast
|
||||
AND c.linked_chat_id <> 0
|
||||
AND (viewer.user_id IS NULL OR viewer.status = 'left')
|
||||
AND NOT COALESCE((viewer.banned_rights->>'ViewMessages')::boolean, false)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM channels parent
|
||||
JOIN channel_members parent_member
|
||||
ON parent_member.channel_id = parent.id
|
||||
AND parent_member.user_id = $1
|
||||
AND parent_member.status = 'active'
|
||||
AND NOT COALESCE((parent_member.banned_rights->>'ViewMessages')::boolean, false)
|
||||
WHERE parent.id = c.linked_chat_id
|
||||
AND parent.broadcast
|
||||
AND NOT parent.deleted
|
||||
AND parent.linked_chat_id = c.id
|
||||
)
|
||||
)
|
||||
)
|
||||
), target AS (
|
||||
SELECT CASE
|
||||
WHEN broadcast AND discussion_channel_id <> 0 AND discussion_message_id <> 0
|
||||
THEN discussion_channel_id
|
||||
ELSE $2
|
||||
END AS channel_id,
|
||||
CASE
|
||||
WHEN broadcast AND discussion_channel_id <> 0 AND discussion_message_id <> 0
|
||||
THEN discussion_message_id
|
||||
ELSE $3
|
||||
END AS root_id,
|
||||
broadcast,
|
||||
linked_guest,
|
||||
discussion_channel_id,
|
||||
discussion_message_id
|
||||
FROM source
|
||||
)
|
||||
SELECT target.channel_id, target.root_id,
|
||||
target_channel.top_message_id,
|
||||
COALESCE(target_member.read_inbox_max_id, 0),
|
||||
COALESCE(target_member.unread_mark, false),
|
||||
COALESCE(
|
||||
target_member.status = 'active'
|
||||
AND NOT COALESCE((target_member.banned_rights->>'ViewMessages')::boolean, false),
|
||||
false
|
||||
) OR target.linked_guest OR (
|
||||
target.channel_id <> $2
|
||||
AND target.broadcast
|
||||
AND target_channel.linked_chat_id = $2
|
||||
AND (
|
||||
target_member.user_id IS NULL
|
||||
OR target_member.status = 'left'
|
||||
)
|
||||
) AS target_visible,
|
||||
(
|
||||
target.linked_guest OR (
|
||||
target.channel_id <> $2
|
||||
AND target.broadcast
|
||||
AND target_channel.linked_chat_id = $2
|
||||
AND (
|
||||
target_member.user_id IS NULL
|
||||
OR target_member.status = 'left'
|
||||
)
|
||||
)
|
||||
) AS target_guest
|
||||
FROM target
|
||||
JOIN channels target_channel
|
||||
ON target_channel.id = target.channel_id
|
||||
AND NOT target_channel.deleted
|
||||
LEFT JOIN channel_members target_member
|
||||
ON target_member.channel_id = target.channel_id
|
||||
AND target_member.user_id = $1
|
||||
WHERE (
|
||||
NOT target.broadcast
|
||||
OR (target.discussion_channel_id = 0 AND target.discussion_message_id = 0)
|
||||
OR (
|
||||
target.discussion_channel_id <> 0
|
||||
AND target.discussion_message_id <> 0
|
||||
AND target_channel.megagroup
|
||||
AND NOT target_channel.broadcast
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM channel_messages root
|
||||
WHERE root.channel_id = target.channel_id
|
||||
AND root.id = target.root_id
|
||||
AND NOT root.deleted
|
||||
)
|
||||
)
|
||||
)`, userID, sourceChannelID, sourceMessageID).Scan(&out.ChannelID, &out.RootID, &targetTopMessageID, &readInboxMaxID, &unreadMark, &targetVisible, &targetGuest)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.ChannelDiscussionReadTarget{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return domain.ChannelDiscussionReadTarget{}, fmt.Errorf("resolve discussion read target: %w", err)
|
||||
}
|
||||
if !targetVisible {
|
||||
return domain.ChannelDiscussionReadTarget{}, domain.ErrChannelPrivate
|
||||
}
|
||||
effectiveMaxID := readMaxID
|
||||
if effectiveMaxID <= 0 || effectiveMaxID > targetTopMessageID {
|
||||
effectiveMaxID = targetTopMessageID
|
||||
}
|
||||
out.AlreadyRead = effectiveMaxID <= readInboxMaxID && !unreadMark
|
||||
out.Guest = targetGuest
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) readChannelHistoryOnce(ctx context.Context, req domain.ReadChannelHistoryRequest) (domain.ReadChannelHistoryResult, error) {
|
||||
channel, _, err := s.getChannelForMember(ctx, s.db, req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -64,9 +64,23 @@ 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 {
|
||||
channel = candidate
|
||||
}
|
||||
} else {
|
||||
err = candidateErr
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return domain.SendChannelMessageResult{}, err
|
||||
}
|
||||
if member.Guest && channel.JoinToSend {
|
||||
return domain.SendChannelMessageResult{}, domain.ErrChannelWriteForbidden
|
||||
}
|
||||
fromBoostsApplied := 0
|
||||
if channel.Megagroup {
|
||||
fromBoostsApplied, err = countActiveUserBoostsForPeer(ctx, tx, req.UserID, domain.Peer{Type: domain.PeerTypeChannel, ID: req.ChannelID}, req.Date)
|
||||
|
|
|
|||
|
|
@ -108,13 +108,16 @@ func (s *ChannelStore) ReadChannelTopicHistory(ctx context.Context, req domain.R
|
|||
if req.UserID == 0 || req.ChannelID == 0 || req.TopicID <= 0 {
|
||||
return domain.ReadChannelTopicHistoryResult{}, domain.ErrChannelInvalid
|
||||
}
|
||||
channel, member, err := s.getChannelForMember(ctx, s.db, req.UserID, req.ChannelID)
|
||||
channel, member, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ReadChannelTopicHistoryResult{}, err
|
||||
}
|
||||
if !channel.Forum {
|
||||
return domain.ReadChannelTopicHistoryResult{}, domain.ErrChannelForumMissing
|
||||
}
|
||||
if member.Guest {
|
||||
return domain.ReadChannelTopicHistoryResult{}, domain.ErrChannelPrivate
|
||||
}
|
||||
topMax, err := s.channelTopicTopMessageID(ctx, req.ChannelID, req.TopicID, member.AvailableMinID)
|
||||
if err != nil {
|
||||
return domain.ReadChannelTopicHistoryResult{}, err
|
||||
|
|
|
|||
|
|
@ -586,7 +586,7 @@ ORDER BY pinned DESC, pinned_order DESC, date DESC, topic_id DESC`, channelID, m
|
|||
}
|
||||
|
||||
func (s *ChannelStore) ListChannelReplies(ctx context.Context, viewerUserID int64, filter domain.ChannelRepliesFilter) (domain.ChannelHistory, error) {
|
||||
source, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, filter.ChannelID)
|
||||
source, member, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, filter.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelHistory{}, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue