owpengram-server/internal/store/memory/channel_monoforum.go

252 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package memory
import (
"context"
"sort"
"strings"
"time"
"telesrv/internal/domain"
"telesrv/internal/store"
)
const paidMessageChannelCommissionPermille int64 = 850
// SendMonoforumMessage 向 monoforum(频道私信)虚拟频道发一条消息,按 saved_peer 分订阅者子会话。
// 与 postgres 行为一致:复用 channel pts/事件;订阅者无需成员记录且只能写自己的 saved_peer
// 母频道管理员可以回复任意订阅者。
func (s *ChannelStore) SendMonoforumMessage(_ context.Context, req domain.SendMonoforumMessageRequest) (domain.SendChannelMessageResult, error) {
if req.MonoforumID == 0 || req.SenderUserID == 0 || req.SavedPeer.ID == 0 ||
req.SavedPeer.Type != domain.PeerTypeUser || strings.TrimSpace(req.Message) == "" && req.Media == nil {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
var fingerprint []byte
var err error
if req.RandomID != 0 {
fingerprint, err = store.MonoforumSendFingerprint(req)
if err != nil {
return domain.SendChannelMessageResult{}, err
}
req.IdempotencyFingerprint = fingerprint
}
s.mu.Lock()
defer s.mu.Unlock()
if req.RandomID != 0 {
if replay, found, replayErr := s.lookupChannelSendReplayLocked(domain.ChannelSendReplayRequest{
ChannelID: req.MonoforumID,
SenderUserID: req.SenderUserID,
SavedPeer: req.SavedPeer,
RandomID: req.RandomID,
IdempotencyFingerprint: fingerprint,
}); replayErr != nil || found {
return replay, replayErr
}
}
channel, ok := s.channels[req.MonoforumID]
if !ok || channel.Deleted || !channel.Monoforum {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
parent, ok := s.channels[channel.LinkedMonoforumID]
if !ok || parent.Deleted || !parent.BroadcastMessagesAllowed || parent.LinkedMonoforumID != channel.ID {
return domain.SendChannelMessageResult{}, domain.ErrChannelPrivate
}
parentMember, parentMemberOK := s.members[parent.ID][req.SenderUserID]
isAdmin := parentMemberOK && parentMember.CanManageDirectMessages()
if req.SenderUserID != req.SavedPeer.ID && !isAdmin {
return domain.SendChannelMessageResult{}, domain.ErrChannelAdminRequired
}
if req.ReplyTo != nil {
if req.ReplyTo.MessageID <= 0 || req.ReplyTo.Peer != (domain.Peer{Type: domain.PeerTypeChannel, ID: channel.ID}) {
return domain.SendChannelMessageResult{}, domain.ErrReplyMessageIDInvalid
}
found := false
for _, candidate := range s.messages[channel.ID] {
if candidate.ID == req.ReplyTo.MessageID && !candidate.Deleted && candidate.SavedPeer == req.SavedPeer {
found = true
break
}
}
if !found {
return domain.SendChannelMessageResult{}, domain.ErrReplyMessageIDInvalid
}
}
// telesrv has no Stars economy: Direct Messages are always free, so no
// balance is ever checked or debited here regardless of any stale
// per-channel price.
paidMessageStars := int64(0)
from := domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID}
if isAdmin {
from = domain.Peer{Type: domain.PeerTypeChannel, ID: parent.ID}
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
pts := s.nextChannelPtsLocked(req.MonoforumID)
msgID := s.nextChannelMessageIDLocked(req.MonoforumID)
msg := domain.ChannelMessage{
ChannelID: req.MonoforumID,
ID: msgID,
RandomID: req.RandomID,
SenderUserID: req.SenderUserID,
From: from,
SavedPeer: req.SavedPeer,
SuggestedPost: req.SuggestedPost,
PaidMessageStars: paidMessageStars,
Date: req.Date,
Silent: req.Silent,
NoForwards: req.NoForwards,
Body: req.Message,
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
// SuggestedPost/Media pointers after SendMonoforumMessage returns.
msg = cloneChannelMessage(msg)
var sendSnapshot []byte
if req.RandomID != 0 {
var snapshotErr error
sendSnapshot, snapshotErr = store.EncodeChannelSendSnapshot(msg)
if snapshotErr != nil {
return domain.SendChannelMessageResult{}, snapshotErr
}
}
event := domain.ChannelUpdateEvent{
ChannelID: req.MonoforumID,
Type: domain.ChannelUpdateNewMessage,
Pts: pts,
PtsCount: 1,
Date: req.Date,
Message: cloneChannelMessage(msg),
SenderUserID: req.SenderUserID,
}
s.messages[req.MonoforumID] = append(s.messages[req.MonoforumID], msg)
if req.RandomID != 0 {
replayKey := channelMessageReplayKey{channelID: req.MonoforumID, messageID: msg.ID}
s.sendSnapshots[replayKey] = sendSnapshot
s.sendFingerprints[replayKey] = append([]byte(nil), fingerprint...)
}
s.appendChannelEventLocked(event)
channel.TopMessageID = msgID
channel.Pts = pts
s.channels[req.MonoforumID] = channel
recipients := []int64{req.SavedPeer.ID}
for userID, member := range s.members[parent.ID] {
if member.CanManageDirectMessages() {
recipients = append(recipients, userID)
}
}
return domain.SendChannelMessageResult{Channel: cloneChannel(channel), Message: cloneChannelMessage(msg), Event: cloneChannelEvent(event), Recipients: uniqueNonZero(recipients, 0)}, nil
}
// findMonoforumDuplicateLocked 按 (sender, saved_peer, random_id) 查 monoforum 子会话内的重发消息。
func (s *ChannelStore) findMonoforumDuplicateLocked(monoforumID, senderUserID int64, savedPeer domain.Peer, randomID int64) (domain.ChannelMessage, bool) {
if randomID == 0 {
return domain.ChannelMessage{}, false
}
msgs := s.messages[monoforumID]
for i := len(msgs) - 1; i >= 0; i-- {
m := msgs[i]
if m.RandomID == randomID && m.SenderUserID == senderUserID && m.SavedPeer == savedPeer {
return m, true
}
}
return domain.ChannelMessage{}, false
}
// ListMonoforumHistory 拉取某订阅者(saved_peer)在 monoforum 内的私信历史,id 倒序分页。
func (s *ChannelStore) ListMonoforumHistory(_ context.Context, filter domain.MonoforumHistoryFilter) (domain.ChannelHistory, error) {
if filter.MonoforumID == 0 || filter.SavedPeer.ID == 0 {
return domain.ChannelHistory{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[filter.MonoforumID]
if !ok || !channel.Monoforum {
return domain.ChannelHistory{}, domain.ErrChannelInvalid
}
limit := filter.Limit
if limit <= 0 || limit > 100 {
limit = 100
}
all := s.messages[filter.MonoforumID]
var msgs []domain.ChannelMessage
count := 0
for i := len(all) - 1; i >= 0; i-- {
m := all[i]
if m.Deleted || m.SavedPeer != filter.SavedPeer {
continue
}
count++
if filter.OffsetID > 0 && m.ID >= filter.OffsetID {
continue
}
if len(msgs) < limit {
msgs = append(msgs, cloneChannelMessage(m))
}
}
return domain.ChannelHistory{Messages: msgs, Count: count, Channel: cloneChannel(channel)}, nil
}
// ResolveMonoforumSend 按 id 取 monoforum 频道(不要求调用者是 monoforum 成员——订阅者私信频道时
// 并非 monoforum 成员),并返回调用者是否可管理其母广播频道的 Direct Messages。非 monoforum/不存在 → ErrChannelInvalid。
func (s *ChannelStore) ResolveMonoforumSend(_ context.Context, viewerUserID, monoforumID int64) (domain.Channel, bool, error) {
if viewerUserID == 0 || monoforumID == 0 {
return domain.Channel{}, false, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
mono, ok := s.channels[monoforumID]
if !ok || mono.Deleted || !mono.Monoforum || mono.LinkedMonoforumID == 0 {
return domain.Channel{}, false, domain.ErrChannelInvalid
}
member, ok := s.members[mono.LinkedMonoforumID][viewerUserID]
isAdmin := ok && member.CanManageDirectMessages()
return cloneChannel(mono), isAdmin, nil
}
// ListMonoforumDialogs 列出 monoforum 的订阅者子会话(每个 saved_peer 一条,取其 top 消息),
// 按 top 消息 id 倒序分页。
func (s *ChannelStore) ListMonoforumDialogs(_ context.Context, filter domain.MonoforumDialogsFilter) (domain.MonoforumDialogList, error) {
if filter.MonoforumID == 0 {
return domain.MonoforumDialogList{}, domain.ErrChannelInvalid
}
s.mu.RLock()
defer s.mu.RUnlock()
channel, ok := s.channels[filter.MonoforumID]
if !ok || !channel.Monoforum {
return domain.MonoforumDialogList{}, domain.ErrChannelInvalid
}
limit := filter.Limit
if limit <= 0 || limit > 100 {
limit = 100
}
tops := map[domain.Peer]domain.ChannelMessage{}
for _, m := range s.messages[filter.MonoforumID] {
if m.Deleted || m.SavedPeer.ID == 0 {
continue
}
if cur, ok := tops[m.SavedPeer]; !ok || m.ID > cur.ID {
tops[m.SavedPeer] = m
}
}
ordered := make([]domain.ChannelMessage, 0, len(tops))
for _, m := range tops {
ordered = append(ordered, m)
}
sort.Slice(ordered, func(i, j int) bool { return ordered[i].ID > ordered[j].ID })
out := domain.MonoforumDialogList{MonoforumID: filter.MonoforumID, Channel: cloneChannel(channel), Count: len(ordered)}
for _, m := range ordered {
if filter.OffsetID > 0 && m.ID >= filter.OffsetID {
continue
}
if len(out.Dialogs) >= limit {
break
}
out.Dialogs = append(out.Dialogs, domain.MonoforumDialog{SavedPeer: m.SavedPeer, TopMessageID: m.ID, TopMessageDate: m.Date})
out.Messages = append(out.Messages, cloneChannelMessage(m))
}
return out, nil
}