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 }