fix(channels): sync monoforum suggested forwards and reactions

This commit is contained in:
iamxvbaba 2026-07-30 16:05:25 +08:00
parent 45a7e117ea
commit fd47dd765a
17 changed files with 650 additions and 23 deletions

View file

@ -458,6 +458,19 @@ func (s *ChannelStore) channelForViewerLocked(userID, channelID int64) (domain.C
return channel, publicPreviewMember(channel, userID, existing, found), true, nil
}
// channelMessageVisibleToViewerLocked applies the message-level half of synthetic monoforum
// access. The channel shell is visible without a channel_members row, but a subscriber may only
// address messages in saved_peer=self; managers may address every subscriber sub-dialog.
func channelMessageVisibleToViewerLocked(channel domain.Channel, member domain.ChannelMember, viewerUserID int64, msg domain.ChannelMessage) bool {
if !channel.Monoforum {
return true
}
if member.CanManageDirectMessages() {
return true
}
return msg.SavedPeer == (domain.Peer{Type: domain.PeerTypeUser, ID: viewerUserID})
}
func (s *ChannelStore) dialogForUserLocked(userID int64, channel domain.Channel) domain.ChannelDialog {
return s.dialogForMemberLocked(userID, channel, s.members[channel.ID][userID])
}

View file

@ -368,6 +368,9 @@ func (s *ChannelStore) GetChannelMessages(_ context.Context, viewerUserID, chann
if msg.Deleted || msg.ID <= member.AvailableMinID {
continue
}
if !channelMessageVisibleToViewerLocked(channel, member, viewerUserID, msg) {
continue
}
messages = append(messages, cloneChannelMessage(msg))
delete(wanted, msg.ID)
}

View file

@ -119,6 +119,7 @@ func (s *ChannelStore) SendMonoforumMessage(_ context.Context, req domain.SendMo
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Media: req.Media,
ReplyTo: req.ReplyTo,
Forward: req.Forward,
Pts: pts,
}
// Store owns the persisted snapshot; callers must not be able to mutate it through

View file

