fix: align private and channel update semantics

(cherry picked from commit c65f76f56278f74082c4fa792ed49104d5d33c38)
This commit is contained in:
A 2026-06-07 22:22:23 +08:00
parent dce7b92772
commit d84fa6e126
36 changed files with 1765 additions and 382 deletions

View file

@ -38,7 +38,7 @@ Date: 2026-06-01
- `channels.setStickers`files/sticker store 未接入前只允许 megagroup `inputStickerSetEmpty` 清空 no-op 兼容;非空 sticker set 返回 `STICKERSET_INVALID`,避免 TDesktop 误以为群贴纸集已持久化。`channels.reorderUsernames/toggleUsername/deactivateAllUsernames` 仍先做权限校验型兼容响应Fragment/多 username 入口不改主 username主 username 只允许 `channels.updateUsername` 设置或清除。
- TDesktop 已有调用证据但非首批真实业务的入口统一注册为显式 stub`channels.reportSpam/editLocation/convertToGigagroup/reportAntiSpamFalsePositive/setBoostsToUnblockRestrictions/setEmojiStickers/checkSearchPostsFlood/setMainProfileTab``channels.searchPosts` 已提升为公开频道/超级群帖子真实搜索,`checkSearchPostsFlood` 仍保持免费额度兼容 stub二者共享 query 长度边界。`setBoostsToUnblockRestrictions` 按 Layer225 限制 0..8。`channels.restrictSponsoredMessages/updatePaidMessagesPrice/toggleAutotranslation` 已提升为最小真实设置持久化:分别回填 `ChannelFull.restricted_sponsored``Channel/ChannelFull.send_paid_messages_stars` + broadcast `Channel.broadcast_messages_allowed``Channel.autotranslation`,其中 `updatePaidMessagesPrice` 按 TDesktop 默认 app config 限制 stars<=10000并允许 broadcast direct messages 用 `-1` 表示关闭真实广告投放、boost/premium 校验、paid messages/monoforum/结算后续补。`channels.exportMessageLink``channels.readMessageContents``channels.getMessageAuthor``channels.deleteParticipantHistory``getInactiveChannels``getChannelRecommendations``toggleJoinToSend``toggleJoinRequest``toggleParticipantsHidden``toggleForum``toggleViewForumAsMessages``toggleAntiSpam``getLeftChannels``getGroupsForDiscussion``setDiscussionGroup` 已从该列表提升为真实实现。
- `channels.getGroupsForDiscussion/setDiscussionGroup` 真实维护 broadcast channel 与 megagroup 的双向 `linked_chat_id`。候选列表只返回当前用户可管理的 supergroup不返回 legacy basic group设置时校验 access_hash、broadcast/group 类型、管理权限与 hidden prehistory替换链接会同步清理旧 group/old broadcastTDesktop 通过 `Channel.has_link``ChannelFull.linked_chat_id` 刷新讨论组入口。linked broadcast 发新 post 时会在 discussion megagroup 创建一条 forwarded root messagesource post 保存 `discussion_channel_id/message_id``messages.getDiscussionMessage/getReplies/readDiscussion` 都映射到该 root 和 target channel read state。
- TDesktop 已有源码调用但当前业务模型尚未维护独立 media/sticker/custom-emoji/saved-message tag assignment/poll/todo/scheduled 的 `messages.reportSpam/report/reportReaction/reportMessagesDelivery/reportReadMetrics/reportMusicListen/reportSponsoredMessage/getSavedReactionTags/updateSavedReactionTag/getDefaultTagReactions/getExtendedMedia/getAttachedStickers/getCustomEmojiDocuments/searchStickerSets/searchStickers/getEmojiKeywords/getEmojiKeywordsDifference/sendVote/getPollResults/getPollVotes/addPollAnswer/deletePollAnswer/getUnreadPollVotes/readPollVotes/appendTodoList/toggleTodoCompleted/getSearchCounters/getSearchResultsCalendar/getSearchResultsPositions/getOnlines/getWebPagePreview/uploadMedia/sendMedia/sendMultiMedia/getScheduledHistory/getScheduledMessages/sendScheduledMessages/deleteScheduledMessages` 均显式注册统一做参数上限、peer 校验和空/零兼容响应,禁止落到未知 RPC。其中 `messages.readMessageContents` 已提升为 private exact-id real-partial只校验当前账号可见 message box 并向其它 session 推 content-read update不维护 media/reaction content-read 状态channel/supergroup 的 `channels.readMessageContents` 会按可见消息 ID 清理当前作者视角的 unread reaction、重算 `channel_dialogs.unread_reactions_count`,并向当前账号 session 推 `updateMessageReactions` 让 TDesktop 立即去掉 dialog reaction 角标;`messages.getMessagesViews` 已维护 channel-scoped views 去重计数并返回 replies/comment 统计;`messages.getUnreadMentions/readMentions` 已维护 channel-scoped unread mention indexreadMentions 返回 channel pts`messages.sendReaction/getMessagesReactions/getMessageReactionsList/getUnreadReactions/readReactions/getRecentReactions/clearRecentReactions/getTopReactions` 已提升为 private + channel/supergroup emoji reaction 最小真实实现private 写 `private_message_reactions` 并按双端 owner-visible message box 返回/推送 `updateMessageReactions`channel 写 `channel_message_reactions` 并按消息作者维护 unread reaction 状态history/getMessages/search 均回填 `message.reactions``add_to_recent` 写账号级最近 channel reaction 列表且 get/clear 支持 hash/notModified`getTopReactions` 按账号使用次数排序并优先用真实 `available_reactions` catalog 兜底;`messages.getSavedReactionTags/updateSavedReactionTag` 已持久化账号级 emoji saved reaction tag 标题并向其它 session 推 `updateSavedReactionTags`,但 peer 维度 count 与 saved-message tag assignment 仍返回空;`messages.getForumTopics/getForumTopicsByID/createForumTopic/editForumTopic/updatePinnedForumTopic/reorderPinnedForumTopics/deleteTopicHistory` 已从纯空响应提升为真实最小 topic store虚拟 General + root service message topic + bounded delete`messages.getCommonChats` 已提升为真实共同超级群查询并回填 `users.getFullUser.common_chats_count``messages.report` 保持 TDesktop 分步举报 UI 所需的 choose option/add comment/reported 形态但暂不落库report/metrics/music/sponsored 这类 telemetry 入口只返回 BoolTrue/reported不写业务状态TDesktop 资料页 shared media count 会用 `messages.search(limit=0, filter=photo/video/document/url/gif/music/roundVoice/poll)``messages.channelMessages.count`,当前显式返回空页/count=0避免纯文本 channel history 污染 photos/videos/files 等计数search calendar 空结果会回填请求 offset date/id避免空月份重复拉取default tag、sticker/custom emoji 与 extended media 均只返回空/notModified 或明确错误,不伪造 paid media 或 tag 绑定状态poll/todo mutating 入口在缺少媒体 store 时返回可解释错误,不伪造 `updateMessagePoll` 或 todo service messageweb preview 返回 `messageMediaEmpty` 且不会抓外网,`sendMedia(inputMediaWebPage)` 降级为纯文本发送,真实 photo/document/poll/album、todo、scheduled store 留待后续模型。
- TDesktop 已有源码调用但当前业务模型尚未维护独立 media/sticker/custom-emoji/saved-message tag assignment/poll/todo/scheduled 的 `messages.reportSpam/report/reportReaction/reportMessagesDelivery/reportReadMetrics/reportMusicListen/reportSponsoredMessage/getSavedReactionTags/updateSavedReactionTag/getDefaultTagReactions/getExtendedMedia/getAttachedStickers/getCustomEmojiDocuments/searchStickerSets/searchStickers/getEmojiKeywords/getEmojiKeywordsDifference/sendVote/getPollResults/getPollVotes/addPollAnswer/deletePollAnswer/getUnreadPollVotes/readPollVotes/appendTodoList/toggleTodoCompleted/getSearchCounters/getSearchResultsCalendar/getSearchResultsPositions/getOnlines/getWebPagePreview/uploadMedia/sendMedia/sendMultiMedia/getScheduledHistory/getScheduledMessages/sendScheduledMessages/deleteScheduledMessages` 均显式注册统一做参数上限、peer 校验和空/零兼容响应,禁止落到未知 RPC。其中 `messages.readMessageContents` 已提升为 private real-content-read清理 owner 侧 `message_boxes.media_unread/reaction_unread`,有变化才生成 durable `updateReadMessagesContents`channel/supergroup 的 `channels.readMessageContents` 会按可见消息 ID 清理当前作者视角的 unread reaction、重算 `channel_dialogs.unread_reactions_count`,并向当前账号 session 推 `updateMessageReactions` 让 TDesktop 立即去掉 dialog reaction 角标;`messages.getMessagesViews` 已维护 channel-scoped views 去重计数并返回 replies/comment 统计;`messages.getUnreadMentions/readMentions` 已维护 channel-scoped unread mention indexhistory/getMessages/difference/online update 按 viewer 回填 `mentioned/media_unread`readMentions 返回 channel pts`messages.sendReaction/getMessagesReactions/getMessageReactionsList/getUnreadReactions/readReactions/getRecentReactions/clearRecentReactions/getTopReactions` 已提升为 private + channel/supergroup emoji reaction 最小真实实现private 写 `private_message_reactions` 并按双端 owner-visible message box 返回/推送 `updateMessageReactions`channel 写 `channel_message_reactions` 并按消息作者维护 unread reaction 状态history/getMessages/search 均回填 `message.reactions``add_to_recent` 写账号级最近 channel reaction 列表且 get/clear 支持 hash/notModified`getTopReactions` 按账号使用次数排序并优先用真实 `available_reactions` catalog 兜底;`messages.getSavedReactionTags/updateSavedReactionTag` 已持久化账号级 emoji saved reaction tag 标题并向其它 session 推 `updateSavedReactionTags`,但 peer 维度 count 与 saved-message tag assignment 仍返回空;`messages.getForumTopics/getForumTopicsByID/createForumTopic/editForumTopic/updatePinnedForumTopic/reorderPinnedForumTopics/deleteTopicHistory` 已从纯空响应提升为真实最小 topic store虚拟 General + root service message topic + bounded delete`messages.getCommonChats` 已提升为真实共同超级群查询并回填 `users.getFullUser.common_chats_count``messages.report` 保持 TDesktop 分步举报 UI 所需的 choose option/add comment/reported 形态但暂不落库report/metrics/music/sponsored 这类 telemetry 入口只返回 BoolTrue/reported不写业务状态TDesktop 资料页 shared media count 会用 `messages.search(limit=0, filter=photo/video/document/url/gif/music/roundVoice/poll)``messages.channelMessages.count`,当前显式返回空页/count=0避免纯文本 channel history 污染 photos/videos/files 等计数search calendar 空结果会回填请求 offset date/id避免空月份重复拉取default tag、sticker/custom emoji 与 extended media 均只返回空/notModified 或明确错误,不伪造 paid media 或 tag 绑定状态poll/todo mutating 入口在缺少媒体 store 时返回可解释错误,不伪造 `updateMessagePoll` 或 todo service messageweb preview 返回 `messageMediaEmpty` 且不会抓外网,`sendMedia(inputMediaWebPage)` 降级为纯文本发送,真实 photo/document/poll/album、todo、scheduled store 留待后续模型。
- legacy chat 管理入口 `messages.getChats/getFullChat/addChatUser/deleteChatUser/editChatTitle/editChatPhoto/editChatAdmin/editChatAbout/editChatDefaultBannedRights/editChatParticipantRank` 统一映射到 megagroup/channel 语义;其中 `about` 与 default banned rights 真实持久化default banned rights 参与普通成员发送和邀请权限校验。`editChatCreator` 已显式注册并校验 peer/user但账号 2FA/SRP 与所有权转移事务未接入前返回可解释的密码错误,不进入 fallback。
## Reference Audit
@ -114,7 +114,7 @@ username 与管理项方面,参考实现 的 `channels.checkUsername` 只校
参考实现 的 `messages.toggleNoForwards` 已实现 channel 路径:先校验 access hash再发布 `ToggleChannelNoForwardsCommand`,通过 channel 聚合返回 updates`messages.setChatAvailableReactions``messages.setChatTheme` handler 仍未实现,但 `ChannelFullReadModel/ChannelFullMapper` 已有 `ReactionType/AvailableReactions/ReactionsLimit``chatReactions*` 的映射。telesrv 因此不把 reactions 当纯兼容噪声,而是持久化到 channel setting并在 full channel 中恢复给 TDesktop。
参考实现 对 TDesktop 长尾入口多采用空/BoolTrue 响应:`readMessageContents/reportSpam/setEmojiStickers/getInactiveChannels/getChannelRecommendations/checkSearchPostsFlood/setMainProfileTab` 等直接返回兼容值;`channels.getMessageAuthor` handler 存在但未实现。参考实现 的 `messages.readMessageContents` / `channels.readMessageContents` 会先查当前用户可见消息、过滤 mention/media_unread/reaction 内容状态,再向 not-me 推 `updateReadMessagesContents``updateChannelReadMessagesContents`;未找到 `channels.getMessageAuthor` 对应实现。telesrv 当前没有 media/reaction content-read 持久表,因此 private 与 channel 都只用现有 exact-id message store 校验可见消息,并把存在的 id 推给当前用户其它在线 session缺失 id 仍按参考项目返回当前 pts/BoolTrue`getMessageAuthor` 则按 Layer225/TDesktop monoforum 右键菜单的最小可用语义,只查可见 channel message 的 `SenderUserID` 并返回 user完整 monoforum 管理员权限留后续模型。`deleteParticipantHistory` 做管理员权限后按固定 page size 分批删除;`searchPosts` 按 参考实现 的 PublicPosts 语义落到本项目公开 username channel_messages 查询,只返回公开频道/超级群文本消息,满页用 `next_rate + offset_peer + offset_id` seek 翻页,付费 flood/限额继续由 `checkSearchPostsFlood` 免费 stub 承接;`getInactiveChannels` 按 TDesktop Premium 限额弹窗消费方式返回当前用户 active 频道/超级群,`dates[i]``chats[i]` 对齐并按最久未活跃排序;`getChannelRecommendations` 则从空响应提升为公开 broadcast channel 推荐,指定来源时排除 source无来源时排除当前账号已加入频道`getGroupsForDiscussion/setDiscussionGroup` 是讨论组链路。telesrv 借鉴其删除边界:按 sender 分页取一批 message id生成一条 channel delete update通过 `offset` 提示客户端续删,禁止一次性展开超大历史。
参考实现 对 TDesktop 长尾入口多采用空/BoolTrue 响应:`readMessageContents/reportSpam/setEmojiStickers/getInactiveChannels/getChannelRecommendations/checkSearchPostsFlood/setMainProfileTab` 等直接返回兼容值;`channels.getMessageAuthor` handler 存在但未实现。参考实现 的 `messages.readMessageContents` / `channels.readMessageContents` 会先查当前用户可见消息、过滤 mention/media_unread/reaction 内容状态,再向 not-me 推 `updateReadMessagesContents``updateChannelReadMessagesContents`;未找到 `channels.getMessageAuthor` 对应实现。telesrv 已为 private 接入 owner 侧 `media_unread/reaction_unread` 持久状态与 durable `read_message_contents`channel mention/media unread 则来自 `channel_unread_mentions` viewer-statechannel unread reaction 继续由 `channels.readMessageContents/readReactions` 清理`getMessageAuthor` 则按 Layer225/TDesktop monoforum 右键菜单的最小可用语义,只查可见 channel message 的 `SenderUserID` 并返回 user完整 monoforum 管理员权限留后续模型。`deleteParticipantHistory` 做管理员权限后按固定 page size 分批删除;`searchPosts` 按 参考实现 的 PublicPosts 语义落到本项目公开 username channel_messages 查询,只返回公开频道/超级群文本消息,满页用 `next_rate + offset_peer + offset_id` seek 翻页,付费 flood/限额继续由 `checkSearchPostsFlood` 免费 stub 承接;`getInactiveChannels` 按 TDesktop Premium 限额弹窗消费方式返回当前用户 active 频道/超级群,`dates[i]``chats[i]` 对齐并按最久未活跃排序;`getChannelRecommendations` 则从空响应提升为公开 broadcast channel 推荐,指定来源时排除 source无来源时排除当前账号已加入频道`getGroupsForDiscussion/setDiscussionGroup` 是讨论组链路。telesrv 借鉴其删除边界:按 sender 分页取一批 message id生成一条 channel delete update通过 `offset` 提示客户端续删,禁止一次性展开超大历史。
参考实现 的 `channels.getAdminLog` 当前返回空结果,但保留了 read model/query 模型:按 `channel_id`、action types、skip/limit 拉取 admin log event。telesrv 采用“有真实 event store、但只实现 TDesktop 首批可见 action”的路线避免管理入口只能打开空页。
@ -165,9 +165,9 @@ username 与管理项方面,参考实现 的 `channels.checkUsername` 只校
- `ChannelMember.AvailableMinPts`:当前成员可恢复 channel difference 的 pts 下界;新加入/导入/受邀/重新加入成员初始化为加入前 `channels.pts``updates.getChannelDifference(pts=0)` 也会先抬到该值,避免入群前消息类 durable event 泄漏。
- `ChannelMember.ReadInboxDate`:当前成员最后一次推进 `read_inbox_max_id` 的时间,用于 `messages.getMessageReadParticipants` 返回 `readParticipantDate.date`;不参与 channel pts。
- `ChannelMember.SlowmodeLastSendDate`:普通成员最近一次成功发言时间,用于服务端按 channel 维度返回 `SLOWMODE_WAIT_X`creator/admin 不受首批 slowmode 限制。
- `ChannelMessage``ChannelID``ID``RandomID``SenderUserID``From``SendAs``Date``EditDate``Post``Silent``NoForwards``Body``Entities``ReplyTo``Forward``Action``Pts``Deleted`
- `ChannelMessage``ChannelID``ID``RandomID``SenderUserID``From``SendAs``Date``EditDate``Post``Silent``NoForwards``Body``Entities``ReplyTo``Forward``Action``Pts``Deleted``Mentioned` / `MediaUnread` 是按当前 viewer 回填的瞬时字段,不是全局消息真值
- `ChannelDialog`:当前 user 对 channel 的会话摘要,保存 `FolderID``Pinned``PinnedOrder``TopMessageID``ReadInboxMaxID``ReadOutboxMaxID``UnreadCount``UnreadMentions``UnreadMark``ViewForumAsMessages``NotifySettings`。channel message id 是 channel-scoped跨频道 dialog 排序不能只看 `top_message_id`;统一按 `pinned DESC, pinned_order DESC, top_message_date DESC, top_message_id DESC, channel_id DESC` 排序,并用 `offset_date + offset_id + offset_peer(channel_id)` 做 seek cursor。folder include/exclude/read/archive/type 条件必须在 SQL `LIMIT` 前下推,避免 TDesktop dialogs 翻页重复顶部频道或自定义分组漏掉旧频道。
- `ChannelUnreadMention`:按 `(user_id, channel_id, message_id)` 保存未读提及,`top_message_id` 支持 thread/topic 级清除;它是 owner 视角状态,不写入 `channel_update_events``messages.readMentions` 返回当前 channel pts 供客户端清本地 badge。
- `ChannelUnreadMention`:按 `(user_id, channel_id, message_id)` 保存未读提及,`top_message_id` 支持 thread/topic 级清除`media_unread` 表示该未读提及同时对应媒体内容未读;它是 owner 视角状态,不写入 `channel_update_events``messages.readMentions` 返回当前 channel pts 供客户端清本地 badge。
- `ChannelUpdateEvent``ChannelID``Pts``PtsCount``Type``Date``MessageID``MessageIDs``SenderUserID``UserIDs``Payload`
- `ChannelAdminLogEvent``ChannelID``ID``UserID``Date``Type`、前后字符串/布尔/整数、前后 participant、相关 message、`Query`;用于 `channels.getAdminLog`,不参与 channel pts。
@ -192,7 +192,7 @@ username 与管理项方面,参考实现 的 `channels.checkUsername` 只校
| `channel_message_reactions` | hash(`channel_id`) | `(channel_id,message_id,reacted_user_id,reaction)` 当前 reaction 状态,支持 `sendReaction/getMessagesReactions/getMessageReactionsList` |
| `private_message_reactions` | message id fk | `(private_message_id,user_id,reaction)` 当前私聊 reaction 状态,双端 message box 共享聚合,支持 private `sendReaction/getMessagesReactions/getMessageReactionsList` |
| `user_saved_reaction_tags` | user_id prefix | 账号级 saved-message reaction tag 标题;支持 `messages.getSavedReactionTags/updateSavedReactionTag`,不承载 message assignment/count |
| `channel_unread_mentions` | hash(`user_id`) | owner 视角未读提及索引,支持 `messages.getUnreadMentions/readMentions`,不复制 channel message body |
| `channel_unread_mentions` | hash(`user_id`) | owner 视角未读提及/媒体未读索引,支持 `messages.getUnreadMentions/readMentions`,不复制 channel message body |
| `channel_update_events` | hash(`channel_id`) | channel pts durable log`updates.getChannelDifference` |
| `channel_dialogs` | hash(`user_id`) | 当前账号对 channel 的 dialog/read/folder/pin/mute 摘要 |
| `dialog_drafts` | hash(`user_id`) | user/channel peer 云草稿;支持 forum `top_msg_id`,不复制 channel message |
@ -213,7 +213,7 @@ username 与管理项方面,参考实现 的 `channels.checkUsername` 只校
- `channel_messages(discussion_channel_id, discussion_message_id) WHERE discussion_channel_id <> 0 AND discussion_message_id <> 0 AND NOT deleted` 支持 broadcast post 到 discussion root 的反查与排查。
- `channel_message_viewers(channel_id, message_id, viewer_user_id)` 主键去重;`messages.getMessagesViews` 先按最多 100 个 id 过滤可见 `channel_messages`,只对首次插入的 viewer 更新 `channel_messages.views_count`,不按 viewer 做 count 聚合扫描。
- `channel_message_reactions(channel_id, message_id, reaction_date DESC, reacted_user_id DESC, reaction_value ASC)` 支持最近 reaction 回填和列表 seek`(channel_id,message_id,reaction_type,reaction_value,reaction_date DESC,reacted_user_id DESC)` 支持按单个 emoji 过滤,不使用 SQL OFFSET。
- `channel_unread_mentions(user_id, channel_id, top_message_id, message_id DESC)` 支持当前账号 unread mentions seek发送时只为解析出的 active/可见/未读成员插入,单条消息最多 100 个候选,清除时单批最多 1000 条。
- `channel_unread_mentions(user_id, channel_id, top_message_id, message_id DESC)` 支持当前账号 unread mentions seek发送/编辑时只为解析出的 active/可见/未读成员插入,单条消息最多 100 个候选,清除时单批最多 1000 条`media_unread` 随同一行保存,供 history/getMessages/getChannelDifference/online update 按 viewer 回填 `message.media_unread`
- `channel_update_events(channel_id, pts)` 主键;`(channel_id, pts)` 升序扫描差量。
- `channel_dialogs(user_id, folder_id, pinned DESC, pinned_order DESC, top_message_date DESC, top_message_id DESC, channel_id DESC)` 支持 dialogs seek`default_send_as_peer_type/default_send_as_peer_id` 只保存当前 owner 的默认发送身份,不参与列表排序。
- `dialog_drafts(user_id, date DESC, peer_type, peer_id, top_message_id)` 支持 `messages.getAllDrafts/clearAllDrafts` 有界扫描draft 内容用 domain JSON 保存,业务层不持有 `tg.*`
@ -237,6 +237,16 @@ Redis miss 恢复来源:
分配必须使用 Redis Lua 的初始化+递增原子脚本;批量 delete/clear 这类 `pts_count>1` 的操作必须一次性分配连续 rangePG fallback 也要实现 `NextChannelPtsN(current+count)`,不能用多次读取 `MAX(pts)+1` 模拟。事务失败但 pts 已分配时必须写 `noop` channel update 占位,避免 TDesktop channel PtsWaiter 永久 gap。
## Viewer State / Difference Nudge
`tg.Message.mentioned``media_unread` 对 channel/supergroup 来说是 viewer-specific 状态,来源只允许是 `channel_unread_mentions` 等 owner 视角 unread 表,不写入 `channel_messages` 全局行:
1. 发送或编辑 channel message 时RPC 层解析 mention-name entity / `@username`store 只为 active、可见、未读且不是 sender 自己的成员写 unread mention。带媒体的 mention 同行保存 `media_unread=true`
2. `messages.getHistory``messages.getMessages``updates.getChannelDifference` 与在线 `updateNewChannelMessage/updateEditChannelMessage` 都按当前 viewer 重取 unread mention 状态,再设置 TL `mentioned/media_unread`。未被 mention 的 viewer 看到同一条消息时两个 flag 必须为 false。
3. `messages.readMentions` 清理当前 owner 的 unread mention 后,后续 history/difference 不再带 `mentioned/media_unread`。编辑移除 mention 会删除旧 unread mention新增 mention 会补写新 owner 的 unread mention供在线和离线差量恢复。
4. 账号级 `updates.getDifference` 不承载 channel message 本体。若请求 date 之后当前用户 active joined channel 有 `channel_update_events`RPC 层追加计算型 `updateChannelTooLong(channel_id, pts)` nudge提示 TDesktop 随后调用 `updates.getChannelDifference`;该 nudge 不写 `user_update_events`,不推进账号 pts也不影响 account difference 连续性。
5. create/invite/join/leave/import/hide request 等 membership/state 操作即使没有 service message pts也必须在 RPC response 和在线推送里带 `updateChannel`。megagroup 有可见 service message 时返回/推送 `updateNewChannelMessage` 占 channel pts同时追加 `updateChannel` 刷新 participant count/self statebroadcast invite/join/leave 不占 channel pts但仍带 viewer-specific chats 与 `updateChannel`
## Create Flow
`messages.createChat`