package rpc import ( "github.com/iamxvbaba/td/tg" "telesrv/internal/compat/tdesktop" "telesrv/internal/domain" ) const defaultChatBannedRightsUntilDate = 2147483647 func tgChannelChatsWithPrimarySelf(viewerUserID int64, primary domain.Channel, extras []domain.Channel, primarySelf domain.ChannelMember) []tg.ChatClass { channels := make([]domain.Channel, 0, len(extras)+1) channels = append(channels, primary) channels = append(channels, extras...) 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{}{} if ch.ID != primary.ID { // Companion channels have no matching viewer membership in // ChannelHistory. Keep them minimal so they cannot overwrite the // client's cached left/creator/admin/banned state. out = append(out, tgChannelChatMin(viewerUserID, ch)) continue } var self *domain.ChannelMember if primarySelf.ChannelID == ch.ID && primarySelf.UserID == viewerUserID { self = &primarySelf } out = append(out, tgChannelChat(viewerUserID, ch, self)) } return out } func tgChannelHistoryMessages(viewerUserID int64, list domain.ChannelHistory) tg.MessagesMessagesClass { messages := make([]tg.MessageClass, 0, len(list.Messages)) for _, msg := range list.Messages { if item := tgChannelMessage(viewerUserID, msg); item != nil { messages = append(messages, item) } } topics := make([]tg.ForumTopicClass, 0, len(list.Topics)) for _, topic := range list.Topics { item := tgForumTopicFromDomain(viewerUserID, topic) item.SetShort(true) topics = append(topics, item) } // viewer 自己发过言时会出现在 history.Users 里;必须经 viewer 分支打 self 标志, // 否则 DrKLO putUsers 会用 self=false 覆盖账号缓存,Saved Messages 退化为普通自聊。 users := tgUsersForViewer(viewerUserID, list.Users) chats := tgChannelChatsWithPrimarySelf(viewerUserID, list.Channel, list.Channels, list.Self) if list.Channel.ID != 0 { return &tg.MessagesChannelMessages{ Pts: list.Channel.Pts, Count: list.Count, Messages: messages, Topics: topics, Chats: chats, Users: users, } } if list.Count > len(messages) { return &tg.MessagesMessagesSlice{ Count: list.Count, Messages: messages, Topics: topics, Chats: chats, Users: users, } } return &tg.MessagesMessages{Messages: messages, Topics: topics, Chats: chats, Users: users} } func tgChannelMessage(viewerUserID int64, m domain.ChannelMessage) tg.MessageClass { if m.ID == 0 || m.ChannelID == 0 { return nil } peer := &tg.PeerChannel{ChannelID: m.ChannelID} outgoing := m.SenderUserID == viewerUserID && viewerUserID != 0 && (m.SavedPeer.ID != 0 || m.From.Type != domain.PeerTypeChannel) from := tg.PeerClass(nil) if !m.Post && m.SendAs != nil && m.SendAs.ID != 0 { from = tgPeer(*m.SendAs) } if !m.Post && from == nil && m.From.ID != 0 { from = tgPeer(m.From) } if from == nil && m.SenderUserID != 0 && !m.Post { from = &tg.PeerUser{UserID: m.SenderUserID} } if m.Action != nil && m.Action.Type == domain.ChannelActionCreate { // messageActionChannelCreate 是频道级「创建」事件,不带发送者(对齐官方)。否则在 monoforum // 管理视图里,这条 from=管理员 的创建消息会被客户端按发送者归成一个虚假的「管理员」子会话, // 点开 monoforum 就进了那个人的个人资料而非私信管理视图。out 仍保留(管理员触发,= viewer)。 from = nil } if m.Action != nil { msg := &tg.MessageService{ Out: outgoing, Silent: m.Silent, Post: m.Post, ID: m.ID, FromID: from, PeerID: peer, Date: m.Date, Action: tgChannelMessageAction(*m.Action), } if msg.Action == nil { msg.Action = &tg.MessageActionEmpty{} } if m.SavedPeer.ID != 0 { msg.SetSavedPeerID(tgPeer(m.SavedPeer)) } if reply := tgMessageReplyHeader(domain.Message{ Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: m.ChannelID}, ReplyTo: m.ReplyTo, }); reply != nil { msg.SetReplyTo(reply) } return msg } msg := &tg.Message{ Out: outgoing, Silent: m.Silent, Post: m.Post, Noforwards: m.NoForwards, Mentioned: m.Mentioned, MediaUnread: m.MediaUnread, ID: m.ID, FromID: from, PeerID: peer, Date: m.Date, Message: m.Body, Entities: tgMessageEntities(m.Entities), } if m.SavedPeer.ID != 0 { // 频道私信(monoforum):saved_peer_id 让客户端把消息归入对应订阅者子会话。 msg.SetSavedPeerID(tgPeer(m.SavedPeer)) } if suggested, ok := tgSuggestedPost(m.SuggestedPost); ok { if m.Post { if m.SuggestedPost != nil && m.SuggestedPost.Accepted && m.SuggestedPost.Price != nil { switch m.SuggestedPost.Price.Kind { case domain.SuggestedPostPriceStars: msg.SetPaidSuggestedPostStars(true) case domain.SuggestedPostPriceTON: msg.SetPaidSuggestedPostTon(true) } } } else { msg.SetSuggestedPost(suggested) } } if m.PaidMessageStars > 0 { msg.SetPaidMessageStars(m.PaidMessageStars) } if m.Pinned { msg.SetPinned(true) } if m.EditDate != 0 { msg.SetEditDate(m.EditDate) } if reply := tgMessageReplyHeader(domain.Message{ Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: m.ChannelID}, ReplyTo: m.ReplyTo, }); reply != nil { msg.SetReplyTo(reply) } if fwd := tgMessageFwdHeader(m.Forward); fwd != nil { msg.SetFwdFrom(*fwd) } if m.ViaBotID != 0 { msg.SetViaBotID(m.ViaBotID) } if m.GroupedID != 0 { msg.SetGroupedID(m.GroupedID) } if markup := tgReplyMarkup(m.ReplyMarkup); markup != nil { msg.SetReplyMarkup(markup) } if rich := mustTGRichMessage(m.RichMessage); rich != nil { msg.SetRichMessage(*rich) } if replies := tgChannelMessageReplies(m.Replies); replies != nil { msg.SetReplies(*replies) } if reactions := tgMessageReactions(viewerUserID, m.Reactions); reactions != nil { msg.SetReactions(*reactions) } if !m.Media.IsZero() { msg.SetMedia(tgMessageMedia(m.Media)) if m.Media.InvertMedia { msg.SetInvertMedia(true) } } if m.TTLPeriod > 0 { msg.SetTTLPeriod(m.TTLPeriod) } if m.FromBoostsApplied > 0 { msg.SetFromBoostsApplied(m.FromBoostsApplied) } if m.Post { // 官方频道 post 自带 views 计数器(初始 1);signatures 开启时附作者签名。 views := m.ViewsCount if views < 1 { views = 1 } msg.SetViews(views) if m.PostAuthor != "" { msg.SetPostAuthor(m.PostAuthor) } } return msg } func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageActionClass { switch action.Type { case domain.ChannelActionCreate: return &tg.MessageActionChannelCreate{Title: action.Title} case domain.ChannelActionHistoryClear: return &tg.MessageActionHistoryClear{} case domain.ChannelActionChatAddUser, domain.ChannelActionChatJoined: return &tg.MessageActionChatAddUser{Users: append([]int64(nil), action.UserIDs...)} case domain.ChannelActionChatJoinedByLink: return &tg.MessageActionChatJoinedByLink{InviterID: action.InviterUserID} case domain.ChannelActionChatDelete: var userID int64 if len(action.UserIDs) > 0 { userID = action.UserIDs[0] } return &tg.MessageActionChatDeleteUser{UserID: userID} case domain.ChannelActionChatEditPhoto: if action.Photo == nil { return nil } photo := tgPhoto(*action.Photo) if _, empty := photo.(*tg.PhotoEmpty); empty { return nil } return &tg.MessageActionChatEditPhoto{Photo: photo} case domain.ChannelActionChatDeletePhoto: return &tg.MessageActionChatDeletePhoto{} case domain.ChannelActionEditTitle: return &tg.MessageActionChatEditTitle{Title: action.Title} case domain.ChannelActionTopicCreate: return &tg.MessageActionTopicCreate{ TitleMissing: action.TitleMissing, Title: action.Title, IconColor: action.IconColor, IconEmojiID: action.IconEmojiID, } case domain.ChannelActionTopicEdit: out := &tg.MessageActionTopicEdit{} if action.Title != "" { out.SetTitle(action.Title) } if action.IconEmojiIDSet { out.SetIconEmojiID(action.IconEmojiID) } if action.Closed != nil { out.SetClosed(*action.Closed) } if action.Hidden != nil { out.SetHidden(*action.Hidden) } return out case domain.ChannelActionTodoCompletions: return &tg.MessageActionTodoCompletions{ Completed: append([]int(nil), action.Completed...), Incompleted: append([]int(nil), action.Incompleted...), } case domain.ChannelActionTodoAppendTasks: return &tg.MessageActionTodoAppendTasks{ List: tgTodoItems(action.TodoItems), } case domain.ChannelActionGroupCall: // started(无 duration)与 ended(带 duration)共用 messageActionGroupCall。 out := &tg.MessageActionGroupCall{ Call: &tg.InputGroupCall{ID: action.CallID, AccessHash: action.CallAccessHash}, } if action.CallDuration > 0 { out.SetDuration(action.CallDuration) } return out case domain.ChannelActionGroupCallScheduled: return &tg.MessageActionGroupCallScheduled{ Call: &tg.InputGroupCall{ID: action.CallID, AccessHash: action.CallAccessHash}, ScheduleDate: action.CallScheduleDate, } case domain.ChannelActionInviteToGroupCall: return &tg.MessageActionInviteToGroupCall{ Call: &tg.InputGroupCall{ID: action.CallID, AccessHash: action.CallAccessHash}, Users: append([]int64(nil), action.UserIDs...), } case domain.ChannelActionBoostApply: return &tg.MessageActionBoostApply{Boosts: action.Boosts} case domain.ChannelActionPaidMessagesPrice: return &tg.MessageActionPaidMessagesPrice{ BroadcastMessagesAllowed: action.BroadcastMessagesAllowed, Stars: action.Stars, } case domain.ChannelActionSetChatWallpaper: if wallpaper := tgWallpaper(action.Wallpaper); wallpaper != nil { return &tg.MessageActionSetChatWallPaper{Wallpaper: wallpaper} } return nil case domain.ChannelActionChangeCommunity: out := &tg.MessageActionChangeCommunity{} if action.CommunityID != 0 { out.SetCommunityID(action.CommunityID) } return out case domain.ChannelActionSuggestedPostApproval: out := &tg.MessageActionSuggestedPostApproval{ Rejected: action.SuggestedPostRejected, BalanceTooLow: action.SuggestedPostBalanceTooLow, } if action.SuggestedPostRejectComment != "" { out.SetRejectComment(action.SuggestedPostRejectComment) } if action.SuggestedPostScheduleDate > 0 { out.SetScheduleDate(action.SuggestedPostScheduleDate) } if price := tgSuggestedPostPrice(action.SuggestedPostPrice); price != nil { out.SetPrice(price) } return out case domain.ChannelActionSuggestedPostSuccess: if price := tgSuggestedPostPrice(action.SuggestedPostPrice); price != nil { return &tg.MessageActionSuggestedPostSuccess{Price: price} } return nil case domain.ChannelActionSuggestedPostRefund: return &tg.MessageActionSuggestedPostRefund{PayerInitiated: action.SuggestedPostPayerInitiated} default: return nil } } func tgSuggestedPostPrice(price *domain.SuggestedPostPrice) tg.StarsAmountClass { if price == nil { return nil } switch price.Kind { case domain.SuggestedPostPriceStars: return &tg.StarsAmount{Amount: price.Amount, Nanos: price.Nanos} case domain.SuggestedPostPriceTON: return &tg.StarsTonAmount{Amount: price.Amount} default: return nil } } func tgChannelMessageReplies(in *domain.ChannelMessageReplies) *tg.MessageReplies { if in == nil { return nil } out := &tg.MessageReplies{ Comments: in.Comments, Replies: in.Replies, RepliesPts: in.RepliesPts, } if len(in.RecentRepliers) > 0 { repliers := make([]tg.PeerClass, 0, len(in.RecentRepliers)) for _, peer := range in.RecentRepliers { if converted := tgPeer(peer); converted != nil { repliers = append(repliers, converted) } } if len(repliers) > 0 { out.SetRecentRepliers(repliers) } } if in.ChannelID != 0 { out.SetChannelID(in.ChannelID) } if in.MaxID > 0 { out.SetMaxID(in.MaxID) } if in.ReadMaxID > 0 { out.SetReadMaxID(in.ReadMaxID) } return out } func tgChannels(viewerUserID int64, channels []domain.Channel) []tg.ChatClass { 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{}{} out = append(out, tgChannelChatMin(viewerUserID, ch)) } return out } func tgChannelChat(viewerUserID int64, ch domain.Channel, self *domain.ChannelMember) tg.ChatClass { if ch.Deleted { return tgChannelForbidden(ch) } return tgChannel(viewerUserID, ch, self) } // tgChannelChatMin 是消息类 update/伴随 chats 的 min channel 形态: // 客户端对 min 对象不应用 admin/banned rights 与 left/creator,避免不带 // 接收者 member 投影的推送把本地权限状态清掉。 func tgChannelChatMin(viewerUserID int64, ch domain.Channel) tg.ChatClass { if ch.Deleted { return tgChannelForbidden(ch) } out := tgChannel(viewerUserID, ch, nil) out.Min = true out.Creator = false return out } // tgChannelChatForView 把按 viewer 投影后的 ChannelView 转成查询响应 chat: // 被踢/被禁止查看的 viewer 收到 channelForbidden,借此感知自己已离开会话。 func tgChannelChatForView(viewerUserID int64, view domain.ChannelView) tg.ChatClass { if view.Forbidden { return tgChannelForbidden(view.Channel) } self := view.Self return tgChannelChat(viewerUserID, view.Channel, &self) } func tgChannelForbidden(ch domain.Channel) *tg.ChannelForbidden { return &tg.ChannelForbidden{ Broadcast: ch.Broadcast, Megagroup: ch.Megagroup, ID: ch.ID, AccessHash: ch.AccessHash, Title: ch.Title, } } func tgChannel(viewerUserID int64, ch domain.Channel, self *domain.ChannelMember) *tg.Channel { out := &tg.Channel{ Creator: ch.CreatorUserID == viewerUserID && viewerUserID != 0, Verified: ch.Verified, Scam: ch.Scam, Fake: ch.Fake, Gigagroup: ch.Gigagroup, Broadcast: ch.Broadcast, Megagroup: ch.Megagroup, Forum: ch.Forum, ForumTabs: ch.ForumTabs, Noforwards: ch.NoForwards, Signatures: ch.Signatures, ID: ch.ID, Title: ch.Title, Photo: tgChatPhoto(ch), Date: ch.Date, } out.SetAccessHash(ch.AccessHash) out.SetJoinToSend(ch.JoinToSend) out.SetJoinRequest(ch.JoinRequest) out.SetAutotranslation(ch.Autotranslation) // broadcast_messages_allowed 只属于广播母频道。monoforum 内部用同名字段镜像 DM 启用状态(见下), // 但它是 megagroup,绝不能把该 flag 投影出去(官方 monoforum 对象也不带它)。 if ch.Broadcast { out.SetBroadcastMessagesAllowed(ch.BroadcastMessagesAllowed) } if ch.SendPaidMessagesStars > 0 || ch.BroadcastMessagesAllowed || ch.Monoforum { out.SetSendPaidMessagesStars(ch.SendPaidMessagesStars) } if ch.HasLink || ch.LinkedChatID != 0 { out.SetHasLink(true) } if ch.Monoforum { out.SetMonoforum(true) } // linked_monoforum_id 仅在 DM 启用时下发(母频道与 monoforum 都用 BroadcastMessagesAllowed 表示 // DM 启用状态,monoforum 由 SetPaidMessagesPrice 镜像母频道)。关闭 Direct Messages 时**双方都**隐藏 // 该 id:母频道隐藏触发 monoforum 的 MonoforumDisabled;monoforum 也必须隐藏,否则打开 monoforum // 重新拉取时 mono 仍带 link → setMonoforumLink(parent) 会把 MonoforumDisabled 清掉、停用页脚消失。 // 内部关联行仍保留以便重新开启复用。 if ch.LinkedMonoforumID != 0 && ch.BroadcastMessagesAllowed { out.SetLinkedMonoforumID(ch.LinkedMonoforumID) } if ch.LinkedCommunityID != 0 { out.SetLinkedCommunityID(ch.LinkedCommunityID) } if ch.Username != "" { out.SetUsername(ch.Username) } if color := tgPeerColor(ch.Color); color != nil { out.SetColor(color) } if profileColor := tgPeerColor(ch.ProfileColor); profileColor != nil { out.SetProfileColor(profileColor) } if status := tgChannelEmojiStatus(ch.EmojiStatus); status != nil { out.SetEmojiStatus(status) } if ch.SlowmodeSeconds > 0 { out.SetSlowmodeEnabled(true) } if ch.ParticipantsCount > 0 { out.SetParticipantsCount(ch.ParticipantsCount) } out.SetDefaultBannedRights(tgDefaultChatBannedRights(ch.DefaultBannedRights)) // 群通话 banner 数据源:call_active/call_not_empty flag(Android 对 flag 依赖 // 更重,call_not_empty 翻转时还会经 pushChannelStateToMembers 补推)。 out.CallActive = ch.ActiveCallID != 0 out.CallNotEmpty = ch.ActiveCallNotEmpty if ch.Monoforum { // Monoforum(频道私信容器)绝不能在自身对象上带 Creator/admin_rights。TDesktop 的 // NeedAboutGroup 对 megagroup 看 amCreator() 决定是否画 "You created a group" 群聊空状态, // 且订阅数/Leave/Channel 等 chrome 也 key off amCreator/asMegagroup —— 一旦 mono 自身 // Creator=true 就会被渲染成普通群。Direct-Messages 容器身份(本地 MonoforumAdmin 标志)由 // 客户端从母频道 canAccessMonoforum(amCreator 或 manage_direct_messages)派生,与 mono 自身 // 的 Creator/admin 无关。服务端的私信发送鉴权走母频道 membership,不依赖此 flag。 out.Creator = false } else if self != nil { if self.Status == domain.ChannelMemberLeft || self.Guest { out.Left = true } switch self.Role { case domain.ChannelRoleCreator: out.Creator = true out.SetAdminRights(tgChatAdminRights(creatorProjectionAdminRights(self.AdminRights))) case domain.ChannelRoleAdmin: out.SetAdminRights(tgChatAdminRights(self.AdminRights)) } if !zeroChannelBannedRights(self.BannedRights) { out.SetBannedRights(tgChatBannedRights(self.BannedRights)) } } return out } func tgChannelFull(view domain.ChannelView, publicBaseURL ...string) *tg.ChannelFull { ch := view.Channel full := &tg.ChannelFull{ // 广播频道的订阅者列表仅管理员可见(官方语义):非管理员订阅者拿到 // can_view_participants=false,Profile 的 Subscribers/Administrators/Channel Settings // 三行随之隐藏。megagroup 成员可见(隐藏成员时仅管理员)。 CanViewParticipants: channelMemberIsAdmin(view.Self) || !ch.MembersListAdminOnly(), CanSetUsername: view.Self.Role == domain.ChannelRoleCreator, CanDeleteChannel: view.Self.Role == domain.ChannelRoleCreator, ID: ch.ID, // Official clients render localized warnings from scam/fake flags. // About remains the owner's unmodified description. About: ch.About, ReadInboxMaxID: view.Dialog.ReadInboxMaxID, ReadOutboxMaxID: view.Dialog.ReadOutboxMaxID, UnreadCount: view.Dialog.UnreadCount, ChatPhoto: tgChannelChatPhotoFull(ch), NotifySettings: *tdesktop.NotifySettings(), Pts: ch.Pts, } if ch.ParticipantsCount > 0 { full.SetParticipantsCount(ch.ParticipantsCount) } if ch.ActiveCallID != 0 { // 入会面板/banner 点击的入口:客户端拿它调 phone.getGroupCall。 full.SetCall(&tg.InputGroupCall{ID: ch.ActiveCallID, AccessHash: ch.ActiveCallAccessHash}) } if view.Self.AvailableMinID > 0 { // 客户端用它裁剪本地缓存的入群前/已清空历史(clearUpTill)。 full.SetAvailableMinID(view.Self.AvailableMinID) } if view.ExportedInvite != nil && !view.ExportedInvite.Revoked { baseURL := "" if len(publicBaseURL) > 0 { baseURL = publicBaseURL[0] } full.SetExportedInvite(tgExportedChannelInvite(*view.ExportedInvite, baseURL)) } if ch.AdminsCount > 0 { full.SetAdminsCount(ch.AdminsCount) } if ch.KickedCount > 0 { full.SetKickedCount(ch.KickedCount) } if ch.BannedCount > 0 { full.SetBannedCount(ch.BannedCount) } if view.Dialog.FolderID != domain.DialogMainFolderID { full.SetFolderID(view.Dialog.FolderID) } if ch.TTLPeriod > 0 { full.SetTTLPeriod(ch.TTLPeriod) } if view.Dialog.HasScheduled { full.SetHasScheduled(true) } if ch.PreHistoryHidden { full.SetHiddenPrehistory(true) } if ch.ParticipantsHidden { full.SetParticipantsHidden(true) } if ch.AntiSpam { full.SetAntispam(true) } if ch.RestrictedSponsored { full.SetRestrictedSponsored(true) } if ch.SendPaidMessagesStars > 0 || ch.BroadcastMessagesAllowed { full.SetPaidMessagesAvailable(true) full.SetSendPaidMessagesStars(ch.SendPaidMessagesStars) } if view.Dialog.ViewForumAsMessages { full.SetViewForumAsMessages(true) } if ch.LinkedChatID != 0 { full.SetLinkedChatID(ch.LinkedChatID) } if defaultSendAs := validDefaultSendAsPeer(view); defaultSendAs != nil { full.SetDefaultSendAs(tgPeer(*defaultSendAs)) } if ch.SlowmodeSeconds > 0 { full.SetSlowmodeSeconds(ch.SlowmodeSeconds) if view.Self.SlowmodeLastSendDate > 0 { full.SetSlowmodeNextSendDate(view.Self.SlowmodeLastSendDate + ch.SlowmodeSeconds) } } if view.SelfBoostsApplied > 0 { full.SetBoostsApplied(view.SelfBoostsApplied) } if ch.BoostsUnrestrict > 0 { full.SetBoostsUnrestrict(ch.BoostsUnrestrict) } if ch.PinnedMessageID > 0 { full.SetPinnedMsgID(ch.PinnedMessageID) } if wallpaper := tgWallpaper(ch.Wallpaper); wallpaper != nil { full.SetWallpaper(wallpaper) } if reactions := tgChannelReactionPolicy(ch.ReactionPolicy); reactions != nil { full.SetAvailableReactions(reactions) } if ch.ReactionPolicy.Limit > 0 { full.SetReactionsLimit(ch.ReactionPolicy.Limit) } return full } func channelMemberIsAdmin(member domain.ChannelMember) bool { return member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin } func tgChannelReactionPolicy(policy domain.ChannelReactionPolicy) tg.ChatReactionsClass { switch policy.Type { case domain.ChannelReactionPolicyNone: return &tg.ChatReactionsNone{} case domain.ChannelReactionPolicyAll: out := &tg.ChatReactionsAll{} if policy.AllowCustom { out.SetAllowCustom(true) } return out case domain.ChannelReactionPolicySome: reactions := make([]tg.ReactionClass, 0, len(policy.Emoticons)+len(policy.CustomEmojiIDs)) for _, emoticon := range policy.Emoticons { if emoticon == "" { continue } reactions = append(reactions, &tg.ReactionEmoji{Emoticon: emoticon}) } for _, id := range policy.CustomEmojiIDs { if id <= 0 { continue } reactions = append(reactions, &tg.ReactionCustomEmoji{DocumentID: id}) } return &tg.ChatReactionsSome{Reactions: reactions} default: return nil } } func tgPeerColor(color domain.ChannelPeerColor) tg.PeerColorClass { if color.Empty() { return nil } out := &tg.PeerColor{} if color.HasColor { out.SetColor(color.Color) } if color.BackgroundEmojiID != 0 { out.SetBackgroundEmojiID(color.BackgroundEmojiID) } return out } func tgChannelParticipant(selfUserID int64, member domain.ChannelMember) tg.ChannelParticipantClass { switch member.Role { case domain.ChannelRoleCreator: out := &tg.ChannelParticipantCreator{ UserID: member.UserID, AdminRights: tgChatAdminRights(creatorProjectionAdminRights(member.AdminRights)), } if member.Rank != "" { out.SetRank(member.Rank) } return out case domain.ChannelRoleAdmin: out := &tg.ChannelParticipantAdmin{ Self: member.UserID == selfUserID, UserID: member.UserID, PromotedBy: member.InviterUserID, Date: member.JoinedAt, AdminRights: tgChatAdminRights(member.AdminRights), } if member.InviterUserID != 0 { out.SetInviterID(member.InviterUserID) } if member.Rank != "" { out.SetRank(member.Rank) } return out default: if member.Status != domain.ChannelMemberActive { return &tg.ChannelParticipantLeft{Peer: &tg.PeerUser{UserID: member.UserID}} } if member.UserID == selfUserID { out := &tg.ChannelParticipantSelf{ UserID: member.UserID, InviterID: member.InviterUserID, Date: member.JoinedAt, } if member.Rank != "" { out.SetRank(member.Rank) } return out } out := &tg.ChannelParticipant{UserID: member.UserID, Date: member.JoinedAt} if member.Rank != "" { out.SetRank(member.Rank) } return out } } func tgChannelAdminLogEvents(viewerUserID int64, events []domain.ChannelAdminLogEvent) []tg.ChannelAdminLogEvent { out := make([]tg.ChannelAdminLogEvent, 0, len(events)) for _, event := range events { if item, ok := tgChannelAdminLogEvent(viewerUserID, event); ok { out = append(out, item) } } return out } func tgChannelAdminLogEvent(viewerUserID int64, event domain.ChannelAdminLogEvent) (tg.ChannelAdminLogEvent, bool) { action := tg.ChannelAdminLogEventActionClass(nil) switch event.Type { case domain.ChannelAdminLogChangeTitle: action = &tg.ChannelAdminLogEventActionChangeTitle{PrevValue: event.PrevString, NewValue: event.NewString} case domain.ChannelAdminLogChangeUsername: action = &tg.ChannelAdminLogEventActionChangeUsername{PrevValue: event.PrevString, NewValue: event.NewString} case domain.ChannelAdminLogChangeLinkedChat: action = &tg.ChannelAdminLogEventActionChangeLinkedChat{PrevValue: int64(event.PrevInt), NewValue: int64(event.NewInt)} case domain.ChannelAdminLogToggleSignatures: action = &tg.ChannelAdminLogEventActionToggleSignatures{NewValue: event.NewBool} case domain.ChannelAdminLogTogglePreHistoryHidden: action = &tg.ChannelAdminLogEventActionTogglePreHistoryHidden{NewValue: event.NewBool} case domain.ChannelAdminLogToggleForum: action = &tg.ChannelAdminLogEventActionToggleForum{NewValue: event.NewBool} case domain.ChannelAdminLogToggleAutotranslation: action = &tg.ChannelAdminLogEventActionToggleAutotranslation{NewValue: event.NewBool} case domain.ChannelAdminLogToggleAntiSpam: action = &tg.ChannelAdminLogEventActionToggleAntiSpam{NewValue: event.NewBool} case domain.ChannelAdminLogToggleSlowMode: action = &tg.ChannelAdminLogEventActionToggleSlowMode{PrevValue: event.PrevInt, NewValue: event.NewInt} case domain.ChannelAdminLogParticipantJoin: action = &tg.ChannelAdminLogEventActionParticipantJoin{} case domain.ChannelAdminLogParticipantLeave: action = &tg.ChannelAdminLogEventActionParticipantLeave{} case domain.ChannelAdminLogParticipantInvite: if event.Participant == nil { return tg.ChannelAdminLogEvent{}, false } action = &tg.ChannelAdminLogEventActionParticipantInvite{ Participant: tgChannelParticipantForUpdate(viewerUserID, *event.Participant), } case domain.ChannelAdminLogParticipantPromote, domain.ChannelAdminLogParticipantDemote: if event.PrevParticipant == nil || event.NewParticipant == nil { return tg.ChannelAdminLogEvent{}, false } action = &tg.ChannelAdminLogEventActionParticipantToggleAdmin{ PrevParticipant: tgChannelParticipantForUpdate(viewerUserID, *event.PrevParticipant), NewParticipant: tgChannelParticipantForUpdate(viewerUserID, *event.NewParticipant), } case domain.ChannelAdminLogParticipantEditRank: if event.Participant == nil { return tg.ChannelAdminLogEvent{}, false } action = &tg.ChannelAdminLogEventActionParticipantEditRank{ UserID: event.Participant.UserID, PrevRank: event.PrevString, NewRank: event.NewString, } case domain.ChannelAdminLogParticipantBan, domain.ChannelAdminLogParticipantUnban, domain.ChannelAdminLogParticipantKick, domain.ChannelAdminLogParticipantUnkick: if event.PrevParticipant == nil || event.NewParticipant == nil { return tg.ChannelAdminLogEvent{}, false } action = &tg.ChannelAdminLogEventActionParticipantToggleBan{ PrevParticipant: tgChannelParticipantForUpdate(viewerUserID, *event.PrevParticipant), NewParticipant: tgChannelParticipantForUpdate(viewerUserID, *event.NewParticipant), } case domain.ChannelAdminLogUpdatePinned: action = &tg.ChannelAdminLogEventActionUpdatePinned{Message: tgAdminLogMessage(viewerUserID, event.ChannelID, event.Message)} case domain.ChannelAdminLogSendMessage: action = &tg.ChannelAdminLogEventActionSendMessage{Message: tgAdminLogMessage(viewerUserID, event.ChannelID, event.Message)} case domain.ChannelAdminLogEditMessage: action = &tg.ChannelAdminLogEventActionEditMessage{ PrevMessage: tgAdminLogMessage(viewerUserID, event.ChannelID, event.PrevMessage), NewMessage: tgAdminLogMessage(viewerUserID, event.ChannelID, event.NewMessage), } case domain.ChannelAdminLogDeleteMessage: action = &tg.ChannelAdminLogEventActionDeleteMessage{Message: tgAdminLogMessage(viewerUserID, event.ChannelID, event.Message)} default: return tg.ChannelAdminLogEvent{}, false } if action == nil { return tg.ChannelAdminLogEvent{}, false } return tg.ChannelAdminLogEvent{ ID: event.ID, Date: event.Date, UserID: event.UserID, Action: action, }, true } func tgAdminLogMessage(viewerUserID, channelID int64, msg *domain.ChannelMessage) tg.MessageClass { if msg == nil || msg.ID == 0 { out := &tg.MessageEmpty{ID: 0} out.SetPeerID(&tg.PeerChannel{ChannelID: channelID}) return out } return tgChannelMessage(viewerUserID, *msg) } func tgChatAdminRights(rights domain.ChannelAdminRights) tg.ChatAdminRights { return tg.ChatAdminRights{ ChangeInfo: rights.ChangeInfo, PostMessages: rights.PostMessages, EditMessages: rights.EditMessages, DeleteMessages: rights.DeleteMessages, PostStories: rights.PostStories, EditStories: rights.EditStories, DeleteStories: rights.DeleteStories, BanUsers: rights.BanUsers, InviteUsers: rights.InviteUsers, PinMessages: rights.PinMessages, AddAdmins: rights.AddAdmins, Anonymous: rights.Anonymous, ManageCall: rights.ManageCall, Other: true, ManageTopics: rights.ManageTopics, ManageRanks: rights.ManageRanks, ManageLinkedPeers: rights.ManageLinkedPeers, // manage_direct_messages(flags.17):客户端据此在母频道上判定 canAccessMonoforum, // 从而为关联 monoforum 派生 MonoforumAdmin(Direct-Messages 容器渲染所需)。 ManageDirectMessages: rights.ManageDirectMessages, } } func creatorProjectionAdminRights(rights domain.ChannelAdminRights) domain.ChannelAdminRights { return domain.CreatorProjectionAdminRights(rights) } func domainChannelAdminRights(rights tg.ChatAdminRights) domain.ChannelAdminRights { return domain.ChannelAdminRights{ ChangeInfo: rights.ChangeInfo, PostMessages: rights.PostMessages, EditMessages: rights.EditMessages, DeleteMessages: rights.DeleteMessages, PostStories: rights.PostStories, EditStories: rights.EditStories, DeleteStories: rights.DeleteStories, BanUsers: rights.BanUsers, InviteUsers: rights.InviteUsers, PinMessages: rights.PinMessages, AddAdmins: rights.AddAdmins, Anonymous: rights.Anonymous, ManageCall: rights.ManageCall, ManageChat: rights.Other, ManageTopics: rights.ManageTopics, ManageRanks: rights.ManageRanks, ManageLinkedPeers: rights.ManageLinkedPeers, ManageDirectMessages: rights.ManageDirectMessages, } } func tgChatBannedRights(rights domain.ChannelBannedRights) tg.ChatBannedRights { return tg.ChatBannedRights{ ViewMessages: rights.ViewMessages, SendMessages: rights.SendMessages, SendMedia: rights.SendMedia, SendStickers: rights.SendStickers, SendGifs: rights.SendGifs, SendGames: rights.SendGames, SendInline: rights.SendInline, EmbedLinks: rights.EmbedLinks, SendPolls: rights.SendPolls, ChangeInfo: rights.ChangeInfo, InviteUsers: rights.InviteUsers, PinMessages: rights.PinMessages, ManageTopics: rights.ManageTopics, SendPhotos: rights.SendPhotos, SendVideos: rights.SendVideos, SendRoundvideos: rights.SendRoundvideos, SendAudios: rights.SendAudios, SendVoices: rights.SendVoices, SendDocs: rights.SendDocs, SendPlain: rights.SendPlain, EditRank: rights.EditRank, SendReactions: rights.SendReactions, ManageLinkedPeers: rights.ManageLinkedPeers, UntilDate: rights.UntilDate, } } func tgDefaultChatBannedRights(rights domain.ChannelBannedRights) tg.ChatBannedRights { out := tgChatBannedRights(rights) if out.UntilDate == 0 { out.UntilDate = defaultChatBannedRightsUntilDate } return out } func domainChannelBannedRights(rights tg.ChatBannedRights) domain.ChannelBannedRights { return domain.ChannelBannedRights{ ViewMessages: rights.ViewMessages, SendMessages: rights.SendMessages, SendMedia: rights.SendMedia, SendStickers: rights.SendStickers, SendGifs: rights.SendGifs, SendGames: rights.SendGames, SendInline: rights.SendInline, EmbedLinks: rights.EmbedLinks, SendPolls: rights.SendPolls, ChangeInfo: rights.ChangeInfo, InviteUsers: rights.InviteUsers, PinMessages: rights.PinMessages, ManageTopics: rights.ManageTopics, SendPhotos: rights.SendPhotos, SendVideos: rights.SendVideos, SendRoundvideos: rights.SendRoundvideos, SendAudios: rights.SendAudios, SendVoices: rights.SendVoices, SendDocs: rights.SendDocs, SendPlain: rights.SendPlain, EditRank: rights.EditRank, SendReactions: rights.SendReactions, ManageLinkedPeers: rights.ManageLinkedPeers, UntilDate: rights.UntilDate, } } func zeroChannelBannedRights(rights domain.ChannelBannedRights) bool { return rights == domain.ChannelBannedRights{} }