270 lines
7.7 KiB
Go
270 lines
7.7 KiB
Go
package memory
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sort"
|
||
"time"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// savedDialogTopsLocked 聚合 self-chat 按 saved_peer 分组的 top message。
|
||
// 返回按 top box id 降序。
|
||
func (s *MessageStore) savedDialogTopsLocked(userID int64) []domain.Message {
|
||
tops := make(map[domain.Peer]domain.Message)
|
||
selfPeer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
|
||
for _, msg := range s.m[userID] {
|
||
if msg.Peer != selfPeer || msg.SavedPeer.ID == 0 {
|
||
continue
|
||
}
|
||
if cur, ok := tops[msg.SavedPeer]; !ok || msg.ID > cur.ID {
|
||
tops[msg.SavedPeer] = msg
|
||
}
|
||
}
|
||
out := make([]domain.Message, 0, len(tops))
|
||
for _, msg := range tops {
|
||
out = append(out, msg)
|
||
}
|
||
sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID })
|
||
return out
|
||
}
|
||
|
||
func (s *MessageStore) savedPinIndexLocked(userID int64, peer domain.Peer) int {
|
||
for i, p := range s.savedPins[userID] {
|
||
if p == peer {
|
||
return i
|
||
}
|
||
}
|
||
return -1
|
||
}
|
||
|
||
func (s *MessageStore) appendSavedDialogLocked(out *domain.SavedDialogList, msg domain.Message, pinned bool) {
|
||
out.Dialogs = append(out.Dialogs, domain.SavedDialog{
|
||
Peer: msg.SavedPeer,
|
||
TopMessage: msg.ID,
|
||
Pinned: pinned,
|
||
})
|
||
out.Messages = append(out.Messages, cloneMessage(msg))
|
||
}
|
||
|
||
func (s *MessageStore) ListSavedDialogs(_ context.Context, userID int64, filter domain.SavedDialogsFilter) (domain.SavedDialogList, error) {
|
||
out := domain.SavedDialogList{}
|
||
if userID == 0 {
|
||
return out, fmt.Errorf("list saved dialogs: missing user id")
|
||
}
|
||
limit := filter.Limit
|
||
if limit <= 0 || limit > domain.MaxSavedDialogsLimit {
|
||
limit = domain.MaxSavedDialogsLimit
|
||
}
|
||
offsetID := filter.OffsetID
|
||
// DrKLO Android 首页发 offset_id = MaxMessageBoxID(int32 max),用 >= 命中首页
|
||
// 分支,避免被误判为续页跳过置顶块(与 postgres 对齐)。
|
||
firstPage := offsetID <= 0 || offsetID >= domain.MaxMessageBoxID
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
tops := s.savedDialogTopsLocked(userID)
|
||
topByPeer := make(map[domain.Peer]domain.Message, len(tops))
|
||
for _, msg := range tops {
|
||
topByPeer[msg.SavedPeer] = msg
|
||
}
|
||
// 首页且不排除置顶:置顶块按 pinned_order 在前。
|
||
if firstPage && !filter.ExcludePinned {
|
||
for _, peer := range s.savedPins[userID] {
|
||
if len(out.Dialogs) >= limit {
|
||
break
|
||
}
|
||
if msg, ok := topByPeer[peer]; ok {
|
||
s.appendSavedDialogLocked(&out, msg, true)
|
||
}
|
||
}
|
||
}
|
||
// 普通块恒排除置顶(置顶只随首页返回)。
|
||
hasMore := false
|
||
for _, msg := range tops {
|
||
if len(out.Dialogs) >= limit {
|
||
if s.savedPinIndexLocked(userID, msg.SavedPeer) < 0 &&
|
||
(firstPage || msg.ID < offsetID) {
|
||
hasMore = true
|
||
}
|
||
continue
|
||
}
|
||
if s.savedPinIndexLocked(userID, msg.SavedPeer) >= 0 {
|
||
continue
|
||
}
|
||
if !firstPage && msg.ID >= offsetID {
|
||
continue
|
||
}
|
||
s.appendSavedDialogLocked(&out, msg, false)
|
||
}
|
||
total := 0
|
||
for _, msg := range tops {
|
||
if filter.ExcludePinned && s.savedPinIndexLocked(userID, msg.SavedPeer) >= 0 {
|
||
continue
|
||
}
|
||
total++
|
||
}
|
||
out.Count = total
|
||
out.Full = !hasMore
|
||
return out, nil
|
||
}
|
||
|
||
func (s *MessageStore) ListPinnedSavedDialogs(_ context.Context, userID int64) (domain.SavedDialogList, error) {
|
||
out := domain.SavedDialogList{Full: true}
|
||
if userID == 0 {
|
||
return out, fmt.Errorf("list pinned saved dialogs: missing user id")
|
||
}
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
tops := s.savedDialogTopsLocked(userID)
|
||
topByPeer := make(map[domain.Peer]domain.Message, len(tops))
|
||
for _, msg := range tops {
|
||
topByPeer[msg.SavedPeer] = msg
|
||
}
|
||
for _, peer := range s.savedPins[userID] {
|
||
if msg, ok := topByPeer[peer]; ok {
|
||
s.appendSavedDialogLocked(&out, msg, true)
|
||
}
|
||
}
|
||
out.Count = len(out.Dialogs)
|
||
return out, nil
|
||
}
|
||
|
||
func (s *MessageStore) ListSavedDialogsByPeers(_ context.Context, userID int64, peers []domain.Peer) (domain.SavedDialogList, error) {
|
||
out := domain.SavedDialogList{Full: true}
|
||
if userID == 0 {
|
||
return out, fmt.Errorf("list saved dialogs by peers: missing user id")
|
||
}
|
||
s.mu.RLock()
|
||
defer s.mu.RUnlock()
|
||
tops := s.savedDialogTopsLocked(userID)
|
||
topByPeer := make(map[domain.Peer]domain.Message, len(tops))
|
||
for _, msg := range tops {
|
||
topByPeer[msg.SavedPeer] = msg
|
||
}
|
||
seen := make(map[domain.Peer]struct{}, len(peers))
|
||
for _, peer := range peers {
|
||
if peer.Type == "" || peer.ID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[peer]; ok {
|
||
continue
|
||
}
|
||
seen[peer] = struct{}{}
|
||
if msg, ok := topByPeer[peer]; ok {
|
||
s.appendSavedDialogLocked(&out, msg, s.savedPinIndexLocked(userID, peer) >= 0)
|
||
}
|
||
}
|
||
out.Count = len(out.Dialogs)
|
||
return out, nil
|
||
}
|
||
|
||
func (s *MessageStore) ToggleSavedDialogPin(_ context.Context, userID int64, peer domain.Peer, pinned bool) (bool, error) {
|
||
if userID == 0 || peer.Type == "" || peer.ID == 0 {
|
||
return false, fmt.Errorf("toggle saved dialog pin: invalid input")
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
idx := s.savedPinIndexLocked(userID, peer)
|
||
if !pinned {
|
||
if idx < 0 {
|
||
return false, nil
|
||
}
|
||
s.savedPins[userID] = append(s.savedPins[userID][:idx], s.savedPins[userID][idx+1:]...)
|
||
return true, nil
|
||
}
|
||
if idx >= 0 {
|
||
return false, nil
|
||
}
|
||
if len(s.savedPins[userID]) >= domain.MaxPinnedSavedDialogs {
|
||
return false, domain.ErrPinnedSavedDialogsTooMuch
|
||
}
|
||
s.savedPins[userID] = append([]domain.Peer{peer}, s.savedPins[userID]...)
|
||
return true, nil
|
||
}
|
||
|
||
func (s *MessageStore) ReorderPinnedSavedDialogs(_ context.Context, userID int64, order []domain.Peer, force bool) error {
|
||
if userID == 0 {
|
||
return fmt.Errorf("reorder pinned saved dialogs: missing user id")
|
||
}
|
||
if len(order) > domain.MaxPinnedSavedDialogs {
|
||
return domain.ErrPinnedSavedDialogsTooMuch
|
||
}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
next := make([]domain.Peer, 0, len(order))
|
||
seen := make(map[domain.Peer]struct{}, len(order))
|
||
for _, peer := range order {
|
||
if peer.Type == "" || peer.ID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[peer]; ok {
|
||
continue
|
||
}
|
||
seen[peer] = struct{}{}
|
||
next = append(next, peer)
|
||
}
|
||
if !force {
|
||
// 非 force:order 之外的既有置顶保持原相对顺序排在后面。
|
||
for _, peer := range s.savedPins[userID] {
|
||
if _, ok := seen[peer]; !ok {
|
||
next = append(next, peer)
|
||
}
|
||
}
|
||
}
|
||
s.savedPins[userID] = next
|
||
return nil
|
||
}
|
||
|
||
func (s *MessageStore) DeleteSavedHistory(_ context.Context, req domain.DeleteSavedHistoryRequest) (domain.DeleteSavedHistoryResult, error) {
|
||
res := domain.DeleteSavedHistoryResult{}
|
||
if req.OwnerUserID == 0 || req.SavedPeer.Type == "" || req.SavedPeer.ID == 0 {
|
||
return res, fmt.Errorf("delete saved history: invalid input")
|
||
}
|
||
if req.Date == 0 {
|
||
req.Date = int(time.Now().Unix())
|
||
}
|
||
selfPeer := domain.Peer{Type: domain.PeerTypeUser, ID: req.OwnerUserID}
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
match := func(msg domain.Message) bool {
|
||
if msg.Peer != selfPeer || msg.SavedPeer != req.SavedPeer {
|
||
return false
|
||
}
|
||
if req.MaxID > 0 && msg.ID > req.MaxID {
|
||
return false
|
||
}
|
||
if req.MinDate > 0 && msg.Date < req.MinDate {
|
||
return false
|
||
}
|
||
if req.MaxDate > 0 && msg.Date > req.MaxDate {
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
deleted, _, more := s.deleteMemoryMessagesLocked(req.OwnerUserID, domain.MaxDeleteHistoryBatch, match)
|
||
delRes := s.finishMemoryDeleteLocked(domain.DeleteMessagesResult{OwnerUserID: req.OwnerUserID}, deleted, req.Date, false)
|
||
res.More = more
|
||
for _, d := range delRes.Deleted {
|
||
if d.UserID == req.OwnerUserID {
|
||
res.MessageIDs = d.MessageIDs
|
||
res.Event = d.Event
|
||
}
|
||
}
|
||
if len(deleted) > 0 && !more {
|
||
// 子会话删空时清掉它的置顶行(与 PG 实现同语义)。
|
||
alive := false
|
||
for _, msg := range s.m[req.OwnerUserID] {
|
||
if msg.Peer == selfPeer && msg.SavedPeer == req.SavedPeer {
|
||
alive = true
|
||
break
|
||
}
|
||
}
|
||
if !alive {
|
||
if idx := s.savedPinIndexLocked(req.OwnerUserID, req.SavedPeer); idx >= 0 {
|
||
s.savedPins[req.OwnerUserID] = append(s.savedPins[req.OwnerUserID][:idx], s.savedPins[req.OwnerUserID][idx+1:]...)
|
||
}
|
||
}
|
||
}
|
||
return res, nil
|
||
}
|