owpengram-server/internal/rpc/messages_saved_dialogs.go

387 lines
12 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 rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"telesrv/internal/domain"
)
// Saved Messages 分会话saved dialogs真实实现。
// 纯 saved 形态(无 parent_peer按 self-chat 消息的 saved_peer 分组返回;
// parent_peer = monoforum topic list 仍是范围外,仅校验 parent channel 后
// 返回空(与既有 monoforum stub 行为一致,见 compatibility-matrix
func (r *Router) onMessagesGetSavedDialogs(ctx context.Context, req *tg.MessagesGetSavedDialogsRequest) (tg.MessagesSavedDialogsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if parent, hasParent, err := r.validateSavedHistoryParentPeer(ctx, userID, req.GetParentPeer); err != nil {
return nil, err
} else if hasParent {
if req.Limit < 0 {
return nil, limitInvalidErr()
}
offsetID := req.OffsetID
if offsetID < 0 {
offsetID = 0
}
mono, isMono, err := r.resolveMonoforumForAdmin(ctx, userID, parent)
if err != nil {
return nil, err
}
if !isMono {
// 普通频道(非 monoforum)传 parent_peer:保持旧的良性空响应 + parent 上下文。
return &tg.MessagesSavedDialogs{
Dialogs: []tg.SavedDialogClass{},
Messages: []tg.MessageClass{},
Chats: r.savedHistoryChats(ctx, userID, true, parent, nil),
Users: []tg.UserClass{},
}, nil
}
// parent_peer = monoforum:返回该频道私信的订阅者子会话列表(管理员视角)。
return r.monoforumSavedDialogs(ctx, userID, mono, req.Limit, offsetID)
}
if req.Limit < 0 {
return nil, limitInvalidErr()
}
offsetID := req.OffsetID
if offsetID < 0 {
offsetID = 0
}
offsetDate := req.OffsetDate
if offsetDate < 0 {
offsetDate = 0
}
filter := domain.SavedDialogsFilter{
ExcludePinned: req.ExcludePinned,
OffsetID: offsetID,
OffsetDate: offsetDate,
Limit: req.Limit,
}
if req.OffsetPeer != nil {
filter.OffsetPeer, _ = r.domainPeerFromInputPeer(userID, req.OffsetPeer)
}
if r.deps.Messages == nil {
if req.Hash != 0 {
return &tg.MessagesSavedDialogsNotModified{Count: 0}, nil
}
return &tg.MessagesSavedDialogs{
Dialogs: []tg.SavedDialogClass{},
Messages: []tg.MessageClass{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
list, err := r.deps.Messages.GetSavedDialogs(ctx, userID, filter)
if err != nil {
return nil, internalErr()
}
if req.Hash != 0 && savedDialogsHash(list) == req.Hash {
return &tg.MessagesSavedDialogsNotModified{Count: list.Count}, nil
}
return r.tgMessagesSavedDialogs(ctx, userID, list), nil
}
func (r *Router) onMessagesGetPinnedSavedDialogs(ctx context.Context) (tg.MessagesSavedDialogsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if r.deps.Messages == nil {
return &tg.MessagesSavedDialogs{
Dialogs: []tg.SavedDialogClass{},
Messages: []tg.MessageClass{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
list, err := r.deps.Messages.GetPinnedSavedDialogs(ctx, userID)
if err != nil {
return nil, internalErr()
}
return r.tgMessagesSavedDialogs(ctx, userID, list), nil
}
func (r *Router) onMessagesGetSavedDialogsByID(ctx context.Context, req *tg.MessagesGetSavedDialogsByIDRequest) (tg.MessagesSavedDialogsClass, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, hasParent, err := r.validateSavedHistoryParentPeer(ctx, userID, req.GetParentPeer); err != nil {
return nil, err
} else if hasParent {
return &tg.MessagesSavedDialogs{
Dialogs: []tg.SavedDialogClass{},
Messages: []tg.MessageClass{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
if len(req.IDs) > maxDialogInputPeers {
return nil, limitInvalidErr()
}
peers := make([]domain.Peer, 0, len(req.IDs))
for _, input := range req.IDs {
// 未知/已删除 peer 静默缺席(客户端用本接口补 stale 数据,
// 部分命中是正常结果);显式非法构造仍拒绝。
peer, ok := r.domainPeerFromInputPeer(userID, input)
if !ok {
return nil, peerIDInvalidErr()
}
peers = append(peers, peer)
}
if r.deps.Messages == nil {
return &tg.MessagesSavedDialogs{
Dialogs: []tg.SavedDialogClass{},
Messages: []tg.MessageClass{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
list, err := r.deps.Messages.GetSavedDialogsByPeers(ctx, userID, peers)
if err != nil {
return nil, internalErr()
}
return r.tgMessagesSavedDialogs(ctx, userID, list), nil
}
func (r *Router) onMessagesToggleSavedDialogPin(ctx context.Context, req *tg.MessagesToggleSavedDialogPinRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
peers, err := r.dialogPeersFromInput(ctx, userID, []tg.InputDialogPeerClass{req.Peer})
if err != nil {
return false, err
}
if len(peers) != 1 {
return false, peerIDInvalidErr()
}
pinned := req.GetPinned()
if r.deps.Messages == nil {
return true, nil
}
changed, err := r.deps.Messages.ToggleSavedDialogPin(ctx, userID, peers[0], pinned)
if err != nil {
if errors.Is(err, domain.ErrPinnedSavedDialogsTooMuch) {
return false, savedPinnedTooMuchErr()
}
return false, internalErr()
}
if changed {
date := int(r.clock.Now().Unix())
var recorded domain.UpdateEvent
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
event, state, err := r.deps.Updates.RecordSavedDialogPinned(ctx, authKeyID, userID, peers[0], pinned, sessionID)
if err != nil {
return false, internalErr()
}
date = state.Date
recorded = event
}
r.bookkeepAuxPtsForCurrentSession(ctx, recorded)
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: appendAuxPtsBookkeeping([]tg.UpdateClass{&tg.UpdateSavedDialogPinned{
Pinned: pinned,
Peer: tgDialogPeer(peers[0]),
}}, recorded),
Date: date,
Seq: 0,
})
}
return true, nil
}
func (r *Router) onMessagesReorderPinnedSavedDialogs(ctx context.Context, req *tg.MessagesReorderPinnedSavedDialogsRequest) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if len(req.Order) > maxDialogInputPeers {
return false, limitInvalidErr()
}
peers, err := r.dialogPeersFromInput(ctx, userID, req.Order)
if err != nil {
return false, err
}
if r.deps.Messages == nil {
return true, nil
}
if err := r.deps.Messages.ReorderPinnedSavedDialogs(ctx, userID, peers, req.GetForce()); err != nil {
if errors.Is(err, domain.ErrPinnedSavedDialogsTooMuch) {
return false, savedPinnedTooMuchErr()
}
return false, internalErr()
}
date := int(r.clock.Now().Unix())
var recorded domain.UpdateEvent
if r.deps.Updates != nil {
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
event, state, err := r.deps.Updates.RecordPinnedSavedDialogs(ctx, authKeyID, userID, peers, sessionID)
if err != nil {
return false, internalErr()
}
date = state.Date
recorded = event
}
r.bookkeepAuxPtsForCurrentSession(ctx, recorded)
update := &tg.UpdatePinnedSavedDialogs{}
if len(peers) > 0 {
update.SetOrder(tgDialogPeers(peers))
}
r.pushUserUpdatesIfNoReliableDispatch(ctx, userID, &tg.Updates{
Updates: appendAuxPtsBookkeeping([]tg.UpdateClass{update}, recorded),
Date: date,
Seq: 0,
})
return true, nil
}
// tgMessagesSavedDialogs 把业务结果转 TL 输出Full 映射 savedDialogs已到
// 末尾),否则 savedDialogsSlice + 总数。
func (r *Router) tgMessagesSavedDialogs(ctx context.Context, userID int64, list domain.SavedDialogList) tg.MessagesSavedDialogsClass {
dialogs := make([]tg.SavedDialogClass, 0, len(list.Dialogs))
for _, d := range list.Dialogs {
peer := tgPeer(d.Peer)
if peer == nil {
continue
}
sd := &tg.SavedDialog{Peer: peer, TopMessage: d.TopMessage}
if d.Pinned {
sd.SetPinned(true)
}
dialogs = append(dialogs, sd)
}
messages := make([]tg.MessageClass, 0, len(list.Messages))
for _, msg := range list.Messages {
if item := tgMessage(msg); item != nil {
messages = append(messages, item)
}
}
users, chats := r.savedDialogsProjection(ctx, userID, list)
if list.Full {
return &tg.MessagesSavedDialogs{
Dialogs: dialogs,
Messages: messages,
Chats: chats,
Users: users,
}
}
return &tg.MessagesSavedDialogsSlice{
Count: list.Count,
Dialogs: dialogs,
Messages: messages,
Chats: chats,
Users: users,
}
}
// savedDialogsProjection 投影子会话引用到的 users/chatsself 恒带self-chat
// 上下文 + My Notes 子会话saved peer / fwd 作者 / 跳回源会话批量补齐,
// hidden author 占位用户在库中不存在、按需合成。
func (r *Router) savedDialogsProjection(ctx context.Context, userID int64, list domain.SavedDialogList) ([]tg.UserClass, []tg.ChatClass) {
userIDs := make([]int64, 0, len(list.Dialogs))
channelIDs := make([]int64, 0)
seenUsers := map[int64]struct{}{}
seenChannels := map[int64]struct{}{}
needHidden := false
addPeer := func(p domain.Peer) {
switch p.Type {
case domain.PeerTypeUser:
if p.ID == 0 || p.ID == userID {
return
}
if p.ID == domain.SavedHiddenAuthorUserID {
needHidden = true
return
}
if _, ok := seenUsers[p.ID]; ok {
return
}
seenUsers[p.ID] = struct{}{}
userIDs = append(userIDs, p.ID)
case domain.PeerTypeChannel:
if p.ID == 0 {
return
}
if _, ok := seenChannels[p.ID]; ok {
return
}
seenChannels[p.ID] = struct{}{}
channelIDs = append(channelIDs, p.ID)
}
}
for _, d := range list.Dialogs {
addPeer(d.Peer)
}
for _, msg := range list.Messages {
if msg.Forward != nil {
addPeer(msg.Forward.From)
addPeer(msg.Forward.SavedFrom)
}
}
users := make([]tg.UserClass, 0, len(userIDs)+2)
if r.deps.Users != nil {
if self, err := r.deps.Users.Self(ctx, userID); err == nil && self.ID != 0 {
users = append(users, r.tgSelfUser(self))
}
if len(userIDs) > 0 {
if found, err := r.deps.Users.ByIDs(ctx, userID, userIDs); err == nil {
users = append(users, r.tgUsers(found)...)
}
}
}
if needHidden {
users = append(users, tgSavedHiddenAuthorUser())
}
chats := make([]tg.ChatClass, 0, len(channelIDs))
if r.deps.Channels != nil && len(channelIDs) > 0 {
if views, err := r.deps.Channels.GetChannels(ctx, userID, channelIDs); err == nil {
for _, view := range views {
if view.Channel.ID == 0 {
continue
}
chats = append(chats, tgChannelChatForView(userID, view))
}
}
}
return users, chats
}
// tgSavedHiddenAuthorUser 合成 hidden-author 占位用户(官方 user 2666000
// TDesktop/Android 都按 id 特判本地展示,名字仅作兜底。
func tgSavedHiddenAuthorUser() *tg.User {
return &tg.User{ID: domain.SavedHiddenAuthorUserID, FirstName: "Anonymous"}
}
// savedDialogsHash 与 DrKLO Android SavedMessagesController 的请求 hash 算法
// 对齐:对返回序列逐项喂 pinned/abs(peerId)/top_message_id/top dateTDesktop
// hash 恒 0不触发 notModified。
func savedDialogsHash(list domain.SavedDialogList) int64 {
dateByID := make(map[int]int, len(list.Messages))
for _, msg := range list.Messages {
dateByID[msg.ID] = msg.Date
}
var hash uint64
for _, d := range list.Dialogs {
pinned := int64(0)
if d.Pinned {
pinned = 1
}
id := d.Peer.ID
if id < 0 {
id = -id
}
hash = tdesktopHashUpdate(hash, pinned)
hash = tdesktopHashUpdate(hash, id)
hash = tdesktopHashUpdate(hash, int64(d.TopMessage))
hash = tdesktopHashUpdate(hash, int64(dateByID[d.TopMessage]))
}
return int64(hash)
}