diff --git a/docs/compatibility-matrix.md b/docs/compatibility-matrix.md index 2a7f9b05..13f316ff 100644 --- a/docs/compatibility-matrix.md +++ b/docs/compatibility-matrix.md @@ -88,6 +88,13 @@ status 取值:done(真实实现) / stub(兼容响应) / todo(已发现未实 | help.getPromoData | stub | empty | 无 PSA/MTProxy 推广,返回空并设置短期 expires | | help.getTermsOfServiceUpdate | stub | empty | 第一阶段无 TOS 更新 | | help.getPremiumPromo | stub | empty | 第一阶段不做 Premium 展示数据 | +| help.getInviteText | stub | default-text | DrKLO Android 主界面预取邀请文案;当前不接配置中心 invite text,返回默认短文避免启动后 NOT_IMPLEMENTED | + +## Payments / Stars + +| method | status | behavior | note | +|---|---|---|---| +| payments.getStarsStatus | stub | zero-balance | DrKLO Android 消息页会后台预取 stars/TON 余额;当前不实现 Stars 商业模型,按请求类型返回零余额 `payments.starsStatus`,history/chats/users 为空,避免发送后 NOT_IMPLEMENTED | ## Auth @@ -117,7 +124,7 @@ status 取值:done(真实实现) / stub(兼容响应) / todo(已发现未实 | account.setPrivacy | done | real-privacy/online-update | 校验 `InputPrivacyKey/InputPrivacyRule` 与 input user access_hash 后持久化;返回 `account.privacyRules` 并向当前账号在线 session 推 `updatePrivacy`;Stars/Premium/Bot 等高级产品只保存规则,不扩展商业模型。离线 `updates.getDifference` 恢复 `updatePrivacy` 仍需扩展 `user_update_events` privacy payload | | account.getAuthorizations / account.resetAuthorization | done | real-devices | 从 `authorizations` 返回当前账号设备列表,带 current/hash/device/app/ip/date;resetAuthorization 按 hash 删除同账号授权并清理 session/auth cache | | account.getWebAuthorizations / account.resetWebAuthorization / account.resetWebAuthorizations | stub | empty/ack | 当前无 Telegram Login Widget/Web 授权模型,返回空列表并接受清理请求 | -| account.registerDevice / account.unregisterDevice | stub | ack | 当前未接真实 APNS/FCM 推送网关,接受 TDesktop 设备 token 注册/注销避免后台噪声 | +| account.registerDevice / account.unregisterDevice | stub | ack+legacy-adapter | 当前未接真实 APNS/FCM 推送网关,接受 TDesktop/DrKLO 设备 token 注册/注销避免后台噪声;DrKLO Android 仍会发 legacy `account.registerDevice#637ea878 token_type:int token:string`,RPC edge 解码后返回 true | | account.getDefaultEmojiStatuses / account.getRecentEmojiStatuses / account.clearRecentEmojiStatuses / account.updateEmojiStatus | stub | empty/notModified | 当前不提供云 emoji status;读取返回空或 notModified,写入/清理返回成功 | | account.getCollectibleEmojiStatuses | stub | empty | TDesktop 启动/emoji status 面板会刷新 collectible gift emoji statuses;当前无 gift/status 模型,返回空 `account.emojiStatuses` | | account.getDefaultProfilePhotoEmojis / account.getDefaultGroupPhotoEmojis / account.getDefaultBackgroundEmojis / account.getChannelDefaultEmojiStatuses / account.getChannelRestrictedStatusEmojis | stub | empty | TDesktop 头像/背景/频道状态编辑页默认 emoji 入口;当前无 custom-emoji 候选,返回空 `emojiList` 或空 statuses | @@ -131,6 +138,7 @@ status 取值:done(真实实现) / stub(兼容响应) / todo(已发现未实 | account.getAutoDownloadSettings / account.saveAutoDownloadSettings | stub | defaults/ack | TDesktop 自动下载设置预取;返回保守默认下载阈值,写入接受但不持久化 | | account.setAuthorizationTTL / account.changeAuthorizationSettings | stub | ack | Settings 设备管理长尾写入入口;当前只维护 active authorization 删除,不持久化授权 TTL/设备名设置 | | account.getSavedMusicIds | stub | empty | TDesktop saved/profile music 预取;当前没有 profile music media store,返回空 id vector | +| account.getSavedRingtones | stub | empty | DrKLO Android 主界面通知音预取;当前无 saved ringtone store,返回空 `account.savedRingtones` | | account.updateStatus | done | persisted-presence | 记录 `auth_key/session/user` 维度运行时在线状态并持久化 `users.last_seen_at`;`offline=false` 与重连后 session 身份恢复都会写入 last_seen 并推送 `updateUserStatus(userStatusOnline expires=now+5m)`,`offline=true` 写入精确 `userStatusOffline.was_online`;最后一个 MTProto session 断开/destroy_session 也会写 last_seen 并推送 offline。状态推送给当前用户其它在线 session、在线联系人、以及已有私聊 dialog 的在线对端;session 恢复时还会向当前 session 补发在线联系人/私聊对端状态。users/dialog/history projection 已按 StatusTimestamp privacy 裁剪返回值;presence fanout 的精细 privacy 过滤仍为后续项 | ## Updates @@ -138,7 +146,7 @@ status 取值:done(真实实现) / stub(兼容响应) / todo(已发现未实 | method | status | behavior | note | |---|---|---|---| | updates.getState | done | real | auth_key+user 维度持久化 update_states;账号级当前 pts 报告**最大连续已提交 pts**(MaxContiguousPts,非 allocator 最大已分配值),避免同设备换号/多账号串差分,也避免越过在途空洞 | -| updates.getDifference | done | real | 按 user_id 从 user_update_events 拉取 pts 后增量;只返回从客户端 pts 起**连续**的事件(遇在途空洞即截断),超 100 条置 differenceSlice;支持 new_message、read_history_inbox/read_history_outbox(私聊与 channel peer,channel read 映射 updateReadChannelInbox)、edit_message、message_reactions(输出带最新 `message.reactions` 的 affected message + `updateMessageReactions`,并按 viewer 重取最新聚合,避免 TDesktop 离线恢复时本地 message cache 不刷新)、read_message_contents、delete_messages、contacts_reset、dialog pinned/order/manual unread、peer_settings、dialog filters/folder peers 与 noop gap;消息事件携带 fwd/reply 所需 users/chats,payload 来自 durable log。若请求 date 后存在当前账号 active channel 的 channel durable events,会追加计算型 `updateChannelTooLong(channel_id,pts)` nudge,提示 TDesktop 再走 `updates.getChannelDifference`;该 nudge 不写 `user_update_events`,不消耗账号 pts。**账号级绝不返回 `differenceTooLong`**:已核对 TDesktop 基线 `api_updates.cpp:516`——对账号级 differenceTooLong 只打一行日志、不读 pts 且漏 `setRequesting(false)`,会永久锁死 update 引擎(`:689` 早退,重连/新 session 不可恢复);落后客户端改用 `differenceSlice` 续传。因此 `user_update_events` **永久保留、不做 retention 裁剪**(对齐 参考实现),详见 docs/performance-audit.md 附录 C | +| updates.getDifference | done | real+legacy-adapter | 按 user_id 从 user_update_events 拉取 pts 后增量;只返回从客户端 pts 起**连续**的事件(遇在途空洞即截断),超 100 条置 differenceSlice;支持 new_message、read_history_inbox/read_history_outbox(私聊与 channel peer,channel read 映射 updateReadChannelInbox)、edit_message、message_reactions(输出带最新 `message.reactions` 的 affected message + `updateMessageReactions`,并按 viewer 重取最新聚合,避免 TDesktop 离线恢复时本地 message cache 不刷新)、read_message_contents、delete_messages、contacts_reset、dialog pinned/order/manual unread、peer_settings、dialog filters/folder peers 与 noop gap;消息事件携带 fwd/reply 所需 users/chats,payload 来自 durable log。若请求 date 后存在当前账号 active channel 的 channel durable events,会追加计算型 `updateChannelTooLong(channel_id,pts)` nudge,提示 TDesktop 再走 `updates.getChannelDifference`;该 nudge 不写 `user_update_events`,不消耗账号 pts。DrKLO Android 仍会发 legacy `#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int`,RPC edge 映射到当前 Layer225 request 后复用本实现。**账号级绝不返回 `differenceTooLong`**:已核对 TDesktop 基线 `api_updates.cpp:516`——对账号级 differenceTooLong 只打一行日志、不读 pts 且漏 `setRequesting(false)`,会永久锁死 update 引擎(`:689` 早退,重连/新 session 不可恢复);落后客户端改用 `differenceSlice` 续传。因此 `user_update_events` **永久保留、不做 retention 裁剪**(对齐 参考实现),详见 docs/performance-audit.md 附录 C | | updates.getChannelDifference | done | real-channel | 超级群/频道使用 channel 维度 durable log:`channel_update_events(channel_id, pts, pts_count, ...)`;按 channel pts 返回 `channelDifferenceEmpty/channelDifference/channelDifferenceTooLong`,limit cap=100;`pts < 0` 或 `pts > current_channel_pts` 返回 `PERSISTENT_TIMESTAMP_INVALID`;当前 member 的 `available_min_pts` 会抬高请求 pts,避免新加入/重新加入成员拉到入群前消息类 durable 事件;公共 username 频道允许非成员以只读预览身份拉可见差分并返回 synthetic read dialog,私有频道和禁看用户仍返回权限错误;当 `current_channel_pts-pts > cap` 时返回带当前 dialog pts 和最新有界消息快照的 `channelDifferenceTooLong`,避免大频道旧 pts 客户端循环拉大量页;普通 difference 优先使用事件 payload 中的 message 快照,连续编辑/删除后不会被当前消息状态污染;`updateChannelParticipant` 不进入 channel pts log,成员权限/封禁状态靠在线 `updateChannelParticipant/updateChannel` 与 `channels.getFullChannel/getParticipants/getParticipant` 刷新;本页消息的 sender/send_as/fwd_from/reply_to/action peers 会随 users/chats 返回,禁止复用 user_update_events | ### 兼容硬约束:pts / pts_count(跨所有产生 update 的 RPC) @@ -234,6 +242,7 @@ outbox 多 worker 并发 + 发送事务乱序提交 → **主动推送可能乱 | messages.getRecentStickers | stub | empty/notModified | TDesktop 输入框/emoji 面板预取最近贴纸;第一阶段返回空列表或 hash notModified | | messages.getFavedStickers | stub | empty/notModified | TDesktop 输入框/emoji 面板预取收藏贴纸;第一阶段返回空列表或 hash notModified | | messages.getStickers | stub | notModified | 第一阶段不提供贴纸搜索结果 | +| messages.getArchivedStickers | stub | empty | DrKLO Android 聊天页/贴纸设置后台预取归档贴纸;当前无 archived sticker store,返回空 `messages.archivedStickers`,避免后台 NOT_IMPLEMENTED 重试 | | messages.getStickerSet | done | real-sets | `stickerSetRefFromInput` 把 `inputStickerSetID/ShortName/AnimatedEmoji/Dice/EmojiGenericAnimations` 解析为 `domain.StickerSetRef`,从 `sticker_sets` 加载集元数据 + 按 `document_ids` 顺序加载真实文档(含 packs;document id / pack document id / thumb_document_id 均为 seed 阶段归一后的服务端 id),返回完整 `messages.stickerSet`;hash 命中返回 notModified;未 seed 的系统集/未知短名回退空集 stub 避免破坏客户端 | | messages.getFeaturedStickers | stub | empty/notModified | TDesktop 输入框/emoji 面板预取推荐贴纸;第一阶段返回空列表或 hash notModified | | messages.getEmojiGroups | stub | notModified | 第一阶段不提供 emoji 分组目录 | @@ -375,6 +384,7 @@ outbox 多 worker 并发 + 发送事务乱序提交 → **主动推送可能乱 | contacts.block | done | real-blocklist | 写入当前 owner blocklist,幂等;同步刷新 peer settings,story-only block flag 当前按主 blocklist 处理,完整 stories privacy 留后续 | | contacts.unblock | done | real-blocklist | 从当前 owner blocklist 删除 peer,幂等;同步刷新 peer settings | | contacts.getBlocked | done | real-blocklist | Settings 隐私/安全预取;`contacts.block/unblock/getBlocked` 维护 `owner_user_id + blocked_user_id` 唯一 blocklist,limit cap=100,按 date/user_id 返回 `peerBlocked` + users;`contacts.getPeerSettings` 按当前 owner block 状态返回 block/unblock action,并按 PhoneNumber privacy 决定 `ShareContact`。blocklist 仍是私聊 send/edit/delete 的拒绝 gate,完整 privacy key 用于资料投影/手机号分享 | +| contacts.getBirthdays | stub | empty | DrKLO Android 主界面周期性预取联系人生日;当前无 birthday reminder store,返回空 contacts/users | | contacts.getTopPeers | stub | disabled | 第一阶段不维护 top peers 统计 | | contacts.getSponsoredPeers | stub | empty | 第一阶段不做 sponsored peers,TDesktop 搜索框分支返回 sponsoredPeersEmpty | | users.getUsers | done | real | InputUserSelf 与已知 InputUser 返回用户(含 777000 官方账号);未登录则跳过(空列表) | @@ -390,6 +400,8 @@ outbox 多 worker 并发 + 发送事务乱序提交 → **主动推送可能乱 | stories.getStoriesArchive | stub | empty | TDesktop 主界面后台拉 archived stories;第一阶段返回空 archive | | stories.getPinnedStories | stub | empty | TDesktop 资料页 stories 区域后台查询;当前返回空 `stories.stories` | | stories.getAlbums | stub | empty | TDesktop 资料页 story albums 后台查询;当前无 story album 模型,返回空 `stories.albums` | +| stories.getAllReadPeerStories | stub | empty-updates | DrKLO Android 登录后拉已读 story 状态;当前无 story read store,返回空 updates | +| stories.getPeerMaxIDs | stub | empty-recent | DrKLO Android 登录后按 peer 拉已读 story max id;当前无 story read store,按输入 peer 数返回空 `recentStory` | | stories.sendReaction | stub | validated-noop | 当前无 story store;校验 peer/story_id/reaction,`add_to_recent` 会更新账号 top/recent emoji reaction,返回空 updates,避免 story 反应入口落 unknown | | payments.getStarGiftActiveAuctions | stub | notModified | 第一阶段不做 stars/gifts | | payments.getSavedStarGifts | stub | empty | TDesktop 资料页 gifts 区域后台查询;当前返回空 `payments.savedStarGifts` | diff --git a/internal/rpc/account.go b/internal/rpc/account.go index 4f3a6264..d3faa2d9 100644 --- a/internal/rpc/account.go +++ b/internal/rpc/account.go @@ -4,12 +4,15 @@ import ( "context" "errors" + "github.com/gotd/td/bin" "github.com/gotd/td/tg" "telesrv/internal/compat/tdesktop" "telesrv/internal/domain" ) +const legacyAccountRegisterDeviceTypeID uint32 = 0x637ea878 + // registerAccount 注册 account.* RPC handler。 func (r *Router) registerAccount(d *tg.ServerDispatcher) { d.OnAccountRegisterDevice(func(ctx context.Context, req *tg.AccountRegisterDeviceRequest) (bool, error) { @@ -136,6 +139,12 @@ func (r *Router) registerAccount(d *tg.ServerDispatcher) { } return &tg.AccountSavedMusicIDs{IDs: []int64{}}, nil }) + d.OnAccountGetSavedRingtones(func(ctx context.Context, hash int64) (tg.AccountSavedRingtonesClass, error) { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + return &tg.AccountSavedRingtones{Hash: 0, Ringtones: []tg.DocumentClass{}}, nil + }) d.OnAccountGetAccountTTL(r.onAccountGetAccountTTL) d.OnAccountSetAccountTTL(func(ctx context.Context, ttl tg.AccountDaysTTL) (bool, error) { return true, nil @@ -204,6 +213,23 @@ func (r *Router) onAccountGetPasswordSettings(ctx context.Context, password tg.I return tgPasswordSettings(settings), nil } +func (r *Router) handleLegacyAccountRegisterDevice(ctx context.Context, b *bin.Buffer) (bin.Encoder, error) { + if err := b.ConsumeID(legacyAccountRegisterDeviceTypeID); err != nil { + return nil, err + } + if _, err := b.Int(); err != nil { + return nil, err + } + if _, err := b.String(); err != nil { + return nil, err + } + _, _, err := r.currentUserID(ctx) + if err != nil { + return nil, err + } + return &tg.BoolTrue{}, nil +} + func (r *Router) onAccountUpdatePasswordSettings(ctx context.Context, req *tg.AccountUpdatePasswordSettingsRequest) (bool, error) { userID, _, err := r.currentUserID(ctx) if err != nil { diff --git a/internal/rpc/compat.go b/internal/rpc/compat.go index e6171695..f43e6a25 100644 --- a/internal/rpc/compat.go +++ b/internal/rpc/compat.go @@ -21,6 +21,12 @@ func (r *Router) dispatchCompat(ctx context.Context, b *bin.Buffer, id uint32) ( ) switch id { + case legacyAccountRegisterDeviceTypeID: + name = "account.registerDevice#637ea878" + enc, err = r.handleLegacyAccountRegisterDevice(ctx, b) + case legacyUpdatesGetDifferenceTypeID: + name = "updates.getDifference#25939651" + enc, err = r.handleLegacyUpdatesGetDifference(ctx, b) case legacyLangpackGetLangPackTypeID: name = "langpack.getLangPack#9ab5c58e" enc, err = r.handleLegacyLangpackGetLangPack(ctx, b) diff --git a/internal/rpc/contacts.go b/internal/rpc/contacts.go index 8ecbf730..25afe230 100644 --- a/internal/rpc/contacts.go +++ b/internal/rpc/contacts.go @@ -7,6 +7,7 @@ import ( "unicode/utf8" "github.com/gotd/td/tg" + "go.uber.org/zap" "telesrv/internal/app/contacts" "telesrv/internal/compat/tdesktop" @@ -42,6 +43,12 @@ func (r *Router) registerContacts(d *tg.ServerDispatcher) { return tdesktop.TopPeers(), nil }) d.OnContactsGetBlocked(r.onContactsGetBlocked) + d.OnContactsGetBirthdays(func(ctx context.Context) (*tg.ContactsContactBirthdays, error) { + if _, _, err := r.currentUserID(ctx); err != nil { + return nil, internalErr() + } + return &tg.ContactsContactBirthdays{Contacts: []tg.ContactBirthday{}, Users: []tg.UserClass{}}, nil + }) d.OnContactsGetSponsoredPeers(func(ctx context.Context, q string) (tg.ContactsSponsoredPeersClass, error) { if utf8.RuneCountInString(q) > maxContactSearchQLen { return nil, limitInvalidErr() @@ -250,6 +257,7 @@ func (r *Router) onContactsImportContacts(ctx context.Context, input []tg.InputP } res, err := r.deps.Contacts.ImportContacts(ctx, userID, items) if err != nil { + r.log.Warn("contacts.importContacts service failed", append(r.contextLogFields(ctx), zap.Error(err), zap.Int("contacts", len(items)))...) return nil, internalErr() } out := &tg.ContactsImportedContacts{ @@ -267,18 +275,22 @@ func (r *Router) onContactsImportContacts(ctx context.Context, input []tg.InputP peer := domain.Peer{Type: domain.PeerTypeUser, ID: contact.User.ID} settings, err := r.deps.Contacts.GetPeerSettings(ctx, userID, peer) if err != nil { + r.log.Warn("contacts.importContacts peer settings failed", append(r.contextLogFields(ctx), zap.Error(err), zap.Int64("peer_user_id", contact.User.ID))...) return nil, internalErr() } if err := r.recordPeerSettings(ctx, userID, peer, settings); err != nil { + r.log.Warn("contacts.importContacts record peer settings failed", append(r.contextLogFields(ctx), zap.Error(err), zap.Int64("peer_user_id", contact.User.ID))...) return nil, internalErr() } if contact.Mutual { if err := r.recordAcceptedContactTargetUpdates(ctx, userID, contact.User.ID); err != nil { + r.log.Warn("contacts.importContacts record accepted target failed", append(r.contextLogFields(ctx), zap.Error(err), zap.Int64("peer_user_id", contact.User.ID))...) return nil, err } } } if err := r.recordContactsReset(ctx, userID); err != nil { + r.log.Warn("contacts.importContacts record contacts reset failed", append(r.contextLogFields(ctx), zap.Error(err), zap.Int("contacts", len(items)))...) return nil, internalErr() } r.pushContactsReset(ctx, userID) diff --git a/internal/rpc/help.go b/internal/rpc/help.go index 8d2b713e..28c6639f 100644 --- a/internal/rpc/help.go +++ b/internal/rpc/help.go @@ -16,6 +16,9 @@ func (r *Router) registerHelp(d *tg.ServerDispatcher) { d.OnHelpGetNearestDC(func(ctx context.Context) (*tg.NearestDC, error) { return tdesktop.NearestDC(r.cfg.DC), nil }) + d.OnHelpGetInviteText(func(ctx context.Context) (*tg.HelpInviteText, error) { + return &tg.HelpInviteText{Message: "Join me on Telegram."}, nil + }) d.OnHelpGetAppConfig(func(ctx context.Context, hash int) (tg.HelpAppConfigClass, error) { if r.deps.Help == nil { return tdesktop.AppConfig(hash), nil diff --git a/internal/rpc/langpack_compat_test.go b/internal/rpc/langpack_compat_test.go index 296e0fee..bb37551b 100644 --- a/internal/rpc/langpack_compat_test.go +++ b/internal/rpc/langpack_compat_test.go @@ -79,6 +79,60 @@ func TestLegacyLangpackGetStrings(t *testing.T) { } } +func TestLegacyUpdatesGetDifference(t *testing.T) { + r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System) + + var flags bin.Fields + flags.Set(0) + var in bin.Buffer + in.PutID(legacyUpdatesGetDifferenceTypeID) + if err := flags.Encode(&in); err != nil { + t.Fatalf("encode flags: %v", err) + } + in.PutInt(10) + in.PutInt(100) + in.PutInt(123456) + in.PutInt(0) + + enc, err := r.Dispatch(WithUserID(androidClientContext(), 1000000001), [8]byte{}, 0, &in) + if err != nil { + t.Fatalf("dispatch legacy updates.getDifference: %v", err) + } + var out bin.Buffer + if err := enc.Encode(&out); err != nil { + t.Fatalf("encode response: %v", err) + } + var diff tg.UpdatesDifferenceEmpty + if err := diff.Decode(&out); err != nil { + t.Fatalf("decode response: %v", err) + } +} + +func TestLegacyAccountRegisterDevice(t *testing.T) { + r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System) + + var in bin.Buffer + in.PutID(legacyAccountRegisterDeviceTypeID) + in.PutInt(2) + in.PutString("android-fcm-token") + + enc, err := r.Dispatch(WithUserID(androidClientContext(), 1000000001), [8]byte{}, 0, &in) + if err != nil { + t.Fatalf("dispatch legacy account.registerDevice: %v", err) + } + var out bin.Buffer + if err := enc.Encode(&out); err != nil { + t.Fatalf("encode response: %v", err) + } + ok, err := tg.DecodeBool(&out) + if err != nil { + t.Fatalf("decode response: %v", err) + } + if _, ok := ok.(*tg.BoolTrue); !ok { + t.Fatalf("legacy account.registerDevice = false, want true") + } +} + func assertLangpackLanguages(t *testing.T, r *Router, ctx context.Context, in *bin.Buffer) { t.Helper() enc, err := r.Dispatch(ctx, [8]byte{}, 0, in) diff --git a/internal/rpc/messages.go b/internal/rpc/messages.go index ad6f35b7..28730317 100644 --- a/internal/rpc/messages.go +++ b/internal/rpc/messages.go @@ -183,6 +183,12 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) { d.OnMessagesGetStickers(func(ctx context.Context, req *tg.MessagesGetStickersRequest) (tg.MessagesStickersClass, error) { return tdesktop.Stickers(), nil }) + d.OnMessagesGetArchivedStickers(func(ctx context.Context, req *tg.MessagesGetArchivedStickersRequest) (*tg.MessagesArchivedStickers, error) { + return &tg.MessagesArchivedStickers{ + Count: 0, + Sets: []tg.StickerSetCoveredClass{}, + }, nil + }) d.OnMessagesGetStickerSet(r.onMessagesGetStickerSet) d.OnMessagesGetEmojiGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) { return tdesktop.EmojiGroups(), nil diff --git a/internal/rpc/payments.go b/internal/rpc/payments.go index 39f7aa92..eabfb288 100644 --- a/internal/rpc/payments.go +++ b/internal/rpc/payments.go @@ -10,6 +10,17 @@ import ( // registerPayments 注册第一阶段 TDesktop 启动所需 payments.* RPC 兼容响应。 func (r *Router) registerPayments(d *tg.ServerDispatcher) { + d.OnPaymentsGetStarsStatus(func(ctx context.Context, req *tg.PaymentsGetStarsStatusRequest) (*tg.PaymentsStarsStatus, error) { + balance := tg.StarsAmountClass(&tg.StarsAmount{}) + if req.GetTon() { + balance = &tg.StarsTonAmount{} + } + return &tg.PaymentsStarsStatus{ + Balance: balance, + Chats: []tg.ChatClass{}, + Users: []tg.UserClass{}, + }, nil + }) d.OnPaymentsGetStarGiftActiveAuctions(func(ctx context.Context, hash int64) (tg.PaymentsStarGiftActiveAuctionsClass, error) { return tdesktop.StarGiftActiveAuctions(), nil }) diff --git a/internal/rpc/router_test.go b/internal/rpc/router_test.go index e7a9ac26..cb99b0ec 100644 --- a/internal/rpc/router_test.go +++ b/internal/rpc/router_test.go @@ -401,6 +401,7 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) { {name: "help.getPromoData", req: &tg.HelpGetPromoDataRequest{}}, {name: "help.getTermsOfServiceUpdate", req: &tg.HelpGetTermsOfServiceUpdateRequest{}}, {name: "help.getPremiumPromo", req: &tg.HelpGetPremiumPromoRequest{}}, + {name: "help.getInviteText", req: &tg.HelpGetInviteTextRequest{}}, {name: "auth.initPasskeyLogin", req: &tg.AuthInitPasskeyLoginRequest{APIID: 4, APIHash: "test"}}, {name: "account.getPassword", req: &tg.AccountGetPasswordRequest{}}, {name: "account.getNotifySettings", req: &tg.AccountGetNotifySettingsRequest{Peer: &tg.InputNotifyUsers{}}}, @@ -426,8 +427,10 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) { {name: "account.getPasskeys", req: &tg.AccountGetPasskeysRequest{}}, {name: "account.getAutoDownloadSettings", req: &tg.AccountGetAutoDownloadSettingsRequest{}}, {name: "account.getSavedMusicIds", req: &tg.AccountGetSavedMusicIDsRequest{}}, + {name: "account.getSavedRingtones", req: &tg.AccountGetSavedRingtonesRequest{}}, {name: "account.resetPassword", req: &tg.AccountResetPasswordRequest{}}, {name: "account.updateStatus", req: &tg.AccountUpdateStatusRequest{Offline: true}}, + {name: "payments.getStarsStatus", req: &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerSelf{}}}, {name: "updates.getDifference", req: &tg.UpdatesGetDifferenceRequest{}}, {name: "users.getFullUser", req: &tg.UsersGetFullUserRequest{ID: &tg.InputUserSelf{}}}, {name: "users.getSavedMusic", req: &tg.UsersGetSavedMusicRequest{ID: &tg.InputUserSelf{}, Limit: 20}}, @@ -439,6 +442,7 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) { {name: "messages.getAvailableReactions", req: &tg.MessagesGetAvailableReactionsRequest{}}, {name: "messages.getAvailableEffects", req: &tg.MessagesGetAvailableEffectsRequest{}}, {name: "messages.getStickers", req: &tg.MessagesGetStickersRequest{}}, + {name: "messages.getArchivedStickers", req: &tg.MessagesGetArchivedStickersRequest{Limit: 20}}, {name: "messages.getStickerSet", req: &tg.MessagesGetStickerSetRequest{Stickerset: &tg.InputStickerSetEmpty{}}}, {name: "messages.getEmojiGroups", req: &tg.MessagesGetEmojiGroupsRequest{}}, {name: "messages.getEmojiStickerGroups", req: &tg.MessagesGetEmojiStickerGroupsRequest{}}, @@ -459,12 +463,15 @@ func TestTDesktopStartupRPCsEncode(t *testing.T) { {name: "contacts.getContacts", req: &tg.ContactsGetContactsRequest{}}, {name: "contacts.search", req: &tg.ContactsSearchRequest{Q: "Test", Limit: 20}}, {name: "contacts.getBlocked", req: &tg.ContactsGetBlockedRequest{Limit: 20}}, + {name: "contacts.getBirthdays", req: &tg.ContactsGetBirthdaysRequest{}}, {name: "contacts.getTopPeers", req: &tg.ContactsGetTopPeersRequest{Correspondents: true, Limit: 10}}, {name: "contacts.getSponsoredPeers", req: &tg.ContactsGetSponsoredPeersRequest{Q: "Test"}}, {name: "stories.getAllStories", req: &tg.StoriesGetAllStoriesRequest{}}, {name: "stories.getStoriesArchive", req: &tg.StoriesGetStoriesArchiveRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}}, {name: "stories.getPinnedStories", req: &tg.StoriesGetPinnedStoriesRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}}, {name: "stories.getAlbums", req: &tg.StoriesGetAlbumsRequest{Peer: &tg.InputPeerSelf{}}}, + {name: "stories.getAllReadPeerStories", req: &tg.StoriesGetAllReadPeerStoriesRequest{}}, + {name: "stories.getPeerMaxIDs", req: &tg.StoriesGetPeerMaxIDsRequest{ID: []tg.InputPeerClass{&tg.InputPeerSelf{}}}}, {name: "payments.getStarGiftActiveAuctions", req: &tg.PaymentsGetStarGiftActiveAuctionsRequest{}}, {name: "payments.getSavedStarGifts", req: &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 20}}, {name: "payments.getSavedStarGift", req: &tg.PaymentsGetSavedStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{}}}, diff --git a/internal/rpc/stories.go b/internal/rpc/stories.go index b59d8001..12259d79 100644 --- a/internal/rpc/stories.go +++ b/internal/rpc/stories.go @@ -23,6 +23,13 @@ func (r *Router) registerStories(d *tg.ServerDispatcher) { d.OnStoriesGetAlbums(func(ctx context.Context, req *tg.StoriesGetAlbumsRequest) (tg.StoriesAlbumsClass, error) { return tdesktop.StoryAlbums(), nil }) + d.OnStoriesGetAllReadPeerStories(func(ctx context.Context) (tg.UpdatesClass, error) { + return tgEmptyUpdates(int(r.clock.Now().Unix())), nil + }) + d.OnStoriesGetPeerMaxIDs(func(ctx context.Context, id []tg.InputPeerClass) ([]tg.RecentStory, error) { + out := make([]tg.RecentStory, len(id)) + return out, nil + }) d.OnStoriesSendReaction(r.onStoriesSendReaction) } diff --git a/internal/rpc/updates.go b/internal/rpc/updates.go index b34c6c1a..f2fe915f 100644 --- a/internal/rpc/updates.go +++ b/internal/rpc/updates.go @@ -4,12 +4,14 @@ import ( "context" "time" + "github.com/gotd/td/bin" "github.com/gotd/td/tg" "telesrv/internal/domain" ) const updatesTooLongNudgeDelay = 300 * time.Millisecond +const legacyUpdatesGetDifferenceTypeID uint32 = 0x25939651 // registerUpdates 注册 updates.* RPC handler。 func (r *Router) registerUpdates(d *tg.ServerDispatcher) { @@ -71,6 +73,42 @@ func (r *Router) onUpdatesGetDifference(ctx context.Context, req *tg.UpdatesGetD return tgUpdatesDifference(st), nil } +func (r *Router) handleLegacyUpdatesGetDifference(ctx context.Context, b *bin.Buffer) (bin.Encoder, error) { + if err := b.ConsumeID(legacyUpdatesGetDifferenceTypeID); err != nil { + return nil, err + } + var flags bin.Fields + if err := flags.Decode(b); err != nil { + return nil, err + } + pts, err := b.Int() + if err != nil { + return nil, err + } + var ptsTotalLimit int + if flags.Has(0) { + ptsTotalLimit, err = b.Int() + if err != nil { + return nil, err + } + } + date, err := b.Int() + if err != nil { + return nil, err + } + qts, err := b.Int() + if err != nil { + return nil, err + } + return r.onUpdatesGetDifference(ctx, &tg.UpdatesGetDifferenceRequest{ + Flags: flags, + Pts: pts, + PtsTotalLimit: ptsTotalLimit, + Date: date, + Qts: qts, + }) +} + func (r *Router) accountChannelDifferenceNudges(ctx context.Context, userID int64, sinceDate int) []domain.ChannelDifferenceNudge { if r.deps.Channels == nil || userID == 0 || sinceDate <= 0 { return nil