@ -27,8 +27,9 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
}
sub := domain.Peer{Type: domain.PeerTypeUser, ID: 42}
forward := &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, Date: 1_700_000_999}
m1, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 42, SavedPeer: sub, RandomID: 111, Message: "hi", Date: 1_700_001_001})
m1, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 42, SavedPeer: sub, RandomID: 111, Message: "hi", Forward: forward, Date: 1_700_001_001})
if err != nil {
t.Fatalf("subscriber send 1: %v", err)
}
@ -75,7 +76,7 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
}
// 幂等:相同 randomID 返回原消息、不重复。
dup, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 42, SavedPeer: sub, RandomID: 111, Message: "hi", Date: 1_700_001_004})
dup, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 42, SavedPeer: sub, RandomID: 111, Message: "hi", Forward: forward, Date: 1_700_001_004})
if err != nil {
t.Fatalf("dup send: %v", err)
}
@ -99,6 +100,9 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
if hist.Messages[0].ReplyTo == nil || hist.Messages[0].ReplyTo.MessageID != m1.Message.ID {
t.Fatalf("history[0] reply = %+v, want message %d", hist.Messages[0].ReplyTo, m1.Message.ID)
}
if oldest := hist.Messages[len(hist.Messages)-1]; oldest.Forward == nil || oldest.Forward.From.ID != 1 || oldest.Forward.Date != 1_700_000_999 {
t.Fatalf("persisted monoforum forward = %+v, want source user 1/date 1700000999", oldest.Forward)
}
if _, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 1, SavedPeer: sub, RandomID: 114, Message: "cross reply", ReplyTo: &domain.MessageReply{MessageID: 999999, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: monoID}}, Date: 1_700_001_004}); !errors.Is(err, domain.ErrReplyMessageIDInvalid) {
t.Fatalf("invalid monoforum reply err = %v, want ErrReplyMessageIDInvalid", err)
}
@ -110,7 +114,8 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
// 另一个订阅者的私信不串会话。
other := domain.Peer{Type: domain.PeerTypeUser, ID: 99}
if _, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 99, SavedPeer: other, RandomID: 201, Message: "other", Date: 1_700_001_005}); err != nil {
otherMessage, err := store.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: 99, SavedPeer: other, RandomID: 201, Message: "other", Date: 1_700_001_005})
if err != nil {
t.Fatalf("other subscriber send: %v", err)
}
subHist, _ := store.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{MonoforumID: monoID, SavedPeer: sub, Limit: 10})
@ -129,6 +134,67 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
t.Fatalf("subscriber channel history leaked message %+v", message)
}
}
exactMessages, err := store.GetChannelMessages(ctx, 42, monoID, []int{m1.Message.ID, otherMessage.Message.ID})
if err != nil {
t.Fatalf("subscriber exact monoforum messages: %v", err)
}
if len(exactMessages.Messages) != 1 || exactMessages.Messages[0].ID != m1.Message.ID {
t.Fatalf("subscriber exact monoforum messages = %+v, want only own message %d", exactMessages.Messages, m1.Message.ID)
}
if _, err := store.SetChannelMessageReactions(ctx, domain.SetChannelMessageReactionsRequest{
UserID: 42, ChannelID: monoID, MessageID: m1.Message.ID,
Reactions: []domain.MessageReaction{{Type: domain.MessageReactionEmoji, Emoticon: "\U0001f44d"}},
Date: 1_700_001_006,
}); err != nil {
t.Fatalf("subscriber react to own monoforum message: %v", err)
}
if _, err := store.SetChannelMessageReactions(ctx, domain.SetChannelMessageReactionsRequest{
UserID: 42, ChannelID: monoID, MessageID: otherMessage.Message.ID,
Reactions: []domain.MessageReaction{{Type: domain.MessageReactionEmoji, Emoticon: "\U0001f525"}},
Date: 1_700_001_006,
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
t.Fatalf("subscriber react to another saved_peer err = %v, want ErrMessageIDInvalid", err)
}
subReactions, err := store.GetChannelMessageReactions(ctx, domain.ChannelMessageReactionsRequest{
UserID: 42, ChannelID: monoID, IDs: []int{m1.Message.ID, otherMessage.Message.ID},
})
if err != nil {
t.Fatalf("subscriber get monoforum reactions: %v", err)
}
if len(subReactions.Messages) != 1 || subReactions.Messages[0].ID != m1.Message.ID {
t.Fatalf("subscriber monoforum reactions = %+v, want only own message %d", subReactions.Messages, m1.Message.ID)
}
adminReactions, err := store.GetChannelMessageReactions(ctx, domain.ChannelMessageReactionsRequest{
UserID: 1, ChannelID: monoID, IDs: []int{m1.Message.ID, otherMessage.Message.ID},
})
if err != nil {
t.Fatalf("admin get monoforum reactions: %v", err)
}
if len(adminReactions.Messages) != 2 {
t.Fatalf("admin monoforum reactions = %+v, want both subscriber messages", adminReactions.Messages)
}
reactionList, err := store.ListChannelMessageReactions(ctx, domain.ChannelMessageReactionsListRequest{
UserID: 42, ChannelID: monoID, MessageID: m1.Message.ID, Limit: 10,
})
if err != nil || reactionList.Count != 1 || len(reactionList.Reactions) != 1 {
t.Fatalf("subscriber monoforum reaction list = %+v, %v; want one", reactionList, err)
}
if _, err := store.ListChannelMessageReactions(ctx, domain.ChannelMessageReactionsListRequest{
UserID: 42, ChannelID: monoID, MessageID: otherMessage.Message.ID, Limit: 10,
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
t.Fatalf("subscriber list another saved_peer reactions err = %v, want ErrMessageIDInvalid", err)
}
reactionLookup, found, err := store.FindChannelMessageReaction(ctx, domain.ChannelMessageReactionLookupRequest{
ViewerUserID: 42, ChannelID: monoID, MessageID: m1.Message.ID, ReactorUserID: 42,
})
if err != nil || !found || len(reactionLookup.Reactions) != 1 {
t.Fatalf("subscriber monoforum reaction lookup = %+v, %v, %v; want one", reactionLookup, found, err)
}
if _, _, err := store.FindChannelMessageReaction(ctx, domain.ChannelMessageReactionLookupRequest{
ViewerUserID: 42, ChannelID: monoID, MessageID: otherMessage.Message.ID, ReactorUserID: 99,
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
t.Fatalf("subscriber lookup another saved_peer reaction err = %v, want ErrMessageIDInvalid", err)
}
diff, err := store.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{UserID: 42, ChannelID: monoID, Pts: 0, Limit: 100})
if err != nil {
t.Fatalf("subscriber channel difference: %v", err)

View file

@ -46,7 +46,7 @@ func (s *ChannelStore) SetChannelMessageReactions(_ context.Context, req domain.
}
s.mu.Lock()
defer s.mu.Unlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
channel, member, _, err := s.channelForViewerLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
@ -64,7 +64,8 @@ func (s *ChannelStore) SetChannelMessageReactions(_ context.Context, req domain.
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
msg := s.messages[req.ChannelID][idx]
if msg.Deleted || msg.Action != nil || msg.ID <= member.AvailableMinID {
if msg.Deleted || msg.Action != nil || msg.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewerLocked(channel, member, req.UserID, msg) {
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
// 仅新增/替换受策略约束;空向量是撤销,策略收紧后也必须允许撤销存量 reaction。
@ -385,7 +386,7 @@ func (s *ChannelStore) GetChannelMessageReactions(_ context.Context, req domain.
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
channel, member, _, err := s.channelForViewerLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
@ -401,7 +402,8 @@ func (s *ChannelStore) GetChannelMessageReactions(_ context.Context, req domain.
if _, ok := wanted[msg.ID]; !ok {
continue
}
if msg.Deleted || msg.ID <= member.AvailableMinID {
if msg.Deleted || msg.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewerLocked(channel, member, req.UserID, msg) {
continue
}
item := cloneChannelMessage(msg)
@ -432,7 +434,7 @@ func (s *ChannelStore) ListChannelMessageReactions(_ context.Context, req domain
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
channel, member, _, err := s.channelForViewerLocked(req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsList{}, err
}
@ -440,7 +442,8 @@ func (s *ChannelStore) ListChannelMessageReactions(_ context.Context, req domain
return domain.ChannelMessageReactionsList{}, domain.ErrChannelRightForbidden
}
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID {
if !ok || msg.Deleted || msg.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewerLocked(channel, member, req.UserID, msg) {
return domain.ChannelMessageReactionsList{}, domain.ErrMessageIDInvalid
}
rows := s.channelMessageReactionRowsLocked(req.ChannelID, req.MessageID, req.UserID, req.Reaction)
@ -486,12 +489,13 @@ func (s *ChannelStore) FindChannelMessageReaction(_ context.Context, req domain.
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, member, err := s.channelAndMemberLocked(req.ViewerUserID, req.ChannelID)
channel, member, _, err := s.channelForViewerLocked(req.ViewerUserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionLookup{}, false, err
}
message, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
if !ok || message.Deleted || message.ID <= member.AvailableMinID {
if !ok || message.Deleted || message.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewerLocked(channel, member, req.ViewerUserID, message) {
return domain.ChannelMessageReactionLookup{}, false, domain.ErrMessageIDInvalid
}
rows := cloneChannelPeerReactions(s.reactions[req.ChannelID][req.MessageID][req.ReactorUserID])

View file

@ -501,6 +501,19 @@ func (s *ChannelStore) getChannelForViewer(ctx context.Context, db sqlcgen.DBTX,
return ch, member, true, nil
}
// channelMessageVisibleToViewer applies the message-level half of synthetic monoforum access.
// Subscribers do not have channel_members rows and may only address saved_peer=self; a synthetic
// manager view may address every subscriber sub-dialog.
func channelMessageVisibleToViewer(channel domain.Channel, member domain.ChannelMember, viewerUserID int64, msg domain.ChannelMessage) bool {
if !channel.Monoforum {
return true
}
if member.CanManageDirectMessages() {
return true
}
return msg.SavedPeer == (domain.Peer{Type: domain.PeerTypeUser, ID: viewerUserID})
}
func getChannelByID(ctx context.Context, db sqlcgen.DBTX, channelID int64) (domain.Channel, error) {
ch, err := scanChannel(db.QueryRow(ctx, `SELECT `+channelColumns+` FROM channels c WHERE c.id = $1 AND NOT c.deleted`, channelID))
if errors.Is(err, pgx.ErrNoRows) {

View file

@ -516,6 +516,10 @@ func (s *ChannelStore) getChannelMessagesForMember(ctx context.Context, viewerUs
AND id = ANY($2::int[])
AND (NOT deleted OR ($4 > 0 AND id = $4))
AND (($3 <= 0 OR id > $3) OR ($4 > 0 AND id = $4))`
if channel.Monoforum && !member.CanManageDirectMessages() {
args = append(args, string(domain.PeerTypeUser), viewerUserID)
where += fmt.Sprintf("\nAND saved_peer_type = $%d AND saved_peer_id = $%d", len(args)-1, len(args))
}
rows, err := s.db.Query(ctx, `
SELECT `+channelMessageColumns+`
FROM channel_messages

View file

@ -189,6 +189,7 @@ SELECT EXISTS (
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Media: req.Media,
ReplyTo: req.ReplyTo,
Forward: req.Forward,
Pts: pts,
}
event := domain.ChannelUpdateEvent{

View file

@ -79,9 +79,10 @@ func TestSendMonoforumMessageAndHistoryPostgres(t *testing.T) {
}
suggestedPost := &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}, ScheduleDate: 1700100000}
forward := &domain.MessageForward{From: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, Date: 1700000999}
m1, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
MonoforumID: monoID, SenderUserID: sub.ID, SavedPeer: subPeer, RandomID: 111, Message: "hi", Date: 1700001001,
SuggestedPost: suggestedPost,
SuggestedPost: suggestedPost, Forward: forward,
})
if err != nil {
t.Fatalf("subscriber send 1: %v", err)
@ -128,7 +129,7 @@ func TestSendMonoforumMessageAndHistoryPostgres(t *testing.T) {
}
// 幂等。
dup, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: sub.ID, SavedPeer: subPeer, RandomID: 111, Message: "hi", SuggestedPost: suggestedPost, Date: 1700001004})
dup, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: sub.ID, SavedPeer: subPeer, RandomID: 111, Message: "hi", SuggestedPost: suggestedPost, Forward: forward, Date: 1700001004})
if err != nil {
t.Fatalf("dup send: %v", err)
}
@ -166,6 +167,9 @@ func TestSendMonoforumMessageAndHistoryPostgres(t *testing.T) {
if oldest.SuggestedPost == nil || oldest.SuggestedPost.Price == nil || oldest.SuggestedPost.Price.Kind != domain.SuggestedPostPriceStars || oldest.SuggestedPost.Price.Amount != 10 || oldest.SuggestedPost.ScheduleDate != 1700100000 {
t.Fatalf("persisted suggested post = %+v, want 10 Stars + schedule", oldest.SuggestedPost)
}
if oldest.Forward == nil || oldest.Forward.From.ID != owner.ID || oldest.Forward.Date != 1700000999 {
t.Fatalf("persisted monoforum forward = %+v, want source user %d/date 1700000999", oldest.Forward, owner.ID)
}
if newest := hist.Messages[0]; newest.ReplyTo == nil || newest.ReplyTo.MessageID != m1.Message.ID {
t.Fatalf("persisted admin reply = %+v, want message %d", newest.ReplyTo, m1.Message.ID)
}
@ -175,7 +179,8 @@ func TestSendMonoforumMessageAndHistoryPostgres(t *testing.T) {
// 另一个订阅者不串会话。
otherPeer := domain.Peer{Type: domain.PeerTypeUser, ID: other.ID}
if _, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: other.ID, SavedPeer: otherPeer, RandomID: 201, Message: "other", Date: 1700001005}); err != nil {
otherMessage, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{MonoforumID: monoID, SenderUserID: other.ID, SavedPeer: otherPeer, RandomID: 201, Message: "other", Date: 1700001005})
if err != nil {
t.Fatalf("other subscriber send: %v", err)
}
subHist, _ := channels.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{MonoforumID: monoID, SavedPeer: subPeer, Limit: 10})
@ -191,6 +196,67 @@ func TestSendMonoforumMessageAndHistoryPostgres(t *testing.T) {
t.Fatalf("subscriber channel history leaked message %+v", message)
}
}
exactMessages, err := channels.GetChannelMessages(ctx, sub.ID, monoID, []int{m1.Message.ID, otherMessage.Message.ID})
if err != nil {
t.Fatalf("subscriber exact monoforum messages: %v", err)
}
if len(exactMessages.Messages) != 1 || exactMessages.Messages[0].ID != m1.Message.ID {
t.Fatalf("subscriber exact monoforum messages = %+v, want only own message %d", exactMessages.Messages, m1.Message.ID)
}
if _, err := channels.SetChannelMessageReactions(ctx, domain.SetChannelMessageReactionsRequest{
UserID: sub.ID, ChannelID: monoID, MessageID: m1.Message.ID,
Reactions: []domain.MessageReaction{{Type: domain.MessageReactionEmoji, Emoticon: "\U0001f44d"}},
Date: 1700001006,
}); err != nil {
t.Fatalf("subscriber react to own monoforum message: %v", err)
}
if _, err := channels.SetChannelMessageReactions(ctx, domain.SetChannelMessageReactionsRequest{
UserID: sub.ID, ChannelID: monoID, MessageID: otherMessage.Message.ID,
Reactions: []domain.MessageReaction{{Type: domain.MessageReactionEmoji, Emoticon: "\U0001f525"}},
Date: 1700001006,
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
t.Fatalf("subscriber react to another saved_peer err = %v, want ErrMessageIDInvalid", err)
}
subReactions, err := channels.GetChannelMessageReactions(ctx, domain.ChannelMessageReactionsRequest{
UserID: sub.ID, ChannelID: monoID, IDs: []int{m1.Message.ID, otherMessage.Message.ID},
})
if err != nil {
t.Fatalf("subscriber get monoforum reactions: %v", err)
}
if len(subReactions.Messages) != 1 || subReactions.Messages[0].ID != m1.Message.ID {
t.Fatalf("subscriber monoforum reactions = %+v, want only own message %d", subReactions.Messages, m1.Message.ID)
}
adminReactions, err := channels.GetChannelMessageReactions(ctx, domain.ChannelMessageReactionsRequest{
UserID: owner.ID, ChannelID: monoID, IDs: []int{m1.Message.ID, otherMessage.Message.ID},
})
if err != nil {
t.Fatalf("admin get monoforum reactions: %v", err)
}
if len(adminReactions.Messages) != 2 {
t.Fatalf("admin monoforum reactions = %+v, want both subscriber messages", adminReactions.Messages)
}
reactionList, err := channels.ListChannelMessageReactions(ctx, domain.ChannelMessageReactionsListRequest{
UserID: sub.ID, ChannelID: monoID, MessageID: m1.Message.ID, Limit: 10,
})
if err != nil || reactionList.Count != 1 || len(reactionList.Reactions) != 1 {
t.Fatalf("subscriber monoforum reaction list = %+v, %v; want one", reactionList, err)
}
if _, err := channels.ListChannelMessageReactions(ctx, domain.ChannelMessageReactionsListRequest{
UserID: sub.ID, ChannelID: monoID, MessageID: otherMessage.Message.ID, Limit: 10,
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
t.Fatalf("subscriber list another saved_peer reactions err = %v, want ErrMessageIDInvalid", err)
}
reactionLookup, found, err := channels.FindChannelMessageReaction(ctx, domain.ChannelMessageReactionLookupRequest{
ViewerUserID: sub.ID, ChannelID: monoID, MessageID: m1.Message.ID, ReactorUserID: sub.ID,
})
if err != nil || !found || len(reactionLookup.Reactions) != 1 {
t.Fatalf("subscriber monoforum reaction lookup = %+v, %v, %v; want one", reactionLookup, found, err)
}
if _, _, err := channels.FindChannelMessageReaction(ctx, domain.ChannelMessageReactionLookupRequest{
ViewerUserID: sub.ID, ChannelID: monoID, MessageID: otherMessage.Message.ID, ReactorUserID: other.ID,
}); !errors.Is(err, domain.ErrMessageIDInvalid) {
t.Fatalf("subscriber lookup another saved_peer reaction err = %v, want ErrMessageIDInvalid", err)
}
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{UserID: sub.ID, ChannelID: monoID, Pts: 0, Limit: 100})
if err != nil {
t.Fatalf("subscriber channel difference: %v", err)

View file

@ -37,7 +37,7 @@ func (s *ChannelStore) SetChannelMessageReactions(ctx context.Context, req domai
_ = tx.Rollback(ctx)
}
}()
channel, member, err := s.getChannelForMember(ctx, tx, req.UserID, req.ChannelID)
channel, member, _, err := s.getChannelForViewer(ctx, tx, req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
@ -57,7 +57,8 @@ func (s *ChannelStore) SetChannelMessageReactions(ctx context.Context, req domai
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
if msg.Deleted || msg.Action != nil || msg.ID <= member.AvailableMinID {
if msg.Deleted || msg.Action != nil || msg.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewer(channel, member, req.UserID, msg) {
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
}
// 仅新增/替换受策略约束;空向量是撤销,策略收紧后也必须允许撤销存量 reaction。
@ -474,7 +475,7 @@ func (s *ChannelStore) GetChannelMessageReactions(ctx context.Context, req domai
if len(req.IDs) > domain.MaxGetMessageIDs {
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
}
channel, member, err := s.getChannelForMember(ctx, s.db, req.UserID, req.ChannelID)
channel, member, _, err := s.getChannelForViewer(ctx, s.db, req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsResult{}, err
}
@ -491,6 +492,10 @@ func (s *ChannelStore) GetChannelMessageReactions(ctx context.Context, req domai
args = append(args, member.AvailableMinID)
where += fmt.Sprintf(" AND id > $%d", len(args))
}
if channel.Monoforum && !member.CanManageDirectMessages() {
args = append(args, string(domain.PeerTypeUser), req.UserID)
where += fmt.Sprintf(" AND saved_peer_type = $%d AND saved_peer_id = $%d", len(args)-1, len(args))
}
rows, err := s.db.Query(ctx, `
SELECT `+channelMessageColumns+`
FROM channel_messages
@ -532,7 +537,7 @@ func (s *ChannelStore) ListChannelMessageReactions(ctx context.Context, req doma
if req.Limit <= 0 || req.Limit > domain.MaxChannelMessageReactionListLimit {
req.Limit = domain.MaxChannelMessageReactionListLimit
}
channel, member, err := s.getChannelForMember(ctx, s.db, req.UserID, req.ChannelID)
channel, member, _, err := s.getChannelForViewer(ctx, s.db, req.UserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionsList{}, err
}
@ -543,7 +548,8 @@ func (s *ChannelStore) ListChannelMessageReactions(ctx context.Context, req doma
if err != nil {
return domain.ChannelMessageReactionsList{}, err
}
if msg.Deleted || msg.ID <= member.AvailableMinID {
if msg.Deleted || msg.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewer(channel, member, req.UserID, msg) {
return domain.ChannelMessageReactionsList{}, domain.ErrMessageIDInvalid
}
baseWhere := []string{"channel_id = $1", "message_id = $2"}
@ -618,7 +624,7 @@ func (s *ChannelStore) FindChannelMessageReaction(ctx context.Context, req domai
req.MessageID > domain.MaxMessageBoxID || req.ReactorUserID == 0 {
return domain.ChannelMessageReactionLookup{}, false, domain.ErrChannelInvalid
}
channel, member, err := s.getChannelForMember(ctx, s.db, req.ViewerUserID, req.ChannelID)
channel, member, _, err := s.getChannelForViewer(ctx, s.db, req.ViewerUserID, req.ChannelID)
if err != nil {
return domain.ChannelMessageReactionLookup{}, false, err
}
@ -626,7 +632,8 @@ func (s *ChannelStore) FindChannelMessageReaction(ctx context.Context, req domai
if err != nil {
return domain.ChannelMessageReactionLookup{}, false, err
}
if message.Deleted || message.ID <= member.AvailableMinID {
if message.Deleted || message.ID <= member.AvailableMinID ||
!channelMessageVisibleToViewer(channel, member, req.ViewerUserID, message) {
return domain.ChannelMessageReactionLookup{}, false, domain.ErrMessageIDInvalid
}
rows, err := s.db.Query(ctx, `

View file

@ -76,6 +76,7 @@ type monoforumSendFingerprintPayload struct {
Entities []domain.MessageEntity `json:"entities"`
Media *domain.MessageMedia `json:"media"`
ReplyTo *domain.MessageReply `json:"reply_to"`
Forward *domain.MessageForward `json:"forward,omitempty"`
Silent bool `json:"silent"`
NoForwards bool `json:"noforwards"`
SuggestedPost *domain.SuggestedPost `json:"suggested_post,omitempty"`
@ -168,6 +169,7 @@ func MonoforumSendFingerprint(req domain.SendMonoforumMessageRequest) ([]byte, e
Entities: req.Entities,
Media: req.Media,
ReplyTo: req.ReplyTo,
Forward: req.Forward,
Silent: req.Silent,
NoForwards: req.NoForwards,
SuggestedPost: req.SuggestedPost,