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

179 lines
6.8 KiB
Go

package memory
import (
"context"
"sort"
"strings"
"time"
"telesrv/internal/domain"
)
// SendMonoforumMessage 向 monoforum(频道私信)虚拟频道发一条消息,按 saved_peer 分订阅者子会话。
// 与 postgres 行为一致:复用 channel pts/事件;只校验 monoforum 存在,不要求发件人是成员。
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) == "" {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
s.mu.Lock()
defer s.mu.Unlock()
channel, ok := s.channels[req.MonoforumID]
if !ok || channel.Deleted || !channel.Monoforum {
return domain.SendChannelMessageResult{}, domain.ErrChannelInvalid
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
if req.RandomID != 0 {
// 去重维度 = (sender, saved_peer, random_id),与 postgres 迁移 0022 的唯一索引一致;
// 不复用账号级 randomToID(其按 channel+sender+random_id 三元组,会被跨子会话同 random_id 互相覆盖)。
if dup, ok := s.findMonoforumDuplicateLocked(req.MonoforumID, req.SenderUserID, req.SavedPeer, req.RandomID); ok {
event := s.eventForMessageLocked(req.MonoforumID, dup.ID)
if event.Message.ID != 0 {
dup = event.Message
}
return domain.SendChannelMessageResult{Channel: cloneChannel(channel), Message: cloneChannelMessage(dup), Event: event, Duplicate: true}, nil
}
}
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: domain.Peer{Type: domain.PeerTypeUser, ID: req.SenderUserID},
SavedPeer: req.SavedPeer,
Date: req.Date,
Body: req.Message,
Entities: append([]domain.MessageEntity(nil), req.Entities...),
Pts: pts,
}
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)
s.events[req.MonoforumID] = append(s.events[req.MonoforumID], event)
channel.TopMessageID = msgID
channel.Pts = pts
s.channels[req.MonoforumID] = channel
return domain.SendChannelMessageResult{Channel: cloneChannel(channel), Message: cloneChannelMessage(msg), Event: cloneChannelEvent(event)}, 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.Deleted && 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 成员),并返回调用者是否为其母广播频道的创建者/管理员。非 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.Status == domain.ChannelMemberActive &&
(member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin)
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
}