owpengram-server/internal/store/memory/message_history.go
A da6a57e1a3 feat: sync community aggregates
Sync telesrv 36eda30 (feat(communities): implement Layer 228 community aggregates).

Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
2026-07-20 16:46:02 +08:00

462 lines
13 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"
"telesrv/internal/domain"
"time"
)
func (s *MessageStore) GetByIDs(_ context.Context, userID int64, ids []int) (domain.MessageList, error) {
if userID == 0 || len(ids) == 0 {
return domain.MessageList{}, nil
}
s.mu.RLock()
byID := make(map[int]domain.Message, len(s.m[userID]))
for _, msg := range s.m[userID] {
item := cloneMessage(msg)
reactions := s.privateMessageReactionsForMessageLocked(item)
if len(reactions.Results) > 0 || len(reactions.Recent) > 0 {
item.Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
byID[msg.ID] = item
}
s.mu.RUnlock()
out := domain.MessageList{Messages: make([]domain.Message, 0, len(ids))}
for _, id := range ids {
if msg, ok := byID[id]; ok {
out.Messages = append(out.Messages, msg)
}
}
s.enrichPrivateMessagePolls(out.Messages, int(time.Now().Unix()))
out.Users = usersForMessages(out.Messages)
out.Hash = messageListHash(out.Messages)
return out, nil
}
// GetByUID resolves one owner's box row by the shared private message id. Callback delivery
// uses it to translate the clicker's box id to the bot's box id without scanning history.
func (s *MessageStore) GetByUID(_ context.Context, userID, uid int64) (domain.Message, bool, error) {
if userID == 0 || uid == 0 {
return domain.Message{}, false, nil
}
s.mu.RLock()
var found domain.Message
for _, msg := range s.m[userID] {
if msg.UID == uid {
found = cloneMessage(msg)
reactions := s.privateMessageReactionsForMessageLocked(found)
if len(reactions.Results) > 0 || len(reactions.Recent) > 0 {
found.Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
break
}
}
s.mu.RUnlock()
if found.ID == 0 {
return domain.Message{}, false, nil
}
items := []domain.Message{found}
s.enrichPrivateMessagePolls(items, int(time.Now().Unix()))
return items[0], true, nil
}
func (s *MessageStore) ListByUser(_ context.Context, userID int64, filter domain.MessageFilter) (domain.MessageList, error) {
s.mu.RLock()
messages := cloneMessages(s.m[userID])
for i := range messages {
reactions := s.privateMessageReactionsForMessageLocked(messages[i])
if len(reactions.Results) > 0 || len(reactions.Recent) > 0 {
messages[i].Reactions = cloneChannelMessageReactionsPtr(&reactions)
}
}
s.mu.RUnlock()
s.enrichPrivateMessagePolls(messages, int(time.Now().Unix()))
return filterMessageList(messages, filter), nil
}
func (s *MessageStore) ReadHistory(_ context.Context, req domain.ReadHistoryRequest) (domain.ReadHistoryResult, error) {
res := domain.ReadHistoryResult{OwnerUserID: req.OwnerUserID, Peer: req.Peer, MaxID: req.MaxID}
if req.OwnerUserID == 0 || req.Peer.ID == 0 {
return res, nil
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
if s.dialogs == nil {
return res, nil
}
s.dialogs.mu.Lock()
defer s.dialogs.mu.Unlock()
list := s.dialogs.m[req.OwnerUserID]
for i, dialog := range list.Dialogs {
if dialog.Peer != req.Peer {
continue
}
readMax := req.MaxID
if readMax <= 0 {
readMax = dialog.TopMessage
}
if readMax > dialog.TopMessage {
readMax = dialog.TopMessage
}
oldRead := dialog.ReadInboxMaxID
res.MaxID = readMax
advancesRead := readMax > oldRead
if !advancesRead {
if dialog.UnreadCount > 0 {
unread := 0
for _, msg := range s.m[req.OwnerUserID] {
if msg.Peer == req.Peer && !msg.Out && msg.ID > oldRead {
unread++
}
}
dialog.UnreadCount = unread
dialog.UnreadMentions = 0
// readHistory 不清 reaction 角标(与 PG 一致reaction 未读由
// readReactions/readMessageContents 单独清,否则角标数与 getUnreadReactions
// 跳转列表对不上)。
dialog.UnreadMark = false
res.MaxID = dialog.ReadInboxMaxID
res.StillUnreadCount = unread
list.Dialogs[i] = dialog
s.dialogs.m[req.OwnerUserID] = list
}
return res, nil
}
res.Changed = true
var latestIncoming domain.Message
unread := 0
for _, msg := range s.m[req.OwnerUserID] {
if msg.Peer != req.Peer || msg.Out {
continue
}
if msg.ID > readMax {
unread++
continue
}
if msg.ID > oldRead && msg.ID > latestIncoming.ID {
latestIncoming = msg
}
}
if readMax > dialog.ReadInboxMaxID {
dialog.ReadInboxMaxID = readMax
}
dialog.UnreadCount = unread
dialog.UnreadMentions = 0
// readHistory 不清 reaction 角标(与 PG 一致,见上)。
dialog.UnreadMark = false
res.StillUnreadCount = unread
pts := s.nextPtsLocked(req.OwnerUserID)
res.InboxEvent = domain.UpdateEvent{
UserID: req.OwnerUserID,
Type: domain.UpdateEventReadHistoryInbox,
Pts: pts,
PtsCount: 1,
Date: req.Date,
Peer: req.Peer,
MaxID: readMax,
StillUnreadCount: unread,
}
list.Dialogs[i] = dialog
s.dialogs.m[req.OwnerUserID] = list
if latestIncoming.ID != 0 && latestIncoming.From.ID != 0 && latestIncoming.From.ID != req.OwnerUserID {
senderUserID := latestIncoming.From.ID
senderBoxID := 0
for _, msg := range s.m[senderUserID] {
if msg.UID == latestIncoming.UID && msg.Out {
senderBoxID = msg.ID
break
}
}
if senderBoxID > 0 {
senderList := s.dialogs.m[senderUserID]
for j, senderDialog := range senderList.Dialogs {
if senderDialog.Peer != (domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}) {
continue
}
if senderBoxID <= senderDialog.ReadOutboxMaxID {
break
}
oldOutbox := senderDialog.ReadOutboxMaxID
senderDialog.ReadOutboxMaxID = senderBoxID
senderList.Dialogs[j] = senderDialog
s.dialogs.m[senderUserID] = senderList
for _, msg := range s.m[senderUserID] {
if msg.Peer == (domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}) && msg.Out && msg.ID > oldOutbox && msg.ID <= senderBoxID {
s.readOutboxDates[readOutboxDateKey{ownerUserID: senderUserID, peerID: req.OwnerUserID, msgID: msg.ID}] = req.Date
}
}
outPts := s.nextPtsLocked(senderUserID)
res.OutboxChanged = true
res.OutboxUserID = senderUserID
res.OutboxEvent = domain.UpdateEvent{
UserID: senderUserID,
Type: domain.UpdateEventReadHistoryOutbox,
Pts: outPts,
PtsCount: 1,
Date: req.Date,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID},
MaxID: senderBoxID,
}
break
}
}
}
return res, nil
}
return res, nil
}
func (s *MessageStore) DeleteHistory(_ context.Context, req domain.DeleteHistoryRequest) (domain.DeleteMessagesResult, error) {
res := domain.DeleteMessagesResult{OwnerUserID: req.OwnerUserID}
if req.OwnerUserID == 0 || req.Peer.ID == 0 {
return res, nil
}
if req.Date == 0 {
req.Date = int(time.Now().Unix())
}
s.mu.Lock()
defer s.mu.Unlock()
inDateRange := func(msg domain.Message) bool {
if req.MinDate > 0 && msg.Date < req.MinDate {
return false
}
if req.MaxDate > 0 && msg.Date > req.MaxDate {
return false
}
return true
}
deleted, revokeUIDs, more := s.deleteMemoryMessagesLocked(req.OwnerUserID, domain.MaxDeleteHistoryBatch, func(msg domain.Message) bool {
return msg.Peer == req.Peer && (req.MaxID <= 0 || msg.ID <= req.MaxID) && inDateRange(msg)
})
if req.Revoke {
if len(revokeUIDs) > 0 {
deleted = append(deleted, s.deleteMemoryMessagesByUIDLocked(revokeUIDs, req.OwnerUserID)...)
}
// 与 PG 同语义:全量/按日期的双向清史直扫对端残余,我方早已
// 单向删除的消息不能在对端残留。
if req.MaxID <= 0 && req.Peer.ID != req.OwnerUserID {
peerDeleted, _, peerMore := s.deleteMemoryMessagesLocked(req.Peer.ID, domain.MaxDeleteHistoryBatch, func(msg domain.Message) bool {
return msg.Peer == (domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}) && inDateRange(msg)
})
deleted = append(deleted, peerDeleted...)
more = more || peerMore
}
}
res = s.finishMemoryDeleteLocked(res, deleted, req.Date, req.JustClear)
if more {
res.Offset = 1
}
return res, nil
}
func filterMessageList(messages []domain.Message, filter domain.MessageFilter) domain.MessageList {
filter.AddOffset = domain.ClampMessageHistoryAddOffset(filter.AddOffset)
sort.SliceStable(messages, func(i, j int) bool {
return messageLess(messages[i], messages[j])
})
query := strings.ToLower(filter.Query)
peerIDs := make(map[int64]struct{}, len(filter.PeerIDs))
for _, id := range filter.PeerIDs {
peerIDs[id] = struct{}{}
}
base := make([]domain.Message, 0, len(messages))
for _, msg := range messages {
if filter.HasPeer && msg.Peer != filter.Peer {
continue
}
if filter.RestrictPeerIDs {
if msg.Peer.Type != domain.PeerTypeUser {
continue
}
if _, ok := peerIDs[msg.Peer.ID]; !ok {
continue
}
}
if query != "" && !strings.Contains(strings.ToLower(msg.Body), query) {
continue
}
if filter.MaxID > 0 && msg.ID >= filter.MaxID {
continue
}
if filter.MinID > 0 && msg.ID <= filter.MinID {
continue
}
if filter.PinnedOnly && !msg.Pinned {
continue
}
if filter.MusicOnly && !msg.Media.IsMusic() {
continue
}
if filter.SavedPeer.ID != 0 && msg.SavedPeer != filter.SavedPeer {
continue
}
base = append(base, msg)
}
limit := filter.Limit
if limit <= 0 {
limit = 100
}
if limit > 500 {
limit = 500
}
page := pageMessageHistory(base, filter, limit)
return domain.MessageList{
Messages: page,
Users: usersForMessages(page),
Count: len(base),
Hash: messageListHash(base),
}
}
func pageMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int) []domain.Message {
if limit <= 0 || len(base) == 0 {
return nil
}
switch messageHistoryLoadType(filter.AddOffset, limit) {
case messageHistoryLoadForward:
return cloneMessages(forwardMessageHistory(base, filter, limit))
case messageHistoryLoadAround:
forwardLimit := -filter.AddOffset
if forwardLimit > limit {
forwardLimit = limit
}
backwardLimit := limit + filter.AddOffset
if backwardLimit < 0 {
backwardLimit = 0
}
page := make([]domain.Message, 0, limit)
page = append(page, forwardMessageHistory(base, filter, forwardLimit)...)
page = append(page, backwardMessageHistory(base, filter, backwardLimit, true)...)
sort.SliceStable(page, func(i, j int) bool {
return messageLess(page[i], page[j])
})
return cloneMessages(page)
default:
start := filter.AddOffset
if start < 0 {
start = 0
}
candidates := backwardMessageHistory(base, filter, limit+start, false)
if start >= len(candidates) {
return nil
}
return cloneMessages(candidates[start:])
}
}
type messageHistoryLoad int
const (
messageHistoryLoadBackward messageHistoryLoad = iota
messageHistoryLoadForward
messageHistoryLoadAround
)
func messageHistoryLoadType(addOffset, limit int) messageHistoryLoad {
if addOffset >= 0 {
return messageHistoryLoadBackward
}
if addOffset+limit > 0 {
return messageHistoryLoadAround
}
return messageHistoryLoadForward
}
func backwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int, includeOffset bool) []domain.Message {
if limit <= 0 {
return nil
}
out := make([]domain.Message, 0, limit)
for _, msg := range base {
if !messageBeforeHistoryOffset(msg, filter, includeOffset) {
continue
}
out = append(out, msg)
if len(out) == limit {
break
}
}
return out
}
func forwardMessageHistory(base []domain.Message, filter domain.MessageFilter, limit int) []domain.Message {
if limit <= 0 {
return nil
}
out := make([]domain.Message, 0, limit)
for i := len(base) - 1; i >= 0; i-- {
msg := base[i]
if !messageAfterHistoryOffset(msg, filter) {
continue
}
out = append(out, msg)
if len(out) == limit {
break
}
}
sort.SliceStable(out, func(i, j int) bool {
return messageLess(out[i], out[j])
})
return out
}
func messageBeforeHistoryOffset(msg domain.Message, filter domain.MessageFilter, includeOffset bool) bool {
if filter.OffsetDate > 0 {
if includeOffset {
return msg.Date <= filter.OffsetDate
}
return msg.Date < filter.OffsetDate
}
if filter.OffsetID <= 0 {
return true
}
if includeOffset {
return msg.ID <= filter.OffsetID
}
return msg.ID < filter.OffsetID
}
func messageAfterHistoryOffset(msg domain.Message, filter domain.MessageFilter) bool {
if filter.OffsetDate > 0 {
return msg.Date >= filter.OffsetDate
}
if filter.OffsetID <= 0 {
return false
}
return msg.ID > filter.OffsetID
}
func messageLess(a, b domain.Message) bool {
if a.Date != b.Date {
return a.Date > b.Date
}
return a.ID > b.ID
}
func usersForMessages(messages []domain.Message) []domain.User {
seen := map[int64]struct{}{}
users := make([]domain.User, 0, 1)
for _, msg := range messages {
for _, peer := range []domain.Peer{msg.Peer, msg.From} {
if peer.Type != domain.PeerTypeUser {
continue
}
if _, ok := seen[peer.ID]; ok {
continue
}
seen[peer.ID] = struct{}{}
if u, ok := domain.SystemUserByID(peer.ID); ok {
users = append(users, u)
}
}
}
return users
}