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

270 lines
7.7 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"
"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 = MaxMessageBoxIDint32 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 {
// 非 forceorder 之外的既有置顶保持原相对顺序排在后面。
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, nil)
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
}