owpengram-server/internal/rpc/convert_updates.go
2026-07-24 11:57:00 +08:00

618 lines
20 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 (
"github.com/iamxvbaba/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.UpdateEventUserPhone:
if event.UserID == 0 || event.Phone == "" {
return nil
}
return &tg.UpdateUserPhone{UserID: event.UserID, Phone: event.Phone}
case domain.UpdateEventUserEmojiStatus:
if event.UserID == 0 || !event.EmojiStatus.Valid() {
return nil
}
return &tg.UpdateUserEmojiStatus{UserID: event.UserID, EmojiStatus: tgUserEmojiStatusValue(event.EmojiStatus)}
case domain.UpdateEventUserProfile:
if event.Peer.Type != domain.PeerTypeUser || event.Peer.ID == 0 {
return nil
}
return &tg.UpdateUser{UserID: event.Peer.ID}
case domain.UpdateEventPrivacy:
if event.Privacy.OwnerUserID == 0 || event.Privacy.Key == "" || len(event.Privacy.Rules) == 0 {
return nil
}
return &tg.UpdatePrivacy{
Key: tgPrivacyKey(event.Privacy.Key),
Rules: tgPrivacyRules(event.Privacy.Rules),
}
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),
}
}