owpengram-server/internal/rpc/convert_channels_core.go

980 lines
34 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/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 计数器(初始 1signatures 开启时附作者签名。
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.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.ChannelActionStarGift:
return tgMessageActionStarGift(action.StarGift)
case domain.ChannelActionStarGiftUnique:
return tgMessageActionStarGiftUnique(action.StarGiftUnique)
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,
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)
out.SetUsernames(tgUsernames(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 flagAndroid 对 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=falseProfile 的 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,
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)
}
if ch.Broadcast && !ch.Megagroup {
// Android uses paid_media_allowed as the capability gate for showing the
// paid-reaction setting; paid_reactions_available below remains the saved
// on/off state.
full.SetPaidMediaAllowed(true)
full.SetStargiftsAvailable(true)
}
// paid_reactions_available reflects the saved chat policy, not mere broadcast
// capability. Android counts this flag as an extra available reaction in the
// settings row, so advertising it without paid_enabled corrupts the UI count.
if ch.Broadcast && !ch.Megagroup && ch.ReactionPolicy.PaidEnabled {
full.SetPaidReactionsAvailable(true)
}
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{}
}