SYNC_INBOX 补 conversation_type、peer_id、group_id 和 last_read_msg_id。历史消息在 sender 为 0 时用 peer_id 定位对方。已读清未读时保留会话 ZSET 分数,避免被毫秒时间戳冲到游标后面。 Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
4.2 KiB
Go
154 lines
4.2 KiB
Go
// Package logic is SYNC_INBOX. HTTP and TCP share it.
|
|
package logic
|
|
|
|
import (
|
|
"context"
|
|
|
|
conventity "spark-server/internal/module/conversation/entity"
|
|
inboxentity "spark-server/internal/module/inbox/entity"
|
|
inboxlogic "spark-server/internal/module/inbox/logic"
|
|
"spark-server/internal/module/sync/entity"
|
|
"spark-server/internal/pkg/errs"
|
|
)
|
|
|
|
const defaultInboxLimit = 200
|
|
|
|
type Mailbox interface {
|
|
Put(ctx context.Context, userID int64, eventType entity.EventType, conversationID int64, payload []byte) error
|
|
ListSince(ctx context.Context, userID int64, deviceID uint64, seq int64, limit int) ([]*entity.MailboxItem, error)
|
|
SaveCursor(ctx context.Context, userID int64, deviceID uint64, mailboxSeq, dialogScore int64) error
|
|
PurgeUser(ctx context.Context, userID int64) error
|
|
}
|
|
|
|
type InboxResponse struct {
|
|
Dialogs []inboxentity.Dialog `json:"dialogs"`
|
|
Mailbox []*entity.MailboxItem `json:"mailbox"`
|
|
DialogScore int64 `json:"dialog_score"`
|
|
MailboxSeq int64 `json:"mailbox_seq"`
|
|
HasMore bool `json:"has_more"`
|
|
}
|
|
|
|
// Service is SYNC_INBOX. HTTP and TCP share it.
|
|
// DialogFaces resolves viewer-relative peer / group / read cursor. Nil leaves those fields empty.
|
|
type DialogFaces interface {
|
|
DialogFaces(ctx context.Context, viewerID int64, conversationIDs []int64) (map[int64]conventity.DialogFace, error)
|
|
}
|
|
|
|
type Service struct {
|
|
inbox inboxlogic.Store
|
|
box Mailbox
|
|
faces DialogFaces
|
|
}
|
|
|
|
func NewService(inbox inboxlogic.Store, box Mailbox) *Service {
|
|
return &Service{inbox: inbox, box: box}
|
|
}
|
|
|
|
func (s *Service) BindFaces(faces DialogFaces) {
|
|
if s == nil {
|
|
return
|
|
}
|
|
s.faces = faces
|
|
}
|
|
|
|
func (s *Service) ready() bool {
|
|
return s != nil && s.inbox != nil
|
|
}
|
|
|
|
func (s *Service) SyncInbox(ctx context.Context, req entity.InboxRequest) (*InboxResponse, error) {
|
|
if !s.ready() || req.UserID <= 0 {
|
|
return nil, errs.ErrUnauthorized
|
|
}
|
|
if req.MailboxSeq < 0 || req.DialogScore < 0 {
|
|
return nil, errs.ErrInvalidArgument
|
|
}
|
|
limit := req.Limit
|
|
if limit <= 0 || limit > defaultInboxLimit {
|
|
limit = defaultInboxLimit
|
|
}
|
|
hits, err := s.inbox.ListUpdatedSince(ctx, req.UserID, req.DialogScore, limit+1)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hasMore := len(hits) > limit
|
|
if hasMore {
|
|
hits = hits[:limit]
|
|
}
|
|
dialogs, err := s.inbox.LoadDialogs(ctx, req.UserID, hits)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if dialogs == nil {
|
|
dialogs = []inboxentity.Dialog{}
|
|
}
|
|
if err := s.attachFaces(ctx, req.UserID, dialogs); err != nil {
|
|
return nil, err
|
|
}
|
|
nextScore := maxScore(req.DialogScore, dialogs)
|
|
mailbox, nextSeq, err := s.loadMailbox(ctx, req, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if s.box != nil {
|
|
_ = s.box.SaveCursor(ctx, req.UserID, req.DeviceID, nextSeq, nextScore)
|
|
}
|
|
return &InboxResponse{
|
|
Dialogs: dialogs, Mailbox: mailbox,
|
|
DialogScore: nextScore, MailboxSeq: nextSeq, HasMore: hasMore,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) loadMailbox(ctx context.Context, req entity.InboxRequest, limit int) ([]*entity.MailboxItem, int64, error) {
|
|
nextSeq := req.MailboxSeq
|
|
if s.box == nil {
|
|
return []*entity.MailboxItem{}, nextSeq, nil
|
|
}
|
|
mailbox, err := s.box.ListSince(ctx, req.UserID, req.DeviceID, req.MailboxSeq, limit)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if mailbox == nil {
|
|
mailbox = []*entity.MailboxItem{}
|
|
}
|
|
for _, item := range mailbox {
|
|
if item != nil && item.Seq > nextSeq {
|
|
nextSeq = item.Seq
|
|
}
|
|
}
|
|
return mailbox, nextSeq, nil
|
|
}
|
|
|
|
func (s *Service) attachFaces(ctx context.Context, viewerID int64, dialogs []inboxentity.Dialog) error {
|
|
if s == nil || s.faces == nil || len(dialogs) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]int64, len(dialogs))
|
|
for i := range dialogs {
|
|
ids[i] = dialogs[i].ConversationID
|
|
}
|
|
faces, err := s.faces.DialogFaces(ctx, viewerID, ids)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range dialogs {
|
|
face, ok := faces[dialogs[i].ConversationID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
dialogs[i].ConversationType = int(face.Type)
|
|
dialogs[i].PeerID = face.PeerID
|
|
dialogs[i].GroupID = face.GroupID
|
|
dialogs[i].LastReadMsgID = face.LastReadMsgID
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func maxScore(cur int64, dialogs []inboxentity.Dialog) int64 {
|
|
for i := range dialogs {
|
|
if dialogs[i].Score > cur {
|
|
cur = dialogs[i].Score
|
|
}
|
|
}
|
|
return cur
|
|
}
|