chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,375 @@
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 || req.OffsetID < 0 {
return nil, limitInvalidErr()
}
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, req.OffsetID)
}
if req.Limit < 0 || req.OffsetID < 0 || req.OffsetDate < 0 {
return nil, limitInvalidErr()
}
filter := domain.SavedDialogsFilter{
ExcludePinned: req.ExcludePinned,
OffsetID: req.OffsetID,
OffsetDate: req.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/chats:self 恒带(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 date;TDesktop
// 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)
}