339 lines
9.9 KiB
Go
339 lines
9.9 KiB
Go
package rpc
|
||
|
||
import (
|
||
"github.com/iamxvbaba/td/tg"
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
func tgMessagesDialogs(viewerUserID int64, list domain.DialogList) tg.MessagesDialogsClass {
|
||
dialogs := make([]tg.DialogClass, 0, len(list.Dialogs)+1)
|
||
// dialogFolder 条目排在最前:TDesktop 据它发现 archive folder 并渲染
|
||
// 归档行的未读徽章(Folder::applyDialog / MainList::updateCloudUnread)。
|
||
if folder := tgDialogFolder(list.ArchiveSummary); folder != nil {
|
||
dialogs = append(dialogs, folder)
|
||
}
|
||
for _, d := range list.Dialogs {
|
||
if dialog := tgDialog(d); dialog != nil {
|
||
dialogs = append(dialogs, dialog)
|
||
}
|
||
}
|
||
messages := make([]tg.MessageClass, 0, len(list.Messages))
|
||
for _, msg := range list.Messages {
|
||
if item := tgMessage(msg); item != nil {
|
||
messages = append(messages, item)
|
||
}
|
||
}
|
||
for _, msg := range list.ChannelMessages {
|
||
if item := tgChannelMessage(viewerUserID, msg); item != nil {
|
||
messages = append(messages, item)
|
||
}
|
||
}
|
||
users := tgUsersForViewer(viewerUserID, list.Users)
|
||
chats := tgChannelsForDialogs(viewerUserID, list.Channels, list.Dialogs)
|
||
if list.Count > len(dialogs) {
|
||
return &tg.MessagesDialogsSlice{
|
||
Count: list.Count,
|
||
Dialogs: dialogs,
|
||
Messages: messages,
|
||
Chats: chats,
|
||
Users: users,
|
||
}
|
||
}
|
||
return &tg.MessagesDialogs{
|
||
Dialogs: dialogs,
|
||
Messages: messages,
|
||
Chats: chats,
|
||
Users: users,
|
||
}
|
||
}
|
||
|
||
func tgPeerDialogs(viewerUserID int64, list domain.DialogList, st domain.UpdateState) *tg.MessagesPeerDialogs {
|
||
out := &tg.MessagesPeerDialogs{
|
||
Dialogs: make([]tg.DialogClass, 0, len(list.Dialogs)+1),
|
||
Messages: make([]tg.MessageClass, 0, len(list.Messages)+len(list.ChannelMessages)),
|
||
Users: make([]tg.UserClass, 0, len(list.Users)),
|
||
Chats: make([]tg.ChatClass, 0, len(list.Channels)),
|
||
State: tgUpdateState(st),
|
||
}
|
||
// getPinnedDialogs(folder_id=0) 必须带 dialogFolder 条目:DrKLO 主列表
|
||
// getDialogs 一律 exclude_pinned,archive 行只能从 pinned 响应发现
|
||
// (fetchFolderInLoadedPinnedDialogs,且要求 top_message/peer 非零)。
|
||
if folder := tgDialogFolder(list.ArchiveSummary); folder != nil {
|
||
out.Dialogs = append(out.Dialogs, folder)
|
||
}
|
||
for _, d := range list.Dialogs {
|
||
if dialog := tgDialog(d); dialog != nil {
|
||
out.Dialogs = append(out.Dialogs, dialog)
|
||
}
|
||
}
|
||
for _, msg := range list.Messages {
|
||
if item := tgMessage(msg); item != nil {
|
||
out.Messages = append(out.Messages, item)
|
||
}
|
||
}
|
||
for _, msg := range list.ChannelMessages {
|
||
if item := tgChannelMessage(viewerUserID, msg); item != nil {
|
||
out.Messages = append(out.Messages, item)
|
||
}
|
||
}
|
||
for _, u := range list.Users {
|
||
if viewerUserID != 0 && u.ID == viewerUserID {
|
||
out.Users = append(out.Users, tgSelfUser(u))
|
||
} else {
|
||
out.Users = append(out.Users, tgUser(u))
|
||
}
|
||
}
|
||
out.Chats = append(out.Chats, tgChannelsForDialogs(viewerUserID, list.Channels, list.Dialogs)...)
|
||
return out
|
||
}
|
||
|
||
// tgDialogFolder 把归档摘要转成 dialogFolder#71bd134c 条目。当前未接
|
||
// per-peer mute 状态,未读计数全部归入 unmuted 桶(TDesktop 用 unmuted
|
||
// 桶渲染亮色徽章,muted 桶渲染灰色,全归 unmuted 只影响徽章颜色不丢计数)。
|
||
func tgDialogFolder(summary *domain.DialogArchiveSummary) tg.DialogClass {
|
||
if summary == nil {
|
||
return nil
|
||
}
|
||
peer := tgPeer(summary.TopPeer)
|
||
if peer == nil {
|
||
return nil
|
||
}
|
||
return &tg.DialogFolder{
|
||
Pinned: summary.Pinned,
|
||
Folder: tg.Folder{
|
||
ID: domain.DialogArchiveFolderID,
|
||
Title: "Archived Chats",
|
||
},
|
||
Peer: peer,
|
||
TopMessage: summary.TopMessage,
|
||
UnreadUnmutedPeersCount: summary.UnreadPeersCount,
|
||
UnreadUnmutedMessagesCount: summary.UnreadMessagesCount,
|
||
}
|
||
}
|
||
|
||
func tgDialog(d domain.Dialog) *tg.Dialog {
|
||
peer := tgPeer(d.Peer)
|
||
if peer == nil {
|
||
return nil
|
||
}
|
||
out := &tg.Dialog{
|
||
Pinned: d.Pinned,
|
||
UnreadMark: d.UnreadMark,
|
||
ViewForumAsMessages: d.ViewForumAsMessages,
|
||
Peer: peer,
|
||
TopMessage: d.TopMessage,
|
||
ReadInboxMaxID: d.ReadInboxMaxID,
|
||
ReadOutboxMaxID: d.ReadOutboxMaxID,
|
||
UnreadCount: d.UnreadCount,
|
||
UnreadMentionsCount: d.UnreadMentions,
|
||
UnreadReactionsCount: d.UnreadReactions,
|
||
NotifySettings: *tgPeerNotifySettings(d.NotifySettings),
|
||
}
|
||
if d.FolderID != domain.DialogMainFolderID {
|
||
out.SetFolderID(d.FolderID)
|
||
}
|
||
if d.TTLPeriod > 0 {
|
||
out.SetTTLPeriod(d.TTLPeriod)
|
||
}
|
||
if d.Peer.Type == domain.PeerTypeChannel && d.Pts > 0 {
|
||
// 客户端用 dialog.pts 初始化 channel 本地序列;缺失会让
|
||
// getChannelDifference 起点失效(冷启动 gap 不被发现)。
|
||
out.SetPts(d.Pts)
|
||
}
|
||
if d.Draft != nil {
|
||
out.SetDraft(tgDialogDraft(*d.Draft))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgDialogDraft(d domain.DialogDraft) tg.DraftMessageClass {
|
||
if d.Empty() {
|
||
out := &tg.DraftMessageEmpty{}
|
||
out.SetDate(d.Date)
|
||
return out
|
||
}
|
||
out := &tg.DraftMessage{
|
||
NoWebpage: d.NoWebpage,
|
||
InvertMedia: d.InvertMedia,
|
||
ReplyTo: tgDraftReplyTo(d),
|
||
Message: d.Message,
|
||
Entities: tgMessageEntities(d.Entities),
|
||
Media: tgDraftWebPage(d.WebPage),
|
||
Date: d.Date,
|
||
Effect: d.Effect,
|
||
}
|
||
if rich := mustTGRichMessage(d.RichMessage); rich != nil {
|
||
out.SetRichMessage(*rich)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgDraftReplyTo(d domain.DialogDraft) tg.InputReplyToClass {
|
||
if d.ReplyTo == nil {
|
||
return nil
|
||
}
|
||
reply := &tg.InputReplyToMessage{ReplyToMsgID: d.ReplyTo.MessageID}
|
||
if d.ReplyTo.TopMessageID > 0 {
|
||
reply.SetTopMsgID(d.ReplyTo.TopMessageID)
|
||
}
|
||
if d.ReplyTo.QuoteText != "" {
|
||
reply.SetQuoteText(d.ReplyTo.QuoteText)
|
||
}
|
||
if len(d.ReplyTo.QuoteEntities) > 0 {
|
||
reply.SetQuoteEntities(tgMessageEntities(d.ReplyTo.QuoteEntities))
|
||
}
|
||
if d.ReplyTo.QuoteOffset > 0 {
|
||
reply.SetQuoteOffset(d.ReplyTo.QuoteOffset)
|
||
}
|
||
return reply
|
||
}
|
||
|
||
func tgDraftWebPage(webpage *domain.DialogDraftWebPage) tg.InputMediaClass {
|
||
if webpage == nil || webpage.URL == "" {
|
||
return nil
|
||
}
|
||
return &tg.InputMediaWebPage{
|
||
ForceLargeMedia: webpage.ForceLargeMedia,
|
||
ForceSmallMedia: webpage.ForceSmallMedia,
|
||
Optional: webpage.Optional,
|
||
URL: webpage.URL,
|
||
}
|
||
}
|
||
|
||
func tgDialogPeer(p domain.Peer) tg.DialogPeerClass {
|
||
peer := tgPeer(p)
|
||
if peer == nil {
|
||
return nil
|
||
}
|
||
return &tg.DialogPeer{Peer: peer}
|
||
}
|
||
|
||
func tgDialogPeers(peers []domain.Peer) []tg.DialogPeerClass {
|
||
out := make([]tg.DialogPeerClass, 0, len(peers))
|
||
for _, peer := range peers {
|
||
item := tgDialogPeer(peer)
|
||
if item != nil {
|
||
out = append(out, item)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgDialogFilters(list domain.DialogFolderList) *tg.MessagesDialogFilters {
|
||
filters := make([]tg.DialogFilterClass, 0, len(list.Folders)+1)
|
||
filters = append(filters, &tg.DialogFilterDefault{})
|
||
for _, folder := range list.Folders {
|
||
if item := tgDialogFilter(folder); item != nil {
|
||
filters = append(filters, item)
|
||
}
|
||
}
|
||
return &tg.MessagesDialogFilters{TagsEnabled: list.TagsEnabled, Filters: filters}
|
||
}
|
||
|
||
func tgDialogFilter(folder domain.DialogFolder) tg.DialogFilterClass {
|
||
title := tg.TextWithEntities{Text: folder.Title, Entities: tgMessageEntities(folder.TitleEntities)}
|
||
if folder.IsChatlist {
|
||
out := &tg.DialogFilterChatlist{
|
||
HasMyInvites: folder.HasMyInvites,
|
||
TitleNoanimate: folder.TitleNoanimate,
|
||
ID: folder.ID,
|
||
Title: title,
|
||
PinnedPeers: tgDialogFolderInputPeers(folder.PinnedPeers),
|
||
IncludePeers: tgDialogFolderInputPeers(folder.IncludePeers),
|
||
}
|
||
if folder.HasEmoticon {
|
||
out.SetEmoticon(folder.Emoticon)
|
||
}
|
||
if folder.HasColor {
|
||
out.SetColor(folder.Color)
|
||
}
|
||
return out
|
||
}
|
||
out := &tg.DialogFilter{
|
||
Contacts: folder.Contacts,
|
||
NonContacts: folder.NonContacts,
|
||
Groups: folder.Groups,
|
||
Broadcasts: folder.Broadcasts,
|
||
Bots: folder.Bots,
|
||
ExcludeMuted: folder.ExcludeMuted,
|
||
ExcludeRead: folder.ExcludeRead,
|
||
ExcludeArchived: folder.ExcludeArchived,
|
||
TitleNoanimate: folder.TitleNoanimate,
|
||
ID: folder.ID,
|
||
Title: title,
|
||
PinnedPeers: tgDialogFolderInputPeers(folder.PinnedPeers),
|
||
IncludePeers: tgDialogFolderInputPeers(folder.IncludePeers),
|
||
ExcludePeers: tgDialogFolderInputPeers(folder.ExcludePeers),
|
||
}
|
||
if folder.HasEmoticon {
|
||
out.SetEmoticon(folder.Emoticon)
|
||
}
|
||
if folder.HasColor {
|
||
out.SetColor(folder.Color)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgDialogFolderInputPeers(peers []domain.DialogFolderPeer) []tg.InputPeerClass {
|
||
if len(peers) == 0 {
|
||
return nil
|
||
}
|
||
out := make([]tg.InputPeerClass, 0, len(peers))
|
||
for _, item := range peers {
|
||
switch item.Peer.Type {
|
||
case domain.PeerTypeUser:
|
||
out = append(out, &tg.InputPeerUser{UserID: item.Peer.ID, AccessHash: item.AccessHash})
|
||
case domain.PeerTypeChannel:
|
||
out = append(out, &tg.InputPeerChannel{ChannelID: item.Peer.ID, AccessHash: item.AccessHash})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgFolderPeers(peers []domain.FolderPeerUpdate) []tg.FolderPeer {
|
||
out := make([]tg.FolderPeer, 0, len(peers))
|
||
for _, item := range peers {
|
||
peer := tgPeer(item.Peer)
|
||
if peer == nil {
|
||
continue
|
||
}
|
||
out = append(out, tg.FolderPeer{Peer: peer, FolderID: item.FolderID})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgChannelsForDialogs(viewerUserID int64, channels []domain.Channel, dialogs []domain.Dialog) []tg.ChatClass {
|
||
left := make(map[int64]struct{})
|
||
for _, dialog := range dialogs {
|
||
if dialog.Peer.Type == domain.PeerTypeChannel && dialog.Peer.ID != 0 && dialog.ChannelLeft {
|
||
left[dialog.Peer.ID] = struct{}{}
|
||
}
|
||
}
|
||
out := make([]tg.ChatClass, 0, len(channels))
|
||
seen := make(map[int64]struct{}, len(channels))
|
||
for _, ch := range channels {
|
||
if ch.ID == 0 {
|
||
continue
|
||
}
|
||
if _, ok := seen[ch.ID]; ok {
|
||
continue
|
||
}
|
||
seen[ch.ID] = struct{}{}
|
||
var self *domain.ChannelMember
|
||
if _, ok := left[ch.ID]; ok {
|
||
self = &domain.ChannelMember{
|
||
ChannelID: ch.ID,
|
||
UserID: viewerUserID,
|
||
Status: domain.ChannelMemberLeft,
|
||
}
|
||
} else {
|
||
self = &domain.ChannelMember{
|
||
ChannelID: ch.ID,
|
||
UserID: viewerUserID,
|
||
Status: domain.ChannelMemberActive,
|
||
}
|
||
}
|
||
out = append(out, tgChannelChat(viewerUserID, ch, self))
|
||
}
|
||
return out
|
||
}
|