fix(channels): sync monoforum suggested forwards and reactions
This commit is contained in:
parent
45a7e117ea
commit
fd47dd765a
17 changed files with 650 additions and 23 deletions
|
|
@ -1627,6 +1627,7 @@ type SendMonoforumMessageRequest struct {
|
||||||
Entities []MessageEntity
|
Entities []MessageEntity
|
||||||
Media *MessageMedia
|
Media *MessageMedia
|
||||||
ReplyTo *MessageReply
|
ReplyTo *MessageReply
|
||||||
|
Forward *MessageForward
|
||||||
Silent bool
|
Silent bool
|
||||||
NoForwards bool
|
NoForwards bool
|
||||||
SuggestedPost *SuggestedPost
|
SuggestedPost *SuggestedPost
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,39 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
|
||||||
return nil, internalErr()
|
return nil, internalErr()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
suggestedInput, hasSuggestedPost := req.GetSuggestedPost()
|
||||||
|
var mono domain.Channel
|
||||||
|
var monoforum, monoforumAdmin bool
|
||||||
|
if toPeer.Type == domain.PeerTypeChannel && r.deps.Channels != nil {
|
||||||
|
mono, monoforumAdmin, err = r.deps.Channels.ResolveMonoforumSend(ctx, userID, toPeer.ID)
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
monoforum = true
|
||||||
|
case !errors.Is(err, domain.ErrChannelInvalid):
|
||||||
|
return nil, internalErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasSuggestedPost && !monoforum {
|
||||||
|
return nil, suggestedPostPeerInvalidErr()
|
||||||
|
}
|
||||||
|
if monoforum {
|
||||||
|
suggestedPost, err := domainSuggestedPost(suggestedInput, hasSuggestedPost)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.forwardMessagesToMonoforum(
|
||||||
|
ctx,
|
||||||
|
userID,
|
||||||
|
toPeer,
|
||||||
|
mono,
|
||||||
|
monoforumAdmin,
|
||||||
|
req,
|
||||||
|
idempotencyFingerprints,
|
||||||
|
suggestedPost,
|
||||||
|
topMsgID,
|
||||||
|
topMsgIDSet,
|
||||||
|
)
|
||||||
|
}
|
||||||
immediate := req.ScheduleDate == 0 || scheduleDateIsImmediate(req.ScheduleDate, int(r.clock.Now().Unix()))
|
immediate := req.ScheduleDate == 0 || scheduleDateIsImmediate(req.ScheduleDate, int(r.clock.Now().Unix()))
|
||||||
replays := make([]outgoingReplayLookup, len(req.ID))
|
replays := make([]outgoingReplayLookup, len(req.ID))
|
||||||
absentIndexes := make([]int, 0, len(req.ID))
|
absentIndexes := make([]int, 0, len(req.ID))
|
||||||
|
|
@ -307,6 +340,132 @@ func (r *Router) onMessagesForwardMessages(ctx context.Context, req *tg.Messages
|
||||||
return nil, peerIDInvalidErr()
|
return nil, peerIDInvalidErr()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Router) forwardMessagesToMonoforum(
|
||||||
|
ctx context.Context,
|
||||||
|
userID int64,
|
||||||
|
toPeer domain.Peer,
|
||||||
|
mono domain.Channel,
|
||||||
|
monoforumAdmin bool,
|
||||||
|
req *tg.MessagesForwardMessagesRequest,
|
||||||
|
idempotencyFingerprints [][]byte,
|
||||||
|
suggestedPost *domain.SuggestedPost,
|
||||||
|
topMsgID int,
|
||||||
|
topMsgIDSet bool,
|
||||||
|
) (tg.UpdatesClass, error) {
|
||||||
|
if req.ScheduleDate != 0 && !scheduleDateIsImmediate(req.ScheduleDate, int(r.clock.Now().Unix())) {
|
||||||
|
return nil, scheduleDateInvalidErr()
|
||||||
|
}
|
||||||
|
savedPeer, err := r.monoforumSavedPeerForSender(userID, monoforumAdmin, req.ReplyTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
replyTo, err := r.monoforumMessageReplyFromInput(ctx, userID, toPeer, req.ReplyTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// DrKLO currently mirrors the replied suggestion into top_msg_id as well as reply_to.
|
||||||
|
// Monoforum has no forum root: accept only the redundant equal value and never persist it
|
||||||
|
// as a topic root.
|
||||||
|
if topMsgIDSet && topMsgID != 0 && (replyTo == nil || replyTo.MessageID != topMsgID) {
|
||||||
|
return nil, replyMessageIDInvalidErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
replays := make([]outgoingReplayLookup, len(req.ID))
|
||||||
|
absentIndexes := make([]int, 0, len(req.ID))
|
||||||
|
for i := range req.ID {
|
||||||
|
replay, err := r.lookupChannelSendReplay(
|
||||||
|
ctx,
|
||||||
|
userID,
|
||||||
|
toPeer.ID,
|
||||||
|
savedPeer,
|
||||||
|
req.RandomID[i],
|
||||||
|
idempotencyFingerprints[i],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
replays[i] = replay
|
||||||
|
if !replay.found {
|
||||||
|
absentIndexes = append(absentIndexes, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(absentIndexes) == 0 {
|
||||||
|
results := make([]tg.UpdatesClass, 0, len(replays))
|
||||||
|
for _, replay := range replays {
|
||||||
|
results = append(results, r.monoforumSendUpdates(ctx, userID, mono, savedPeer, replay.channel))
|
||||||
|
}
|
||||||
|
return combineSendUpdates(results), nil
|
||||||
|
}
|
||||||
|
if err := r.checkSendRateLimit(ctx, userID, len(absentIndexes)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
checkedPeer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.ToPeer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if checkedPeer != toPeer {
|
||||||
|
return nil, peerIDInvalidErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
absentIDs := make([]int, len(absentIndexes))
|
||||||
|
absentRandomIDs := make([]int64, len(absentIndexes))
|
||||||
|
for i, originalIndex := range absentIndexes {
|
||||||
|
absentIDs[i] = req.ID[originalIndex]
|
||||||
|
absentRandomIDs[i] = req.RandomID[originalIndex]
|
||||||
|
}
|
||||||
|
fromPeer, preloadedSources, err := r.forwardFromPeerAndSources(ctx, userID, req.FromPeer, absentIDs, absentRandomIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fromPeer.Type == domain.PeerTypeUser && fromPeer.ID != userID && r.deps.Users != nil {
|
||||||
|
if _, found, err := r.deps.Users.ByID(ctx, userID, fromPeer.ID); err != nil {
|
||||||
|
return nil, internalErr()
|
||||||
|
} else if !found {
|
||||||
|
return nil, peerIDInvalidErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
absentSources, err := r.forwardSourcesForRequest(ctx, userID, fromPeer, absentIDs, preloadedSources)
|
||||||
|
if err != nil {
|
||||||
|
return nil, messageForwardErr(err)
|
||||||
|
}
|
||||||
|
sources := make([]forwardSource, len(req.ID))
|
||||||
|
for i, originalIndex := range absentIndexes {
|
||||||
|
sources[originalIndex] = absentSources[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make([]tg.UpdatesClass, 0, len(req.ID))
|
||||||
|
for i, source := range sources {
|
||||||
|
if replays[i].found {
|
||||||
|
results = append(results, r.monoforumSendUpdates(ctx, userID, mono, savedPeer, replays[i].channel))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
forward := source.forward
|
||||||
|
if req.DropAuthor {
|
||||||
|
forward = nil
|
||||||
|
}
|
||||||
|
updates, err := r.sendMonoforumMessage(ctx, userID, checkedPeer, mono, monoforumAdmin, domain.SendMonoforumMessageRequest{
|
||||||
|
SavedPeer: savedPeer,
|
||||||
|
RandomID: req.RandomID[i],
|
||||||
|
IdempotencyFingerprint: idempotencyFingerprints[i],
|
||||||
|
IdempotencyPreflighted: replays[i].checked,
|
||||||
|
Message: source.body,
|
||||||
|
Entities: source.entities,
|
||||||
|
Media: source.media,
|
||||||
|
ReplyTo: replyTo,
|
||||||
|
Forward: forward,
|
||||||
|
Silent: req.Silent,
|
||||||
|
NoForwards: req.Noforwards,
|
||||||
|
SuggestedPost: suggestedPost,
|
||||||
|
AllowPaidStars: req.AllowPaidStars,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, updates)
|
||||||
|
}
|
||||||
|
return combineSendUpdates(results), nil
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeForwardMessageVectors(ids []int, randomIDs []int64) ([]int, []int64, bool) {
|
func normalizeForwardMessageVectors(ids []int, randomIDs []int64) ([]int, []int64, bool) {
|
||||||
if len(ids) == 0 || len(randomIDs) == 0 {
|
if len(ids) == 0 || len(randomIDs) == 0 {
|
||||||
return nil, nil, false
|
return nil, nil, false
|
||||||
|
|
|
||||||
|
|
@ -414,8 +414,6 @@ func forwardMessagesUnsupportedOptionErr(req *tg.MessagesForwardMessagesRequest)
|
||||||
return starsAmountInvalidErr()
|
return starsAmountInvalidErr()
|
||||||
case req.AllowPaidFloodskip:
|
case req.AllowPaidFloodskip:
|
||||||
return paymentUnsupportedErr()
|
return paymentUnsupportedErr()
|
||||||
case !req.SuggestedPost.Zero():
|
|
||||||
return suggestedPostPeerInvalidErr()
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -467,3 +467,287 @@ func TestMonoforumSendMessageWritePath(t *testing.T) {
|
||||||
t.Fatalf("suggested_post schedule = %d/%v, want 1700100000/true", scheduleDate, ok)
|
t.Fatalf("suggested_post schedule = %d/%v, want 1700100000/true", scheduleDate, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMonoforumForwardSuggestedPostAndReactionWritePath 回归 DrKLO 的三条真实路径:
|
||||||
|
// Add Offer 用 forwardMessages+suggested_post 新建建议,Edit Price/Edit Time 再回复原建议;
|
||||||
|
// get/send reaction 则必须允许无 member 行的订阅者访问自己的 saved_peer,且不能按猜测 id
|
||||||
|
// 跨到另一订阅者的子会话。
|
||||||
|
func TestMonoforumForwardSuggestedPostAndReactionWritePath(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
userStore := memory.NewUserStore()
|
||||||
|
owner, err := userStore.Create(ctx, domain.User{AccessHash: 31, Phone: "15550004001", FirstName: "Owner"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create owner: %v", err)
|
||||||
|
}
|
||||||
|
sub, err := userStore.Create(ctx, domain.User{AccessHash: 32, Phone: "15550004002", FirstName: "Sub"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sub: %v", err)
|
||||||
|
}
|
||||||
|
other, err := userStore.Create(ctx, domain.User{AccessHash: 33, Phone: "15550004003", FirstName: "Other"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create other: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelStore := memory.NewChannelStore()
|
||||||
|
channelSvc := appchannels.NewService(channelStore)
|
||||||
|
r := New(Config{}, Deps{
|
||||||
|
Users: appusers.NewService(userStore),
|
||||||
|
Channels: channelSvc,
|
||||||
|
}, zaptest.NewLogger(t), clock.System)
|
||||||
|
|
||||||
|
created, err := channelSvc.CreateChannel(ctx, owner.ID, domain.CreateChannelRequest{Title: "DM Suggested", Broadcast: true, Date: 2000})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create channel: %v", err)
|
||||||
|
}
|
||||||
|
enabled, err := channelStore.SetPaidMessagesPrice(ctx, owner.ID, created.Channel.ID, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("enable DM: %v", err)
|
||||||
|
}
|
||||||
|
monoID := enabled.Channel.LinkedMonoforumID
|
||||||
|
mono, err := channelStore.GetChannelByID(ctx, monoID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get monoforum: %v", err)
|
||||||
|
}
|
||||||
|
monoInput := &tg.InputPeerChannel{ChannelID: monoID, AccessHash: mono.AccessHash}
|
||||||
|
subPeer := domain.Peer{Type: domain.PeerTypeUser, ID: sub.ID}
|
||||||
|
original, err := channelStore.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
|
||||||
|
MonoforumID: monoID, SenderUserID: sub.ID, SavedPeer: subPeer,
|
||||||
|
RandomID: 7001, Message: "please publish this", Date: 2001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("seed subscriber message: %v", err)
|
||||||
|
}
|
||||||
|
otherMessage, err := channelStore.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
|
||||||
|
MonoforumID: monoID, SenderUserID: other.ID,
|
||||||
|
SavedPeer: domain.Peer{Type: domain.PeerTypeUser, ID: other.ID},
|
||||||
|
RandomID: 7002, Message: "another subscriber", Date: 2002,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("seed other subscriber message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 打开 reaction 状态时会先发 getMessagesReactions。订阅者没有 channel_members
|
||||||
|
// 行,但自己的 saved_peer 消息必须正常返回,并携带 saved_peer_id 供客户端归组。
|
||||||
|
reactionState, err := r.onMessagesGetMessagesReactions(WithUserID(ctx, sub.ID), &tg.MessagesGetMessagesReactionsRequest{
|
||||||
|
Peer: monoInput,
|
||||||
|
ID: []int{original.Message.ID},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("subscriber getMessagesReactions(monoforum): %v", err)
|
||||||
|
}
|
||||||
|
reactionUpdates, ok := reactionState.(*tg.Updates)
|
||||||
|
if !ok || len(reactionUpdates.Updates) != 1 {
|
||||||
|
t.Fatalf("getMessagesReactions = %#v, want one update", reactionState)
|
||||||
|
}
|
||||||
|
reactionUpdate, ok := reactionUpdates.Updates[0].(*tg.UpdateMessageReactions)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("getMessagesReactions update = %T, want UpdateMessageReactions", reactionUpdates.Updates[0])
|
||||||
|
}
|
||||||
|
savedPeer, ok := reactionUpdate.GetSavedPeerID()
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("getMessagesReactions update missing saved_peer_id")
|
||||||
|
}
|
||||||
|
if peer, ok := savedPeer.(*tg.PeerUser); !ok || peer.UserID != sub.ID {
|
||||||
|
t.Fatalf("reaction saved_peer_id = %#v, want subscriber %d", savedPeer, sub.ID)
|
||||||
|
}
|
||||||
|
sendReaction := &tg.MessagesSendReactionRequest{Peer: monoInput, MsgID: original.Message.ID}
|
||||||
|
sendReaction.SetReaction([]tg.ReactionClass{&tg.ReactionEmoji{Emoticon: "\U0001f44d"}})
|
||||||
|
sentReaction, err := r.onMessagesSendReaction(WithUserID(ctx, sub.ID), sendReaction)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("subscriber sendReaction(monoforum): %v", err)
|
||||||
|
}
|
||||||
|
sentReactionUpdates, ok := sentReaction.(*tg.Updates)
|
||||||
|
if !ok || len(sentReactionUpdates.Updates) != 1 {
|
||||||
|
t.Fatalf("sendReaction = %#v, want one update", sentReaction)
|
||||||
|
}
|
||||||
|
sentReactionUpdate, ok := sentReactionUpdates.Updates[0].(*tg.UpdateMessageReactions)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("sendReaction update = %T, want UpdateMessageReactions", sentReactionUpdates.Updates[0])
|
||||||
|
}
|
||||||
|
if saved, ok := sentReactionUpdate.GetSavedPeerID(); !ok {
|
||||||
|
t.Fatalf("sendReaction update missing saved_peer_id")
|
||||||
|
} else if peer, ok := saved.(*tg.PeerUser); !ok || peer.UserID != sub.ID {
|
||||||
|
t.Fatalf("sendReaction saved_peer_id = %#v, want subscriber %d", saved, sub.ID)
|
||||||
|
}
|
||||||
|
if _, err := r.onMessagesSendReaction(WithUserID(ctx, sub.ID), &tg.MessagesSendReactionRequest{
|
||||||
|
Peer: monoInput, MsgID: otherMessage.Message.ID,
|
||||||
|
}); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
|
||||||
|
t.Fatalf("cross-saved-peer sendReaction err = %v, want MESSAGE_ID_INVALID", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Offer:源消息和目标会话都是 monoforum,自身 topic 由
|
||||||
|
// inputReplyToMonoForum(self) 选择;这不是普通 forum reply root。
|
||||||
|
addOffer := &tg.MessagesForwardMessagesRequest{
|
||||||
|
FromPeer: monoInput,
|
||||||
|
ID: []int{original.Message.ID},
|
||||||
|
RandomID: []int64{8001},
|
||||||
|
ToPeer: monoInput,
|
||||||
|
}
|
||||||
|
addOffer.SetDropAuthor(true)
|
||||||
|
addOffer.SetReplyTo(&tg.InputReplyToMonoForum{MonoforumPeerID: &tg.InputPeerSelf{}})
|
||||||
|
offer := tg.SuggestedPost{}
|
||||||
|
offer.SetPrice(&tg.StarsAmount{Amount: 25})
|
||||||
|
offer.SetScheduleDate(2_000_000_000)
|
||||||
|
addOffer.SetSuggestedPost(offer)
|
||||||
|
|
||||||
|
addOfferResult, err := r.onMessagesForwardMessages(WithUserID(ctx, sub.ID), addOffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("subscriber Add Offer forwardMessages(monoforum): %v", err)
|
||||||
|
}
|
||||||
|
proposal, proposalUpdate := requireMonoforumNewChannelMessage(t, addOfferResult)
|
||||||
|
if proposal.Message != original.Message.Body {
|
||||||
|
t.Fatalf("proposal body = %q, want copied source %q", proposal.Message, original.Message.Body)
|
||||||
|
}
|
||||||
|
if proposalUpdate.PtsCount != 1 || proposalUpdate.Pts <= original.Message.Pts {
|
||||||
|
t.Fatalf("proposal pts/count = %d/%d, want one real channel event after %d", proposalUpdate.Pts, proposalUpdate.PtsCount, original.Message.Pts)
|
||||||
|
}
|
||||||
|
if _, ok := proposal.GetFwdFrom(); ok {
|
||||||
|
t.Fatalf("drop_author proposal unexpectedly has fwd_from: %#v", proposal)
|
||||||
|
}
|
||||||
|
if saved, ok := proposal.GetSavedPeerID(); !ok {
|
||||||
|
t.Fatalf("proposal missing saved_peer_id")
|
||||||
|
} else if peer, ok := saved.(*tg.PeerUser); !ok || peer.UserID != sub.ID {
|
||||||
|
t.Fatalf("proposal saved_peer_id = %#v, want subscriber %d", saved, sub.ID)
|
||||||
|
}
|
||||||
|
proposalSuggested, ok := proposal.GetSuggestedPost()
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("proposal missing suggested_post")
|
||||||
|
}
|
||||||
|
if price, ok := proposalSuggested.GetPrice(); !ok {
|
||||||
|
t.Fatalf("proposal suggested_post missing price")
|
||||||
|
} else if stars, ok := price.(*tg.StarsAmount); !ok || stars.Amount != 25 {
|
||||||
|
t.Fatalf("proposal price = %#v, want 25 Stars", price)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit Price/Edit Time:Android 同时带 reply_to、monoforum_peer_id 和冗余
|
||||||
|
// top_msg_id;服务端保留真实 reply,接受相等的冗余值但不制造 forum topic root。
|
||||||
|
editOffer := &tg.MessagesForwardMessagesRequest{
|
||||||
|
FromPeer: monoInput,
|
||||||
|
ID: []int{proposal.ID},
|
||||||
|
RandomID: []int64{8002},
|
||||||
|
ToPeer: monoInput,
|
||||||
|
}
|
||||||
|
editOffer.SetDropAuthor(true)
|
||||||
|
editReply := &tg.InputReplyToMessage{ReplyToMsgID: proposal.ID}
|
||||||
|
editReply.SetMonoforumPeerID(&tg.InputPeerSelf{})
|
||||||
|
editOffer.SetReplyTo(editReply)
|
||||||
|
editOffer.SetTopMsgID(proposal.ID)
|
||||||
|
edited := tg.SuggestedPost{}
|
||||||
|
edited.SetPrice(&tg.StarsAmount{Amount: 40})
|
||||||
|
edited.SetScheduleDate(2_000_100_000)
|
||||||
|
editOffer.SetSuggestedPost(edited)
|
||||||
|
|
||||||
|
editResult, err := r.onMessagesForwardMessages(WithUserID(ctx, sub.ID), editOffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("subscriber Edit Offer forwardMessages(monoforum): %v", err)
|
||||||
|
}
|
||||||
|
editedProposal, editedUpdate := requireMonoforumNewChannelMessage(t, editResult)
|
||||||
|
if editedUpdate.PtsCount != 1 || editedUpdate.Pts != proposalUpdate.Pts+1 {
|
||||||
|
t.Fatalf("edited proposal pts/count = %d/%d, want %d/1", editedUpdate.Pts, editedUpdate.PtsCount, proposalUpdate.Pts+1)
|
||||||
|
}
|
||||||
|
replyHeader, ok := editedProposal.ReplyTo.(*tg.MessageReplyHeader)
|
||||||
|
if !ok || replyHeader.ReplyToMsgID != proposal.ID {
|
||||||
|
t.Fatalf("edited proposal reply = %#v, want message %d", editedProposal.ReplyTo, proposal.ID)
|
||||||
|
}
|
||||||
|
if topID, ok := replyHeader.GetReplyToTopID(); ok || topID != 0 {
|
||||||
|
t.Fatalf("edited proposal acquired forum top_msg_id = %d/%v, want absent", topID, ok)
|
||||||
|
}
|
||||||
|
editedSuggested, ok := editedProposal.GetSuggestedPost()
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("edited proposal missing suggested_post")
|
||||||
|
}
|
||||||
|
if price, ok := editedSuggested.GetPrice(); !ok {
|
||||||
|
t.Fatalf("edited proposal missing price")
|
||||||
|
} else if stars, ok := price.(*tg.StarsAmount); !ok || stars.Amount != 40 {
|
||||||
|
t.Fatalf("edited proposal price = %#v, want 40 Stars", price)
|
||||||
|
}
|
||||||
|
if schedule, ok := editedSuggested.GetScheduleDate(); !ok || schedule != 2_000_100_000 {
|
||||||
|
t.Fatalf("edited proposal schedule = %d/%v, want 2000100000/true", schedule, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeReplay, err := channelStore.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{
|
||||||
|
MonoforumID: monoID, SavedPeer: subPeer, Limit: 100,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("history before replay: %v", err)
|
||||||
|
}
|
||||||
|
replayResult, err := r.onMessagesForwardMessages(WithUserID(ctx, sub.ID), editOffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exact Edit Offer replay: %v", err)
|
||||||
|
}
|
||||||
|
replayedProposal, replayedUpdate := requireMonoforumNewChannelMessage(t, replayResult)
|
||||||
|
afterReplay, err := channelStore.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{
|
||||||
|
MonoforumID: monoID, SavedPeer: subPeer, Limit: 100,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("history after replay: %v", err)
|
||||||
|
}
|
||||||
|
if replayedProposal.ID != editedProposal.ID || replayedUpdate.Pts != editedUpdate.Pts || afterReplay.Count != beforeReplay.Count {
|
||||||
|
t.Fatalf("exact replay id/pts/count = %d/%d/%d, want %d/%d/%d",
|
||||||
|
replayedProposal.ID, replayedUpdate.Pts, afterReplay.Count,
|
||||||
|
editedProposal.ID, editedUpdate.Pts, beforeReplay.Count)
|
||||||
|
}
|
||||||
|
conflict := *editOffer
|
||||||
|
conflictingSuggested := tg.SuggestedPost{}
|
||||||
|
conflictingSuggested.SetPrice(&tg.StarsAmount{Amount: 41})
|
||||||
|
conflict.SetSuggestedPost(conflictingSuggested)
|
||||||
|
if _, err := r.onMessagesForwardMessages(WithUserID(ctx, sub.ID), &conflict); err == nil || !strings.Contains(err.Error(), "RANDOM_ID_DUPLICATE") {
|
||||||
|
t.Fatalf("conflicting Edit Offer replay err = %v, want RANDOM_ID_DUPLICATE", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 精确 id 查询必须服从 saved_peer 过滤,不能把其它订阅者的消息作为转发源。
|
||||||
|
crossSource := &tg.MessagesForwardMessagesRequest{
|
||||||
|
FromPeer: monoInput,
|
||||||
|
ID: []int{otherMessage.Message.ID},
|
||||||
|
RandomID: []int64{8003},
|
||||||
|
ToPeer: monoInput,
|
||||||
|
}
|
||||||
|
crossSource.SetDropAuthor(true)
|
||||||
|
crossSource.SetReplyTo(&tg.InputReplyToMonoForum{MonoforumPeerID: &tg.InputPeerSelf{}})
|
||||||
|
crossSource.SetSuggestedPost(offer)
|
||||||
|
if _, err := r.onMessagesForwardMessages(WithUserID(ctx, sub.ID), crossSource); err == nil || !strings.Contains(err.Error(), "MESSAGE_ID_INVALID") {
|
||||||
|
t.Fatalf("cross-saved-peer forward source err = %v, want MESSAGE_ID_INVALID", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员可见全部子会话,但目标仍必须显式指定,写入同一 subscriber saved_peer。
|
||||||
|
adminOffer := &tg.MessagesForwardMessagesRequest{
|
||||||
|
FromPeer: monoInput,
|
||||||
|
ID: []int{original.Message.ID},
|
||||||
|
RandomID: []int64{8004},
|
||||||
|
ToPeer: monoInput,
|
||||||
|
}
|
||||||
|
adminOffer.SetDropAuthor(true)
|
||||||
|
adminOffer.SetReplyTo(&tg.InputReplyToMonoForum{MonoforumPeerID: &tg.InputPeerUser{UserID: sub.ID}})
|
||||||
|
adminOffer.SetSuggestedPost(offer)
|
||||||
|
adminResult, err := r.onMessagesForwardMessages(WithUserID(ctx, owner.ID), adminOffer)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin Add Offer forwardMessages(monoforum): %v", err)
|
||||||
|
}
|
||||||
|
adminProposal, _ := requireMonoforumNewChannelMessage(t, adminResult)
|
||||||
|
if saved, ok := adminProposal.GetSavedPeerID(); !ok {
|
||||||
|
t.Fatalf("admin proposal missing saved_peer_id")
|
||||||
|
} else if peer, ok := saved.(*tg.PeerUser); !ok || peer.UserID != sub.ID {
|
||||||
|
t.Fatalf("admin proposal saved_peer_id = %#v, want subscriber %d", saved, sub.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireMonoforumNewChannelMessage(t *testing.T, updatesClass tg.UpdatesClass) (*tg.Message, *tg.UpdateNewChannelMessage) {
|
||||||
|
t.Helper()
|
||||||
|
updates, ok := updatesClass.(*tg.Updates)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("updates = %T, want *tg.Updates", updatesClass)
|
||||||
|
}
|
||||||
|
for _, update := range updates.Updates {
|
||||||
|
newMessage, ok := update.(*tg.UpdateNewChannelMessage)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
message, ok := newMessage.Message.(*tg.Message)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("new channel message = %T, want *tg.Message", newMessage.Message)
|
||||||
|
}
|
||||||
|
return message, newMessage
|
||||||
|
}
|
||||||
|
t.Fatalf("updates missing UpdateNewChannelMessage: %#v", updates.Updates)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,8 @@ func messageReactionErr(err error) error {
|
||||||
|
|
||||||
func channelReactionErr(err error) error {
|
func channelReactionErr(err error) error {
|
||||||
switch {
|
switch {
|
||||||
|
case errors.Is(err, domain.ErrMessageIDInvalid):
|
||||||
|
return messageIDInvalidErr()
|
||||||
case errors.Is(err, domain.ErrReactionInvalid):
|
case errors.Is(err, domain.ErrReactionInvalid):
|
||||||
return reactionInvalidErr()
|
return reactionInvalidErr()
|
||||||
case errors.Is(err, domain.ErrReactionsTooMany):
|
case errors.Is(err, domain.ErrReactionsTooMany):
|
||||||
|
|
|
||||||
|
|
@ -434,6 +434,9 @@ func (r *Router) channelMessagesReactionsUpdates(ctx context.Context, viewerUser
|
||||||
if topID := channelMessageThreadRootID(msg); topID > 0 && topID != id {
|
if topID := channelMessageThreadRootID(msg); topID > 0 && topID != id {
|
||||||
update.SetTopMsgID(topID)
|
update.SetTopMsgID(topID)
|
||||||
}
|
}
|
||||||
|
if msg.SavedPeer.ID != 0 {
|
||||||
|
update.SetSavedPeerID(tgPeer(msg.SavedPeer))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
updates = append(updates, update)
|
updates = append(updates, update)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,19 @@ func (s *ChannelStore) channelForViewerLocked(userID, channelID int64) (domain.C
|
||||||
return channel, publicPreviewMember(channel, userID, existing, found), true, nil
|
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 {
|
func (s *ChannelStore) dialogForUserLocked(userID int64, channel domain.Channel) domain.ChannelDialog {
|
||||||
return s.dialogForMemberLocked(userID, channel, s.members[channel.ID][userID])
|
return s.dialogForMemberLocked(userID, channel, s.members[channel.ID][userID])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,9 @@ func (s *ChannelStore) GetChannelMessages(_ context.Context, viewerUserID, chann
|
||||||
if msg.Deleted || msg.ID <= member.AvailableMinID {
|
if msg.Deleted || msg.ID <= member.AvailableMinID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !channelMessageVisibleToViewerLocked(channel, member, viewerUserID, msg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
messages = append(messages, cloneChannelMessage(msg))
|
messages = append(messages, cloneChannelMessage(msg))
|
||||||
delete(wanted, msg.ID)
|
delete(wanted, msg.ID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ func (s *ChannelStore) SendMonoforumMessage(_ context.Context, req domain.SendMo
|
||||||
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
||||||
Media: req.Media,
|
Media: req.Media,
|
||||||
ReplyTo: req.ReplyTo,
|
ReplyTo: req.ReplyTo,
|
||||||
|
Forward: req.Forward,
|
||||||
Pts: pts,
|
Pts: pts,
|
||||||
}
|
}
|
||||||
// Store owns the persisted snapshot; callers must not be able to mutate it through
|
// Store owns the persisted snapshot; callers must not be able to mutate it through
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,9 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sub := domain.Peer{Type: domain.PeerTypeUser, ID: 42}
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("subscriber send 1: %v", err)
|
t.Fatalf("subscriber send 1: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +76,7 @@ func TestSendMonoforumMessageAndHistory(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 幂等:相同 randomID 返回原消息、不重复。
|
// 幂等:相同 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 {
|
if err != nil {
|
||||||
t.Fatalf("dup send: %v", err)
|
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 {
|
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)
|
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) {
|
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)
|
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}
|
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)
|
t.Fatalf("other subscriber send: %v", err)
|
||||||
}
|
}
|
||||||
subHist, _ := store.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{MonoforumID: monoID, SavedPeer: sub, Limit: 10})
|
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)
|
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})
|
diff, err := store.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{UserID: 42, ChannelID: monoID, Pts: 0, Limit: 100})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("subscriber channel difference: %v", err)
|
t.Fatalf("subscriber channel difference: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func (s *ChannelStore) SetChannelMessageReactions(_ context.Context, req domain.
|
||||||
}
|
}
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsResult{}, err
|
return domain.ChannelMessageReactionsResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +64,8 @@ func (s *ChannelStore) SetChannelMessageReactions(_ context.Context, req domain.
|
||||||
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
msg := s.messages[req.ChannelID][idx]
|
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
|
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
// 仅新增/替换受策略约束;空向量是撤销,策略收紧后也必须允许撤销存量 reaction。
|
// 仅新增/替换受策略约束;空向量是撤销,策略收紧后也必须允许撤销存量 reaction。
|
||||||
|
|
@ -385,7 +386,7 @@ func (s *ChannelStore) GetChannelMessageReactions(_ context.Context, req domain.
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsResult{}, err
|
return domain.ChannelMessageReactionsResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -401,7 +402,8 @@ func (s *ChannelStore) GetChannelMessageReactions(_ context.Context, req domain.
|
||||||
if _, ok := wanted[msg.ID]; !ok {
|
if _, ok := wanted[msg.ID]; !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if msg.Deleted || msg.ID <= member.AvailableMinID {
|
if msg.Deleted || msg.ID <= member.AvailableMinID ||
|
||||||
|
!channelMessageVisibleToViewerLocked(channel, member, req.UserID, msg) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
item := cloneChannelMessage(msg)
|
item := cloneChannelMessage(msg)
|
||||||
|
|
@ -432,7 +434,7 @@ func (s *ChannelStore) ListChannelMessageReactions(_ context.Context, req domain
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsList{}, err
|
return domain.ChannelMessageReactionsList{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -440,7 +442,8 @@ func (s *ChannelStore) ListChannelMessageReactions(_ context.Context, req domain
|
||||||
return domain.ChannelMessageReactionsList{}, domain.ErrChannelRightForbidden
|
return domain.ChannelMessageReactionsList{}, domain.ErrChannelRightForbidden
|
||||||
}
|
}
|
||||||
msg, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
|
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
|
return domain.ChannelMessageReactionsList{}, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
rows := s.channelMessageReactionRowsLocked(req.ChannelID, req.MessageID, req.UserID, req.Reaction)
|
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()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionLookup{}, false, err
|
return domain.ChannelMessageReactionLookup{}, false, err
|
||||||
}
|
}
|
||||||
message, ok := s.findMessageLocked(req.ChannelID, req.MessageID)
|
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
|
return domain.ChannelMessageReactionLookup{}, false, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
rows := cloneChannelPeerReactions(s.reactions[req.ChannelID][req.MessageID][req.ReactorUserID])
|
rows := cloneChannelPeerReactions(s.reactions[req.ChannelID][req.MessageID][req.ReactorUserID])
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,19 @@ func (s *ChannelStore) getChannelForViewer(ctx context.Context, db sqlcgen.DBTX,
|
||||||
return ch, member, true, nil
|
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) {
|
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))
|
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) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
|
|
||||||
|
|
@ -516,6 +516,10 @@ func (s *ChannelStore) getChannelMessagesForMember(ctx context.Context, viewerUs
|
||||||
AND id = ANY($2::int[])
|
AND id = ANY($2::int[])
|
||||||
AND (NOT deleted OR ($4 > 0 AND id = $4))
|
AND (NOT deleted OR ($4 > 0 AND id = $4))
|
||||||
AND (($3 <= 0 OR id > $3) 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, `
|
rows, err := s.db.Query(ctx, `
|
||||||
SELECT `+channelMessageColumns+`
|
SELECT `+channelMessageColumns+`
|
||||||
FROM channel_messages
|
FROM channel_messages
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ SELECT EXISTS (
|
||||||
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
Entities: append([]domain.MessageEntity(nil), req.Entities...),
|
||||||
Media: req.Media,
|
Media: req.Media,
|
||||||
ReplyTo: req.ReplyTo,
|
ReplyTo: req.ReplyTo,
|
||||||
|
Forward: req.Forward,
|
||||||
Pts: pts,
|
Pts: pts,
|
||||||
}
|
}
|
||||||
event := domain.ChannelUpdateEvent{
|
event := domain.ChannelUpdateEvent{
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,10 @@ func TestSendMonoforumMessageAndHistoryPostgres(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
suggestedPost := &domain.SuggestedPost{Price: &domain.SuggestedPostPrice{Kind: domain.SuggestedPostPriceStars, Amount: 10}, ScheduleDate: 1700100000}
|
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{
|
m1, err := channels.SendMonoforumMessage(ctx, domain.SendMonoforumMessageRequest{
|
||||||
MonoforumID: monoID, SenderUserID: sub.ID, SavedPeer: subPeer, RandomID: 111, Message: "hi", Date: 1700001001,
|
MonoforumID: monoID, SenderUserID: sub.ID, SavedPeer: subPeer, RandomID: 111, Message: "hi", Date: 1700001001,
|
||||||
SuggestedPost: suggestedPost,
|
SuggestedPost: suggestedPost, Forward: forward,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("subscriber send 1: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("dup send: %v", err)
|
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 {
|
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)
|
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 {
|
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)
|
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}
|
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)
|
t.Fatalf("other subscriber send: %v", err)
|
||||||
}
|
}
|
||||||
subHist, _ := channels.ListMonoforumHistory(ctx, domain.MonoforumHistoryFilter{MonoforumID: monoID, SavedPeer: subPeer, Limit: 10})
|
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)
|
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})
|
diff, err := channels.ListChannelDifference(ctx, domain.ChannelDifferenceRequest{UserID: sub.ID, ChannelID: monoID, Pts: 0, Limit: 100})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("subscriber channel difference: %v", err)
|
t.Fatalf("subscriber channel difference: %v", err)
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func (s *ChannelStore) SetChannelMessageReactions(ctx context.Context, req domai
|
||||||
_ = tx.Rollback(ctx)
|
_ = 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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsResult{}, err
|
return domain.ChannelMessageReactionsResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +57,8 @@ func (s *ChannelStore) SetChannelMessageReactions(ctx context.Context, req domai
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsResult{}, err
|
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
|
return domain.ChannelMessageReactionsResult{}, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
// 仅新增/替换受策略约束;空向量是撤销,策略收紧后也必须允许撤销存量 reaction。
|
// 仅新增/替换受策略约束;空向量是撤销,策略收紧后也必须允许撤销存量 reaction。
|
||||||
|
|
@ -474,7 +475,7 @@ func (s *ChannelStore) GetChannelMessageReactions(ctx context.Context, req domai
|
||||||
if len(req.IDs) > domain.MaxGetMessageIDs {
|
if len(req.IDs) > domain.MaxGetMessageIDs {
|
||||||
return domain.ChannelMessageReactionsResult{}, domain.ErrChannelInvalid
|
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsResult{}, err
|
return domain.ChannelMessageReactionsResult{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -491,6 +492,10 @@ func (s *ChannelStore) GetChannelMessageReactions(ctx context.Context, req domai
|
||||||
args = append(args, member.AvailableMinID)
|
args = append(args, member.AvailableMinID)
|
||||||
where += fmt.Sprintf(" AND id > $%d", len(args))
|
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, `
|
rows, err := s.db.Query(ctx, `
|
||||||
SELECT `+channelMessageColumns+`
|
SELECT `+channelMessageColumns+`
|
||||||
FROM channel_messages
|
FROM channel_messages
|
||||||
|
|
@ -532,7 +537,7 @@ func (s *ChannelStore) ListChannelMessageReactions(ctx context.Context, req doma
|
||||||
if req.Limit <= 0 || req.Limit > domain.MaxChannelMessageReactionListLimit {
|
if req.Limit <= 0 || req.Limit > domain.MaxChannelMessageReactionListLimit {
|
||||||
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsList{}, err
|
return domain.ChannelMessageReactionsList{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -543,7 +548,8 @@ func (s *ChannelStore) ListChannelMessageReactions(ctx context.Context, req doma
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionsList{}, err
|
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
|
return domain.ChannelMessageReactionsList{}, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
baseWhere := []string{"channel_id = $1", "message_id = $2"}
|
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 {
|
req.MessageID > domain.MaxMessageBoxID || req.ReactorUserID == 0 {
|
||||||
return domain.ChannelMessageReactionLookup{}, false, domain.ErrChannelInvalid
|
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 {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionLookup{}, false, err
|
return domain.ChannelMessageReactionLookup{}, false, err
|
||||||
}
|
}
|
||||||
|
|
@ -626,7 +632,8 @@ func (s *ChannelStore) FindChannelMessageReaction(ctx context.Context, req domai
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelMessageReactionLookup{}, false, err
|
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
|
return domain.ChannelMessageReactionLookup{}, false, domain.ErrMessageIDInvalid
|
||||||
}
|
}
|
||||||
rows, err := s.db.Query(ctx, `
|
rows, err := s.db.Query(ctx, `
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ type monoforumSendFingerprintPayload struct {
|
||||||
Entities []domain.MessageEntity `json:"entities"`
|
Entities []domain.MessageEntity `json:"entities"`
|
||||||
Media *domain.MessageMedia `json:"media"`
|
Media *domain.MessageMedia `json:"media"`
|
||||||
ReplyTo *domain.MessageReply `json:"reply_to"`
|
ReplyTo *domain.MessageReply `json:"reply_to"`
|
||||||
|
Forward *domain.MessageForward `json:"forward,omitempty"`
|
||||||
Silent bool `json:"silent"`
|
Silent bool `json:"silent"`
|
||||||
NoForwards bool `json:"noforwards"`
|
NoForwards bool `json:"noforwards"`
|
||||||
SuggestedPost *domain.SuggestedPost `json:"suggested_post,omitempty"`
|
SuggestedPost *domain.SuggestedPost `json:"suggested_post,omitempty"`
|
||||||
|
|
@ -168,6 +169,7 @@ func MonoforumSendFingerprint(req domain.SendMonoforumMessageRequest) ([]byte, e
|
||||||
Entities: req.Entities,
|
Entities: req.Entities,
|
||||||
Media: req.Media,
|
Media: req.Media,
|
||||||
ReplyTo: req.ReplyTo,
|
ReplyTo: req.ReplyTo,
|
||||||
|
Forward: req.Forward,
|
||||||
Silent: req.Silent,
|
Silent: req.Silent,
|
||||||
NoForwards: req.NoForwards,
|
NoForwards: req.NoForwards,
|
||||||
SuggestedPost: req.SuggestedPost,
|
SuggestedPost: req.SuggestedPost,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue