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,595 @@
package rpc
import (
"github.com/gotd/td/tg"
"telesrv/internal/domain"
)
func tgUpdateState(st domain.UpdateState) tg.UpdatesState {
return tg.UpdatesState{Pts: st.Pts, Qts: st.Qts, Date: st.Date, Seq: st.Seq}
}
func tgUpdatesDifference(viewerUserID int64, diff domain.UpdateDifference) tg.UpdatesDifferenceClass {
out := &tg.UpdatesDifference{
NewMessages: make([]tg.MessageClass, 0, len(diff.Events)),
OtherUpdates: make([]tg.UpdateClass, 0, len(diff.Events)),
Users: make([]tg.UserClass, 0, 1),
Chats: make([]tg.ChatClass, 0, 1),
}
seenUsers := make(map[int64]struct{})
seenChats := make(map[int64]struct{})
for _, event := range diff.Events {
addUsers(out, seenUsers, viewerUserID, event.Users)
addChannels(out, seenChats, event.UserID, event.Channels)
switch event.Type {
case domain.UpdateEventNewMessage:
if msg := tgMessage(event.Message); msg != nil {
out.NewMessages = append(out.NewMessages, msg)
addMessageUsers(out, seenUsers, event.Message)
}
case domain.UpdateEventReadHistoryInbox:
if update := tgReadHistoryInboxUpdate(event); update != nil {
out.OtherUpdates = append(out.OtherUpdates, update)
}
case domain.UpdateEventReadHistoryOutbox:
if update := tgReadHistoryOutboxUpdate(event); update != nil {
out.OtherUpdates = append(out.OtherUpdates, update)
}
case domain.UpdateEventMessageReactions, domain.UpdateEventMessagePoll:
// 同时下发消息快照(含最新聚合)与对应通知 update事件无 TL pts
// pts 推进靠 difference state 本身。
if msg := tgMessage(event.Message); msg != nil {
out.NewMessages = append(out.NewMessages, msg)
addMessageUsers(out, seenUsers, event.Message)
}
if update := tgOtherUpdateFromEvent(event); update != nil {
out.OtherUpdates = append(out.OtherUpdates, update)
}
default:
if update := tgOtherUpdateFromEvent(event); update != nil {
out.OtherUpdates = append(out.OtherUpdates, update)
}
}
}
for _, nudge := range diff.ChannelNudges {
if nudge.ChannelID == 0 {
continue
}
update := &tg.UpdateChannelTooLong{ChannelID: nudge.ChannelID}
if nudge.Pts > 0 {
update.SetPts(nudge.Pts)
}
out.OtherUpdates = append(out.OtherUpdates, update)
if nudge.Channel != nil && nudge.Channel.Channel.ID != 0 {
addChannelNudgeChat(out, seenChats, tgChannelChatForView(viewerUserID, *nudge.Channel))
}
}
// Partial连续事件被 limit 截断、后面还有 → updates.differenceSlice客户端据 IntermediateState 续拉。
if diff.Partial {
return &tg.UpdatesDifferenceSlice{
NewMessages: out.NewMessages,
OtherUpdates: out.OtherUpdates,
Chats: out.Chats,
Users: out.Users,
IntermediateState: tgUpdateState(diff.State),
}
}
out.State = tgUpdateState(diff.State)
return out
}
func tgChannelDifference(viewerUserID int64, diff domain.ChannelDifference) tg.UpdatesChannelDifferenceClass {
if diff.TooLong {
messages := make([]tg.MessageClass, 0, len(diff.NewMessages))
for _, msg := range diff.NewMessages {
if item := tgChannelMessage(viewerUserID, msg); item != nil {
messages = append(messages, item)
}
}
dialog := tgDialog(domain.Dialog{
Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: diff.Channel.ID},
TopMessage: diff.Dialog.TopMessageID,
TopMessageDate: diff.Dialog.TopMessageDate,
ReadInboxMaxID: diff.Dialog.ReadInboxMaxID,
ReadOutboxMaxID: diff.Dialog.ReadOutboxMaxID,
UnreadCount: diff.Dialog.UnreadCount,
UnreadMentions: diff.Dialog.UnreadMentions,
UnreadReactions: diff.Dialog.UnreadReactions,
UnreadMark: diff.Dialog.UnreadMark,
FolderID: diff.Dialog.FolderID,
})
if dialog != nil {
dialog.SetPts(diff.Pts)
}
return &tg.UpdatesChannelDifferenceTooLong{
Final: diff.Final,
Timeout: diff.Timeout,
Dialog: dialog,
Messages: messages,
Chats: tgChannelDifferenceChats(viewerUserID, diff),
Users: tgUsersForViewer(viewerUserID, diff.Users),
}
}
if len(diff.Events) == 0 && len(diff.NewMessages) == 0 && len(diff.OtherUpdates) == 0 {
return &tg.UpdatesChannelDifferenceEmpty{
Final: diff.Final,
Pts: diff.Pts,
Timeout: diff.Timeout,
}
}
messages := make([]tg.MessageClass, 0, len(diff.NewMessages))
for _, msg := range diff.NewMessages {
if item := tgChannelMessage(viewerUserID, msg); item != nil {
messages = append(messages, item)
}
}
updates := make([]tg.UpdateClass, 0, len(diff.OtherUpdates))
// 断线后经差量首次看到自己消息时updateMessageID 让客户端把本地
// pending 按 random_id 对账,避免短暂的重复气泡。
for _, msg := range diff.NewMessages {
if msg.SenderUserID == viewerUserID && viewerUserID != 0 && msg.RandomID != 0 && msg.ID > 0 {
updates = append(updates, &tg.UpdateMessageID{ID: msg.ID, RandomID: msg.RandomID})
}
}
for _, event := range diff.OtherUpdates {
if update := tgChannelUpdate(viewerUserID, event); update != nil {
updates = append(updates, update)
}
}
chats := tgChannelDifferenceChats(viewerUserID, diff)
users := tgUsersForViewer(viewerUserID, diff.Users)
return &tg.UpdatesChannelDifference{
Final: diff.Final,
Pts: diff.Pts,
Timeout: diff.Timeout,
NewMessages: messages,
OtherUpdates: updates,
Chats: chats,
Users: users,
}
}
func tgChannelDifferenceChats(viewerUserID int64, diff domain.ChannelDifference) []tg.ChatClass {
return tgChannelChatsWithPrimarySelf(viewerUserID, diff.Channel, diff.Channels, diff.Self)
}
func tgChannelUpdate(viewerUserID int64, event domain.ChannelUpdateEvent) tg.UpdateClass {
switch event.Type {
case domain.ChannelUpdateNewMessage:
msg := tgChannelMessage(viewerUserID, event.Message)
if msg == nil {
return nil
}
return &tg.UpdateNewChannelMessage{Message: msg, Pts: event.Pts, PtsCount: event.PtsCount}
case domain.ChannelUpdateEditMessage:
msg := tgChannelMessage(viewerUserID, event.Message)
if msg == nil {
return nil
}
return &tg.UpdateEditChannelMessage{Message: msg, Pts: event.Pts, PtsCount: event.PtsCount}
case domain.ChannelUpdateWebPage:
// 频道链接预览就地替换updateChannelWebPage 仅携已解析 webPage按 id 关联消息里的
// pending 占位),客户端就地换卡片、不触碰 edit_date。
if event.Message.Media == nil || event.Message.Media.WebPage == nil {
return nil
}
return &tg.UpdateChannelWebPage{
ChannelID: event.ChannelID,
Webpage: tgWebPage(*event.Message.Media.WebPage),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.ChannelUpdateDeleteMessages:
if len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdateDeleteChannelMessages{
ChannelID: event.ChannelID,
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.ChannelUpdatePinnedMessages:
if len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdatePinnedChannelMessages{
Pinned: event.Pinned,
ChannelID: event.ChannelID,
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.ChannelUpdateParticipant:
userID := event.Participant.UserID
if userID == 0 {
userID = event.Previous.UserID
}
if userID == 0 {
return nil
}
actorID := event.SenderUserID
if actorID == 0 {
actorID = userID
}
update := &tg.UpdateChannelParticipant{
ChannelID: event.ChannelID,
Date: event.Date,
ActorID: actorID,
UserID: userID,
}
if event.Previous.UserID != 0 {
update.SetPrevParticipant(tgChannelParticipantForUpdate(viewerUserID, event.Previous))
}
if event.Participant.UserID != 0 {
update.SetNewParticipant(tgChannelParticipantForUpdate(viewerUserID, event.Participant))
}
return update
default:
return nil
}
}
func tgOtherUpdateFromEvent(event domain.UpdateEvent) tg.UpdateClass {
switch event.Type {
case domain.UpdateEventChannelState:
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 {
return nil
}
return &tg.UpdateChannel{ChannelID: event.Peer.ID}
case domain.UpdateEventStory:
peer := tgPeer(event.Peer)
if peer == nil {
peer = tgPeer(event.Story.Owner)
}
if peer == nil || event.Story.ID == 0 {
return nil
}
return &tg.UpdateStory{Peer: peer, Story: tgStoryItem(event.Story)}
case domain.UpdateEventReadStories:
peer := tgPeer(event.Peer)
if peer == nil || event.MaxID <= 0 {
return nil
}
return &tg.UpdateReadStories{Peer: peer, MaxID: event.MaxID}
case domain.UpdateEventSentStoryReaction:
peer := tgPeer(event.Peer)
if peer == nil || event.MaxID <= 0 {
return nil
}
reaction := tg.ReactionClass(&tg.ReactionEmpty{})
if event.Reaction != nil {
reaction = tgMessageReaction(*event.Reaction)
}
return &tg.UpdateSentStoryReaction{Peer: peer, StoryID: event.MaxID, Reaction: reaction}
case domain.UpdateEventNewStoryReaction:
peer := tgPeer(event.Peer)
if peer == nil || event.MaxID <= 0 || event.Reaction == nil {
return nil
}
return &tg.UpdateNewStoryReaction{Peer: peer, StoryID: event.MaxID, Reaction: tgMessageReaction(*event.Reaction)}
case domain.UpdateEventQuickReplies:
return &tg.UpdateQuickReplies{QuickReplies: tgQuickReplies(event.QuickReplies)}
case domain.UpdateEventNewQuickReply:
return &tg.UpdateNewQuickReply{QuickReply: tgQuickReply(event.QuickReply)}
case domain.UpdateEventDeleteQuickReply:
if event.MaxID <= 0 {
return nil
}
return &tg.UpdateDeleteQuickReply{ShortcutID: event.MaxID}
case domain.UpdateEventQuickReplyMessage:
msg := tgQuickReplyMessage(event.QuickReplyMessage)
if msg == nil {
return nil
}
return &tg.UpdateQuickReplyMessage{Message: msg}
case domain.UpdateEventDeleteQuickReplyMessages:
if event.MaxID <= 0 || len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdateDeleteQuickReplyMessages{
ShortcutID: event.MaxID,
Messages: append([]int(nil), event.MessageIDs...),
}
case domain.UpdateEventContactsReset:
return &tg.UpdateContactsReset{}
case domain.UpdateEventDialogPinned:
// archive folder 行自身的置顶peer 是 dialogPeerFolder 且绝不能带
// folder_id flagTDesktop Folder::applyPinnedUpdate 视其为
// "Nested folders" 错误)。
if event.Peer.Type == domain.PeerTypeFolder {
if event.Peer.ID == 0 {
return nil
}
return &tg.UpdateDialogPinned{Pinned: event.Bool, Peer: &tg.DialogPeerFolder{FolderID: int(event.Peer.ID)}}
}
peer := tgDialogPeer(event.Peer)
if peer == nil {
return nil
}
// FolderID 零值不编码 flagEncodeBare 自动 SetFlags归档内置顶
// 必须带 folder_id=1否则离线重放会把它应用到主列表。
return &tg.UpdateDialogPinned{Pinned: event.Bool, Peer: peer, FolderID: event.FolderID}
case domain.UpdateEventPinnedDialogs:
update := &tg.UpdatePinnedDialogs{FolderID: event.FolderID}
if len(event.Peers) > 0 {
update.Order = tgDialogPeers(event.Peers)
}
return update
case domain.UpdateEventSavedDialogPinned:
peer := tgDialogPeer(event.Peer)
if peer == nil {
return nil
}
return &tg.UpdateSavedDialogPinned{Pinned: event.Bool, Peer: peer}
case domain.UpdateEventPinnedSavedDialogs:
update := &tg.UpdatePinnedSavedDialogs{}
if len(event.Peers) > 0 {
update.SetOrder(tgDialogPeers(event.Peers))
}
return update
case domain.UpdateEventDialogUnreadMark:
peer := tgDialogPeer(event.Peer)
if peer == nil {
return nil
}
return &tg.UpdateDialogUnreadMark{Unread: event.Bool, Peer: peer}
case domain.UpdateEventDraftMessage:
peer := tgPeer(event.Peer)
if peer == nil {
return nil
}
// Draft 由 enrichDraftMessageEvent 按当前权威态填充nil/空 = 草稿已删。
var draft tg.DraftMessageClass
if event.Draft != nil && !event.Draft.Empty() {
draft = tgDialogDraft(*event.Draft)
} else {
empty := &tg.DraftMessageEmpty{}
if event.Date > 0 {
empty.SetDate(event.Date)
}
draft = empty
}
update := &tg.UpdateDraftMessage{Peer: peer, Draft: draft}
if event.MaxID > 0 {
update.SetTopMsgID(event.MaxID)
}
return update
case domain.UpdateEventChannelViewForum:
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 {
return nil
}
return &tg.UpdateChannelViewForumAsMessages{ChannelID: event.Peer.ID, Enabled: event.Bool}
case domain.UpdateEventReadChannelDiscussionInbox:
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 {
return nil
}
return &tg.UpdateReadChannelDiscussionInbox{ChannelID: event.Peer.ID, TopMsgID: event.TopMsgID, ReadMaxID: event.MaxID}
case domain.UpdateEventReadChannelDiscussionOutbox:
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 {
return nil
}
return &tg.UpdateReadChannelDiscussionOutbox{ChannelID: event.Peer.ID, TopMsgID: event.TopMsgID, ReadMaxID: event.MaxID}
case domain.UpdateEventPeerSettings:
peer := tgPeer(event.Peer)
if peer == nil {
return nil
}
return &tg.UpdatePeerSettings{Peer: peer, Settings: tgPeerSettings(event.Settings)}
case domain.UpdateEventPeerStoryBlocked:
peer := tgPeer(event.Peer)
if peer == nil {
return nil
}
return &tg.UpdatePeerBlocked{PeerID: peer, Blocked: event.Bool, BlockedMyStoriesFrom: true}
case domain.UpdateEventDeleteMessages:
if len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdateDeleteMessages{
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventPinnedMessages:
peer := tgPeer(event.Peer)
if peer == nil || len(event.MessageIDs) == 0 {
return nil
}
return &tg.UpdatePinnedMessages{
Pinned: event.Bool,
Peer: peer,
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventReadHistoryOutbox:
return tgReadHistoryOutbox(event)
case domain.UpdateEventReadMessageContents:
if len(event.MessageIDs) == 0 {
return nil
}
contents := &tg.UpdateReadMessagesContents{
Messages: append([]int(nil), event.MessageIDs...),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
if event.Date > 0 {
// date 是内容被读取的时刻:客户端用它调度 TTL 媒体的
// "自读取起算"删除与 sender 侧已听时间。
contents.SetDate(event.Date)
}
return contents
case domain.UpdateEventEditMessage:
msg := tgMessage(event.Message)
if msg == nil {
return nil
}
return &tg.UpdateEditMessage{
Message: msg,
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventWebPage:
// 链接预览就地替换updateWebPage 仅携带已解析 webPage按 webPage id 与消息里的
// pending 占位关联),客户端就地换卡片、不触碰 edit_date。
if event.Message.Media == nil || event.Message.Media.WebPage == nil {
return nil
}
return &tg.UpdateWebPage{
Webpage: tgWebPage(*event.Message.Media.WebPage),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventMessagePoll:
if event.Message.ID <= 0 || event.Message.ID > domain.MaxMessageBoxID {
return nil
}
pollPeer := event.Message.Peer
if pollPeer.Type == "" || pollPeer.ID == 0 {
pollPeer = event.Peer
}
media := event.Message.Media
if media == nil || media.Kind != domain.MessageMediaKindPoll || media.Poll == nil {
return nil
}
return tgUpdateMessagePoll(pollPeer, event.Message.ID, media.Poll)
case domain.UpdateEventMessageReactions:
if event.Message.ID <= 0 || event.Message.ID > domain.MaxMessageBoxID {
return nil
}
peer := event.Message.Peer
if peer.Type == "" || peer.ID == 0 {
peer = event.Peer
}
outPeer := tgPeer(peer)
if outPeer == nil {
return nil
}
reactions := event.Message.Reactions
if reactions == nil {
empty := domain.ChannelMessageReactions{CanSeeList: true, Results: []domain.ChannelMessageReactionCount{}, Recent: []domain.ChannelMessagePeerReaction{}}
reactions = &empty
}
converted := tgMessageReactions(event.UserID, reactions)
if converted == nil {
converted = &tg.MessageReactions{Results: []tg.ReactionCount{}}
}
return &tg.UpdateMessageReactions{
Peer: outPeer,
MsgID: event.Message.ID,
Reactions: *converted,
}
case domain.UpdateEventDialogFilter:
update := &tg.UpdateDialogFilter{ID: event.FilterID}
if event.DialogFilter != nil {
update.SetFilter(tgDialogFilter(*event.DialogFilter))
}
return update
case domain.UpdateEventDialogFilterOrder:
return &tg.UpdateDialogFilterOrder{Order: append([]int(nil), event.FilterOrder...)}
case domain.UpdateEventDialogFilters:
return &tg.UpdateDialogFilters{}
case domain.UpdateEventFolderPeers:
if len(event.FolderPeers) == 0 {
return nil
}
return &tg.UpdateFolderPeers{
FolderPeers: tgFolderPeers(event.FolderPeers),
Pts: event.Pts,
PtsCount: event.PtsCount,
}
case domain.UpdateEventChannelAvailable:
if event.Peer.Type != domain.PeerTypeChannel || event.Peer.ID == 0 || event.MaxID <= 0 {
return nil
}
return &tg.UpdateChannelAvailableMessages{
ChannelID: event.Peer.ID,
AvailableMinID: event.MaxID,
}
default:
return nil
}
}
func addChannels(out *tg.UpdatesDifference, seen map[int64]struct{}, viewerUserID int64, channels []domain.Channel) {
for _, ch := range channels {
if ch.ID == 0 {
continue
}
if _, ok := seen[ch.ID]; ok {
continue
}
seen[ch.ID] = struct{}{}
out.Chats = append(out.Chats, tgChannelChatMin(viewerUserID, ch))
}
}
func addChannelNudgeChat(out *tg.UpdatesDifference, seen map[int64]struct{}, chat tg.ChatClass) {
channelID, ok := channelChatID(chat)
if !ok || channelID == 0 {
return
}
if _, exists := seen[channelID]; exists {
upgradeMinChannelChat(out, channelID, chat)
return
}
seen[channelID] = struct{}{}
out.Chats = append(out.Chats, chat)
}
func upgradeMinChannelChat(out *tg.UpdatesDifference, channelID int64, candidate tg.ChatClass) {
next, ok := candidate.(*tg.Channel)
if !ok || next.Min {
return
}
for i, existing := range out.Chats {
existingID, ok := channelChatID(existing)
if !ok || existingID != channelID {
continue
}
if current, ok := existing.(*tg.Channel); ok && current.Min {
out.Chats[i] = candidate
}
return
}
}
func channelChatID(chat tg.ChatClass) (int64, bool) {
switch v := chat.(type) {
case *tg.Channel:
return v.ID, true
case *tg.ChannelForbidden:
return v.ID, true
default:
return 0, false
}
}
func tgChannelParticipantForUpdate(selfUserID int64, member domain.ChannelMember) tg.ChannelParticipantClass {
if member.UserID == 0 {
return nil
}
if member.Status == domain.ChannelMemberKicked || member.Status == domain.ChannelMemberBanned || member.BannedRights.ViewMessages {
return &tg.ChannelParticipantBanned{
Left: member.Status != domain.ChannelMemberActive,
Peer: &tg.PeerUser{UserID: member.UserID},
KickedBy: member.InviterUserID,
Date: member.LeftAt,
BannedRights: tgChatBannedRights(member.BannedRights),
}
}
if member.Status == domain.ChannelMemberLeft {
return &tg.ChannelParticipantLeft{Peer: &tg.PeerUser{UserID: member.UserID}}
}
return tgChannelParticipant(selfUserID, member)
}
func tgLangPackDifference(pack domain.LangPack) *tg.LangPackDifference {
return &tg.LangPackDifference{
LangCode: pack.LangCode,
FromVersion: pack.FromVersion,
Version: pack.Version,
Strings: tgLangPackStrings(pack.Strings),
}
}