Compare commits

...

262 commits

Author SHA1 Message Date
Wizou 3f531f4966 Collect: updated list of merged flags
Some checks failed
Dev build / build (push) Has been cancelled
2026-01-03 13:07:04 +01:00
Wizou 1912632722 Immediate remigrate to MainDC in case reconnection is only possible through a different DcID 2025-12-22 02:34:29 +01:00
Wizou 9c839128bb Update to Telegram API Layer 220
- Added support for passkey-based authentication with new classes (`Passkey`, `Account_Passkeys`, etc.) and methods (`Auth_InitPasskeyLogin`, `Account_RegisterPasskey`, etc.).
- Added new methods for handling star gift offers and upgrades (`Payments_ResolveStarGiftOffer`, `Payments_SendStarGiftOffer`, etc.).
- Enhanced star gift functionality with new fields (`gift_num`, `upgrade_variants`, etc.), flags, and classes (`MessageActionStarGiftPurchaseOffer`, `StarGiftBackground`, etc.).
- Updated `Messages_ForwardMessages` to include a new `effect` parameter.
2025-12-14 20:24:02 +01:00
Wizou d3ad4789a1 WTelegram.Helpers.JsonOptions can now serialize polymorph TL types (useful for logs).
Some checks failed
Dev build / build (push) Has been cancelled
Deserialization is also possible in non-trimmed apps, but not recommended as structures can change.
2025-12-14 20:23:45 +01:00
Wizou 208ab626c1 API Layer 218: Stargift auctions, Story Live broadcast, Paid messages in Live/group calls, Message schedule_repeat_period, Saved Music privacy key, and more... 2025-11-15 22:45:36 +01:00
Wizou bfc8e0e1b5 .NET 10 compatibility
Some checks failed
Dev build / build (push) Has been cancelled
2025-11-15 19:39:10 +01:00
Wizou e923d65d53 Clamp TL stamps to 0..int.MaxValue, mapping edge to DateTime.Min/MaxValueaa 2025-11-15 18:21:15 +01:00
Wizou 4ad2f0a212 Fix incorrect bare type for DestroyAuthKey, RpcDropAnswer, DestroySession
Some checks failed
Dev build / build (push) Has been cancelled
2025-11-13 07:45:57 +01:00
Wizou 30bc536ebc Removed annoying Peer implicit long operator (reverts #229)
Some checks failed
Dev build / build (push) Has been cancelled
2025-11-09 22:32:47 +01:00
Wizou d6fdcab440 ToBytes TL.Serialization helper.
Warning: do not use for long-term storage because TL structures can change in future layers and may not be deserializable
2025-11-05 15:00:48 +01:00
Wizou 9ec2f31f72 Delete alt-session on AUTH_KEY_UNREGISTERED for renegociation 2025-10-31 18:21:11 +01:00
Wizou 4ccfddd22e Collect: Fix Channel losing participants_count 2025-10-31 00:36:24 +01:00
Wizou 9693037ef2 Added missing DownloadFileAsync(doc, videoSize) 2025-10-31 00:31:30 +01:00
Wizou 40bcf69bfb Change UserStatusOffline.was_online to a DateTime 2025-10-31 00:30:02 +01:00
Wizou 4875f75774 API Layer 216: topics methods/updates moved to prefix Messages_* (to support topics for bots), contact notes, groupcall comments, profile color, stargifts stuff 2025-10-31 00:27:32 +01:00
Wizou 2e95576be5 Encryption class public + TL Methods/Helpers 2025-10-31 00:14:48 +01:00
Wizou 48d005b605 Encryption class public + Methods table
Some checks failed
Dev build / build (push) Has been cancelled
2025-10-10 20:27:42 +02:00
Wizou a5323eaa86 api doc 2025-10-06 18:34:25 +02:00
Wizou 610d059b4c Fix: Messages_Search helper parameter name
Some checks failed
Dev build / build (push) Has been cancelled
2025-10-03 13:09:33 +02:00
Wizou 3f1036a559 Fix #335 GetAllDialogs infinite loop when last dialogs' messages are unavailable
Some checks failed
Dev build / build (push) Has been cancelled
2025-09-26 13:34:58 +02:00
Wizou 4578dea3a3 HtmlToEntities tolerate unclosed &.. html entities 2025-09-21 01:55:11 +02:00
Wizou 253249e06a API Layer 214: ChatTheme, main ProfileTab, user saved music, some StarGift & Store Payment stuff...
(that might not be the most recent API layer. check https://patreon.com/wizou for the latest layers)
2025-09-01 13:37:41 +02:00
Wizou eb52dccfa7 Helper OpenChat to monitor a group/channel without joining (#333)
Some checks failed
Dev build / build (push) Has been cancelled
2025-08-27 00:18:34 +02:00
Wizou a9bbdb9fc4 Try to improve diagnostics/handling of weird email login case (#331) 2025-08-27 00:06:30 +02:00
Wizou 5f411d45f9 API Layer 211.2: StarsTransaction flag posts_search 2025-08-16 13:01:43 +02:00
Wizou e16e39bfba Avoid using obsolete DataCenter info on alt-DC connect 2025-08-09 02:43:43 +02:00
Wizou 7faa3873f8 API Layer 211: Stories Albums, user's pending Stars Rating, SearchPosts + Check Flood, ...
(that might not be the most recent API layer. check https://patreon.com/wizou for the latest layers)
2025-08-02 02:05:49 +02:00
Wizou d9e4b7cc0f CollectUsersChats: don't update usernames from min info
Some checks failed
Dev build / build (push) Has been cancelled
(thx @riniba)
2025-08-01 00:17:12 +02:00
Wizou 30a982b0ac API Layer 210: user's Stars Rating, StarGift collections management & more characteristics...
(that might not be the most recent layer. check https://patreon.com/wizou for the latest layers)
2025-07-25 17:01:58 +02:00
Wizou e9543a690b Examples for download abort, and uploading streamable video (fix #325, thx @patelriki13)
Some checks failed
Dev build / build (push) Has been cancelled
2025-07-25 01:03:30 +02:00
Wizou a8fa32dfd5 Support single-quote arguments in HTML 2025-07-18 02:28:43 +02:00
Wizou 56ba15bc13 API Layer 209: Reply-to specific ToDo item
(that might not be the most recent layer. check https://patreon.com/wizou for the latest layers)
2025-07-14 22:38:51 +02:00
Wizou a3f41330b5 API Layer 207: StarGift.ReleaseBy
Some checks failed
Dev build / build (push) Has been cancelled
(that might not be the most recent layer. check https://patreon.com/wizou for the latest layers)
2025-07-10 01:50:09 +02:00
Wizou 52d948af2a Fix DateTime type of *_at fields
Some checks failed
Dev build / build (push) Has been cancelled
2025-07-10 01:49:25 +02:00
Wizou bdcf389ed2 API Layer 206: ToDo lists, suggested posts, choose TON vs Stars for gifts/transactions, ...
(for the very latest layers, go to https://patreon.com/wizou)
2025-07-08 19:47:44 +02:00
Wizou 4f7954db61 API Layer 204: Channel DMs (MonoForum), Forum Tabs, Saved peer/dialog stuff...
(for the very latest layers, go to https://patreon.com/wizou)
2025-07-08 19:47:35 +02:00
Wizou fa90e236e7 Helpers to download animated photos (DownloadFileAsync + photo.LargestVideoSize) 2025-06-29 16:42:27 +02:00
Wizou 25990a8477 Fix Salts management 2025-06-29 16:01:05 +02:00
Wizou 3ff1200068 Use signed DcId values (improved logs) 2025-06-26 22:02:26 +02:00
Wizou 04e043222e ParallelTransfers property can configure how many parallel download/upload of file chunks can occur at the same time. Default is 2 (optimal for non-premium accounts), was 10 in previous versions. 2025-06-24 19:11:14 +02:00
Wizou d49d620edd Fixed possible concurrency issue on msgId/seqno, that could freeze protocol or cause BadMsgNotification (during downloads for example)
Thanks goes to @Deeps00009
2025-06-24 19:01:52 +02:00
Wizou 5358471574 Fix fields serialization order on KeyboardButtonSimpleWebView 2025-06-02 02:41:06 +02:00
Wizou 8836f8372b API Layer 203: Stargift resale, auto-translation... 2025-05-14 18:18:20 +02:00
Wizou 6fb59286bd API Layer 202: E2E group calls
https://core.telegram.org/api/end-to-end/group-calls
2025-05-01 12:17:06 +02:00
Wizou eaea2d051a API Layer 201.2: business bot stars 2025-04-20 03:23:43 +02:00
Wizou 6d238dc528 Store less stuff in session data and reduce save frequency for better performance. 2025-04-06 19:48:43 +02:00
Wizou f495f59bc8 Use media DC for uploads 2025-04-02 05:58:04 +02:00
Wizou 3f1d4eba92 API Layer 201: Paid msg service messages, Gifts settings, Business bot rights, sentCodePayment, sponsored peers ... 2025-03-27 00:59:57 +01:00
Wizou e6a4b802e7 Html/Markdown: prune entities of length=0 (fix wiz0u/WTelegramBot#6) 2025-03-23 03:10:04 +01:00
Wizou 0867c044fa Handle CONNECTION_NOT_INITED 2025-03-12 02:17:36 +01:00
Wizou 1ecd7047ef API Layer 200: Paid messages, details on chat partner, ... 2025-03-08 00:21:50 +01:00
Wizou e67a688baa Building with Github Actions 2025-03-03 02:02:06 +01:00
Wizou b626c6c644 API Layer 199: reCAPTCHA, PaidReactionPrivacy 2025-02-13 14:12:04 +01:00
Wizou 1fab219ef6 Added EmojiStatusBase.DocumentId helper 2025-01-31 20:39:42 +01:00
Wizou edc6019f2e Better support for HTML &entities; in HtmlText 2025-01-30 02:22:05 +01:00
Wizou e6dde32538 Fix Messages_GetAllChats to include only your chats 2025-01-28 13:46:06 +01:00
Wizou e5953994a7 property Title on ForumTopicBase 2025-01-24 00:41:48 +01:00
Wizou fdaaeb901a API Layer 198: stargifts stuff, video cover, ... 2025-01-22 23:28:58 +01:00
Wizou cbcb11e25e API Layer 196.2: Bots_SetCustomVerification, GetBotRecommendations, Messages_ReportMessagesDelivery, ... 2025-01-02 14:25:35 +01:00
Wizou b0f336994b API Layer 196: more starGifts methods, bot verification (see Bot API 8.2), conference calls, ... 2025-01-02 00:35:34 +01:00
Wizou b5aea68f66 api doc 2025-01-02 00:27:45 +01:00
Wizou d8f7304067 Fix broken SendAlbumAsync+InputMediaDocumentExternal 2025-01-02 00:20:16 +01:00
Wizou 2451068a71 API Layer 195: searchStickers, Bot API 8.1 (star referral program) 2024-12-04 20:10:05 +01:00
Wizou d42c211f30 api doc 2024-11-22 19:40:15 +01:00
Wizou 48a1406acc Raise OwnUpdates for Messages_InvitedUsers & Payments_PaymentResult 2024-11-22 16:27:21 +01:00
Wizou ccad6f9f31 Limit parallel transfers to 1 in HTTP mode 2024-11-21 17:08:08 +01:00
Wizou ddc0a3caef HtmlText/Markdown: support for collapsed Blockquote 2024-11-20 22:59:06 +01:00
Wizou b40557f78c API Layer 194: bot star subscriptions feedback 2024-11-18 14:40:01 +01:00
Wizou 9208d0b92c API Layer 193: fullscreen mini-apps, Bot API 8.0 2024-11-18 14:17:28 +01:00
Wizou fc441121a3 Improved HTTP mode reliability 2024-11-18 13:44:32 +01:00
Wizou 9ad6220527 Fix potential deadlock in HTTP mode 2024-11-14 13:18:05 +01:00
Wizou 60dab9e6f3 api doc 2024-11-14 13:16:44 +01:00
Wizou 18fc1b32af Program_DownloadSavedMedia: demo how to abort a transfer 2024-11-14 00:49:45 +01:00
Wizou f1c1d0a6a2 Added helpers Message.ReplyHeader .TopicID. Fix duplicate pinned in Channels_GetAllForumTopics 2024-11-02 22:54:20 +01:00
Wizou 33dd2d02d6 Use ?? default, more clean/readable than GetValueOrDefault 2024-11-02 02:40:34 +01:00
Wizou 3d0de9ef9f added helper method Channels_GetAllForumTopics 2024-11-02 00:14:23 +01:00
Wizou 0cc8a324cb API Layer 192: sponsored messages for bots, paid broadcast for bots (allow_paid_floodskip), video processing flags 2024-10-31 18:19:53 +01:00
Wizou 6317fed8e0 Fix: UpdateManager losing synchronization context 2024-10-29 15:42:02 +01:00
Wizou 322f5f132f Fix: UpdateManager losing synchronization context 2024-10-29 14:39:05 +01:00
Wizou e758e9136c Fix issue with incomplete ForwardMessagesAsync 2024-10-26 17:32:40 +02:00
Wizou cb8bcb5b8b Fix MTProxy broken since HTTP support 2024-10-25 01:35:17 +02:00
Wizou 9a4643ecef added helper method ForwardMessagesAsync 2024-10-24 23:57:26 +02:00
Wizou 6354d0e8b7 Manager: workaround Updates_GetState returning wrong QTS 2024-10-18 01:15:45 +02:00
Wizou 75f5832ef6 API Layer 190: Text attached to gifts 2024-10-15 17:01:14 +02:00
Wizou e4d66925e3 Update System.Text.Json due to vulnerability CVE-2024-43485 2024-10-15 16:49:22 +02:00
Wizou 3b52b3a200 Updated version to 4.2 2024-10-07 14:58:12 +02:00
Wizou 73e4b6c871 API Layer 189: Star Gifts, improved reporting 2024-10-07 14:42:22 +02:00
Wizou 62c105959c Improved HTTP support 2024-10-07 02:43:07 +02:00
Wizou a19db86c1d Support for connecting to Telegram via on-demand HTTP requests instead of permanent TCP connection: client.HttpMode (experimental) 2024-09-30 02:15:10 +02:00
Wizou 4f9accdfc8 SendAlbumAsync: For bots, upload external url natively instead of fetching via HttpClient 2024-09-23 00:21:14 +02:00
Wizou dcfd89c2a8 AnalyzeInviteLink: hacky detection of request_needed/join_request for basic chat links 2024-09-21 19:39:41 +02:00
Wizou 6cfa2a4da6 API Layer 188: Channels_ClickSponsoredMessage, KeyboardButtonCopy, alt_documents 2024-09-19 13:34:56 +02:00
Wizou f0a649c147 Fix #284: OnOwnUpdates null warning on Messages_AffectedMessages 2024-09-08 19:16:23 +02:00
Wizou b6cb62793c Fix infinite recursion on Dispose (#274) 🎬Take 3 2024-09-07 18:50:07 +02:00
Wizou 68a1c8650f Use more ResetAsync/DisposeAsync internally for better async 2024-09-07 18:43:52 +02:00
Wizou 62a691359b API Layer 187: Star Giveaways, private paid reactions, bot msg for paid media 2024-09-07 02:09:42 +02:00
Wizou be7027b318 more IAsyncDisposable stuff 2024-09-07 01:59:27 +02:00
Wizou 9fe6a9d74f Added DisposeAsync, ResetAsync. Now Login() starts Reactor on current context, useful for UI access within OnUpdates/OnOther (LoginUserIfNeeded was already doing that) 2024-09-06 18:22:05 +02:00
Wizou 9315913519 Fix infinite recursion on Dispose (#274) 🎬Take 2 2024-09-05 18:51:53 +02:00
wiz0u 15b6346d2a
Update autolock.yml 2024-08-22 15:14:26 +02:00
Wizou aef4fb795a API Layer 186: star subscriptions, Super Channels, paid reactions, ... 2024-08-14 15:14:41 +02:00
Wizou dea7691075 api doc 2024-08-14 14:41:26 +02:00
Wizou a28b984395 Fix infinite recursion on Dispose after downloads (fix #274) 2024-08-10 23:29:08 +02:00
Wizou e5c6086e11 API Layer 185: Bot stuff, Main Mini App, Stars payment stuff & more... 2024-07-31 19:35:34 +02:00
Wizou 6afb0803bb api doc 2024-07-30 01:54:00 +02:00
Wizou 8654f99d2b Manager: added opportunity to call DropPendingUpdates/StopResync before a resync 2024-07-30 01:50:26 +02:00
Wizou 9712233c00 Process Downloads really on media DCs, including for the main dc_id (fix #261) 2024-07-20 02:13:56 +02:00
Wizou f7b3a56ce3 api doc 2024-07-20 02:10:39 +02:00
Wizou d5101b4f3b No need to escape MarkdownV2 chars in code sections 2024-07-20 02:06:40 +02:00
Wizou 4946322045 No need to escape MarkdownV2 chars in code sections 2024-07-12 14:34:30 +02:00
Wizou 7643ed5ba4 API Layer 184: MessageActionPaymentRefunded, flag can_view_stars_revenue 2024-07-07 22:55:12 +02:00
Wizou 88c05287be Fix HtmlText exception with empty texts 2024-07-06 15:36:08 +02:00
Wizou b9299810b8 API Layer 183: Paid media, more stories areas, Payment stars status methods... 2024-07-02 01:18:09 +02:00
Wizou aa75b20820 Fix some yml 2024-07-02 00:59:25 +02:00
Wizou 1a00ae5a77 TL deserialization errors no longer cause a ReactorError. Renewing session on layer change to prevent old layer messages 2024-06-15 17:35:25 +02:00
Wizou 85cc404213 Prevent recursive issue when client is disposed in OnOther 2024-06-15 02:35:38 +02:00
Wizou 865c841bd6 Html/Markdown: fix-up potential ENTITY_BOUNDS_INVALID 2024-06-04 19:04:22 +02:00
Wizou c1a18a63c0 API Layer 181: Fact check, Message effects, Payment stars... 2024-05-28 17:15:39 +02:00
Wizou c8a0882587 Fix: ReactorError wasn't always triggered correctly on connection lost 2024-05-28 00:37:58 +02:00
Wizou 37b8f6c054 API Layer 179.3: UpdateBroadcastRevenueTransactions 2024-05-07 15:24:28 +02:00
Wizou 7d388e6e75 API Layer 179 (changed): emoji categories 2024-05-01 11:13:28 +02:00
Wizou 4422aad6be Catch/Log sessionStore.Write exceptions (fix #248) 2024-04-28 00:44:36 +02:00
Wizou 3a7c5a0957 API Layer 179: poll texts with entities, new SentCodeTypes 2024-04-27 13:22:05 +02:00
Wizou 3c19be32c7 New method LoginWithQRCode (fix #247) 2024-04-27 12:34:32 +02:00
Wizou 4381781af8 API Layer 178: mostly reactions & sponsored stuff 2024-04-24 17:42:46 +02:00
Wizou 69f9e0c418 better handle sendSemaphore on client dispose scenario 2024-04-24 17:25:45 +02:00
Wizou b6dbf9564f Fix excessive stack usage (#246) 2024-04-22 17:28:44 +02:00
Wizou 8228fede0f Turn "until" fields into DateTime 2024-04-18 18:33:21 +02:00
Wizou 6dcce7f784 Fix support for email code login 2024-04-17 17:33:48 +02:00
Wizou 1d07039f04 Fix potential conflict on System.Collections.Immutable 2024-04-16 15:19:12 +02:00
Wizou 8c271f50f6 Fix crash on Gzipped Vector result 2024-04-14 13:25:45 +02:00
Wizou 741422e17f Manager: prevent raising own updates in HandleDifference 2024-04-13 02:36:46 +02:00
Wizou f8fab6c3e9 some TL generic helpers 2024-04-13 02:34:50 +02:00
Wizou 1a4b606216 API Layer 177: more business stuff, new profile infos, revenue stats 2024-04-05 14:14:39 +02:00
Wizou fc08140995 renaming 2 files 2024-04-05 13:57:47 +02:00
Wizou f3ca76bb8f fix #242: correctly handle UserEmpty in ReadTLDictionary 2024-04-04 11:12:59 +02:00
Wizou abeed476e7 several enhancements:
- fixed UpdateAffectedMessages on wrong mbox
- MarkdownToEntities allow reserved characters in code block
- collector system more flexible & open
- improved UpdateManager resilience
- some MTPG improvements
2024-04-03 21:05:07 +02:00
Wizou 210a3365e5 Introducing the UpdateManager to streamline the handling of continuous updates (see FAQ) 2024-03-30 17:09:54 +01:00
Wizou 3d224afb23 Renamed OnUpdate => OnUpdates (with temporary compatibility shim) 2024-03-29 16:42:58 +01:00
Wizou 270a7d89e6 Using a source generator to make the library compatible with NativeAOT trimming. 2024-03-28 12:31:06 +01:00
Wizou 3918e68945 Using a source generator to make the library compatible with NativeAOT trimming. 2024-03-28 12:13:56 +01:00
Wizou 55feb857d7 more sealed partial 2024-03-26 18:12:39 +01:00
Wizou e2323092dc net8.0 target, compatible with AOT 2024-03-26 12:07:03 +01:00
Wizou 9fe1196606 Mark most classes as sealed & partial 2024-03-26 02:30:16 +01:00
Wizou 018f535655 Fix "wrong timestamp" issues when first msgId is too much in the past 2024-03-23 17:58:46 +01:00
Wizou 3304ba4bac raise event for Pong and Affected* 2024-03-23 17:51:00 +01:00
Wizou 39760f9306 using [] syntax 2024-03-23 17:43:28 +01:00
Wizou fa83787e7f CollectUsersChats update user/chat fields from min info (improved) 2024-03-19 11:50:58 +01:00
Wizou 1d00dc2f9b minor change 2024-03-19 11:48:45 +01:00
Wizou 659906ce01 CollectUsersChats update user/chat fields from min info 2024-03-13 05:03:15 +01:00
Wizou d00725e234 use primary constructors 2024-03-12 19:07:48 +01:00
Wizou 0738adc3bf releasing 3.7.x 2024-03-08 12:22:52 +01:00
Wizou fd3bb731ba API Layer 176: new Business features 2024-03-08 12:19:44 +01:00
Wizou 8eb5b29d97 using primary constructors, and more [] syntax 2024-03-08 12:07:37 +01:00
Wizou d0460f296c Compile using latest SDK 2024-03-08 12:01:43 +01:00
Wizou b5ca3fcc0e Fix SendAlbumAsync videoUrlAsFile. using [] syntax 2024-03-08 11:52:30 +01:00
Wizou 345f10971b using [] syntax 2024-03-08 11:33:18 +01:00
Wizou f958b4081d Fix properties base implementation 2024-03-04 23:54:58 +01:00
Wizou b9f3b2ebb4 text Escape methods accept null 2024-02-25 12:46:20 +01:00
Wizou 33f239fc8e Improved GetMessageByLink (topics, cache) 2024-02-25 03:09:49 +01:00
Wizou b9aad47c8e Better log for wrong timestamps 2024-02-21 14:51:26 +01:00
Wizou cf1c29e9ef Little better fix for #233 2024-02-21 02:11:16 +01:00
Wizou 1c298bfa83 Fix #233: "server_address" can hint the dcId in order to reuse existing AuthKey on last-ditch reconnection 2024-02-19 17:08:26 +01:00
Wizou 6a1114ccd5 ConnectAsync can be used for quickResume of sessions (experimental) 2024-02-18 22:50:42 +01:00
Wizou 0312821068 Alternate simpler byte[] callback session store 2024-02-18 18:02:51 +01:00
Wizou 125c1caeeb Alternate simpler byte[] callback session store 2024-02-18 17:41:30 +01:00
Wizou 288bf7ccf7 API Layer 174: Group boosts & emoji set 2024-02-18 17:00:49 +01:00
Wizou a424219cb6 API Layer 173: no_joined_notifications, SavedReactionTags in peers 2024-02-01 21:29:24 +01:00
Wizou 0ad7d696a5 DownloadFileAsync: don't use stream.Position if !CanSeek 2024-02-01 21:24:49 +01:00
Wizou 6f3c8732ec API Layer 172: Premium last-seen, read-dates, contacts 2024-01-18 15:53:17 +01:00
Wizou a0841fee4c API Layer 171: Saved reactions/tags 2024-01-16 18:13:08 +01:00
Wizou 7dc578f91d api doc 2024-01-16 18:09:45 +01:00
Ali 91a8eab86a
add implicit conversion for Peer (#229) 2024-01-16 14:55:51 +01:00
Wizou 48a1322452 Fix #225: RpcError INPUT_METHOD_INVALID_3105996036 when using client.DisableUpdates() 2024-01-15 14:45:58 +01:00
Wizou a17f13475d API Layer 170: access to saved dialogs/history/peer
(no idea what that means)
2023-12-31 18:29:36 +01:00
Wizou 2d7a64fc2d API Layer 169 (https://corefork.telegram.org/api/layers): Channel appearance, giveaway prize/winners, premium gift payments, stories reposts, bot reactions and more... (https://t.me/tginfoen/1804) 2023-12-23 20:39:48 +01:00
Wizou e6fa972295 Fix #216: The old salt should be accepted for a further 1800 seconds 2023-12-18 00:01:07 +01:00
Wizou cce7a64cd9 api doc 2023-12-17 23:40:26 +01:00
Wizou 5f51b1f77e MBox helpers for updates 2023-12-17 23:39:55 +01:00
Wizou 7c65ce70ec api doc 2023-12-05 01:13:19 +01:00
Wizou d7ecd49b5c API Layer 167: PeerColor, wallpaper for_both, view_forum_as_messages, story forwards and more stats... 2023-11-30 16:19:42 +01:00
Wizou b6c98658db OnOwnUpdate handler can be used to intercept Updates replies to your API calls 2023-11-30 16:06:37 +01:00
Wizou 8f6e6440ba api doc 2023-11-30 01:51:23 +01:00
Wizou 5febd2d27b Abort pending requests on Dispose
(I thought it was already the case!?)
2023-11-29 15:16:35 +01:00
Wizou 807ee0cc9a fix "dev.yml" 2023-11-26 23:53:58 +01:00
Wizou 5624eda8a0 fix "dev.yml" 2023-11-26 23:49:11 +01:00
Wizou 35fab21493 SendAlbumAsync now sets caption on first media instead of last 2023-11-26 23:33:09 +01:00
Wizou 9209d792a5 - InputMediaUploadedDocument ctor accept attributes
- SendMediaAsync: MP4 file or "video" mime type is now sent as streamable video
- SendAlbumAsync: External videos URL are now sent as streamable videos by default

(streamable videos might lack thumbnail if too big for client auto-download)
2023-11-25 19:16:51 +01:00
Wizou 8f44137366 api doc 2023-11-25 19:16:03 +01:00
Wizou 35f2f2530a SendMessageAsync preview param changed to support above/below/disabled 2023-11-17 18:37:22 +01:00
Wizou 6b44dbae8a Support blockquotes in HTML/Markdown 2023-11-17 18:36:49 +01:00
Wizou 3861255710 Support additional connection JSON params with Config("init_params") 2023-11-11 12:25:02 +01:00
Wizou 96ff52fab8 API Layer 166.2: Premium_GetUserBoosts, UpdateBotChatBoost 2023-11-06 23:58:48 +01:00
Wizou df2b2a7907 API Layer 166: colors/emoji, new quotes, new link previews, distant replies, invert_media, premium boosts/giveaway/gifts...
see https://t.me/tginfoen/1760
2023-10-28 23:47:04 +02:00
Wizou eb375824e4 api doc 2023-10-28 23:16:48 +02:00
Wizou 136df62b8f UploadFileAsync: just get rid of MD5 altogether. It works just fine 🤷🏻‍♂️ 2023-10-24 17:33:25 +02:00
Wizou 4a1b2f5f91 Fix #197: wrong MD5 encoding in UploadFileAsync 2023-10-24 17:05:39 +02:00
Wizou 9e92d3d814 api doc 2023-10-24 17:00:14 +02:00
Wizou c059ebf208 UploadFileAsync now supports Stream with unknown Length (!CanSeek) 2023-10-19 23:57:45 +02:00
Wizou fb8d1c2d07 Some more implicit Input conversions 2023-10-09 15:19:03 +02:00
Wizou 2b7868ee16 API Layer 165: minor changes to WebPage, channel boost url 2023-10-04 19:24:53 +02:00
Wizou 88e2f5d71e detect wrong usage of GetMessages 2023-10-04 19:17:49 +02:00
Wizou 4b7205cb68 yaml 2023-09-23 00:07:53 +02:00
Wizou 6d43da3d75 yaml 2023-09-23 00:07:07 +02:00
Wizou e954fdc628 yaml 2023-09-23 00:00:35 +02:00
Wizou 609244a848 yaml 2023-09-22 23:58:03 +02:00
Wizou 5dc8291972 yaml 2023-09-22 23:55:36 +02:00
Wizou fe26ee1b24 yaml 2023-09-22 23:51:02 +02:00
Wizou 07c9118ccd yaml 2023-09-22 23:44:53 +02:00
Wizou d16fa21258 yaml 2023-09-22 23:40:46 +02:00
Wizou 028afa4465 yaml 2023-09-22 23:36:47 +02:00
Wizou e4ed02c9a7 yaml 2023-09-22 23:31:59 +02:00
Wizou c60e4f9be0 yaml 2023-09-22 23:27:14 +02:00
Wizou 392793b390 API Layer 164: Stories in channel, boosts, more stories stuff... 2023-09-22 23:16:21 +02:00
Wizou e7be5ac36f release.yml 2023-09-18 19:34:08 +02:00
Wizou 8f90e88074 API Layer 163: some minor bot & update stuff 2023-09-18 19:21:48 +02:00
Wizou 38efb05923 API Layer 162: more stories & blocking stuff... 2023-09-06 18:29:50 +02:00
Wizou e20d4d715c Updated Examples projects 2023-08-12 15:03:09 +02:00
Wizou c872a51a31 API Layer 160: Stories & more... 2023-07-21 10:52:46 +02:00
Wizou 2b65e8f1ed CollectUsersChats allows null dictionaries 2023-07-08 01:35:15 +02:00
Wizou 8a9f886b62 Fix Naming Styles 2023-07-08 01:34:31 +02:00
Wizou c631072ae4 Prevent "recursive" MsgContainer 2023-07-06 10:17:29 +02:00
wiz0u 1048af4dcf
test "yml" 2023-07-05 16:01:52 +02:00
Wizou bcc62a1356 test "yml" 2023-07-05 15:53:48 +02:00
Wizou 994e0deade test "ReleaseNotes" 2023-07-05 05:08:28 +02:00
Wizou 472b10f155 Fix unencrypted padding length in Padded Intermediate
(official doc says 0..15 bytes, but server & Android app sends 0..256 bytes)
2023-07-05 05:03:08 +02:00
Wizou d50ac0ba51 - Fix Messages_GetAllDialogs (MessageEmpty)
- Fix AnalyzeInviteLink (public channel with join request)
- Added Contacts_ResolvedPeer.Channel property
closes #166
2023-06-27 16:00:55 +02:00
Wizou a31bcc3df6 test "test" 'test' 2023-05-28 00:08:44 +02:00
Wizou abc9435625 yml - "abc" 'def' 2023-05-28 00:04:52 +02:00
Wizou befac0e781 yml - "abc" 'def' 2023-05-28 00:00:36 +02:00
Wizou a5d44bbd93 "yml" 2023-05-27 23:58:07 +02:00
Wizou 19d2c566eb yml 2023-05-27 23:54:18 +02:00
Wizou 2e41a1734a yml 2023-05-27 23:52:51 +02:00
Wizou 25bfacb3f1 yml 2023-05-27 23:51:42 +02:00
Wizou fb8d694886 yml 2023-05-27 23:41:20 +02:00
Wizou 6f8c964f60 updated "Program_ListenUpdates" example 2023-05-27 23:35:14 +02:00
Wizou a69e032354 updated "Program_ListenUpdates" example 2023-05-27 23:33:08 +02:00
Wizou 3603b9e2e3 updated "Program_ListenUpdates" example 2023-05-27 23:30:01 +02:00
Wizou 2937d1c7b9 yml 2023-05-27 23:27:43 +02:00
Wizou 0825527860 yml 2023-05-27 23:25:06 +02:00
Wizou e4c961a697 updated "Program_ListenUpdates" example 2023-05-27 23:20:26 +02:00
Wizou d054ae7eea updated "Program_ListenUpdates" example 2023-05-27 23:17:53 +02:00
Wizou fd48ea2974 updated "Program_ListenUpdates" example 2023-05-27 23:15:21 +02:00
Wizou fc54a93d00 updated "Program_ListenUpdates" example 2023-05-27 23:10:37 +02:00
Wizou 660933d8c7 updated "Program_ListenUpdates" example 2023-05-27 23:08:39 +02:00
Wizou f4d435b807 updated "Program_ListenUpdates" example 2023-05-27 23:05:11 +02:00
Wizou de4b5e606d updated "Program_ListenUpdates" example 2023-05-27 23:01:04 +02:00
Wizou 7feb4a40ec updated "Program_ListenUpdates" example 2023-05-27 18:05:24 +02:00
Wizou b282d2e007 Prevent 'You must connect to Telegram first' during network loss
api doc
2023-05-18 22:02:53 +02:00
Wizou d1e583cc86 Prevent "You must connect to Telegram first" during network loss
(closes #157)
2023-05-18 21:50:19 +02:00
Wizou 131fd36106 Fix ReactorError not correctly raised. Added Program_ReactorError example 2023-05-17 18:26:53 +02:00
Wizou e8b0bb9245 ActiveUsernames helpers
Github action telegram-api
2023-05-17 11:45:06 +02:00
Wizou 98f6a26b09 api doc 2023-05-11 12:46:34 +02:00
wiz0u be0a357b9b
Added section: Get chat and user info from a message 2023-05-09 03:44:27 +02:00
Wizou 30fc1cad8d Support chats cache in AnalyzeInviteLink/GetMessageByLink
(closes #148)
2023-05-04 17:07:47 +02:00
Wizou c052ac2e2c fix issue with Channels_GetAdminLog helper 2023-05-03 14:24:52 +02:00
Wizou 30618cb316 Implement Future Salts mecanism to prevent replay attacks 2023-05-02 22:49:38 +02:00
Wizou 753ac12eb1 OnUpdate is now only for updates. OnOther is used for other notifications 2023-05-01 18:21:03 +02:00
Wizou 5adde27f88 new TLS client hello generation 2023-04-29 16:45:03 +02:00
Wizou 35c492de4f moved IsGroup IsChannel up to ChatBase 2023-04-25 20:47:57 +02:00
Wizou e33184fabb improved api doc for Flags 2023-04-25 16:51:39 +02:00
Wizou 6a75f0a9d8 Fix progressCallback abort/exception handling during DownloadFileAsync 2023-04-25 13:19:15 +02:00
40 changed files with 19866 additions and 4501 deletions

2
.github/FUNDING.yml vendored
View file

@ -1,2 +1,2 @@
github: wiz0u
custom: ["https://www.buymeacoffee.com/wizou", "http://t.me/WTelegramBot?start=donate"]
custom: ["https://www.buymeacoffee.com/wizou", "http://t.me/WTelegramClientBot?start=donate"]

78
.github/dev.yml vendored
View file

@ -1,37 +1,63 @@
pr: none
pr: none
trigger:
- master
branches:
include: [ master ]
paths:
exclude: [ '.github', '*.md', 'Examples' ]
name: 3.4.2-dev.$(Rev:r)
name: 4.3.2-dev.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
Release_Notes: $[replace(variables['Build.SourceVersionMessage'], '"', '''''')]
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.x'
includePreviewVersions: true
stages:
- stage: publish
jobs:
- job: publish
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '9.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
includesymbols: true
versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber'
buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true;ReleaseNotes="$(Build.SourceVersionMessage)"'
# buildProperties: 'NoWarn="0419;1573;1591";AllowedOutputExtensionsInPackageBuildOutputFolder=".dll;.xml;.pdb"'
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
packagesToPack: 'src/WTelegramClient.csproj'
includesymbols: true
versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber'
buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)"
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
publishPackageMetadata: true
nuGetFeedType: 'external'
publishFeedCredentials: 'nuget.org'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
publishPackageMetadata: true
nuGetFeedType: 'external'
publishFeedCredentials: 'nuget.org'
- stage: notify
jobs:
- job: notify
pool:
server
steps:
- task: InvokeRESTAPI@1
inputs:
connectionType: 'connectedServiceName'
serviceConnection: 'Telegram Deploy Notice'
method: 'POST'
body: |
{
"status": "success",
"complete": true,
"message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\", \"commitMessage\": \"$(Release_Notes)\" }"
}
waitForCompletion: 'false'

17
.github/release.yml vendored
View file

@ -1,13 +1,14 @@
pr: none
trigger: none
name: 3.4.$(Rev:r)
name: 4.3.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
Release_Notes: $[replace(variables['releaseNotes'], '"', '''''')]
stages:
- stage: publish
@ -21,17 +22,17 @@ stages:
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '6.0.x'
version: '9.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
packagesToPack: 'src/WTelegramClient.csproj'
includesymbols: true
versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber'
buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true;ReleaseNotes="$(ReleaseNotes)"'
buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)"
- task: NuGetCommand@2
inputs:
@ -58,13 +59,9 @@ stages:
serviceConnection: 'Telegram Deploy Notice'
method: 'POST'
body: |
{
{
"status": "success",
"complete": true,
"message": "{
\"commitId\": \"$(Build.SourceVersion)\",
\"buildNumber\": \"$(Build.BuildNumber)\",
\"teamProjectName\": \"$(system.TeamProject)\"
}"
"message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\"}"
}
waitForCompletion: 'false'

View file

@ -2,21 +2,23 @@ name: 'Auto-Lock Issues'
on:
schedule:
- cron: '0 12 * * *'
- cron: '17 2 * * 1'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
discussions: write
concurrency:
group: lock
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v4
- uses: dessant/lock-threads@v5
with:
issue-inactive-days: '60'
pr-inactive-days: '60'
discussion-inactive-days: '60'

68
.github/workflows/dev.yml vendored Normal file
View file

@ -0,0 +1,68 @@
name: Dev build
on:
push:
branches: [ master ]
paths-ignore: [ '.**', 'Examples/**', '**.md' ]
env:
PROJECT_PATH: src/WTelegramClient.csproj
CONFIGURATION: Release
RELEASE_NOTES: ${{ github.event.head_commit.message }}
jobs:
build:
permissions:
id-token: write # enable GitHub OIDC token issuance for this job
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 30
- name: Determine version
run: |
git fetch --depth=30 --tags
DESCR_TAG=$(git describe --tags)
COMMITS=${DESCR_TAG#*-}
COMMITS=${COMMITS%-*}
LAST_TAG=${DESCR_TAG%%-*}
NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))-dev.$COMMITS
RELEASE_VERSION=${{vars.RELEASE_VERSION}}-dev.$COMMITS
if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi
echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION
echo "VERSION=$VERSION" >> $GITHUB_ENV
# - name: Setup .NET
# uses: actions/setup-dotnet@v4
# with:
# dotnet-version: 8.0.x
- name: Pack
run: |
RELEASE_NOTES=${RELEASE_NOTES//$'\n'/%0A}
RELEASE_NOTES=${RELEASE_NOTES//\"/%22}
RELEASE_NOTES=${RELEASE_NOTES//,/%2C}
RELEASE_NOTES=${RELEASE_NOTES//;/%3B}
dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION -p:ReleaseNotes="$RELEASE_NOTES" --output packages
# - name: Upload artifact
# uses: actions/upload-artifact@v4
# with:
# name: packages
# path: packages/*.nupkg
- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Nuget push
run: dotnet nuget push packages/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
- name: Deployment Notification
env:
JSON: |
{
"status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
"message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
}
run: |
curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }}

82
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,82 @@
name: Release build
on:
workflow_dispatch:
inputs:
release_notes:
description: 'Release notes'
required: true
version:
description: "Release version (leave empty for automatic versioning)"
run-name: '📌 Release build ${{ inputs.version }}'
env:
PROJECT_PATH: src/WTelegramClient.csproj
CONFIGURATION: Release
RELEASE_NOTES: ${{ inputs.release_notes }}
VERSION: ${{ inputs.version }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # For git tag
id-token: write # enable GitHub OIDC token issuance for this job
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 100
- name: Determine version
if: ${{ env.VERSION == '' }}
run: |
git fetch --depth=100 --tags
DESCR_TAG=$(git describe --tags)
LAST_TAG=${DESCR_TAG%%-*}
NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))
RELEASE_VERSION=${{vars.RELEASE_VERSION}}
if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi
echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Pack
run: |
RELEASE_NOTES=${RELEASE_NOTES//|/%0A}
RELEASE_NOTES=${RELEASE_NOTES// - /%0A- }
RELEASE_NOTES=${RELEASE_NOTES// /%0A%0A}
RELEASE_NOTES=${RELEASE_NOTES//$'\n'/%0A}
RELEASE_NOTES=${RELEASE_NOTES//\"/%22}
RELEASE_NOTES=${RELEASE_NOTES//,/%2C}
RELEASE_NOTES=${RELEASE_NOTES//;/%3B}
dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION -p:ReleaseNotes="$RELEASE_NOTES" --output packages
# - name: Upload artifact
# uses: actions/upload-artifact@v4
# with:
# name: packages
# path: packages/*.nupkg
- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Nuget push
run: dotnet nuget push packages/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
- name: Git tag
run: |
git tag $VERSION
git push --tags
- name: Deployment Notification
env:
JSON: |
{
"status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
"message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
}
run: |
curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }}

29
.github/workflows/telegram-api.yml vendored Normal file
View file

@ -0,0 +1,29 @@
name: 'Telegram API issues'
on:
issues:
types: [labeled]
permissions:
issues: write
jobs:
action:
if: contains(github.event.issue.labels.*.name, 'telegram api')
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v4
with:
support-label: 'telegram api'
issue-comment: >
Please note that **Github issues** should be used only for problems with the library code itself.
For questions about Telegram API usage, you can search the [API official documentation](https://core.telegram.org/api#getting-started) and the [full list of methods](https://core.telegram.org/methods).
WTelegramClient covers 100% of the API and let you do anything you can do in an official client.
If the above links didn't answer your problem, [click here to ask your question on **StackOverflow**](https://stackoverflow.com/questions/ask?tags=c%23+wtelegramclient+telegram-api) so the whole community can help and benefit.
close-issue: true
issue-close-reason: 'not planned'

2
.gitignore vendored
View file

@ -3,6 +3,8 @@
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
launchSettings.json
# User-specific files
*.rsuser
*.suo

View file

@ -11,7 +11,7 @@ await client.LoginUserIfNeeded();
```
In this case, environment variables are used for configuration so make sure to
go to your **Project Properties > Debug > Environment variables**
go to your **Project Properties > Debug > Launch Profiles > Environment variables**
and add at least these variables with adequate values: **api_id, api_hash, phone_number**
Remember that these are just simple example codes that you should adjust to your needs.
@ -95,7 +95,7 @@ foreach (Dialog dialog in dialogs.dialogs)
Notes:
- The lists returned by Messages_GetAllDialogs contains the `access_hash` for those chats and users.
- See also the `Main` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L20).
- See also the `Main` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L18).
- To retrieve the dialog information about a specific [peer](README.md#terminology), use `client.Messages_GetPeerDialogs(inputPeer)`
<a name="list-chats"></a>
@ -114,7 +114,7 @@ Notes:
- The list returned by Messages_GetAllChats contains the `access_hash` for those chats. Read [FAQ #4](FAQ.md#access-hash) about this.
- If a basic chat group has been migrated to a supergroup, you may find both the old `Chat` and a `Channel` with different IDs in the `chats.chats` result,
but the old `Chat` will be marked with flag [deactivated] and should not be used anymore. See [Terminology in ReadMe](README.md#terminology).
- You can find a longer version of this method call in [Examples/Program_GetAllChats.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_GetAllChats.cs?ts=4#L32)
- You can find a longer version of this method call in [Examples/Program_GetAllChats.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_GetAllChats.cs?ts=4#L31)
<a name="list-members"></a>
## List the members from a chat
@ -187,19 +187,20 @@ Notes:
<a name="updates"></a>
## Monitor all Telegram events happening for the user
This is done through the `client.OnUpdate` callback event.
Your event handler implementation can either return `Task.CompletedTask` or be an `async Task` method.
This is done through the `client.OnUpdates` callback event, or via the [UpdateManager class](FAQ.md#manager) that simplifies the handling of updates.
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L23).
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21).
<a name="monitor-msg"></a>
## Monitor new messages being posted in chats in real-time
You have to handle `client.OnUpdate` events containing an `UpdateNewMessage`.
You have to handle update events containing an `UpdateNewMessage`.
This can be done through the `client.OnUpdates` callback event, or via the [UpdateManager class](FAQ.md#manager) that simplifies the handling of updates.
See the `DisplayMessage` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L23).
See the `HandleMessage` method in [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21).
You can filter specific chats the message are posted in, by looking at the `Message.peer_id` field.
You can filter specific chats the message are posted in, by looking at the `Message.peer_id` field.
See also [explanation below](#message-user) to extract user/chat info from messages.
<a name="download"></a>
## Downloading photos, medias, files
@ -207,7 +208,9 @@ You can filter specific chats the message are posted in, by looking at the `Mess
This is done using the helper method `client.DownloadFileAsync(file, outputStream)`
that simplifies the download of a photo/document/file once you get a reference to its location *(through updates or API calls)*.
See [Examples/Program_DownloadSavedMedia.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_DownloadSavedMedia.cs?ts=4#L31) that download all media files you forward to yourself (Saved Messages)
See [Examples/Program_DownloadSavedMedia.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_DownloadSavedMedia.cs?ts=4#L28) that download all media files you forward to yourself (Saved Messages)
_Note: To abort an ongoing download, you can throw an exception via the `progress` callback argument. Example: `(t,s) => ct.ThrowIfCancellationRequested()`_
<a name="upload"></a>
## Upload a media file and post it with caption to a chat
@ -221,6 +224,41 @@ var inputFile = await client.UploadFileAsync(Filepath);
await client.SendMediaAsync(peer, "Here is the photo", inputFile);
```
<a name="upload-video"></a>
## Upload a streamable video with optional custom thumbnail
```csharp
var chats = await client.Messages_GetAllChats();
InputPeer peer = chats.chats[1234567890]; // the chat we want
const string videoPath = @"C:\...\video.mp4";
const string thumbnailPath = @"C:\...\thumbnail.jpg";
// Extract video information using FFMpegCore or similar library
var mediaInfo = await FFmpeg.GetMediaInfo(videoPath);
var videoStream = mediaInfo.VideoStreams.FirstOrDefault();
int width = videoStream?.Width ?? 0;
int height = videoStream?.Height ?? 0;
int duration = (int)mediaInfo.Duration.TotalSeconds;
// Upload video file
var inputFile = await Client.UploadFileAsync(videoPath);
// Prepare InputMedia structure with video attributes
var media = new InputMediaUploadedDocument(inputFile, "video/mp4",
new DocumentAttributeVideo { w = width, h = height, duration = duration,
flags = DocumentAttributeVideo.Flags.supports_streaming });
if (thumbnailPath != null)
{
// upload custom thumbnail and complete InputMedia structure
var inputThumb = await client.UploadFileAsync(thumbnailPath);
media.thumb = inputThumb;
media.flags |= InputMediaUploadedDocument.Flags.has_thumb;
}
// Send the media message
await client.SendMessageAsync(peer, "caption", media);
```
*Note: This example requires FFMpegCore NuGet package for video metadata extraction. You can also manually set width, height, and duration if you know the video properties.*
<a name="album"></a>
## Send a grouped media album using photos from various sources
```csharp
@ -237,6 +275,7 @@ var inputMedias = new List<InputMedia>
photoFromTelegram, // PhotoBase has implicit conversion to InputMediaPhoto
new InputMediaUploadedPhoto { file = uploadedFile },
new InputMediaPhotoExternal { url = photoUrl },
//or Document, InputMediaDocument, InputMediaUploadedDocument, InputMediaDocumentExternal...
};
await client.SendAlbumAsync(InputPeer.Self, inputMedias, "My first album");
```
@ -253,10 +292,10 @@ var history = await client.Messages_GetHistory(from_chat, limit: 1);
var msg = history.Messages[0] as Message; // last message of source chat
// • Forward the message (only the source message id is necessary)
await client.Messages_ForwardMessages(from_chat, new[] { msg.ID }, new[] { WTelegram.Helpers.RandomLong() }, to_chat);
await client.ForwardMessagesAsync(from_chat, [msg.ID], to_chat);
// • Copy the message without the "Forwarded" header (only the source message id is necessary)
await client.Messages_ForwardMessages(from_chat, new[] { msg.ID }, new[] { WTelegram.Helpers.RandomLong() }, to_chat, drop_author: true);
await client.ForwardMessagesAsync(from_chat, [msg.ID], to_chat, drop_author: true);
// • Alternative solution to copy the message (the full message is needed)
await client.SendMessageAsync(to_chat, msg.message, msg.media?.ToInputMedia(), entities: msg.entities);
@ -270,6 +309,7 @@ InputPeer peer = chats.chats[1234567890]; // the chat we want
DateTime when = DateTime.UtcNow.AddMinutes(3);
await client.SendMessageAsync(peer, "This will be posted in 3 minutes", schedule_date: when);
```
*Note: Make sure your computer clock is synchronized with Internet time*
<a name="fun"></a>
## Fun with stickers, GIFs, dice, and animated emojies
@ -386,9 +426,9 @@ var chat = chats.chats[1234567890]; // the target chat
After the above code, once you [have obtained](FAQ.md#access-hash) an `InputUser` or `User`, you can:
```csharp
// • Directly add the user to a Chat/Channel/group:
await client.AddChatUser(chat, user);
// You may get exception USER_PRIVACY_RESTRICTED if the user has denied the right to be added to a chat
// or exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
var miu = await client.AddChatUser(chat, user);
// You may get exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
// or a result with miu.missing_invitees listing users that denied the right to be added to a chat
// • Obtain the main invite link for the chat, and send it to the user:
var mcf = await client.GetFullChat(chat);
@ -450,16 +490,17 @@ finally
## Collect Users/Chats description structures and access hash
Many API calls return a structure with a `users` and a `chats` field at the root of the structure.
This is also the case for updates passed to `client.OnUpdate`.
This is also the case for updates passed to `client.OnUpdates`.
These two dictionaries give details about the various users/chats that will be typically referenced in subobjects deeper in the structure,
typically in the form of a `Peer` object or a `user_id` field.
These two dictionaries give details *(including access hash)* about the various users/chats that will be typically referenced in subobjects deeper in the structure,
typically in the form of a `Peer` object or a `user_id`/`chat_id` field.
In such case, the root structure inherits the `IPeerResolver` interface, and you can use the `UserOrChat(peer)` method to resolve a `Peer`
into either a `User` or `ChatBase` (`Chat`,`Channel`...) description structure *(depending on the kind of peer it was describing)*
You can also use the `CollectUsersChats` helper method to collect these 2 fields into 2 aggregate dictionaries to remember details
*(including access hashes)* about all the users/chats you've encountered so far.
This method also helps dealing with [incomplete `min` structures](https://core.telegram.org/api/min).
Example of usage:
```csharp
@ -469,10 +510,9 @@ private Dictionary<long, ChatBase> _chats = new();
var dialogs = await client.Messages_GetAllDialogs();
dialogs.CollectUsersChats(_users, _chats);
private async Task OnUpdate(IObject arg)
private async Task OnUpdates(UpdatesBase updates)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(_users, _chats);
updates.CollectUsersChats(_users, _chats);
...
}
@ -484,6 +524,41 @@ else if (firstPeer is ChatBase firstChat) Console.WriteLine($"First dialog is {f
*Note: If you need to save/restore those dictionaries between runs of your program, it's up to you to serialize their content to disk*
<a name="message-user"></a>
## Get chat and user info from a message
First, you should read the above [section about collecting users/chats](#collect-users-chats), and the [FAQ about dealing with IDs](FAQ.md#access-hash).
A message contains those two fields/properties:
- `peer_id`/`Peer` that identify WHERE the message was posted
- `from_id`/`From` that identify WHO posted the message (it can be `null` in some case of anonymous posting)
These two fields derive from class `Peer` and can be of type `PeerChat`, `PeerChannel` or `PeerUser` depending on the nature of WHERE & WHO
(private chat with a user? message posted BY a channel IN a chat? ...)
> ✳️ It is recommended that you use the [UpdateManager class](FAQ.md#manager), as it handles automatically all of the details below, and you just need to use `Manager.UserOrChat(peer)` or Manager.Users/Chats dictionaries
The root structure where you obtained the message (typically `UpdatesBase` or `Messages_MessagesBase`) inherits from `IPeerResolver`.
This allows you to call `.UserOrChat(peer)` on the root structure, in order to resolve those fields into a `User` class, or a `ChatBase`-derived class
(typically `Chat` or `Channel`) which will give you details about the peer, instead of just the ID, and can be implicitly converted to `InputPeer`.
However, in some case _(typically when dealing with updates)_, Telegram might choose to not include details about a peer
because it expects you to already know about it (`UserOrChat` returns `null`).
That's why you should collect users/chats details each time you're dealing with Updates or other API results inheriting from `IPeerResolver`,
and use the collected dictionaries to find details about users/chats
([see previous section](#collect-users-chats) and [Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) example)
And finally, it may happen that you receive updates of type `UpdateShortMessage` or `UpdateShortChatMessage` with totally unknown peers (even in your collected dictionaries).
In this case, [Telegram recommends](https://core.telegram.org/api/updates#recovering-gaps) that you use the [`Updates_GetDifference`](https://corefork.telegram.org/method/updates.getDifference) method to retrieve the full information associated with the short message.
Here is an example showing how to deal with `UpdateShortMessage`: (same for `UpdateShortChatMessage`)
```csharp
if (updates is UpdateShortMessage usm && !_users.ContainsKey(usm.user_id))
{
var fullDiff = await client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0)
fullDiff.CollectUsersChats(_users, _chats);
}
```
<a name="proxy"></a>
## Use a proxy or MTProxy to connect to Telegram
SOCKS/HTTPS proxies can be used through the `client.TcpHandler` delegate and a proxy library like [StarkSoftProxy](https://www.nuget.org/packages/StarkSoftProxy/) or [xNetStandard](https://www.nuget.org/packages/xNetStandard/):
@ -504,7 +579,7 @@ using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
client.MTProxyUrl = "https://t.me/proxy?server=...&port=...&secret=...";
await client.LoginUserIfNeeded();
```
You can find a list of working MTProxies in channels like [@ProxyMTProto](https://t.me/ProxyMTProto) or [@MTProxyT](https://t.me/MTProxyT) *(right-click the "Connect" buttons)*
You can find a list of working MTProxies in channels like [@ProxyMTProto](https://t.me/s/ProxyMTProto) or [@MTProxyT](https://t.me/s/MTProxyT) *(right-click the "Connect" buttons)*
If your Telegram client is already connected to such MTPROTO proxy, you can also export its URL by clicking on the shield button ![🛡](https://raw.githubusercontent.com/telegramdesktop/tdesktop/dev/Telegram/Resources/icons/proxy_on.png) and then **⋮** > **Share**
*Note: WTelegramClient always uses transport obfuscation when connecting to Telegram servers, even without MTProxy*
@ -546,4 +621,4 @@ This can be done easily using the helper class `WTelegram.SecretChats` offering
You can view a full working example at [Examples/Program_SecretChats.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_SecretChats.cs?ts=4#L11).
Secret Chats have been tested successfully with Telegram Android & iOS official clients.
You can also check our [FAQ for more implementation details](FAQ.md#14-secret-chats-implementation-details).
You can also check our [FAQ for more implementation details](FAQ.md#14-secret-chats-implementation-details).

Binary file not shown.

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TL;
@ -9,19 +8,20 @@ namespace WTelegramClientTest
{
static class Program_DownloadSavedMedia
{
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main(string[] _)
{
Console.WriteLine("The program will download photos/medias from messages you send/forward to yourself (Saved Messages)");
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
var cts = new CancellationTokenSource();
await using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
var user = await client.LoginUserIfNeeded();
client.OnUpdate += Client_OnUpdate;
client.OnUpdates += Client_OnUpdates;
Console.ReadKey();
cts.Cancel();
async Task Client_OnUpdate(IObject arg)
async Task Client_OnUpdates(UpdatesBase updates)
{
if (arg is not Updates { updates: var updates } upd) return;
foreach (var update in updates)
foreach (var update in updates.UpdateList)
{
if (update is not UpdateNewMessage { message: Message message })
continue; // if it's not about a new message, ignore the update
@ -34,7 +34,7 @@ namespace WTelegramClientTest
filename ??= $"{document.id}.{document.mime_type[(document.mime_type.IndexOf('/') + 1)..]}";
Console.WriteLine("Downloading " + filename);
using var fileStream = File.Create(filename);
await client.DownloadFileAsync(document, fileStream);
await client.DownloadFileAsync(document, fileStream, progress: (p, t) => cts.Token.ThrowIfCancellationRequested());
Console.WriteLine("Download finished");
}
else if (message.media is MessageMediaPhoto { photo: Photo photo })

View file

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using TL;
@ -10,7 +9,7 @@ namespace WTelegramClientTest
// This code is similar to what you should have obtained if you followed the README introduction
// I've just added a few comments to explain further what's going on
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static string Config(string what)
{
if (what == "api_id") return Environment.GetEnvironmentVariable("api_id");
@ -25,7 +24,7 @@ namespace WTelegramClientTest
static async Task Main(string[] _)
{
using var client = new WTelegram.Client(Config);
await using var client = new WTelegram.Client(Config);
var user = await client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})");

View file

@ -17,8 +17,8 @@ namespace WTelegramClientTest
{
static WTelegram.Client Client;
static User My;
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
static readonly Dictionary<long, User> Users = [];
static readonly Dictionary<long, ChatBase> Chats = [];
// See steps at the end of this file to setup required Environment variables
static async Task Main(string[] _)
@ -28,9 +28,9 @@ namespace WTelegramClientTest
var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"), Environment.GetEnvironmentVariable("SESSION_NAME"));
// if DB does not contain a session yet, client will be run in interactive mode
Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store);
using (Client)
await using (Client)
{
Client.OnUpdate += Client_OnUpdate;
Client.OnUpdates += Client_OnUpdates;
My = await Client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
var dialogs = await Client.Messages_GetAllDialogs();
@ -39,9 +39,8 @@ namespace WTelegramClientTest
}
}
private static async Task Client_OnUpdate(IObject arg)
private static async Task Client_OnUpdates(UpdatesBase updates)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
{
@ -63,10 +62,8 @@ namespace WTelegramClientTest
{
private readonly NpgsqlConnection _sql;
private readonly string _sessionName;
private byte[] _data;
private int _dataLen;
private DateTime _lastWrite;
private Task _delayedWrite;
private readonly byte[] _data;
private readonly int _dataLen;
/// <param name="databaseUrl">Heroku DB URL of the form "postgres://user:password@host:port/database"</param>
/// <param name="sessionName">Entry name for the session data in the WTelegram_sessions table (default: "Heroku")</param>
@ -76,7 +73,7 @@ namespace WTelegramClientTest
var parts = databaseUrl.Split(':', '/', '@');
_sql = new NpgsqlConnection($"User ID={parts[3]};Password={parts[4]};Host={parts[5]};Port={parts[6]};Database={parts[7]};Pooling=true;SSL Mode=Require;Trust Server Certificate=True;");
_sql.Open();
using (var create = new NpgsqlCommand($"CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
using (var create = new NpgsqlCommand("CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
create.ExecuteNonQuery();
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql);
using var rdr = cmd.ExecuteReader();
@ -86,7 +83,6 @@ namespace WTelegramClientTest
protected override void Dispose(bool disposing)
{
_delayedWrite?.Wait();
_sql.Dispose();
}
@ -98,18 +94,9 @@ namespace WTelegramClientTest
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock()
{
_data = buffer; _dataLen = count;
if (_delayedWrite != null) return;
var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds;
if (left < 0)
{
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
cmd.ExecuteNonQuery();
_lastWrite = DateTime.UtcNow;
}
else // delay writings for a full second
_delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } });
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
cmd.ExecuteNonQuery();
}
public override long Length => _dataLen;
@ -135,7 +122,7 @@ HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files
- In this repository folder, create a new .NET Console project with this Program.cs file
- Add these Nuget packages: WTelegramClient and Npgsql
- In Project properties > Debug > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
- In Project properties > Debug > Launch Profiles > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
- Run the project in Visual Studio. The first time, it should ask you interactively for elements to complete the connection
- On the following runs, the PostgreSQL database contains the session data and it should connect automatically
- You can test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong"

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TL;
@ -8,56 +7,54 @@ namespace WTelegramClientTest
static class Program_ListenUpdates
{
static WTelegram.Client Client;
static WTelegram.UpdateManager Manager;
static User My;
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main(string[] _)
{
Console.WriteLine("The program will display updates received for the logged-in user. Press any key to terminate");
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
using (Client)
await using (Client)
{
Client.OnUpdate += Client_OnUpdate;
Manager = Client.WithUpdateManager(Client_OnUpdate/*, "Updates.state"*/);
My = await Client.LoginUserIfNeeded();
Users[My.id] = My;
// Note: on login, Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
// We collect all infos about the users/chats so that updates can be printed with their names
var dialogs = await Client.Messages_GetAllDialogs(); // dialogs = groups/channels/users
dialogs.CollectUsersChats(Users, Chats);
dialogs.CollectUsersChats(Manager.Users, Manager.Chats);
Console.ReadKey();
}
} // WTelegram.Client gets disposed when exiting this scope
//Manager.SaveState("Updates.state"); // if you want to resume missed updates on the next run (see WithUpdateManager above)
}
// if not using async/await, we could just return Task.CompletedTask
private static async Task Client_OnUpdate(IObject arg)
private static async Task Client_OnUpdate(Update update)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
switch (update)
{
case UpdateNewMessage unm: await DisplayMessage(unm.message); break;
case UpdateEditMessage uem: await DisplayMessage(uem.message, true); break;
// Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break;
case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break;
case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break;
case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break;
case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break;
case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break;
case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break;
case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}"); break;
case UpdateUser uu: Console.WriteLine($"{User(uu.user_id)} has changed infos/photo"); break;
default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above example cases
}
switch (update)
{
case UpdateNewMessage unm: await HandleMessage(unm.message); break;
case UpdateEditMessage uem: await HandleMessage(uem.message, true); break;
// Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break;
case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break;
case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break;
case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break;
case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break;
case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break;
case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break;
case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}"); break;
case UpdateUser uu: Console.WriteLine($"{User(uu.user_id)} has changed infos/photo"); break;
default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above example cases
}
}
// in this example method, we're not using async/await, so we just return Task.CompletedTask
private static Task DisplayMessage(MessageBase messageBase, bool edit = false)
private static Task HandleMessage(MessageBase messageBase, bool edit = false)
{
if (edit) Console.Write("(Edit): ");
switch (messageBase)
@ -68,9 +65,8 @@ namespace WTelegramClientTest
return Task.CompletedTask;
}
private static string User(long id) => Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
private static string Chat(long id) => Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
private static string Peer(Peer peer) => peer is null ? null : peer is PeerUser user ? User(user.user_id)
: peer is PeerChat or PeerChannel ? Chat(peer.ID) : $"Peer {peer.ID}";
private static string User(long id) => Manager.Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
private static string Chat(long id) => Manager.Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
private static string Peer(Peer peer) => Manager.UserOrChat(peer)?.ToString() ?? $"Peer {peer?.ID}";
}
}

View file

@ -0,0 +1,70 @@
using System;
using System.Threading.Tasks;
using TL;
namespace WTelegramClientTest
{
static class Program_ReactorError
{
static WTelegram.Client Client;
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main(string[] _)
{
Console.WriteLine("The program demonstrate how to handle ReactorError. Press any key to terminate");
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
try
{
await CreateAndConnect();
Console.ReadKey();
}
finally
{
if (Client != null) await Client.DisposeAsync();
}
}
private static async Task CreateAndConnect()
{
Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
Client.OnUpdates += Client_OnUpdates;
Client.OnOther += Client_OnOther;
var my = await Client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as " + my);
}
private static async Task Client_OnOther(IObject arg)
{
if (arg is ReactorError err)
{
// typically: network connection was totally lost
Console.WriteLine($"Fatal reactor error: {err.Exception.Message}");
while (true)
{
Console.WriteLine("Disposing the client and trying to reconnect in 5 seconds...");
if (Client != null) await Client.DisposeAsync();
Client = null;
await Task.Delay(5000);
try
{
await CreateAndConnect();
break;
}
catch (Exception ex) when (ex is not ObjectDisposedException)
{
Console.WriteLine("Connection still failing: " + ex.Message);
}
}
}
else
Console.WriteLine("Other: " + arg.GetType().Name);
}
private static Task Client_OnUpdates(UpdatesBase updates)
{
foreach (var update in updates.UpdateList)
Console.WriteLine(update.GetType().Name);
return Task.CompletedTask;
}
}
}

View file

@ -13,10 +13,10 @@ namespace WTelegramClientTest
static Client Client;
static SecretChats Secrets;
static ISecretChat ActiveChat; // the secret chat currently selected
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
static readonly Dictionary<long, User> Users = [];
static readonly Dictionary<long, ChatBase> Chats = [];
// go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main()
{
Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
@ -25,7 +25,7 @@ namespace WTelegramClientTest
AppDomain.CurrentDomain.ProcessExit += (s, e) => { Secrets.Dispose(); Client.Dispose(); };
SelectActiveChat();
Client.OnUpdate += Client_OnUpdate;
Client.OnUpdates += Client_OnUpdates;
var myself = await Client.LoginUserIfNeeded();
Users[myself.id] = myself;
Console.WriteLine($"We are logged-in as {myself}");
@ -76,9 +76,8 @@ Type a command, or a message to send to the active secret chat:");
} while (true);
}
private static async Task Client_OnUpdate(IObject arg)
private static async Task Client_OnUpdates(UpdatesBase updates)
{
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
switch (update)

Binary file not shown.

45
FAQ.md
View file

@ -59,8 +59,8 @@ You also need to obtain their `access_hash` which is specific to the resource yo
This serves as a proof that the logged-in user is entitled to access that channel/user/photo/document/...
(otherwise, anybody with the ID could access it)
> A small private `Chat` don't need an access_hash and can be queried using their `chat_id` only.
However most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
> A small private group `Chat` don't need an access_hash and can be queried using their `chat_id` only.
<ins>However</ins> most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
Some TL methods only applies to private `Chat`, some only applies to `Channel` and some to both.
The `access_hash` must usually be provided within the `Input...` structure you pass in argument to an API method (`InputPeer`, `InputChannel`, `InputUser`, etc...).
@ -108,6 +108,7 @@ To fix this, you should also switch to using the [WTelegramClient Nuget package]
You can get these kind of problems if you abuse Telegram [Terms of Service](https://telegram.org/tos), or the [API Terms of Service](https://core.telegram.org/api/terms), or make excessive requests.
You can try to wait more between the requests, wait for a day or two to see if the requests become possible again.
> For FLOOD_WAIT_X with X < 60 seconds (see `client.FloodRetryThreshold`), WTelegramClient will automatically wait the specified delay and retry the request for you.
For longer delays, you can catch the thrown `RpcException` and check the value of property X.
@ -143,7 +144,7 @@ Here are some advices from [another similar library](https://github.com/gotd/td/
Some additional advices from me:
5. Avoid repetitive polling or repetitive sequence of actions/requests: Save the initial results of your queries, and update those results when you're informed of a change through `OnUpdate` events.
5. Avoid repetitive polling or repetitive sequence of actions/requests: Save the initial results of your queries, and update those results when you're informed of a change through `OnUpdates` events.
6. Don't buy fake user accounts/sessions and don't extract api_id/hash/authkey/sessions from official clients, this is [specifically forbidden by API TOS](https://core.telegram.org/api/terms#2-transparency). You must use your own api_id and create your own sessions associated with it.
7. If a phone number is brand new, it will be closely monitored by Telegram for abuse, and it can even already be considered a bad user due to bad behavior from the previous owner of that phone number (which may happen often with VoIP or other easy-to-buy-online numbers, so expect fast ban)
8. You may want to use your new phone number account with an official Telegram client and act like a normal user for some time (some weeks/months), before using it for automation with WTelegramClient.
@ -203,9 +204,9 @@ If Telegram servers decide to shutdown this secondary connection, it's not an is
This should be transparent and pending API calls should automatically be resent upon reconnection.
You can choose to increase `MaxAutoReconnects` if it happens too often because your Internet connection is unstable.
3) If you reach `MaxAutoReconnects` disconnections, then the **OnUpdate** event handler will receive a `ReactorError` object to notify you of the problem,
3) If you reach `MaxAutoReconnects` disconnections or a reconnection fails, then the **OnOther** event handler will receive a `ReactorError` object to notify you of the problem,
and pending API calls throw the network IOException.
In this case, the recommended action would be to dispose the client and recreate one
In this case, the recommended action would be to dispose the client and recreate one (see example [Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs))
4) In case of slow Internet connection or if you break in the debugger for some time,
you might also get Connection shutdown because your client couldn't send Pings to Telegram in the allotted time.
@ -235,7 +236,7 @@ In particular, it will detect and handle automatically and properly the various
* 2FA password required (your Config needs to provide "password")
* Email registration procedure required (your Config needs to provide "email", "email_verification_code")
* Account registration/sign-up required (your Config needs to provide "first_name", "last_name")
* Request to resend the verification code through alternate ways like SMS (if your Config answer an empty "verification_code" initially)
* Request to resend the verification code through alternate ways (if your Config answer an empty "verification_code" initially)
* Transient failures, slowness to respond, wrong code/password, checks for encryption key safety, etc..
Contrary to TLSharp, WTelegramClient supports MTProto v2.0 (more secured), transport obfuscation, protocol security checks, MTProto [Proxy](EXAMPLES.md#proxy), real-time updates, multiple DC connections, API documentation in Intellisense...
@ -261,7 +262,7 @@ The following choices were made while implementing Secret Chats in WTelegramClie
If for some reason, we received them in incorrect order, messages are kept in memory until the requested missing messages are obtained.
If those missing messages are never obtained during the session, incoming messages might get stuck and lost.
- SecretChats file data is only valid for the current user, so make sure to pick the right file *(or a new file name)* if you change logged-in user.
- If you want to accept incoming Secret Chats request only from specific user, you must check it in OnUpdate before:
- If you want to accept incoming Secret Chats request only from specific user, you must check it in OnUpdates before:
`await Secrets.HandleUpdate(ue, ue.chat is EncryptedChatRequested ecr && ecr.admin_id == EXPECTED_USER_ID);`
- As recommended, new encryption keys are negotiated every 100 sent/received messages or after one week.
If remote client doesn't complete this negotiation before reaching 200 messages, the Secret Chat is aborted.
@ -329,3 +330,33 @@ For a console program, this is typical done by waiting for a key or some close e
5) Is every Telegram API call rejected? (typically with an exception message like `AUTH_RESTART`)
The user authentification might have failed at some point (or the user revoked the authorization).
It is therefore necessary to go through the authentification again. This can be done by deleting the WTelegram.session file, or at runtime by calling `client.Reset()`
<a name="manager"></a>
# About the UpdateManager
The UpdateManager does the following:
- ensure the correct sequential order of receiving updates (Telegram may send them in wrong order)
- fetch the missing updates if there was a gap (missing update) in the flow of incoming updates
- resume the flow of updates where you left off if you stopped your program (with saved state)
- collect the users & chats from updates automatically for you _(by default)_
- simplifies the handling of the various containers of update (UpdatesBase)
To use the UpdateManager, instead of setting `client.OnUpdates`, you call:
```csharp
// if you don't care about missed updates while your program was down:
var manager = client.WithUpdateManager(OnUpdate);
// if you want to recover missed updates using the state saved on the last run of your program
var manager = client.WithUpdateManager(OnUpdate, "Updates.state");
// to save the state later, preferably after disposing the client:
manager.SaveState("Updates.state")
```
Your `OnUpdate` method will directly take a single `Update` as parameter, instead of a container of updates.
The `manager.Users` and `manager.Chats` dictionaries will collect the users/chats data from updates.
You can also feed them manually from result of your API calls by calling `result.CollectUsersChats(manager.Users, manager.Chats);` and resolve Peer fields via `manager.UserOrChat(peer)`
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) for an example of implementation.
Notes:
- set `manager.Log` if you want different logger settings than the client
- `WithUpdateManager()` has other parameters for advanced use

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Olivier Marcoux
Copyright (c) 2021-2024 Olivier Marcoux
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,17 +1,19 @@
[![API Layer](https://img.shields.io/badge/API_Layer-158-blueviolet)](https://corefork.telegram.org/methods)
[![API Layer](https://img.shields.io/badge/API_Layer-220-blueviolet)](https://corefork.telegram.org/methods)
[![NuGet version](https://img.shields.io/nuget/v/WTelegramClient?color=00508F)](https://www.nuget.org/packages/WTelegramClient/)
[![Build Status](https://img.shields.io/azure-devops/build/wiz0u/WTelegramClient/7)](https://dev.azure.com/wiz0u/WTelegramClient/_build?definitionId=7)
[![Donate](https://img.shields.io/badge/Help_this_project:-Donate-ff4444)](https://www.buymeacoffee.com/wizou)
[![NuGet prerelease](https://img.shields.io/nuget/vpre/WTelegramClient?color=C09030&label=dev+nuget)](https://www.nuget.org/packages/WTelegramClient/absoluteLatest)
[![Donate](https://img.shields.io/badge/Help_this_project:-Donate-ff4444)](https://buymeacoffee.com/wizou)
## _Telegram Client API library written 100% in C# and .NET_
## *_Telegram Client API library written 100% in C# and .NET_*
This library allows you to connect to Telegram and control a user programmatically (or a bot, but [Telegram.Bot](https://github.com/TelegramBots/Telegram.Bot) is much easier for that).
This library allows you to connect to Telegram and control a user programmatically (or a bot, but [WTelegramBot](https://www.nuget.org/packages/WTelegramBot) is much easier for that).
All the Telegram Client APIs (MTProto) are supported so you can do everything the user could do with a full Telegram GUI client.
Library was developed solely by one unemployed guy. [Donations](https://buymeacoffee.com/wizou) or [Patreon memberships are welcome](https://patreon.com/wizou).
This ReadMe is a **quick but important tutorial** to learn the fundamentals about this library. Please read it all.
>⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**...
>If you are a beginner in C#, starting a project based on this library might not be a great idea.
> ⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**...
> If you are a beginner in C#, starting a project based on this library might not be a great idea.
# How to use
@ -91,6 +93,8 @@ Since version 3.0.0, a new approach to login/configuration has been added. Some
```csharp
WTelegram.Client client = new WTelegram.Client(YOUR_API_ID, "YOUR_API_HASH"); // this constructor doesn't need a Config method
await DoLogin("+12025550156"); // initial call with user's phone_number
...
//client.Dispose(); // the client must be disposed when you're done running your userbot.
async Task DoLogin(string loginInfo) // (add this method to your code)
{
@ -112,7 +116,7 @@ See [WinForms example](https://wiz0u.github.io/WTelegramClient/Examples/WinForms
# Example of API call
> The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes
> The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes
to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
All the Telegram API classes/methods are fully documented through Intellisense: Place your mouse over a class/method name,
@ -158,15 +162,20 @@ or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (th
See [FAQ #4](https://wiz0u.github.io/WTelegramClient/FAQ#access-hash) to learn more about it.
- **DC** (DataCenter): There are a few datacenters depending on where in the world the user (or an uploaded media file) is from.
- **Session** or **Authorization**: Pairing between a device and a phone number. You can have several active sessions for the same phone number.
- **Participant**: A member/subscriber of a chat group or channel
# Other things to know
The Client class also offers an `OnUpdate` event that is triggered when Telegram servers sends Updates (like new messages or status), independently of your API requests.
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L23)
The Client class offers `OnUpdates` and `OnOther` events that are triggered when Telegram servers sends Updates (like new messages or status) or other notifications, independently of your API requests.
You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates.
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) and [Examples/Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30)
An invalid API request can result in a `RpcException` being raised, reflecting the [error code and status text](https://revgram.github.io/errors.html) of the problem.
The other configuration items that you can provide include: **session_pathname, email, email_verification_code, session_key, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, firebase, user_id, bot_token**
To [prevent getting banned](https://wiz0u.github.io/WTelegramClient/FAQ#prevent-ban) during dev, you can connect to [test servers](https://docs.pyrogram.org/topics/test-servers), by adding this line in your Config callback:
`case "server_address": return "2>149.154.167.40:443"; // test DC`
The other configuration items that you can provide include: **session_pathname, email, email_verification_code, session_key, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, firebase, user_id, bot_token**
Optional API parameters have a default value of `null` when unset. Passing `null` for a required string/array is the same as *empty* (0-length).
Required API parameters/fields can sometimes be set to 0 or `null` when unused (check API documentation or experiment).
@ -182,7 +191,7 @@ This library works best with **.NET 5.0+** (faster, no dependencies) and is also
This library can be used for any Telegram scenario including:
- Sequential or parallel automated steps based on API requests/responses
- Real-time [monitoring](https://wiz0u.github.io/WTelegramClient/EXAMPLES#updates) of incoming Updates/Messages
- Download/upload of files/media
- [Download](https://wiz0u.github.io/WTelegramClient/EXAMPLES#download)/[upload](https://wiz0u.github.io/WTelegramClient/EXAMPLES#upload) of files/media
- Exchange end-to-end encrypted messages/files in [Secret Chats](https://wiz0u.github.io/WTelegramClient/EXAMPLES#e2e)
- Building a full-featured interactive client
@ -195,4 +204,6 @@ as well as the [API Terms of Service](https://core.telegram.org/api/terms) or yo
If you read all this ReadMe, the [Frequently Asked Questions](https://wiz0u.github.io/WTelegramClient/FAQ),
the [Examples codes](https://wiz0u.github.io/WTelegramClient/EXAMPLES) and still have questions, feedback is welcome in our Telegram group [@WTelegramClient](https://t.me/WTelegramClient)
If you like this library, you can [buy me a coffee](https://www.buymeacoffee.com/wizou) ❤ This will help the project keep going.
If you like this library, you can [buy me a coffee](https://buymeacoffee.com/wizou) ❤ This will help the project keep going.
© 2021-2025 Olivier Marcoux

View file

@ -0,0 +1,266 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Text;
#pragma warning disable RS1024 // Symbols should be compared for equality
namespace TL.Generator;
[Generator]
public class MTProtoGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName("TL.TLDefAttribute",
(_, _) => true, (context, _) => (ClassDeclarationSyntax)context.TargetNode);
var source = context.CompilationProvider.Combine(classDeclarations.Collect());
context.RegisterSourceOutput(source, Execute);
}
static void Execute(SourceProductionContext context, (Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes) unit)
{
var object_ = unit.compilation.GetSpecialType(SpecialType.System_Object);
if (unit.compilation.GetTypeByMetadataName("TL.TLDefAttribute") is not { } tlDefAttribute) return;
if (unit.compilation.GetTypeByMetadataName("TL.IfFlagAttribute") is not { } ifFlagAttribute) return;
if (unit.compilation.GetTypeByMetadataName("TL.Layer") is not { } layer) return;
if (unit.compilation.GetTypeByMetadataName("TL.IObject") is not { } iobject) return;
var nullables = LoadNullables(layer);
var namespaces = new Dictionary<string, Dictionary<string, string>>(); // namespace,class,methods
var tableTL = new StringBuilder();
var methodsTL = new StringBuilder();
var source = new StringBuilder();
source
.AppendLine("using System;")
.AppendLine("using System.Collections.Generic;")
.AppendLine("using System.ComponentModel;")
.AppendLine("using System.IO;")
.AppendLine("using System.Linq;")
.AppendLine("using TL;")
.AppendLine()
.AppendLine("#pragma warning disable CS0109")
.AppendLine();
tableTL
.AppendLine("\t\tpublic static readonly Dictionary<uint, Func<BinaryReader, IObject>> Table = new()")
.AppendLine("\t\t{");
methodsTL
.AppendLine("\t\tpublic static readonly Dictionary<uint, Func<BinaryReader, IObject>> Methods = new()")
.AppendLine("\t\t{");
foreach (var classDecl in unit.classes)
{
var semanticModel = unit.compilation.GetSemanticModel(classDecl.SyntaxTree);
if (semanticModel.GetDeclaredSymbol(classDecl) is not { } symbol) continue;
var tldef = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
if (tldef == null) continue;
var id = (uint)tldef.ConstructorArguments[0].Value;
StringBuilder writeTl = new(), readTL = new();
var ns = symbol.BaseType.ContainingNamespace.ToString();
var name = symbol.BaseType.Name;
if (ns != "System")
{
if (!namespaces.TryGetValue(ns, out var parentClasses)) namespaces[ns] = parentClasses = [];
parentClasses.TryGetValue(name, out var parentMethods);
if (symbol.BaseType.IsAbstract)
{
if (parentMethods == null)
{
if (name is "Peer")
writeTl.AppendLine("\t\tpublic virtual void WriteTL(BinaryWriter writer) => throw new NotSupportedException();");
else
writeTl.AppendLine("\t\tpublic abstract void WriteTL(BinaryWriter writer);");
parentClasses[name] = writeTl.ToString();
writeTl.Clear();
}
}
else if (parentMethods?.Contains(" virtual ") == false)
parentClasses[name] = parentMethods.Replace("public void WriteTL(", "public virtual void WriteTL(");
}
ns = symbol.ContainingNamespace.ToString();
name = symbol.Name;
if (!namespaces.TryGetValue(ns, out var classes)) namespaces[ns] = classes = [];
if (name is "_Message" or "MsgCopy")
{
classes[name] = "\t\tpublic void WriteTL(BinaryWriter writer) => throw new NotSupportedException();";
continue;
}
if (id == 0x3072CFA1) // GzipPacked
tableTL.AppendLine($"\t\t\t[0x{id:X8}] = reader => (IObject)reader.ReadTLGzipped(typeof(IObject)),");
else if (name != "Null")
{
if (ns == "TL.Methods")
methodsTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}{(symbol.IsGenericType ? "<object>" : "")}.ReadTL,");
if (ns != "TL.Methods" || name == "Ping")
tableTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}.ReadTL,");
}
var override_ = symbol.BaseType == object_ ? "" : "override ";
if (name == "Messages_AffectedMessages") override_ = "virtual ";
//if (symbol.Constructors[0].IsImplicitlyDeclared)
// ctorTL.AppendLine($"\t\tpublic {name}() {{ }}");
if (symbol.IsGenericType) name += "<X>";
readTL
.AppendLine($"\t\tpublic static new {name} ReadTL(BinaryReader reader)")
.AppendLine("\t\t{")
.AppendLine($"\t\t\tvar r = new {name}();");
writeTl
.AppendLine("\t\t[EditorBrowsable(EditorBrowsableState.Never)]")
.AppendLine($"\t\tpublic {override_}void WriteTL(BinaryWriter writer)")
.AppendLine("\t\t{")
.AppendLine($"\t\t\twriter.Write(0x{id:X8});");
var members = symbol.GetMembers().ToList();
for (var parent = symbol.BaseType; parent != object_; parent = parent.BaseType)
{
var inheritBefore = (bool?)tldef.NamedArguments.FirstOrDefault(k => k.Key == "inheritBefore").Value.Value ?? false;
if (inheritBefore) members.InsertRange(0, parent.GetMembers());
else members.AddRange(parent.GetMembers());
tldef = parent.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
}
foreach (var member in members.OfType<IFieldSymbol>())
{
if (member.DeclaredAccessibility != Accessibility.Public || member.IsStatic) continue;
readTL.Append("\t\t\t");
writeTl.Append("\t\t\t");
var ifFlag = (int?)member.GetAttributes().FirstOrDefault(a => a.AttributeClass == ifFlagAttribute)?.ConstructorArguments[0].Value;
if (ifFlag != null)
{
readTL.Append(ifFlag < 32 ? $"if (((uint)r.flags & 0x{1 << ifFlag:X}) != 0) "
: $"if (((uint)r.flags2 & 0x{1 << (ifFlag - 32):X}) != 0) ");
writeTl.Append(ifFlag < 32 ? $"if (((uint)flags & 0x{1 << ifFlag:X}) != 0) "
: $"if (((uint)flags2 & 0x{1 << (ifFlag - 32):X}) != 0) ");
}
string memberType = member.Type.ToString();
switch (memberType)
{
case "int":
readTL.AppendLine($"r.{member.Name} = reader.ReadInt32();");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "long":
readTL.AppendLine($"r.{member.Name} = reader.ReadInt64();");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "double":
readTL.AppendLine($"r.{member.Name} = reader.ReadDouble();");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "bool":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLBool();");
writeTl.AppendLine($"writer.Write({member.Name} ? 0x997275B5 : 0xBC799737);");
break;
case "System.DateTime":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLStamp();");
writeTl.AppendLine($"writer.WriteTLStamp({member.Name});");
break;
case "string":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLString();");
writeTl.AppendLine($"writer.WriteTLString({member.Name});");
break;
case "byte[]":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLBytes();");
writeTl.AppendLine($"writer.WriteTLBytes({member.Name});");
break;
case "TL.Int128":
readTL.AppendLine($"r.{member.Name} = new Int128(reader);");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "TL.Int256":
readTL.AppendLine($"r.{member.Name} = new Int256(reader);");
writeTl.AppendLine($"writer.Write({member.Name});");
break;
case "System.Collections.Generic.List<TL._Message>":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLRawVector<_Message>(0x5BB8E511);");
writeTl.AppendLine($"writer.WriteTLMessages({member.Name});");
break;
case "TL.IObject": case "TL.IMethod<X>":
readTL.AppendLine($"r.{member.Name} = {(memberType == "TL.IObject" ? "reader.ReadTLObject()" : "reader.ReadTLMethod<X>()")};");
writeTl.AppendLine($"{member.Name}.WriteTL(writer);");
break;
case "System.Collections.Generic.Dictionary<long, TL.User>":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLDictionary<User>();");
writeTl.AppendLine($"writer.WriteTLVector({member.Name}?.Values.ToArray());");
break;
case "System.Collections.Generic.Dictionary<long, TL.ChatBase>":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLDictionary<ChatBase>();");
writeTl.AppendLine($"writer.WriteTLVector({member.Name}?.Values.ToArray());");
break;
case "object":
readTL.AppendLine($"r.{member.Name} = reader.ReadTLObject();");
writeTl.AppendLine($"writer.WriteTLValue({member.Name}, {member.Name}.GetType());");
break;
default:
if (member.Type is IArrayTypeSymbol arrayType)
{
if (name is "FutureSalts")
{
readTL.AppendLine($"r.{member.Name} = reader.ReadTLRawVector<{memberType.Substring(0, memberType.Length - 2)}>(0x0949D9DC).ToArray();");
writeTl.AppendLine($"writer.WriteTLRawVector({member.Name}, 16);");
}
else
{
readTL.AppendLine($"r.{member.Name} = reader.ReadTLVector<{memberType.Substring(0, memberType.Length - 2)}>();");
writeTl.AppendLine($"writer.WriteTLVector({member.Name});");
}
}
else if (member.Type.BaseType.SpecialType == SpecialType.System_Enum)
{
readTL.AppendLine($"r.{member.Name} = ({memberType})reader.ReadUInt32();");
writeTl.AppendLine($"writer.Write((uint){member.Name});");
}
else if (memberType.StartsWith("TL."))
{
readTL.AppendLine($"r.{member.Name} = ({memberType})reader.ReadTLObject();");
var nullStr = nullables.TryGetValue(memberType, out uint nullCtor) ? $"0x{nullCtor:X8}" : "Layer.NullCtor";
writeTl.AppendLine($"if ({member.Name} != null) {member.Name}.WriteTL(writer); else writer.Write({nullStr});");
}
else
writeTl.AppendLine($"Cannot serialize {memberType}");
break;
}
}
readTL.AppendLine("\t\t\treturn r;");
readTL.AppendLine("\t\t}");
writeTl.AppendLine("\t\t}");
readTL.Append(writeTl.ToString());
classes[name] = readTL.ToString();
}
foreach (var nullable in nullables)
tableTL.AppendLine($"\t\t\t[0x{nullable.Value:X8}] = null,");
tableTL.AppendLine("\t\t};");
methodsTL.AppendLine("\t\t};");
namespaces["TL"]["Layer"] = tableTL.ToString() + methodsTL.ToString();
foreach (var namesp in namespaces)
{
source.Append("namespace ").AppendLine(namesp.Key).Append('{');
foreach (var method in namesp.Value)
source.AppendLine().Append("\tpartial class ").AppendLine(method.Key).AppendLine("\t{").Append(method.Value).AppendLine("\t}");
source.AppendLine("}").AppendLine();
}
string text = source.ToString();
Debug.Write(text);
context.AddSource("TL.Generated.cs", text);
}
private static Dictionary<string, uint> LoadNullables(INamedTypeSymbol layer)
{
var nullables = layer.GetMembers("Nullables").Single() as IFieldSymbol;
var initializer = nullables.DeclaringSyntaxReferences[0].GetSyntax().ToString();
var table = new Dictionary<string, uint>();
foreach (var line in initializer.Split('\n'))
{
int index = line.IndexOf("[typeof(");
if (index == -1) continue;
int index2 = line.IndexOf(')', index += 8);
string className = "TL." + line.Substring(index, index2 - index);
index = line.IndexOf("= 0x", index2);
if (index == -1) continue;
index2 = line.IndexOf(',', index += 4);
table[className] = uint.Parse(line.Substring(index, index2 - index), System.Globalization.NumberStyles.HexNumber);
}
return table;
}
}

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IncludeBuildOutput>true</IncludeBuildOutput>
<EnableNETAnalyzers>True</EnableNETAnalyzers>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
<!--<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>-->
</Project>

View file

@ -2,8 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using TL;
@ -15,46 +13,52 @@ namespace WTelegram
{
partial class Client
{
#region Client TL Helpers
/// <summary>Used to indicate progression of file download/upload</summary>
/// <param name="transmitted">transmitted bytes</param>
/// <param name="totalSize">total size of file in bytes, or 0 if unknown</param>
public delegate void ProgressCallback(long transmitted, long totalSize);
/// <summary>Helper function to upload a file to Telegram</summary>
/// <summary>Helper method to upload a file to Telegram</summary>
/// <param name="pathname">Path to the file to upload</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
public Task<InputFileBase> UploadFileAsync(string pathname, ProgressCallback progress = null)
=> UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname), progress);
/// <summary>Helper function to upload a file to Telegram</summary>
/// <summary>Helper method to upload a file to Telegram</summary>
/// <param name="stream">Content of the file to upload. This method close/dispose the stream</param>
/// <param name="filename">Name of the file</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>an <see cref="InputFile"/> or <see cref="InputFileBig"/> than can be used in various requests</returns>
public async Task<InputFileBase> UploadFileAsync(Stream stream, string filename, ProgressCallback progress = null)
{
using var md5 = MD5.Create();
var client = await GetClientForDC(-_dcSession.DcID, true);
using (stream)
{
long transmitted = 0, length = stream.Length;
var isBig = length >= 10 * 1024 * 1024;
int file_total_parts = (int)((length - 1) / FilePartSize) + 1;
const long SMALL_FILE_MAX_SIZE = 10 << 20;
bool hasLength = stream.CanSeek;
long transmitted = 0, length = hasLength ? stream.Length : -1;
bool isBig = !hasLength || length > SMALL_FILE_MAX_SIZE;
int file_total_parts = hasLength ? (int)((length - 1) / FilePartSize) + 1 : -1;
long file_id = Helpers.RandomLong();
int file_part = 0, read;
var tasks = new Dictionary<int, Task>();
bool abort = false;
for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
for (long bytesLeft = hasLength ? length : long.MaxValue; !abort && bytesLeft != 0; file_part++)
{
var bytes = new byte[Math.Min(FilePartSize, bytesLeft)];
read = await stream.FullReadAsync(bytes, bytes.Length, default);
await _parallelTransfers.WaitAsync();
bytesLeft -= read;
if (!hasLength && read < bytes.Length)
{
file_total_parts = file_part;
if (read == 0) break; else file_total_parts++;
bytes = bytes[..read];
bytesLeft = 0;
}
var task = SavePart(file_part, bytes);
lock (tasks) tasks[file_part] = task;
if (!isBig)
md5.TransformBlock(bytes, 0, read, null, 0);
if (read < FilePartSize && bytesLeft != 0) throw new WTException($"Failed to fully read stream ({read},{bytesLeft})");
async Task SavePart(int file_part, byte[] bytes)
@ -62,9 +66,9 @@ namespace WTelegram
try
{
if (isBig)
await this.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
await client.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
else
await this.Upload_SaveFilePart(file_id, file_part, bytes);
await client.Upload_SaveFilePart(file_id, file_part, bytes);
lock (tasks) { transmitted += bytes.Length; tasks.Remove(file_part); }
progress?.Invoke(transmitted, length);
}
@ -80,77 +84,89 @@ namespace WTelegram
}
}
Task[] remainingTasks;
lock (tasks) remainingTasks = tasks.Values.ToArray();
lock (tasks) remainingTasks = [.. tasks.Values];
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
if (!isBig) md5.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
: new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash };
: new InputFile { id = file_id, parts = file_total_parts, name = filename };
}
}
/// <summary>Search messages in chat with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.search"/></para></summary>
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
/// <param name="peer">User or chat, histories with which are searched, or <see langword="null"/> constructor for global search</param>
/// <param name="text">Text search request</param>
/// <param name="q">Text search request</param>
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
public Task<Messages_MessagesBase> Messages_Search<T>(InputPeer peer, string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
=> this.Messages_Search(peer, text, new T(), offset_id: offset_id, limit: limit);
public Task<Messages_MessagesBase> Messages_Search<T>(InputPeer peer, string q = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
=> this.Messages_Search(peer, q, new T(), offset_id: offset_id, limit: limit);
/// <summary>Search messages globally with <see href="https://corefork.telegram.org/type/MessagesFilter">filter</see> and text <para>See <a href="https://corefork.telegram.org/method/messages.searchGlobal"/></para></summary>
/// <typeparam name="T">See <see cref="MessagesFilter"/> for a list of possible filter types</typeparam>
/// <param name="text">Text search request</param>
/// <param name="q">Query</param>
/// <param name="offset_id">Only return messages starting from the specified message ID</param>
/// <param name="limit"><a href="https://corefork.telegram.org/api/offsets">Number of results to return</a></param>
public Task<Messages_MessagesBase> Messages_SearchGlobal<T>(string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
=> this.Messages_SearchGlobal(text, new T(), offset_id: offset_id, limit: limit);
public Task<Messages_MessagesBase> Messages_SearchGlobal<T>(string q = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
=> this.Messages_SearchGlobal(q, new T(), offset_id: offset_id, limit: limit);
/// <summary>Helper function to send a media message more easily</summary>
/// <summary>Helper method to send a media message more easily</summary>
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
/// <param name="mediaFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, or a MIME type to send as a document</param>
/// <param name="uploadedFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, <c>"video"</c> for a streamable MP4 video, or a MIME type to send as a document</param>
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
/// <returns>The transmitted message confirmed by Telegram</returns>
public Task<Message> SendMediaAsync(InputPeer peer, string caption, InputFileBase mediaFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
public Task<Message> SendMediaAsync(InputPeer peer, string caption, InputFileBase uploadedFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
{
mimeType ??= Path.GetExtension(mediaFile.Name)?.ToLowerInvariant() switch
mimeType ??= Path.GetExtension(uploadedFile.Name)?.ToLowerInvariant() switch
{
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
".mp4" => "video",
".gif" => "image/gif",
".webp" => "image/webp",
".mp4" => "video/mp4",
".mp3" => "audio/mpeg",
".wav" => "audio/x-wav",
_ => "", // send as generic document with undefined MIME type
};
if (mimeType == "photo")
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = mediaFile }, reply_to_msg_id, entities, schedule_date);
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(mediaFile, mimeType), reply_to_msg_id, entities, schedule_date);
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = uploadedFile }, reply_to_msg_id, entities, schedule_date);
else if (mimeType == "video")
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(uploadedFile, "video/mp4", new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming }), reply_to_msg_id, entities, schedule_date);
else
return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(uploadedFile, mimeType), reply_to_msg_id, entities, schedule_date);
}
/// <summary>Helper function to send a text or media message easily</summary>
public enum LinkPreview { Disabled = 0, BelowText = 1, AboveText = 2 };
/// <summary>Helper method to send a text or media message easily</summary>
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
/// <param name="text">The plain text of the message (or media caption)</param>
/// <param name="media">An instance of <see cref="InputMedia">InputMedia</see>-derived class, or <see langword="null"/> if there is no associated media</param>
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
/// <param name="entities">Text formatting entities. You can use <see cref="HtmlText.HtmlToEntities">HtmlToEntities</see> or <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
/// <param name="disable_preview">Should website/media preview be shown or not, for URLs in your message</param>
/// <param name="preview">Should website/media preview be shown below, above or not, for URL links in your message</param>
/// <returns>The transmitted message as confirmed by Telegram</returns>
public async Task<Message> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
public async Task<Message> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, LinkPreview preview = LinkPreview.BelowText)
{
UpdatesBase updates;
long random_id = Helpers.RandomLong();
if (media == null)
updates = await this.Messages_SendMessage(peer, text, random_id, no_webpage: disable_preview, entities: entities,
reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
updates = await this.Messages_SendMessage(peer, text, random_id, entities: entities,
no_webpage: preview == LinkPreview.Disabled, invert_media: preview == LinkPreview.AboveText,
reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date == default ? null : schedule_date);
else
updates = await this.Messages_SendMedia(peer, media, text, random_id, entities: entities,
reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
RaiseUpdate(updates);
reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date == default ? null : schedule_date);
if (updates is UpdateShortSentMessage sent)
return new Message
{
flags = (Message.Flags)sent.flags | (reply_to_msg_id == 0 ? 0 : Message.Flags.has_reply_to) | (peer is InputPeerSelf ? 0 : Message.Flags.has_from_id),
id = sent.id, date = sent.date, message = text, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
reply_to = reply_to_msg_id == 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id, flags = MessageReplyHeader.Flags.has_reply_to_msg_id },
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
peer_id = InputToPeer(peer)
};
int msgId = -1;
foreach (var update in updates.UpdateList)
{
@ -161,35 +177,25 @@ namespace WTelegram
case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg;
}
}
if (updates is UpdateShortSentMessage sent)
{
return new Message
{
flags = (Message.Flags)sent.flags | (reply_to_msg_id == 0 ? 0 : Message.Flags.has_reply_to) | (peer is InputPeerSelf ? 0 : Message.Flags.has_from_id),
id = sent.id, date = sent.date, message = text, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
reply_to = reply_to_msg_id == 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id },
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
peer_id = InputToPeer(peer)
};
}
return null;
}
/// <summary>Helper function to send an album (media group) of photos or documents more easily</summary>
/// <summary>Helper method to send an album (media group) of photos or documents more easily</summary>
/// <param name="peer">Destination of message (chat group, channel, user chat, etc..) </param>
/// <param name="medias">An array or List of <see cref="InputMedia">InputMedia</see>-derived class</param>
/// <param name="caption">Caption for the media <i>(in plain text)</i> or <see langword="null"/></param>
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</param>
/// <param name="entities">Text formatting entities for the caption. You can use <see cref="Markdown.MarkdownToEntities">MarkdownToEntities</see> to create these</param>
/// <param name="schedule_date">UTC timestamp when the message should be sent</param>
/// <returns>The last of the media group messages, confirmed by Telegram</returns>
/// <param name="videoUrlAsFile">Any <see cref="InputMediaDocumentExternal"/> URL pointing to a video should be considered as non-streamable</param>
/// <returns>The media group messages, as received by Telegram</returns>
/// <remarks>
/// * The caption/entities are set on the last media<br/>
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported by downloading the file from the web via HttpClient and sending it to Telegram.
/// * The caption/entities are set on the first media<br/>
/// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported natively for bot accounts, and for user accounts by downloading the file from the web via HttpClient and sending it to Telegram.
/// WTelegramClient proxy settings don't apply to HttpClient<br/>
/// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
/// </remarks>
public async Task<Message[]> SendAlbumAsync(InputPeer peer, ICollection<InputMedia> medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
public async Task<Message[]> SendAlbumAsync(InputPeer peer, ICollection<InputMedia> medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool videoUrlAsFile = false)
{
System.Net.Http.HttpClient httpClient = null;
int i = 0, length = medias.Count;
@ -210,42 +216,56 @@ namespace WTelegram
var mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imud);
ism.media = mmd.document;
break;
case InputMediaDocumentExternal imde:
string mimeType = null;
var inputFile = await UploadFromUrl(imde.url);
ism.media = new InputMediaUploadedDocument(inputFile, mimeType);
goto retry;
case InputMediaPhotoExternal impe:
inputFile = await UploadFromUrl(impe.url);
if (User.IsBot)
try
{
mmp = (MessageMediaPhoto)await this.Messages_UploadMedia(peer, impe);
ism.media = mmp.photo;
break;
}
catch (RpcException) { }
var inputFile = await UploadFromUrl(impe.url);
ism.media = new InputMediaUploadedPhoto { file = inputFile };
goto retry;
case InputMediaDocumentExternal imde:
if (!videoUrlAsFile && User.IsBot)
try
{
mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imde);
ism.media = mmd.document;
break;
}
catch (RpcException) { }
string mimeType = null;
inputFile = await UploadFromUrl(imde.url);
if (videoUrlAsFile || mimeType?.StartsWith("video/") != true)
ism.media = new InputMediaUploadedDocument(inputFile, mimeType);
else
ism.media = new InputMediaUploadedDocument(inputFile, mimeType, new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming });
goto retry;
async Task<InputFileBase> UploadFromUrl(string url)
{
var filename = Path.GetFileName(new Uri(url).LocalPath);
httpClient ??= new();
var response = await httpClient.GetAsync(url);
using var response = await httpClient.GetAsync(url, System.Net.Http.HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
mimeType = response.Content.Headers.ContentType?.MediaType;
if (response.Content.Headers.ContentLength is long length)
return await UploadFileAsync(new Helpers.IndirectStream(stream) { ContentLength = length }, filename);
else
{
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
return await UploadFileAsync(ms, filename);
}
return await UploadFileAsync(stream, filename);
}
}
}
var lastMedia = multiMedia[^1];
lastMedia.message = caption;
lastMedia.entities = entities;
if (entities != null) lastMedia.flags = InputSingleMedia.Flags.has_entities;
var firstMedia = multiMedia[0];
firstMedia.message = caption;
firstMedia.entities = entities;
if (entities != null) firstMedia.flags = InputSingleMedia.Flags.has_entities;
var updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to_msg_id: reply_to_msg_id, schedule_date: schedule_date);
RaiseUpdate(updates);
var updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to: reply_to_msg_id == 0 ? null : new InputReplyToMessage { reply_to_msg_id = reply_to_msg_id }, schedule_date: schedule_date);
var msgIds = new int[length];
var result = new Message[length];
foreach (var update in updates.UpdateList)
@ -260,6 +280,34 @@ namespace WTelegram
return result;
}
/// <summary>Helper method to forwards messages more easily by their IDs.</summary>
/// <param name="drop_author">Whether to forward messages without quoting the original author</param>
/// <param name="drop_media_captions">Whether to strip captions from media</param>
/// <param name="from_peer">Source of messages</param>
/// <param name="msg_ids">IDs of messages</param>
/// <param name="to_peer">Destination peer</param>
/// <param name="top_msg_id">Destination <a href="https://corefork.telegram.org/api/forum#forum-topics">forum topic</a></param>
/// <returns>The resulting forwarded messages, as received by Telegram <para>Some of them might be <see langword="null"/> if they could not all be forwarded</para></returns>
public async Task<Message[]> ForwardMessagesAsync(InputPeer from_peer, int[] msg_ids, InputPeer to_peer, int top_msg_id = 0, bool drop_author = false, bool drop_media_captions = false)
{
int msgCount = msg_ids.Length;
var random_id = Helpers.RandomLong();
var random_ids = Enumerable.Range(0, msgCount).Select(i => random_id + i).ToArray();
var updates = await this.Messages_ForwardMessages(from_peer, msg_ids, random_ids, to_peer, top_msg_id == 0 ? null : top_msg_id, drop_author: drop_author, drop_media_captions: drop_media_captions);
var msgIds = new int[msgCount];
var result = new Message[msgCount];
foreach (var update in updates.UpdateList)
{
switch (update)
{
case UpdateMessageID updMsgId: msgIds[updMsgId.random_id - random_id] = updMsgId.id; break;
case UpdateNewMessage { message: Message message }: result[Array.IndexOf(msgIds, message.id)] = message; break;
case UpdateNewScheduledMessage { message: Message schedMsg }: result[Array.IndexOf(msgIds, schedMsg.id)] = schedMsg; break;
}
}
return result;
}
private Peer InputToPeer(InputPeer peer) => peer switch
{
InputPeerSelf => new PeerUser { user_id = _session.UserId },
@ -286,6 +334,18 @@ namespace WTelegram
return await DownloadFileAsync(fileLocation, outputStream, photo.dc_id, photoSize.FileSize, progress);
}
/// <summary>Download an animated photo from Telegram into the outputStream</summary>
/// <param name="photo">The photo to download</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
/// <param name="videoSize">A specific size/version of the animated photo. Use <c>photo.LargestVideoSize</c> to download the largest version of the animated photo</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>The file type of the photo</returns>
public async Task<Storage_FileType> DownloadFileAsync(Photo photo, Stream outputStream, VideoSize videoSize, ProgressCallback progress = null)
{
var fileLocation = photo.ToFileLocation(videoSize);
return await DownloadFileAsync(fileLocation, outputStream, photo.dc_id, videoSize.size, progress);
}
/// <summary>Download a document from Telegram into the outputStream</summary>
/// <param name="document">The document to download</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
@ -301,6 +361,18 @@ namespace WTelegram
return thumbSize == null ? document.mime_type : "image/" + fileType;
}
/// <summary>Download a document from Telegram into the outputStream</summary>
/// <param name="document">The document to download</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
/// <param name="videoSize">A specific size/version of the animated photo. Use <c>photo.LargestVideoSize</c> to download the largest version of the animated photo</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>MIME type of the document/thumbnail</returns>
public async Task<Storage_FileType> DownloadFileAsync(Document document, Stream outputStream, VideoSize videoSize, ProgressCallback progress = null)
{
var fileLocation = document.ToFileLocation(videoSize);
return await DownloadFileAsync(fileLocation, outputStream, document.dc_id, videoSize.size, progress);
}
/// <summary>Download a file from Telegram into the outputStream</summary>
/// <param name="fileLocation">Telegram file identifier, typically obtained with a .ToFileLocation() call</param>
/// <param name="outputStream">Stream to write file content to. This method does not close/dispose the stream</param>
@ -311,10 +383,10 @@ namespace WTelegram
public async Task<Storage_FileType> DownloadFileAsync(InputFileLocationBase fileLocation, Stream outputStream, int dc_id = 0, long fileSize = 0, ProgressCallback progress = null)
{
Storage_FileType fileType = Storage_FileType.unknown;
var client = dc_id == 0 ? this : await GetClientForDC(dc_id, true);
var client = dc_id == 0 ? this : await GetClientForDC(-dc_id, true);
using var writeSem = new SemaphoreSlim(1);
bool canSeek = outputStream.CanSeek;
long streamStartPos = outputStream.Position;
long streamStartPos = canSeek ? outputStream.Position : 0;
long fileOffset = 0, maxOffsetSeen = 0;
long transmitted = 0;
var tasks = new Dictionary<long, Task>();
@ -344,7 +416,7 @@ namespace WTelegram
}
catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X")
{
client = await GetClientForDC(ex.X, true);
client = await GetClientForDC(-ex.X, true);
fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize);
}
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID")
@ -370,7 +442,7 @@ namespace WTelegram
await writeSem.WaitAsync();
try
{
if (streamStartPos + offset != outputStream.Position) // if we're about to write out of order
if (canSeek && streamStartPos + offset != outputStream.Position) // if we're about to write out of order
{
await outputStream.FlushAsync(); // async flush, otherwise Seek would do a sync flush
outputStream.Seek(streamStartPos + offset, SeekOrigin.Begin);
@ -378,6 +450,7 @@ namespace WTelegram
await outputStream.WriteAsync(fileData.bytes, 0, fileData.bytes.Length);
maxOffsetSeen = Math.Max(maxOffsetSeen, offset + fileData.bytes.Length);
transmitted += fileData.bytes.Length;
progress?.Invoke(transmitted, fileSize);
}
catch (Exception)
{
@ -387,7 +460,6 @@ namespace WTelegram
finally
{
writeSem.Release();
progress?.Invoke(transmitted, fileSize);
}
}
lock (tasks) tasks.Remove(offset);
@ -395,7 +467,7 @@ namespace WTelegram
}
}
Task[] remainingTasks;
lock (tasks) remainingTasks = tasks.Values.ToArray();
lock (tasks) remainingTasks = [.. tasks.Values];
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
await outputStream.FlushAsync();
if (canSeek) outputStream.Seek(streamStartPos + maxOffsetSeen, SeekOrigin.Begin);
@ -457,7 +529,11 @@ namespace WTelegram
public async Task<Messages_Chats> Messages_GetAllChats()
{
var dialogs = await Messages_GetAllDialogs();
return new Messages_Chats { chats = dialogs.chats };
var result = new Messages_Chats { chats = [] };
foreach (var dialog in dialogs.dialogs)
if (dialog.Peer is (PeerChat or PeerChannel) and { ID: var id })
result.chats[id] = dialogs.chats[id];
return result;
}
/// <summary>Returns the current user dialog list. <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/messages.getDialogs#possible-errors">details</a>)</para></summary>
@ -471,20 +547,27 @@ namespace WTelegram
case Messages_DialogsSlice mds:
var dialogList = new List<DialogBase>();
var messageList = new List<MessageBase>();
while (dialogs.Dialogs.Length != 0)
int skip = 0;
while (dialogs.Dialogs.Length > skip)
{
dialogList.AddRange(dialogs.Dialogs);
dialogList.AddRange(skip == 0 ? dialogs.Dialogs : dialogs.Dialogs[skip..]);
messageList.AddRange(dialogs.Messages);
var lastDialog = dialogs.Dialogs[^1];
var lastMsg = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage);
var offsetPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
dialogs = await this.Messages_GetDialogs(lastMsg?.Date ?? default, lastDialog.TopMessage, offsetPeer, folder_id: folder_id);
skip = 0;
int last = dialogs.Dialogs.Length - 1;
var lastDialog = dialogs.Dialogs[last];
retryDate:
var lastPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
var lastMsgId = lastDialog.TopMessage;
var lastDate = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage)?.Date ?? default;
if (lastDate == default)
if (--last < 0) break; else { ++skip; lastDialog = dialogs.Dialogs[last]; goto retryDate; }
dialogs = await this.Messages_GetDialogs(lastDate, lastMsgId, lastPeer, folder_id: folder_id);
if (dialogs is not Messages_Dialogs md) break;
foreach (var (key, value) in md.chats) mds.chats[key] = value;
foreach (var (key, value) in md.users) mds.users[key] = value;
}
mds.dialogs = dialogList.ToArray();
mds.messages = messageList.ToArray();
mds.dialogs = [.. dialogList];
mds.messages = [.. messageList];
return mds;
case Messages_Dialogs md: return md;
default: throw new WTException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
@ -497,12 +580,12 @@ namespace WTelegram
/// <param name="alphabet1">first letters used to search for in participants names<br/>(default values crafted with ♥ to find most latin and cyrillic names)</param>
/// <param name="alphabet2">second (and further) letters used to search for in participants names</param>
/// <param name="cancellationToken">Can be used to abort the work of this method</param>
/// <returns>Field count indicates the total count of members. Field participants contains those that were successfully fetched</returns>
/// <returns>Field <c>count</c> indicates the total count of members. Field <c>participants</c> contains those that were successfully fetched</returns>
/// <remarks>⚠ This method can take a few minutes to complete on big broadcast channels. It likely won't be able to obtain the full total count of members</remarks>
public async Task<Channels_ChannelParticipants> Channels_GetAllParticipants(InputChannelBase channel, bool includeKickBan = false, string alphabet1 = "АБCДЕЄЖФГHИІJКЛМНОПQРСТУВWХЦЧШЩЫЮЯЗ", string alphabet2 = "АCЕHИJЛМНОРСТУВWЫ", CancellationToken cancellationToken = default)
{
alphabet2 ??= alphabet1;
var result = new Channels_ChannelParticipants { chats = new(), users = new() };
var result = new Channels_ChannelParticipants { chats = [], users = [] };
var user_ids = new HashSet<long>();
var participants = new List<ChannelParticipantBase>();
@ -518,7 +601,7 @@ namespace WTelegram
await GetWithFilter(new ChannelParticipantsKicked { q = "" }, (f, c) => new ChannelParticipantsKicked { q = f.q + c }, alphabet1);
await GetWithFilter(new ChannelParticipantsBanned { q = "" }, (f, c) => new ChannelParticipantsBanned { q = f.q + c }, alphabet1);
}
result.participants = participants.ToArray();
result.participants = [.. participants];
return result;
async Task GetWithFilter<T>(T filter, Func<T, char, T> recurse = null, string alphabet = null) where T : ChannelParticipantsFilter
@ -555,25 +638,50 @@ namespace WTelegram
{
var admins = admin == null ? null : new[] { admin };
var result = await this.Channels_GetAdminLog(channel, q, events_filter: events_filter, admins: admins);
if (result.events.Length < 100) return result;
var resultFull = result;
List<ChannelAdminLogEvent> events = new(result.events);
do
var events = new List<ChannelAdminLogEvent>(result.events);
while (result.events.Length > 0)
{
result = await this.Channels_GetAdminLog(channel, q, max_id: result.events[^1].id, events_filter: events_filter, admins: admins);
events.AddRange(result.events);
foreach (var kvp in result.chats) resultFull.chats[kvp.Key] = kvp.Value;
foreach (var kvp in result.users) resultFull.users[kvp.Key] = kvp.Value;
} while (result.events.Length >= 100);
resultFull.events = events.ToArray();
}
resultFull.events = [.. events];
return resultFull;
}
/// <summary>Helper simplified method: Get all <a href="https://corefork.telegram.org/api/forum">topics of a forum</a> <para>See <a href="https://corefork.telegram.org/method/channels.getForumTopics"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/channels.getForumTopics#possible-errors">details</a>)</para></summary>
/// <param name="peer">Supergroup or Bot peer</param>
/// <param name="q">Search query</param>
public async Task<Messages_ForumTopics> Channels_GetAllForumTopics(InputPeer peer, string q = null)
{
var result = await this.Messages_GetForumTopics(peer, offset_date: DateTime.MaxValue, q: q);
if (result.topics.Length < result.count)
{
var topics = result.topics.ToList();
var messages = result.messages.ToList();
while (true)
{
var more_topics = await this.Messages_GetForumTopics(peer, messages[^1].Date, messages[^1].ID, topics[^1].ID);
if (more_topics.topics.Length == 0) break;
topics.AddRange(more_topics.topics);
messages.AddRange(more_topics.messages);
foreach (var kvp in more_topics.chats) result.chats[kvp.Key] = kvp.Value;
foreach (var kvp in more_topics.users) result.users[kvp.Key] = kvp.Value;
if (topics.Count >= more_topics.count) break;
}
result.topics = [.. topics];
result.messages = [.. messages];
}
return result;
}
private const string OnlyChatChannel = "This method works on Chat & Channel only";
/// <summary>Generic helper: Adds a single user to a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.addChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.inviteToChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="user">User to be added</param>
public Task<UpdatesBase> AddChatUser(InputPeer peer, InputUserBase user) => peer switch
public Task<Messages_InvitedUsers> AddChatUser(InputPeer peer, InputUserBase user) => peer switch
{
InputPeerChat chat => this.Messages_AddChatUser(chat.chat_id, user, int.MaxValue),
InputPeerChannel channel => this.Channels_InviteToChannel(channel, user),
@ -609,11 +717,11 @@ namespace WTelegram
{
case InputPeerChat chat:
await this.Messages_EditChatAdmin(chat.chat_id, user, is_admin);
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
return new Updates { date = DateTime.UtcNow, users = [], updates = [],
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
case InputPeerChannel channel:
return await this.Channels_EditAdmin(channel, user,
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x8BF : 0 }, null);
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x1E8BF : 0 }, null);
default:
throw new ArgumentException(OnlyChatChannel);
}
@ -656,7 +764,7 @@ namespace WTelegram
{
case InputPeerChat chat:
await this.Messages_DeleteChat(chat.chat_id);
return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
return new Updates { date = DateTime.UtcNow, users = [], updates = [],
chats = (await this.Messages_GetChats(chat.chat_id)).chats };
case InputPeerChannel channel:
return await this.Channels_DeleteChannel(channel);
@ -665,6 +773,11 @@ namespace WTelegram
}
}
/// <summary>If you want to get all messages from a chat, use method Messages_GetHistory</summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822")]
public Task<Messages_MessagesBase> GetMessages(InputPeer peer)
=> throw new WTException("If you want to get all messages from a chat, use method Messages_GetHistory");
/// <summary>Generic helper: Get individual messages by IDs [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.getMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
/// <param name="peer">User/Chat/Channel</param>
/// <param name="id">IDs of messages to get</param>
@ -683,33 +796,35 @@ namespace WTelegram
public async Task<bool> ReadHistory(InputPeer peer, int max_id = default)
=> peer is InputPeerChannel channel ? await this.Channels_ReadHistory(channel, max_id) : (await this.Messages_ReadHistory(peer, max_id)) != null;
private static readonly char[] QueryOrFragment = new[] { '?', '#' };
private static readonly char[] UrlSeparator = ['?', '#', '/'];
/// <summary>Return information about a chat/channel based on Invite Link</summary>
/// <param name="url">Channel or Invite Link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname</param>
/// <summary>Return information about a chat/channel based on Invite Link or Public Link</summary>
/// <param name="url">Public link or Invite link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname<br/>Works also without https:// prefix</param>
/// <param name="join"><see langword="true"/> to also join the chat/channel</param>
/// <param name="chats">previously collected chats, to prevent unnecessary ResolveUsername</param>
/// <returns>a Chat or Channel, possibly partial Channel information only (with flag <see cref="Channel.Flags.min"/>)</returns>
public async Task<ChatBase> AnalyzeInviteLink(string url, bool join = false)
public async Task<ChatBase> AnalyzeInviteLink(string url, bool join = false, IDictionary<long, ChatBase> chats = null)
{
int start = url.IndexOf("//");
start = url.IndexOf('/', start + 2) + 1;
int end = url.IndexOfAny(QueryOrFragment, start);
int end = url.IndexOfAny(UrlSeparator, start);
if (end == -1) end = url.Length;
if (start == 0 || end == start) throw new ArgumentException("Invalid URI");
if (start == 0 || end == start) throw new ArgumentException("Invalid URL");
string hash;
if (url[start] == '+')
hash = url[(start + 1)..end];
hash = url[(start + 1)..end];
else if (string.Compare(url, start, "joinchat/", 0, 9, StringComparison.OrdinalIgnoreCase) == 0)
hash = url[(start + 9)..end];
hash = url[(end + 1)..];
else
{
var resolved = await this.Contacts_ResolveUsername(url[start..end]);
var chat = resolved.Chat;
if (join && chat != null)
{
var res = await this.Channels_JoinChannel((Channel)chat);
chat = res.Chats[chat.ID];
}
var chat = await CachedOrResolveUsername(url[start..end], chats);
if (join && chat is Channel channel)
try
{
var res = await this.Channels_JoinChannel(channel);
chat = res.Chats[channel.id];
}
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "INVITE_REQUEST_SENT") { }
return chat;
}
var chatInvite = await this.Messages_CheckChatInvite(hash);
@ -740,46 +855,96 @@ namespace WTelegram
}
var rrAbout = ci.about == null ? null : new RestrictionReason[] { new() { text = ci.about } };
return !ci.flags.HasFlag(ChatInvite.Flags.channel)
? new Chat { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count }
? new Chat { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count,
flags = ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? (Chat.Flags)Channel.Flags.join_request : 0 }
: new Channel { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count,
restriction_reason = rrAbout,
flags = (ci.flags.HasFlag(ChatInvite.Flags.broadcast) ? Channel.Flags.broadcast | Channel.Flags.min : Channel.Flags.min) |
(ci.flags.HasFlag(ChatInvite.Flags.public_) ? Channel.Flags.has_username : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.megagroup) ? Channel.Flags.megagroup : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? Channel.Flags.join_request : 0) };
restriction_reason = rrAbout, flags = Channel.Flags.min |
(ci.flags.HasFlag(ChatInvite.Flags.broadcast) ? Channel.Flags.broadcast : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.public_) ? Channel.Flags.has_username : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.megagroup) ? Channel.Flags.megagroup : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.verified) ? Channel.Flags.verified : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.scam) ? Channel.Flags.scam : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.fake) ? Channel.Flags.fake : 0) |
(ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? Channel.Flags.join_request : 0) };
}
return null;
}
/// <summary>Return chat and message details based on a message URL</summary>
/// <param name="url">Message Link, like https://t.me/c/1234567890/1234 or https://t.me/channelname/1234</param>
/// <summary>Return chat and message details based on a Message Link (URL)</summary>
/// <param name="url">Message Link, like https://t.me/c/1234567890/1234 or t.me/channelname/1234</param>
/// <param name="chats">previously collected chats, to prevent unnecessary ResolveUsername</param>
/// <returns>Structure containing the message, chat and user details</returns>
/// <remarks>If link is for private group (<c>t.me/c/..</c>), user must have joined the group</remarks>
public async Task<Messages_ChannelMessages> GetMessageByLink(string url)
/// <remarks>If link is for private group (<c>t.me/c/..</c>), user must have joined that group</remarks>
public async Task<Messages_ChannelMessages> GetMessageByLink(string url, IDictionary<long, ChatBase> chats = null)
{
int start = url.IndexOf("//");
start = url.IndexOf('/', start + 2) + 1;
int slash = url.IndexOf('/', start + 2);
if (start == 0 || slash == -1) throw new ArgumentException("Invalid URI");
int end = url.IndexOfAny(QueryOrFragment, slash + 1);
int msgStart = slash + 1;
int end = url.IndexOfAny(UrlSeparator, msgStart);
if (end == -1) end = url.Length;
else if (url[end] == '/' && char.IsDigit(url[msgStart]) && url.Length > end + 1 && char.IsDigit(url[end + 1]))
{
end = url.IndexOfAny(UrlSeparator, msgStart = end + 1);
if (end == -1) end = url.Length;
}
if (start == 0 || slash == -1 || end <= slash + 1 || !char.IsDigit(url[msgStart])) throw new ArgumentException("Invalid URL");
int msgId = int.Parse(url[msgStart..end]);
ChatBase chat;
if (url[start] is 'c' or 'C' && url[start + 1] == '/')
{
long chatId = long.Parse(url[(start + 2)..slash]);
var chats = await this.Channels_GetChannels(new InputChannel(chatId, 0));
if (!chats.chats.TryGetValue(chatId, out chat))
throw new WTException($"Channel {chatId} not found");
if (chats?.TryGetValue(chatId, out chat) != true)
{
var mc = await this.Channels_GetChannels(new InputChannel(chatId, 0));
if (!mc.chats.TryGetValue(chatId, out chat))
throw new WTException($"Channel {chatId} not found");
else if (chats != null)
chats[chatId] = chat;
}
}
else
{
var resolved = await this.Contacts_ResolveUsername(url[start..slash]);
chat = resolved.Chat;
if (chat is null) throw new WTException($"@{url[start..slash]} is not a Chat/Channel");
}
int msgId = int.Parse(url[(slash + 1)..end]);
return await this.Channels_GetMessages((Channel)chat, msgId) as Messages_ChannelMessages;
chat = await CachedOrResolveUsername(url[start..slash], chats);
if (chat is not Channel channel) throw new WTException($"URL does not identify a valid Channel");
return await this.Channels_GetMessages(channel, msgId) as Messages_ChannelMessages;
}
private async Task<ChatBase> CachedOrResolveUsername(string username, IDictionary<long, ChatBase> chats = null)
{
if (chats == null)
return (await this.Contacts_ResolveUsername(username)).Chat;
ChatBase chat;
lock (chats)
chat = chats.Values.OfType<Channel>().FirstOrDefault(ch => ch.ActiveUsernames.Contains(username, StringComparer.OrdinalIgnoreCase));
if (chat == null)
{
chat = (await this.Contacts_ResolveUsername(username)).Chat;
if (chat != null) lock (chats) chats[chat.ID] = chat;
}
return chat;
}
/// <summary>Receive updates for a given group/channel until cancellation is requested.</summary>
/// <param name="channel">Group/channel to monitor without joining</param>
/// <param name="ct">Cancel token to stop the monitoring</param>
/// <remarks>After cancelling, you may still receive updates for a few more seconds</remarks>
public async void OpenChat(InputChannel channel, CancellationToken ct)
{
var cts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, ct);
try
{
while (!cts.IsCancellationRequested)
{
var diff = await this.Updates_GetChannelDifference(channel, null, 1, 1, true);
var timeout = diff.Timeout * 1000;
await Task.Delay(timeout != 0 ? timeout : 30000, cts.Token);
}
}
catch (Exception ex)
{
if (!cts.IsCancellationRequested)
Console.WriteLine($"An exception occured for OpenChat {channel.channel_id}: {ex.Message}");
}
}
#endregion
}
}

File diff suppressed because it is too large Load diff

View file

@ -6,11 +6,12 @@ using System.Linq;
using System.Net;
using System.Numerics;
using System.Security.Cryptography;
using System.Threading.Tasks;
#if NETCOREAPP2_1_OR_GREATER
namespace WTelegram
{
static class Compat
static partial class Compat
{
internal static BigInteger BigEndianInteger(byte[] value) => new(value, true, true);
internal static IPEndPoint IPEndPoint_Parse(string addr) => IPEndPoint.Parse(addr);
@ -19,7 +20,7 @@ namespace WTelegram
#else // Compatibility shims for methods missing in netstandard2.0:
namespace WTelegram
{
static class Compat
static partial class Compat
{
internal static BigInteger BigEndianInteger(byte[] value)
{
@ -76,6 +77,13 @@ static class Convert
internal static string ToHexString(byte[] data) => BitConverter.ToString(data).Replace("-", "");
internal static byte[] FromHexString(string hex) => Enumerable.Range(0, hex.Length / 2).Select(i => System.Convert.ToByte(hex.Substring(i * 2, 2), 16)).ToArray();
}
public class RandomNumberGenerator
{
internal static readonly RNGCryptoServiceProvider RNG = new();
public static RandomNumberGenerator Create() => new();
public void GetBytes(byte[] data) => RNG.GetBytes(data);
public void GetBytes(byte[] data, int offset, int count) => RNG.GetBytes(data, offset, count);
}
#endif
#if NETSTANDARD2_0
@ -87,7 +95,7 @@ namespace System.Runtime.CompilerServices
{
if (array == null) throw new ArgumentNullException();
var (offset, length) = range.GetOffsetAndLength(array.Length);
if (length == 0) return Array.Empty<T>();
if (length == 0) return [];
var dest = typeof(T).IsValueType || typeof(T[]) == array.GetType() ? new T[length]
: (T[])Array.CreateInstance(array.GetType().GetElementType()!, length);
Array.Copy(array, offset, dest, 0, length);
@ -97,4 +105,17 @@ namespace System.Runtime.CompilerServices
[EditorBrowsable(EditorBrowsableState.Never)]
internal class IsExternalInit { }
}
#endif
#endif
namespace WTelegram
{
static partial class Compat
{
internal static Task WaitAsync(this Task source, int timeout)
#if NET8_0_OR_GREATER
=> source?.WaitAsync(TimeSpan.FromMilliseconds(timeout)) ?? Task.CompletedTask;
#else
=> source == null ? Task.CompletedTask : Task.WhenAny(source, Task.Delay(timeout));
#endif
}
}

View file

@ -13,10 +13,10 @@ using static WTelegram.Compat;
namespace WTelegram
{
internal static class Encryption
public static class Encryption
{
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = new();
internal static readonly RNGCryptoServiceProvider RNG = new();
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = [];
internal static readonly RandomNumberGenerator RNG = RandomNumberGenerator.Create();
internal static readonly Aes AesECB = Aes.Create();
static Encryption()
@ -33,7 +33,7 @@ namespace WTelegram
var sha256 = SHA256.Create();
//1)
var nonce = new Int128(RNG);
var nonce = new TL.Int128(RNG);
var resPQ = await client.ReqPqMulti(nonce);
//2)
if (resPQ.nonce != nonce) throw new WTException("Nonce mismatch");
@ -94,7 +94,7 @@ namespace WTelegram
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new WTException("not server_DH_params_ok");
if (serverDHparamsOk.nonce != nonce) throw new WTException("Nonce mismatch");
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, pqInnerData.new_nonce);
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(sha1, resPQ.server_nonce, pqInnerData.new_nonce);
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
using var answerReader = new BinaryReader(new MemoryStream(answer));
@ -110,9 +110,9 @@ namespace WTelegram
var g_a = BigEndianInteger(serverDHinnerData.g_a);
var dh_prime = BigEndianInteger(serverDHinnerData.dh_prime);
CheckGoodPrime(dh_prime, serverDHinnerData.g);
session.LastSentMsgId = 0;
session.ServerTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
Helpers.Log(1, $"Time offset: {session.ServerTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC");
session.lastSentMsgId = 0;
session.serverTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
Helpers.Log(1, $"Time offset: {session.serverTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC");
//6)
var salt = new byte[256];
RNG.GetBytes(salt);
@ -159,29 +159,30 @@ namespace WTelegram
if (!Enumerable.SequenceEqual(dhGenOk.new_nonce_hash1.raw, sha1.ComputeHash(expected_new_nonceN).Skip(4)))
throw new WTException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
session.AuthKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
session.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
session.AuthKey = authKey;
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.OldSalt = session.Salt;
}
(byte[] key, byte[] iv) ConstructTmpAESKeyIV(Int128 server_nonce, Int256 new_nonce)
{
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(server_nonce, 0, 16);
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
sha1.Initialize();
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
sha1.Initialize();
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
sha1.Initialize();
return (tmp_aes_key, tmp_aes_iv);
}
public static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(SHA1 sha1, TL.Int128 server_nonce, Int256 new_nonce)
{
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(server_nonce, 0, 16);
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
sha1.Initialize();
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
sha1.Initialize();
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
sha1.Initialize();
return (tmp_aes_key, tmp_aes_iv);
}
internal static void CheckGoodPrime(BigInteger p, int g)
@ -208,8 +209,8 @@ namespace WTelegram
SafePrimes.Add(p);
}
private static readonly HashSet<BigInteger> SafePrimes = new() { new(new byte[] // C71CAEB9C6B1C904...
{
private static readonly HashSet<BigInteger> SafePrimes = [ new( // C71CAEB9C6B1C904...
[
0x5B, 0xCC, 0x2F, 0xB9, 0xE3, 0xD8, 0x9C, 0x11, 0x03, 0x04, 0xB1, 0x34, 0xF0, 0xAD, 0x4F, 0x6F,
0xBF, 0x54, 0x24, 0x4B, 0xD0, 0x15, 0x4E, 0x2E, 0xEE, 0x05, 0xB1, 0x35, 0xF6, 0x15, 0x81, 0x0D,
0x1F, 0x85, 0x29, 0xE9, 0x0C, 0x85, 0x56, 0xD9, 0x59, 0xF9, 0x7B, 0xF4, 0x49, 0x28, 0xED, 0x0D,
@ -226,7 +227,7 @@ namespace WTelegram
0xDB, 0xF4, 0x30, 0x25, 0xD2, 0x93, 0x94, 0x22, 0x58, 0x40, 0xC1, 0xA7, 0x0A, 0x8A, 0x19, 0x48,
0x0F, 0x93, 0x3D, 0x56, 0x37, 0xD0, 0x34, 0x49, 0xC1, 0x21, 0x3E, 0x8E, 0x23, 0x40, 0x0D, 0x98,
0x73, 0x3F, 0xF1, 0x70, 0x2F, 0x52, 0x6C, 0x8E, 0x04, 0xC9, 0xB1, 0xC6, 0xB9, 0xAE, 0x1C, 0xC7, 0x00
})};
])];
internal static void CheckGoodGaAndGb(BigInteger g, BigInteger dh_prime)
{
@ -236,6 +237,8 @@ namespace WTelegram
throw new WTException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}");
}
/// <summary>Load a specific Telegram server public key</summary>
/// <param name="pem">A string starting with <c>-----BEGIN RSA PUBLIC KEY-----</c></param>
public static void LoadPublicKey(string pem)
{
using var rsa = RSA.Create();
@ -244,10 +247,7 @@ namespace WTelegram
var rsaParam = rsa.ExportParameters(false);
if (rsaParam.Modulus[0] == 0) rsaParam.Modulus = rsaParam.Modulus[1..];
var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent };
using var memStream = new MemoryStream(280);
using (var writer = new BinaryWriter(memStream))
writer.WriteTLObject(publicKey);
var bareData = memStream.ToArray();
var bareData = publicKey.ToBytes();
var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(bareData, 4, bareData.Length - 4).AsSpan(12)); // 64 lower-order bits of SHA1
PublicKeys[fingerprint] = publicKey;
Helpers.Log(1, $"Loaded a public key with fingerprint {fingerprint:X}");
@ -275,7 +275,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
-----END RSA PUBLIC KEY-----");
}
internal static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
public static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
{
// first, construct AES key & IV
byte[] aes_key = new byte[32], aes_iv = new byte[32];
@ -296,7 +296,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt);
}
internal static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
public static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
{
if (input.Length % 16 != 0) throw new WTException("AES_IGE input size not divisible by 16");
@ -304,8 +304,8 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
var output = new byte[input.Length];
var prevBytes = (byte[])aes_iv.Clone();
var span = MemoryMarshal.Cast<byte, long>(input);
var sout = MemoryMarshal.Cast<byte, long>(output);
var prev = MemoryMarshal.Cast<byte, long>(prevBytes);
var sout = MemoryMarshal.Cast<byte, long>(output.AsSpan());
var prev = MemoryMarshal.Cast<byte, long>(prevBytes.AsSpan());
if (!encrypt) { (prev[2], prev[0]) = (prev[0], prev[2]); (prev[3], prev[1]) = (prev[1], prev[3]); }
for (int i = 0, count = input.Length / 8; i < count;)
{
@ -318,33 +318,26 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
}
#if OBFUSCATION
internal class AesCtr : IDisposable
public sealed class AesCtr(byte[] key, byte[] ivec) : IDisposable
{
readonly ICryptoTransform encryptor;
readonly byte[] ivec;
readonly byte[] ecount = new byte[16];
int num;
readonly ICryptoTransform _encryptor = AesECB.CreateEncryptor(key, null);
readonly byte[] _ecount = new byte[16];
int _num;
public AesCtr(byte[] key, byte[] iv)
public void Dispose() => _encryptor.Dispose();
public void EncryptDecrypt(Span<byte> buffer)
{
encryptor = AesECB.CreateEncryptor(key, null);
ivec = iv;
}
public void Dispose() => encryptor.Dispose();
public void EncryptDecrypt(byte[] buffer, int length)
{
for (int i = 0; i < length; i++)
for (int i = 0; i < buffer.Length; i++)
{
if (num == 0)
if (_num == 0)
{
encryptor.TransformBlock(ivec, 0, 16, ecount, 0);
_encryptor.TransformBlock(ivec, 0, 16, _ecount, 0);
for (int n = 15; n >= 0; n--) // increment big-endian counter
if (++ivec[n] != 0) break;
}
buffer[i] ^= ecount[num];
num = (num + 1) % 16;
buffer[i] ^= _ecount[_num];
_num = (_num + 1) % 16;
}
}
}
@ -379,7 +372,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
var sendCtr = new AesCtr(sendKey, sendIV);
var recvCtr = new AesCtr(recvKey, recvIV);
var encrypted = (byte[])preamble.Clone();
sendCtr.EncryptDecrypt(encrypted, 64);
sendCtr.EncryptDecrypt(encrypted);
for (int i = 56; i < 64; i++)
preamble[i] = encrypted[i];
return (sendCtr, recvCtr, preamble);
@ -524,17 +517,17 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
#endif
}
internal class AES_IGE_Stream : Helpers.IndirectStream
internal sealed class AES_IGE_Stream : Helpers.IndirectStream
{
private readonly ICryptoTransform aesCrypto;
private readonly byte[] prevBytes;
private readonly ICryptoTransform _aesCrypto;
private readonly byte[] _prevBytes;
public AES_IGE_Stream(Stream stream, long size, byte[] key, byte[] iv) : this(stream, key, iv, false) { ContentLength = size; }
public AES_IGE_Stream(Stream stream, byte[] key, byte[] iv, bool encrypt) : base(stream)
{
aesCrypto = encrypt ? Encryption.AesECB.CreateEncryptor(key, null) : Encryption.AesECB.CreateDecryptor(key, null);
if (encrypt) prevBytes = (byte[])iv.Clone();
else { prevBytes = new byte[32]; Array.Copy(iv, 0, prevBytes, 16, 16); Array.Copy(iv, 16, prevBytes, 0, 16); }
_aesCrypto = encrypt ? Encryption.AesECB.CreateEncryptor(key, null) : Encryption.AesECB.CreateDecryptor(key, null);
if (encrypt) _prevBytes = (byte[])iv.Clone();
else { _prevBytes = new byte[32]; Array.Copy(iv, 0, _prevBytes, 16, 16); Array.Copy(iv, 16, _prevBytes, 0, 16); }
}
public override long Length => base.Length + 15 & ~15;
@ -563,11 +556,11 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
{
count = count + 15 & ~15;
var span = MemoryMarshal.Cast<byte, long>(buffer.AsSpan(offset, count));
var prev = MemoryMarshal.Cast<byte, long>(prevBytes);
var prev = MemoryMarshal.Cast<byte, long>(_prevBytes.AsSpan());
for (offset = 0, count /= 8; offset < count;)
{
prev[0] ^= span[offset]; prev[1] ^= span[offset + 1];
aesCrypto.TransformBlock(prevBytes, 0, 16, prevBytes, 0);
_aesCrypto.TransformBlock(_prevBytes, 0, 16, _prevBytes, 0);
prev[0] ^= prev[2]; prev[1] ^= prev[3];
prev[2] = span[offset]; prev[3] = span[offset + 1];
span[offset++] = prev[0]; span[offset++] = prev[1];

View file

@ -4,9 +4,18 @@ using System.IO;
using System.Numerics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
#if NET8_0_OR_GREATER
[JsonSerializable(typeof(WTelegram.Session))]
[JsonSerializable(typeof(Dictionary<long, WTelegram.UpdateManager.MBoxState>))]
[JsonSerializable(typeof(IDictionary<long, WTelegram.UpdateManager.MBoxState>))]
[JsonSerializable(typeof(System.Collections.Immutable.ImmutableDictionary<long, WTelegram.UpdateManager.MBoxState>))]
internal partial class WTelegramContext : JsonSerializerContext { }
#endif
namespace WTelegram
{
public static class Helpers
@ -16,10 +25,93 @@ namespace WTelegram
/// <summary>For serializing indented Json with fields included</summary>
public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true,
IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull };
#if NET8_0_OR_GREATER
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? null : WTelegramContext.Default,
Converters = { new TLJsonConverter(), new JsonStringEnumConverter() },
#endif
IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };
private static readonly ConsoleColor[] LogLevelToColor = new[] { ConsoleColor.DarkGray, ConsoleColor.DarkCyan,
ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue };
#if NET8_0_OR_GREATER
public sealed class TLJsonConverter : JsonConverter<object>
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsAbstract || typeToConvert == typeof(Dictionary<long, TL.User>) || typeToConvert == typeof(Dictionary<long, TL.ChatBase>);
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (typeToConvert == typeof(Dictionary<long, TL.User>))
{
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("Expected array for users dictionary");
var users = new Dictionary<long, TL.User>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
var user = JsonSerializer.Deserialize<TL.User>(ref reader, options);
if (user != null) users[user.id] = user;
}
return users;
}
else if (typeToConvert == typeof(Dictionary<long, TL.ChatBase>))
{
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("Expected array for chats dictionary");
var chats = new Dictionary<long, TL.ChatBase>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
var chat = (TL.ChatBase)Read(ref reader, typeof(TL.ChatBase), options);
if (chat != null) chats[chat.ID] = chat;
}
return chats;
}
else if (reader.TokenType == JsonTokenType.Null)
return null;
else if (reader.TokenType == JsonTokenType.StartObject)
{
var typeReader = reader;
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName || typeReader.GetString() != "$")
throw new JsonException("Expected $ type property");
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.String)
throw new JsonException("Invalid $ type property");
var type = typeReader.GetString();
var actualType = typeToConvert.Assembly.GetType("TL." + type);
if (!typeToConvert.IsAssignableFrom(actualType))
throw new JsonException($"Incompatible $ type: {type} -> {typeToConvert}");
return JsonSerializer.Deserialize(ref reader, actualType, options);
}
throw new JsonException($"Unexpected token type: {reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
if (value is Dictionary<long, TL.User> users)
{
writer.WriteStartArray();
foreach (var element in users.Values)
JsonSerializer.Serialize(writer, element, options);
writer.WriteEndArray();
}
else if (value is Dictionary<long, TL.ChatBase> chats)
{
writer.WriteStartArray();
foreach (var element in chats.Values)
Write(writer, element, options);
writer.WriteEndArray();
}
else if (value is null)
writer.WriteNullValue();
else
{
var actualType = value.GetType();
var jsonObject = JsonSerializer.SerializeToElement(value, actualType, options);
writer.WriteStartObject();
writer.WriteString("$", actualType.Name);
foreach (var property in jsonObject.EnumerateObject())
if (char.IsLower(property.Name[0]))
property.WriteTo(writer);
writer.WriteEndObject();
}
}
}
#endif
private static readonly ConsoleColor[] LogLevelToColor = [ ConsoleColor.DarkGray, ConsoleColor.DarkCyan,
ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue ];
private static void DefaultLogger(int level, string message)
{
Console.ForegroundColor = LogLevelToColor[level];
@ -186,7 +278,7 @@ namespace WTelegram
}
internal static readonly byte[] StrippedThumbJPG = // see https://core.telegram.org/api/files#stripped-thumbnails
{
[
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49,
0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x28, 0x1c,
0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37,
@ -223,7 +315,7 @@ namespace WTelegram
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4,
0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00,
0x3f, 0x00
};
];
internal static string GetSystemVersion()
{
@ -235,21 +327,20 @@ namespace WTelegram
internal static string GetAppVersion()
=> (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0";
public class IndirectStream : Stream
public class IndirectStream(Stream innerStream) : Stream
{
public IndirectStream(Stream innerStream) => _innerStream = innerStream;
public long? ContentLength;
protected readonly Stream _innerStream;
protected readonly Stream _innerStream = innerStream;
public override bool CanRead => _innerStream.CanRead;
public override bool CanSeek => _innerStream.CanSeek;
public override bool CanSeek => ContentLength.HasValue || _innerStream.CanSeek;
public override bool CanWrite => _innerStream.CanWrite;
public override long Length => ContentLength ?? _innerStream.Length;
public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }
public override void Flush() => _innerStream.Flush();
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
public override void SetLength(long value) => _innerStream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count) => _innerStream.Write(buffer, offset, count);
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
protected override void Dispose(bool disposing) => _innerStream.Dispose();
}
}

View file

@ -21,48 +21,49 @@ namespace WTelegram
int RemoteLayer { get; }
}
[TLDef(0xFEFEFEFE)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")]
internal sealed partial class SecretChat : IObject, ISecretChat
{
[Flags] public enum Flags : uint { requestChat = 1, renewKey = 2, acceptKey = 4, originator = 8, commitKey = 16 }
public Flags flags;
public InputEncryptedChat peer = new();
public byte[] salt; // contains future/discarded authKey during acceptKey/commitKey
public byte[] authKey;
public DateTime key_created;
public int key_useCount;
public long participant_id;
public int remoteLayer = 46;
public int in_seq_no = -2, out_seq_no = 0;
public long exchange_id;
public int ChatId => peer.chat_id;
public long RemoteUserId => participant_id;
public InputEncryptedChat Peer => peer;
public int RemoteLayer => remoteLayer;
internal long key_fingerprint;
internal SortedList<int, TL.Layer23.DecryptedMessageLayer> pendingMsgs = [];
internal void Discarded() // clear out fields for more security
{
Array.Clear(authKey, 0, authKey.Length);
key_fingerprint = participant_id = peer.access_hash = peer.chat_id = in_seq_no = out_seq_no = remoteLayer = 0;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")]
public sealed class SecretChats : IDisposable
{
public event Action OnChanged;
private readonly Client client;
private readonly FileStream storage;
private readonly Dictionary<int, SecretChat> chats = new();
private readonly Dictionary<int, SecretChat> chats = [];
private Messages_DhConfig dh;
private BigInteger dh_prime;
private readonly SHA256 sha256 = SHA256.Create();
private readonly SHA1 sha1 = SHA1.Create();
private readonly Random random = new();
private const int ThresholdPFS = 100;
[TLDef(0xFEFEFEFE)]
internal class SecretChat : IObject, ISecretChat
{
[Flags] public enum Flags : uint { requestChat = 1, renewKey = 2, acceptKey = 4, originator = 8, commitKey = 16 }
public Flags flags;
public InputEncryptedChat peer = new();
public byte[] salt; // contains future/discarded authKey during acceptKey/commitKey
public byte[] authKey;
public DateTime key_created;
public int key_useCount;
public long participant_id;
public int remoteLayer = 46;
public int in_seq_no = -2, out_seq_no = 0;
public long exchange_id;
public int ChatId => peer.chat_id;
public long RemoteUserId => participant_id;
public InputEncryptedChat Peer => peer;
public int RemoteLayer => remoteLayer;
internal long key_fingerprint;
internal SortedList<int, TL.Layer23.DecryptedMessageLayer> pendingMsgs = new();
internal void Discarded() // clear out fields for more security
{
Array.Clear(authKey, 0, authKey.Length);
key_fingerprint = participant_id = peer.access_hash = peer.chat_id = in_seq_no = out_seq_no = remoteLayer = 0;
}
}
/// <summary>Instantiate a Secret Chats manager</summary>
/// <param name="client">The Telegram client</param>
@ -79,7 +80,7 @@ namespace WTelegram
}
public void Dispose() { OnChanged?.Invoke(); storage?.Dispose(); sha256.Dispose(); sha1.Dispose(); }
public List<ISecretChat> Chats => chats.Values.ToList<ISecretChat>();
public List<ISecretChat> Chats => [.. chats.Values];
public bool IsChatActive(int chat_id) => !(chats.GetValueOrDefault(chat_id)?.flags.HasFlag(SecretChat.Flags.requestChat) ?? true);
@ -169,8 +170,8 @@ namespace WTelegram
return chat_id;
}
/// <summary>Processes the <see cref="UpdateEncryption"/> you received from Telegram (<see cref="Client.OnUpdate"/>).</summary>
/// <remarks>If update.chat is <see cref="EncryptedChatRequested"/>, you might want to first make sure you want to accept this secret chat initiated by user <see cref="EncryptedChatRequested.admin_id"/></remarks>
/// <summary>Processes the <see cref="UpdateEncryption"/> you received from Telegram (<see cref="Client.OnUpdates"/>).</summary>
/// <param name="update">If update.chat is <see cref="EncryptedChatRequested"/>, you might want to first make sure you want to accept this secret chat initiated by user <see cref="EncryptedChatRequested.admin_id"/></param>
/// <param name="acceptChatRequests">Incoming requests for secret chats are automatically: accepted (<see langword="true"/>), rejected (<see langword="false"/>) or ignored (<see langword="null"/>)</param>
/// <returns><see langword="true"/> if the update was handled successfully</returns>
/// <exception cref="WTException"></exception>
@ -382,7 +383,7 @@ namespace WTelegram
if (((dml.out_seq_no ^ dml.in_seq_no) & 1) != 1 || ((dml.out_seq_no ^ chat.in_seq_no) & 1) != 0) throw new WTException("Invalid seq_no parities");
if (dml.layer > chat.remoteLayer) chat.remoteLayer = dml.layer;
//Debug.WriteLine($"<\t{dml.in_seq_no}\t{dml.out_seq_no}\t\t\t\t\t\texpected:{chat.out_seq_no}/{chat.in_seq_no + 2}");
if (dml.out_seq_no <= chat.in_seq_no) return Array.Empty<DecryptedMessageBase>(); // already received message
if (dml.out_seq_no <= chat.in_seq_no) return []; // already received message
var pendingMsgSeqNo = chat.pendingMsgs.Keys;
if (fillGaps && dml.out_seq_no > chat.in_seq_no + 2)
{
@ -392,12 +393,12 @@ namespace WTelegram
if (dml.out_seq_no > lastPending + 2) // send request to resend missing gap asynchronously
_ = SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
action = new TL.Layer23.DecryptedMessageActionResend { start_seq_no = lastPending + 2, end_seq_no = dml.out_seq_no - 2 } });
return Array.Empty<DecryptedMessageBase>();
return [];
}
chat.in_seq_no = dml.out_seq_no;
if (pendingMsgSeqNo.Count == 0 || pendingMsgSeqNo[0] != dml.out_seq_no + 2)
if (HandleAction(chat, dml.message.Action)) return Array.Empty<DecryptedMessageBase>();
else return new[] { dml.message };
if (HandleAction(chat, dml.message.Action)) return [];
else return [dml.message];
else // we have pendingMsgs completing the sequence in order
{
var list = new List<DecryptedMessageBase>();

View file

@ -1,46 +1,129 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using WTelegram; // for GetValueOrDefault
using WTelegram;
namespace TL
{
public static class Extensions
public static class Services
{
private class CollectorPeer : Peer
public sealed partial class CollectorPeer(IDictionary<long, User> _users, IDictionary<long, ChatBase> _chats) : Peer, IPeerCollector
{
public override long ID => 0;
internal IDictionary<long, User> _users;
internal IDictionary<long, ChatBase> _chats;
protected internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
{
if (users != null) Collect(users.Values);
if (chats != null) Collect(chats.Values);
return null;
}
public void Collect(IEnumerable<TL.User> users)
{
lock (_users)
foreach (var user in users.Values)
foreach (var user in users)
if (user != null)
if (!user.flags.HasFlag(User.Flags.min) || !_users.TryGetValue(user.id, out var prevUser) || prevUser.flags.HasFlag(User.Flags.min))
_users[user.id] = user;
lock (_chats)
foreach (var kvp in chats)
if (kvp.Value is not Channel channel)
_chats[kvp.Key] = kvp.Value;
else if (!channel.flags.HasFlag(Channel.Flags.min) || !_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel || prevChannel.flags.HasFlag(Channel.Flags.min))
_chats[kvp.Key] = channel;
return null;
else
{ // update previously full user from min user:
// see https://github.com/tdlib/td/blob/master/td/telegram/UserManager.cpp#L2689
// and https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/data/data_session.cpp#L515
const User.Flags updated_flags = User.Flags.deleted | User.Flags.bot | User.Flags.bot_chat_history |
User.Flags.bot_nochats | User.Flags.verified | User.Flags.restricted | User.Flags.has_bot_inline_placeholder |
User.Flags.bot_inline_geo | User.Flags.support | User.Flags.scam | User.Flags.fake | User.Flags.bot_attach_menu |
User.Flags.premium | User.Flags.has_emoji_status;
const User.Flags2 updated_flags2 = User.Flags2.has_usernames | User.Flags2.stories_unavailable |
User.Flags2.has_color | User.Flags2.has_profile_color | User.Flags2.contact_require_premium |
User.Flags2.bot_business | User.Flags2.bot_has_main_app | User.Flags2.bot_forum_view;
// tdlib updated flags: deleted | bot | bot_chat_history | bot_nochats | verified | bot_inline_geo
// | support | scam | fake | bot_attach_menu | premium
// tdesktop non-updated flags: bot | bot_chat_history | bot_nochats | bot_attach_menu
// updated flags2: stories_unavailable | main_app | bot_business | bot_forum_view (tdlib) | contact_require_premium (tdesktop)
prevUser.flags = (prevUser.flags & ~updated_flags) | (user.flags & updated_flags);
prevUser.flags2 = (prevUser.flags2 & ~updated_flags2) | (user.flags2 & updated_flags2);
prevUser.first_name ??= user.first_name; // tdlib: not updated ; tdesktop: updated only if unknown
prevUser.last_name ??= user.last_name; // tdlib: not updated ; tdesktop: updated only if unknown
//prevUser.username ??= user.username; // tdlib/tdesktop: not updated
prevUser.phone ??= user.phone; // tdlib: updated only if unknown ; tdesktop: not updated
if (prevUser.flags.HasFlag(User.Flags.apply_min_photo) && user.photo != null)
{
prevUser.photo = user.photo; // tdlib/tdesktop: updated on apply_min_photo
prevUser.flags |= User.Flags.has_photo;
}
prevUser.bot_info_version = user.bot_info_version; // tdlib: updated ; tdesktop: not updated
prevUser.restriction_reason = user.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevUser.bot_inline_placeholder = user.bot_inline_placeholder;// tdlib: updated ; tdesktop: ignored
if (user.lang_code != null)
prevUser.lang_code = user.lang_code; // tdlib: updated if present ; tdesktop: ignored
prevUser.emoji_status = user.emoji_status; // tdlib/tdesktop: updated
//prevUser.usernames = user.usernames; // tdlib/tdesktop: not updated
if (user.stories_max_id != null)
prevUser.stories_max_id = user.stories_max_id; // tdlib: updated if > 0 ; tdesktop: not updated
prevUser.color = user.color; // tdlib/tdesktop: updated
prevUser.profile_color = user.profile_color; // tdlib/tdesktop: unimplemented yet
_users[user.id] = prevUser;
}
}
public void Collect(IEnumerable<ChatBase> chats)
{
lock (_chats)
foreach (var chat in chats)
if (chat is not Channel channel)
_chats[chat.ID] = chat;
else if (!_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel)
_chats[channel.id] = channel;
else if (!channel.flags.HasFlag(Channel.Flags.min) || prevChannel.flags.HasFlag(Channel.Flags.min))
{
if (channel.participants_count == 0) channel.participants_count = prevChannel.participants_count; // non-min channel can lack this info
_chats[channel.id] = channel;
}
else
{ // update previously full channel from min channel:
const Channel.Flags updated_flags = (Channel.Flags)0x7FDC0BE0;
const Channel.Flags2 updated_flags2 = (Channel.Flags2)0x781;
// tdesktop updated flags: broadcast | verified | megagroup | signatures | scam | has_link | slowmode_enabled
// | call_active | call_not_empty | fake | gigagroup | noforwards | join_to_send | join_request | forum
// tdlib nonupdated flags: broadcast | signatures | call_active | call_not_empty | noforwards
prevChannel.flags = (prevChannel.flags & ~updated_flags) | (channel.flags & updated_flags);
prevChannel.flags2 = (prevChannel.flags2 & ~updated_flags2) | (channel.flags2 & updated_flags2);
prevChannel.title = channel.title; // tdlib/tdesktop: updated
prevChannel.username = channel.username; // tdlib/tdesktop: updated
prevChannel.photo = channel.photo; // tdlib: updated if not banned ; tdesktop: updated
prevChannel.restriction_reason = channel.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevChannel.default_banned_rights = channel.default_banned_rights; // tdlib/tdesktop: updated
if (channel.participants_count > 0)
prevChannel.participants_count = channel.participants_count; // tdlib/tdesktop: updated if present
prevChannel.usernames = channel.usernames; // tdlib/tdesktop: updated
prevChannel.color = channel.color; // tdlib: not updated ; tdesktop: updated
prevChannel.profile_color = channel.profile_color; // tdlib/tdesktop: ignored
prevChannel.emoji_status = channel.emoji_status; // tdlib: not updated ; tdesktop: updated
prevChannel.level = channel.level; // tdlib: not updated ; tdesktop: updated
_chats[channel.id] = prevChannel;
}
}
public bool HasUser(long id) { lock (_users) return _users.ContainsKey(id); }
public bool HasChat(long id) { lock (_chats) return _chats.ContainsKey(id); }
}
/// <summary>Accumulate users/chats found in this structure in your dictionaries, ignoring <see href="https://core.telegram.org/api/min">Min constructors</see> when the full object is already stored</summary>
/// <param name="structure">The structure having a <c>users</c></param>
public static void CollectUsersChats(this IPeerResolver structure, IDictionary<long, User> users, IDictionary<long, ChatBase> chats)
=> structure.UserOrChat(new CollectorPeer { _users = users, _chats = chats });
=> structure.UserOrChat(new CollectorPeer(users, chats));
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("The method you're looking for is Messages_GetAllChats", true)]
public static Task<Messages_Chats> Messages_GetChats(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllChats");
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("The method you're looking for is Messages_GetAllChats", true)]
public static Task<Messages_Chats> Channels_GetChannels(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllChats");
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("The method you're looking for is Messages_GetAllDialogs", true)]
public static Task<UserBase[]> Users_GetUsers(this Client _) => throw new WTException("The method you're looking for is Messages_GetAllDialogs");
public static Task<Messages_MessagesBase> Messages_GetMessages(this Client _) => throw new WTException("If you want to get the messages from a chat, use Messages_GetHistory");
[EditorBrowsable(EditorBrowsableState.Never)][Obsolete("If you want to get all messages from a chat, use method Messages_GetHistory", true)]
public static Task<Messages_MessagesBase> Messages_GetMessages(this Client _) => throw new WTException("If you want to get all messages from a chat, use method Messages_GetHistory");
}
public static class Markdown
@ -54,16 +137,18 @@ namespace TL
public static MessageEntity[] MarkdownToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary<long, User> users = null)
{
var entities = new List<MessageEntity>();
MessageEntityBlockquote lastBlockQuote = null;
int offset, inCode = 0;
var sb = new StringBuilder(text);
for (int offset = 0; offset < sb.Length;)
for (offset = 0; offset < sb.Length;)
{
switch (sb[offset])
{
case '\r': sb.Remove(offset, 1); break;
case '\\': sb.Remove(offset++, 1); break;
case '*': ProcessEntity<MessageEntityBold>(); break;
case '~': ProcessEntity<MessageEntityStrike>(); break;
case '_':
case '*' when inCode == 0: ProcessEntity<MessageEntityBold>(); break;
case '~' when inCode == 0: ProcessEntity<MessageEntityStrike>(); break;
case '_' when inCode == 0:
if (offset + 1 < sb.Length && sb[offset + 1] == '_')
{
sb.Remove(offset, 1);
@ -73,7 +158,7 @@ namespace TL
ProcessEntity<MessageEntityItalic>();
break;
case '|':
if (offset + 1 < sb.Length && sb[offset + 1] == '|')
if (inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '|')
{
sb.Remove(offset, 1);
ProcessEntity<MessageEntitySpoiler>();
@ -82,6 +167,7 @@ namespace TL
offset++;
break;
case '`':
int count = entities.Count;
if (offset + 2 < sb.Length && sb[offset + 1] == '`' && sb[offset + 2] == '`')
{
int len = 3;
@ -98,16 +184,26 @@ namespace TL
}
else
ProcessEntity<MessageEntityCode>();
if (entities.Count > count) inCode++; else inCode--;
break;
case '!' when offset + 1 < sb.Length && sb[offset + 1] == '[':
case '>' when inCode == 0 && offset == 0 || sb[offset - 1] == '\n':
sb.Remove(offset, 1);
goto case '[';
case '[':
if (lastBlockQuote == null)
entities.Add(lastBlockQuote = new MessageEntityBlockquote { offset = offset, length = -1 });
break;
case '\n' when lastBlockQuote != null:
if (offset + 1 >= sb.Length || sb[offset + 1] != '>') CloseBlockQuote();
offset++;
break;
case '!' when inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '[':
sb.Remove(offset, 1);
break;
case '[' when inCode == 0:
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1 });
sb.Remove(offset, 1);
break;
case ']':
if (offset + 2 < sb.Length && sb[offset + 1] == '(')
if (inCode == 0 && offset + 2 < sb.Length && sb[offset + 1] == '(')
{
var lastIndex = entities.FindLastIndex(e => e.length == -1);
if (lastIndex >= 0 && entities[lastIndex] is MessageEntityTextUrl textUrl)
@ -137,15 +233,31 @@ namespace TL
void ProcessEntity<T>() where T : MessageEntity, new()
{
sb.Remove(offset, 1);
if (entities.LastOrDefault(e => e.length == -1) is T prevEntity)
prevEntity.length = offset - prevEntity.offset;
if (offset == prevEntity.offset)
entities.Remove(prevEntity);
else
prevEntity.length = offset - prevEntity.offset;
else
entities.Add(new T { offset = offset, length = -1 });
sb.Remove(offset, 1);
}
}
if (lastBlockQuote != null) CloseBlockQuote();
HtmlText.FixUps(sb, entities);
text = sb.ToString();
return entities.Count == 0 ? null : entities.ToArray();
return entities.Count == 0 ? null : [.. entities];
void CloseBlockQuote()
{
if (entities[^1] is MessageEntitySpoiler { length: -1 } mes && mes.offset == offset)
{
entities.RemoveAt(entities.Count - 1);
lastBlockQuote.flags = MessageEntityBlockquote.Flags.collapsed;
}
lastBlockQuote.length = offset - lastBlockQuote.offset;
lastBlockQuote = null;
}
}
/// <summary>Converts the (plain text + entities) format used by Telegram messages into a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a></summary>
@ -161,19 +273,23 @@ namespace TL
var sb = new StringBuilder(message);
int entityIndex = 0;
var nextEntity = entities[entityIndex];
bool inBlockQuote = false;
char lastCh = '\0';
for (int offset = 0, i = 0; ; offset++, i++)
{
while (closings.Count != 0 && offset == closings[0].offset)
{
var md = closings[0].md;
if (i > 0 && md[0] == '_' && sb[i - 1] == '_') md = '\r' + md;
sb.Insert(i, md); i += md.Length;
closings.RemoveAt(0);
if (i > 0 && md[0] == '_' && sb[i - 1] == '_') md = '\r' + md;
if (md[0] == '>') { inBlockQuote = false; md = md[1..]; if (lastCh != '\n' && i < sb.Length && sb[i] != '\n') md += '\n'; }
sb.Insert(i, md); i += md.Length;
}
if (i == sb.Length) break;
if (lastCh == '\n' && inBlockQuote) sb.Insert(i++, '>');
for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null)
{
if (entityToMD.TryGetValue(nextEntity.GetType(), out var md))
if (EntityToMD.TryGetValue(nextEntity.GetType(), out var md))
{
var closing = (nextEntity.offset + nextEntity.length, md);
if (md[0] is '[' or '!')
@ -188,6 +304,9 @@ namespace TL
if (premium) closing.md = $"](tg://emoji?id={mecu.document_id})";
else continue;
}
else if (nextEntity is MessageEntityBlockquote mebq)
{ inBlockQuote = true; if (lastCh is not '\n' and not '\0') md = "\n>";
if (mebq.flags == MessageEntityBlockquote.Flags.collapsed) closing.md = ">||"; }
else if (nextEntity is MessageEntityPre mep)
md = $"```{mep.language}\n";
int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1));
@ -196,18 +315,21 @@ namespace TL
sb.Insert(i, md); i += md.Length;
}
}
switch (sb[i])
switch (lastCh = sb[i])
{
case '_': case '*': case '~': case '`': case '#': case '+': case '-': case '=': case '.': case '!':
case '_': case '*': case '~': case '#': case '+': case '-': case '=': case '.': case '!':
case '[': case ']': case '(': case ')': case '{': case '}': case '>': case '|': case '\\':
sb.Insert(i, '\\'); i++;
if (closings.Count != 0 && closings[0].md[0] == '`') break;
goto case '`';
case '`':
sb.Insert(i++, '\\');
break;
}
}
return sb.ToString();
}
static readonly Dictionary<Type, string> entityToMD = new()
static readonly Dictionary<Type, string> EntityToMD = new()
{
[typeof(MessageEntityBold)] = "*",
[typeof(MessageEntityItalic)] = "_",
@ -220,6 +342,7 @@ namespace TL
[typeof(MessageEntityStrike)] = "~",
[typeof(MessageEntitySpoiler)] = "||",
[typeof(MessageEntityCustomEmoji)] = "![",
[typeof(MessageEntityBlockquote)] = ">",
};
/// <summary>Insert backslashes in front of Markdown reserved characters</summary>
@ -227,6 +350,7 @@ namespace TL
/// <returns>The escaped text, ready to be used in <see cref="MarkdownToEntities">MarkdownToEntities</see> without problems</returns>
public static string Escape(string text)
{
if (text == null) return null;
StringBuilder sb = null;
for (int index = 0, added = 0; index < text.Length; index++)
{
@ -261,17 +385,18 @@ namespace TL
char c = sb[offset];
if (c == '&')
{
for (end = offset + 1; end < sb.Length; end++)
if (sb[end] == ';') break;
if (end >= sb.Length) break;
var html = HttpUtility.HtmlDecode(sb.ToString(offset, end - offset + 1));
end = offset + 1;
if (end < sb.Length && sb[end] == '#') end++;
while (end < sb.Length && sb[end] is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9') end++;
var html = HttpUtility.HtmlDecode(end >= sb.Length || sb[end] != ';'
? sb.ToString(offset, end - offset) + ";" : sb.ToString(offset, ++end - offset));
if (html.Length == 1)
{
sb[offset] = html[0];
sb.Remove(++offset, end - offset + 1);
sb.Remove(++offset, end - offset);
}
else
offset = end + 1;
offset = end;
}
else if (c == '<')
{
@ -288,11 +413,16 @@ namespace TL
case "u": case "ins": ProcessEntity<MessageEntityUnderline>(); break;
case "s": case "strike": case "del": ProcessEntity<MessageEntityStrike>(); break;
case "span class=\"tg-spoiler\"":
case "span class='tg-spoiler'":
case "span" when closing:
case "tg-spoiler": ProcessEntity<MessageEntitySpoiler>(); break;
case "code": ProcessEntity<MessageEntityCode>(); break;
case "pre": ProcessEntity<MessageEntityPre>(); break;
case "tg-emoji" when closing: ProcessEntity<MessageEntityCustomEmoji>(); break;
case "blockquote": ProcessEntity<MessageEntityBlockquote>(); break;
case "blockquote expandable":
entities.Add(new MessageEntityBlockquote { offset = offset, length = -1, flags = MessageEntityBlockquote.Flags.collapsed });
break;
default:
if (closing)
{
@ -303,20 +433,22 @@ namespace TL
prevEntity.length = offset - prevEntity.offset;
}
}
else if (tag.StartsWith("a href=\"") && tag.EndsWith("\""))
else if ((tag[^1] == '"' && tag.StartsWith("a href=\""))
|| (tag[^1] == '\'' && tag.StartsWith("a href='")))
{
tag = tag[8..^1];
tag = HttpUtility.HtmlDecode(tag[8..^1]);
if (tag.StartsWith("tg://user?id=") && long.TryParse(tag[13..], out var user_id) && users?.GetValueOrDefault(user_id)?.access_hash is long hash)
entities.Add(new InputMessageEntityMentionName { offset = offset, length = -1, user_id = new InputUser(user_id, hash) });
else
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1, url = tag });
}
else if (tag.StartsWith("code class=\"language-") && tag.EndsWith("\""))
else if ((tag[^1] == '"' && tag.StartsWith("code class=\"language-"))
|| (tag[^1] == '\'' && tag.StartsWith("code class='language-")))
{
if (entities.LastOrDefault(e => e.length == -1) is MessageEntityPre prevEntity)
prevEntity.language = tag[21..^1];
}
else if (premium && (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji id=\"")))
else if (premium && (tag.StartsWith("tg-emoji emoji-id=\"") || tag.StartsWith("tg-emoji emoji-id='")))
entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[(tag.IndexOf('=') + 2)..^1]) });
break;
}
@ -332,8 +464,22 @@ namespace TL
else
offset++;
}
FixUps(sb, entities);
text = sb.ToString();
return entities.Count == 0 ? null : entities.ToArray();
return entities.Count == 0 ? null : [.. entities];
}
internal static void FixUps(StringBuilder sb, List<MessageEntity> entities)
{
int newlen = sb.Length;
while (--newlen >= 0 && char.IsWhiteSpace(sb[newlen]));
if (++newlen != sb.Length) sb.Length = newlen;
for (int i = 0; i < entities.Count; i++)
{
var entity = entities[i];
if (entity.offset + entity.length > newlen) entity.length = newlen - entity.offset;
if (entity.length == 0) entities.RemoveAt(i--);
}
}
/// <summary>Converts the (plain text + entities) format used by Telegram messages into an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a></summary>
@ -360,13 +506,13 @@ namespace TL
if (i == sb.Length) break;
for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null)
{
if (entityToTag.TryGetValue(nextEntity.GetType(), out var tag))
if (EntityToTag.TryGetValue(nextEntity.GetType(), out var tag))
{
var closing = (nextEntity.offset + nextEntity.length, $"</{tag}>");
if (tag[0] == 'a')
{
if (nextEntity is MessageEntityTextUrl metu)
tag = $"<a href=\"{metu.url}\">";
tag = $"<a href=\"{Escape(metu.url)}\">";
else if (nextEntity is MessageEntityMentionName memn)
tag = $"<a href=\"tg://user?id={memn.user_id}\">";
else if (nextEntity is InputMessageEntityMentionName imemn)
@ -380,6 +526,8 @@ namespace TL
closing.Item2 = "</code></pre>";
tag = $"<pre><code class=\"language-{mep.language}\">";
}
else if (nextEntity is MessageEntityBlockquote { flags: MessageEntityBlockquote.Flags.collapsed })
tag = "<blockquote expandable>";
else
tag = $"<{tag}>";
int index = ~closings.BinarySearch(closing, Comparer<(int, string)>.Create((x, y) => x.Item1.CompareTo(y.Item1) | 1));
@ -397,7 +545,7 @@ namespace TL
return sb.ToString();
}
static readonly Dictionary<Type, string> entityToTag = new()
static readonly Dictionary<Type, string> EntityToTag = new()
{
[typeof(MessageEntityBold)] = "b",
[typeof(MessageEntityItalic)] = "i",
@ -410,12 +558,13 @@ namespace TL
[typeof(MessageEntityStrike)] = "s",
[typeof(MessageEntitySpoiler)] = "tg-spoiler",
[typeof(MessageEntityCustomEmoji)] = "tg-emoji",
[typeof(MessageEntityBlockquote)] = "blockquote",
};
/// <summary>Replace special HTML characters with their &amp;xx; equivalent</summary>
/// <param name="text">The text to make HTML-safe</param>
/// <returns>The HTML-safe text, ready to be used in <see cref="HtmlToEntities">HtmlToEntities</see> without problems</returns>
public static string Escape(string text)
=> text.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
=> text?.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;");
}
}

View file

@ -7,68 +7,73 @@ using System.Net;
using System.Security.Cryptography;
using System.Text.Json;
// Don't change this code to lower the security. It's following Telegram security recommendations https://corefork.telegram.org/mtproto/description
namespace WTelegram
{
internal class Session : IDisposable
internal sealed partial class Session : IDisposable
{
public int ApiId;
public long UserId;
public int MainDC;
public Dictionary<int, DCSession> DCSessions = new();
public Dictionary<int, DCSession> DCSessions = [];
public TL.DcOption[] DcOptions;
public class DCSession
public sealed class DCSession
{
public long Id;
public long AuthKeyID;
public byte[] AuthKey; // 2048-bit = 256 bytes
public long UserId;
public long OldSalt; // still accepted for a further 1800 seconds
public long Salt;
public int Seqno;
public long ServerTicksOffset;
public long LastSentMsgId;
public SortedList<DateTime, long> Salts;
public TL.DcOption DataCenter;
public bool WithoutUpdates;
public int Layer;
internal long id = Helpers.RandomLong();
internal long authKeyID;
internal int seqno;
internal long serverTicksOffset;
internal long lastSentMsgId;
internal bool withoutUpdates;
internal Client Client;
internal int DcID => DataCenter?.id ?? 0;
internal int DcID => DataCenter == null ? 0 : DataCenter.flags.HasFlag(TL.DcOption.Flags.media_only) ? -DataCenter.id : DataCenter.id;
internal IPEndPoint EndPoint => DataCenter == null ? null : new(IPAddress.Parse(DataCenter.ip_address), DataCenter.port);
internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); Id = Helpers.RandomLong(); Seqno = 0; LastSentMsgId = 0; }
public void DisableUpdates(bool disable = true) { if (WithoutUpdates != disable) { WithoutUpdates = disable; Renew(); } }
internal void Renew() { Helpers.Log(3, $"Renewing session on DC {DcID}..."); id = Helpers.RandomLong(); seqno = 0; lastSentMsgId = 0; }
public void DisableUpdates(bool disable = true) { if (withoutUpdates != disable) { withoutUpdates = disable; Renew(); } }
const int msgIdsN = 512;
private long[] msgIds;
private int msgIdsHead;
const int MsgIdsN = 512;
private long[] _msgIds;
private int _msgIdsHead;
internal bool CheckNewMsgId(long msg_id)
{
if (msgIds == null)
if (_msgIds == null)
{
msgIds = new long[msgIdsN];
msgIds[0] = msg_id;
_msgIds = new long[MsgIdsN];
_msgIds[0] = msg_id;
msg_id -= 300L << 32; // until the array is filled with real values, allow ids up to 300 seconds in the past
for (int i = 1; i < msgIdsN; i++) msgIds[i] = msg_id;
for (int i = 1; i < MsgIdsN; i++) _msgIds[i] = msg_id;
return true;
}
int newHead = (msgIdsHead + 1) % msgIdsN;
if (msg_id > msgIds[msgIdsHead])
msgIds[msgIdsHead = newHead] = msg_id;
else if (msg_id <= msgIds[newHead])
int newHead = (_msgIdsHead + 1) % MsgIdsN;
if (msg_id > _msgIds[_msgIdsHead])
_msgIds[_msgIdsHead = newHead] = msg_id;
else if (msg_id <= _msgIds[newHead])
return false;
else
{
int min = 0, max = msgIdsN - 1;
int min = 0, max = MsgIdsN - 1;
while (min <= max) // binary search (rotated at newHead)
{
int mid = (min + max) / 2;
int sign = msg_id.CompareTo(msgIds[(mid + newHead) % msgIdsN]);
int sign = msg_id.CompareTo(_msgIds[(mid + newHead) % MsgIdsN]);
if (sign == 0) return false;
else if (sign < 0) max = mid - 1;
else min = mid + 1;
}
msgIdsHead = newHead;
for (min = (min + newHead) % msgIdsN; newHead != min;)
msgIds[newHead] = msgIds[newHead = newHead == 0 ? msgIdsN - 1 : newHead - 1];
msgIds[min] = msg_id;
_msgIdsHead = newHead;
for (min = (min + newHead) % MsgIdsN; newHead != min;)
_msgIds[newHead] = _msgIds[newHead = newHead == 0 ? MsgIdsN - 1 : newHead - 1];
_msgIds[min] = msg_id;
}
return true;
}
@ -112,6 +117,9 @@ namespace WTelegram
throw new WTException("Integrity check failed in session loading");
session = JsonSerializer.Deserialize<Session>(utf8Json.AsSpan(32), Helpers.JsonOptions);
Helpers.Log(2, "Loaded previous session");
using var sha1 = SHA1.Create();
foreach (var dcs in session.DCSessions.Values)
dcs.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(dcs.AuthKey).AsSpan(12));
}
session ??= new Session();
session._store = store;
@ -144,16 +152,23 @@ namespace WTelegram
if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable
using (var aes = Aes.Create())
_encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]);
_store.Position = 0;
_store.Write(_encrypted, 0, encryptedLen);
_store.SetLength(encryptedLen);
try
{
_store.Position = 0;
_store.Write(_encrypted, 0, encryptedLen);
_store.SetLength(encryptedLen);
}
catch (Exception ex)
{
Helpers.Log(4, $"{_store} raised {ex}");
}
}
_jsonStream.Position = 0;
_jsonWriter.Reset();
}
}
internal class SessionStore : FileStream
internal sealed class SessionStore : FileStream // This class is designed to be high-performance and failure-resilient with Writes (but when you're Andrei, you can't understand that)
{
public override long Length { get; }
public override long Position { get => base.Position; set { } }
@ -186,4 +201,10 @@ namespace WTelegram
base.Write(_header, 0, 8);
}
}
internal sealed class ActionStore(byte[] initial, Action<byte[]> save) : MemoryStream(initial ?? [])
{
public override void Write(byte[] buffer, int offset, int count) => save(buffer[offset..(offset + count)]);
public override void SetLength(long value) { }
}
}

View file

@ -5,8 +5,9 @@ using Client = WTelegram.Client;
namespace TL
{
#pragma warning disable IDE1006, CS1574
[TLDef(0x05162463)] //resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes server_public_key_fingerprints:Vector<long> = ResPQ
public class ResPQ : IObject
public sealed partial class ResPQ : IObject
{
public Int128 nonce;
public Int128 server_nonce;
@ -15,7 +16,7 @@ namespace TL
}
[TLDef(0x83C95AEC)] //p_q_inner_data#83c95aec pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data
public class PQInnerData : IObject
public partial class PQInnerData : IObject
{
public byte[] pq;
public byte[] p;
@ -25,24 +26,24 @@ namespace TL
public Int256 new_nonce;
}
[TLDef(0xA9F55F95, inheritBefore = true)] //p_q_inner_data_dc#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data
public class PQInnerDataDc : PQInnerData
public sealed partial class PQInnerDataDc : PQInnerData
{
public int dc;
}
[TLDef(0x3C6A84D4, inheritBefore = true)] //p_q_inner_data_temp#3c6a84d4 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 expires_in:int = P_Q_inner_data
public class PQInnerDataTemp : PQInnerData
public sealed partial class PQInnerDataTemp : PQInnerData
{
public int expires_in;
}
[TLDef(0x56FDDF88, inheritBefore = true)] //p_q_inner_data_temp_dc#56fddf88 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data
public class PQInnerDataTempDc : PQInnerData
public sealed partial class PQInnerDataTempDc : PQInnerData
{
public int dc;
public int expires_in;
}
[TLDef(0x75A3F765)] //bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner
public class BindAuthKeyInner : IObject
public sealed partial class BindAuthKeyInner : IObject
{
public long nonce;
public long temp_auth_key_id;
@ -51,24 +52,24 @@ namespace TL
public DateTime expires_at;
}
public abstract class ServerDHParams : IObject
public abstract partial class ServerDHParams : IObject
{
public Int128 nonce;
public Int128 server_nonce;
}
[TLDef(0x79CB045D, inheritBefore = true)] //server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params
public class ServerDHParamsFail : ServerDHParams
public sealed partial class ServerDHParamsFail : ServerDHParams
{
public Int128 new_nonce_hash;
}
[TLDef(0xD0E8075C, inheritBefore = true)] //server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:bytes = Server_DH_Params
public class ServerDHParamsOk : ServerDHParams
public sealed partial class ServerDHParamsOk : ServerDHParams
{
public byte[] encrypted_answer;
}
[TLDef(0xB5890DBA)] //server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:bytes g_a:bytes server_time:int = Server_DH_inner_data
public class ServerDHInnerData : IObject
public sealed partial class ServerDHInnerData : IObject
{
public Int128 nonce;
public Int128 server_nonce;
@ -79,7 +80,7 @@ namespace TL
}
[TLDef(0x6643B654)] //client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:bytes = Client_DH_Inner_Data
public class ClientDHInnerData : IObject
public sealed partial class ClientDHInnerData : IObject
{
public Int128 nonce;
public Int128 server_nonce;
@ -87,84 +88,82 @@ namespace TL
public byte[] g_b;
}
public abstract class SetClientDHParamsAnswer : IObject
public abstract partial class SetClientDHParamsAnswer : IObject
{
public Int128 nonce;
public Int128 server_nonce;
}
[TLDef(0x3BCBF734, inheritBefore = true)] //dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer
public class DhGenOk : SetClientDHParamsAnswer
public sealed partial class DhGenOk : SetClientDHParamsAnswer
{
public Int128 new_nonce_hash1;
}
[TLDef(0x46DC1FB9, inheritBefore = true)] //dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer
public class DhGenRetry : SetClientDHParamsAnswer
public sealed partial class DhGenRetry : SetClientDHParamsAnswer
{
public Int128 new_nonce_hash2;
}
[TLDef(0xA69DAE02, inheritBefore = true)] //dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer
public class DhGenFail : SetClientDHParamsAnswer
public sealed partial class DhGenFail : SetClientDHParamsAnswer
{
public Int128 new_nonce_hash3;
}
public enum DestroyAuthKeyRes : uint
{
///<summary>See <a href="https://corefork.telegram.org/constructor/destroy_auth_key_ok"/></summary>
Ok = 0xF660E1D4,
///<summary>See <a href="https://corefork.telegram.org/constructor/destroy_auth_key_none"/></summary>
None = 0x0A9F2259,
///<summary>See <a href="https://corefork.telegram.org/constructor/destroy_auth_key_fail"/></summary>
Fail = 0xEA109B13,
}
public abstract partial class DestroyAuthKeyRes : IObject { }
[TLDef(0xF660E1D4)] //destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes
public sealed partial class DestroyAuthKeyOk : DestroyAuthKeyRes { }
[TLDef(0x0A9F2259)] //destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes
public sealed partial class DestroyAuthKeyNone : DestroyAuthKeyRes { }
[TLDef(0xEA109B13)] //destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes
public sealed partial class DestroyAuthKeyFail : DestroyAuthKeyRes { }
[TLDef(0x62D6B459)] //msgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck
public class MsgsAck : IObject
public sealed partial class MsgsAck : IObject
{
public long[] msg_ids;
}
[TLDef(0xA7EFF811)] //bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification
public class BadMsgNotification : IObject
public partial class BadMsgNotification : IObject
{
public long bad_msg_id;
public int bad_msg_seqno;
public int error_code;
}
[TLDef(0xEDAB447B, inheritBefore = true)] //bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification
public class BadServerSalt : BadMsgNotification
public sealed partial class BadServerSalt : BadMsgNotification
{
public long new_server_salt;
}
[TLDef(0xDA69FB52)] //msgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq
public class MsgsStateReq : IObject
public sealed partial class MsgsStateReq : IObject
{
public long[] msg_ids;
}
[TLDef(0x04DEB57D)] //msgs_state_info#04deb57d req_msg_id:long info:bytes = MsgsStateInfo
public class MsgsStateInfo : IObject
public sealed partial class MsgsStateInfo : IObject
{
public long req_msg_id;
public byte[] info;
}
[TLDef(0x8CC0D131)] //msgs_all_info#8cc0d131 msg_ids:Vector<long> info:bytes = MsgsAllInfo
public class MsgsAllInfo : IObject
public sealed partial class MsgsAllInfo : IObject
{
public long[] msg_ids;
public byte[] info;
}
public abstract class MsgDetailedInfoBase : IObject
public abstract partial class MsgDetailedInfoBase : IObject
{
public virtual long AnswerMsgId { get; }
public virtual int Bytes { get; }
public virtual int Status { get; }
public virtual long AnswerMsgId => default;
public virtual int Bytes => default;
public virtual int Status => default;
}
[TLDef(0x276D3EC6)] //msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo
public class MsgDetailedInfo : MsgDetailedInfoBase
public sealed partial class MsgDetailedInfo : MsgDetailedInfoBase
{
public long msg_id;
public long answer_msg_id;
@ -176,7 +175,7 @@ namespace TL
public override int Status => status;
}
[TLDef(0x809DB6DF)] //msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo
public class MsgNewDetailedInfo : MsgDetailedInfoBase
public sealed partial class MsgNewDetailedInfo : MsgDetailedInfoBase
{
public long answer_msg_id;
public int bytes;
@ -188,25 +187,25 @@ namespace TL
}
[TLDef(0x7D861A08)] //msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq
public class MsgResendReq : IObject
public sealed partial class MsgResendReq : IObject
{
public long[] msg_ids;
}
[TLDef(0x2144CA19)] //rpc_error#2144ca19 error_code:int error_message:string = RpcError
public class RpcError : IObject
public sealed partial class RpcError : IObject
{
public int error_code;
public string error_message;
}
public abstract class RpcDropAnswer : IObject { }
public abstract partial class RpcDropAnswer : IObject { }
[TLDef(0x5E2AD36E)] //rpc_answer_unknown#5e2ad36e = RpcDropAnswer
public class RpcAnswerUnknown : RpcDropAnswer { }
public sealed partial class RpcAnswerUnknown : RpcDropAnswer { }
[TLDef(0xCD78E586)] //rpc_answer_dropped_running#cd78e586 = RpcDropAnswer
public class RpcAnswerDroppedRunning : RpcDropAnswer { }
public sealed partial class RpcAnswerDroppedRunning : RpcDropAnswer { }
[TLDef(0xA43AD8B7)] //rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer
public class RpcAnswerDropped : RpcDropAnswer
public sealed partial class RpcAnswerDropped : RpcDropAnswer
{
public long msg_id;
public int seq_no;
@ -214,7 +213,7 @@ namespace TL
}
[TLDef(0x0949D9DC)] //future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt
public class FutureSalt : IObject
public sealed partial class FutureSalt : IObject
{
public DateTime valid_since;
public DateTime valid_until;
@ -222,7 +221,7 @@ namespace TL
}
[TLDef(0xAE500895)] //future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts
public class FutureSalts : IObject
public sealed partial class FutureSalts : IObject
{
public long req_msg_id;
public DateTime now;
@ -230,24 +229,24 @@ namespace TL
}
[TLDef(0x347773C5)] //pong#347773c5 msg_id:long ping_id:long = Pong
public class Pong : IObject
public sealed partial class Pong : IObject
{
public long msg_id;
public long ping_id;
}
public abstract class DestroySessionRes : IObject
public abstract partial class DestroySessionRes : IObject
{
public long session_id;
}
[TLDef(0xE22045FC)] //destroy_session_ok#e22045fc session_id:long = DestroySessionRes
public class DestroySessionOk : DestroySessionRes { }
public sealed partial class DestroySessionOk : DestroySessionRes { }
[TLDef(0x62D350C9)] //destroy_session_none#62d350c9 session_id:long = DestroySessionRes
public class DestroySessionNone : DestroySessionRes { }
public sealed partial class DestroySessionNone : DestroySessionRes { }
public abstract class NewSession : IObject { }
public abstract partial class NewSession : IObject { }
[TLDef(0x9EC20908)] //new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession
public class NewSessionCreated : NewSession
public sealed partial class NewSessionCreated : NewSession
{
public long first_msg_id;
public long unique_id;
@ -255,7 +254,7 @@ namespace TL
}
[TLDef(0x9299359F)] //http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait
public class HttpWait : IObject
public sealed partial class HttpWait : IObject
{
public int max_delay;
public int wait_after;
@ -263,19 +262,19 @@ namespace TL
}
[TLDef(0xD433AD73)] //ipPort#d433ad73 ipv4:int port:int = IpPort
public class IpPort : IObject
public partial class IpPort : IObject
{
public int ipv4;
public int port;
}
[TLDef(0x37982646, inheritBefore = true)] //ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort
public class IpPortSecret : IpPort
public sealed partial class IpPortSecret : IpPort
{
public byte[] secret;
}
[TLDef(0x4679B65F)] //accessPointRule#4679b65f phone_prefix_rules:bytes dc_id:int ips:vector<IpPort> = AccessPointRule
public class AccessPointRule : IObject
public sealed partial class AccessPointRule : IObject
{
public byte[] phone_prefix_rules;
public int dc_id;
@ -283,7 +282,7 @@ namespace TL
}
[TLDef(0x5A592A6C)] //help.configSimple#5a592a6c date:int expires:int rules:vector<AccessPointRule> = help.ConfigSimple
public class Help_ConfigSimple : IObject
public sealed partial class Help_ConfigSimple : IObject
{
public DateTime date;
public DateTime expires;
@ -326,12 +325,12 @@ namespace TL
});
public static Task<DestroyAuthKeyRes> DestroyAuthKey(this Client client)
=> client.InvokeBare(new DestroyAuthKey
=> client.Invoke(new DestroyAuthKey
{
});
public static Task<RpcDropAnswer> RpcDropAnswer(this Client client, long req_msg_id)
=> client.InvokeBare(new Methods.RpcDropAnswer
=> client.Invoke(new Methods.RpcDropAnswer
{
req_msg_id = req_msg_id,
});
@ -356,7 +355,7 @@ namespace TL
});
public static Task<DestroySessionRes> DestroySession(this Client client, long session_id)
=> client.InvokeBare(new DestroySession
=> client.Invoke(new DestroySession
{
session_id = session_id,
});
@ -365,20 +364,21 @@ namespace TL
namespace TL.Methods
{
#pragma warning disable IDE1006
[TLDef(0x60469778)] //req_pq#60469778 nonce:int128 = ResPQ
public class ReqPq : IMethod<ResPQ>
public sealed partial class ReqPq : IMethod<ResPQ>
{
public Int128 nonce;
}
[TLDef(0xBE7E8EF1)] //req_pq_multi#be7e8ef1 nonce:int128 = ResPQ
public class ReqPqMulti : IMethod<ResPQ>
public sealed partial class ReqPqMulti : IMethod<ResPQ>
{
public Int128 nonce;
}
[TLDef(0xD712E4BE)] //req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params
public class ReqDHParams : IMethod<ServerDHParams>
public sealed partial class ReqDHParams : IMethod<ServerDHParams>
{
public Int128 nonce;
public Int128 server_nonce;
@ -389,7 +389,7 @@ namespace TL.Methods
}
[TLDef(0xF5045F1F)] //set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer
public class SetClientDHParams : IMethod<SetClientDHParamsAnswer>
public sealed partial class SetClientDHParams : IMethod<SetClientDHParamsAnswer>
{
public Int128 nonce;
public Int128 server_nonce;
@ -397,35 +397,35 @@ namespace TL.Methods
}
[TLDef(0xD1435160)] //destroy_auth_key#d1435160 = DestroyAuthKeyRes
public class DestroyAuthKey : IMethod<DestroyAuthKeyRes> { }
public sealed partial class DestroyAuthKey : IMethod<DestroyAuthKeyRes> { }
[TLDef(0x58E4A740)] //rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer
public class RpcDropAnswer : IMethod<TL.RpcDropAnswer>
public sealed partial class RpcDropAnswer : IMethod<TL.RpcDropAnswer>
{
public long req_msg_id;
}
[TLDef(0xB921BD04)] //get_future_salts#b921bd04 num:int = FutureSalts
public class GetFutureSalts : IMethod<FutureSalts>
public sealed partial class GetFutureSalts : IMethod<FutureSalts>
{
public int num;
}
[TLDef(0x7ABE77EC)] //ping#7abe77ec ping_id:long = Pong
public class Ping : IMethod<Pong>
public sealed partial class Ping : IMethod<Pong>
{
public long ping_id;
}
[TLDef(0xF3427B8C)] //ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong
public class PingDelayDisconnect : IMethod<Pong>
public sealed partial class PingDelayDisconnect : IMethod<Pong>
{
public long ping_id;
public int disconnect_delay;
}
[TLDef(0xE7512126)] //destroy_session#e7512126 session_id:long = DestroySessionRes
public class DestroySession : IMethod<DestroySessionRes>
public sealed partial class DestroySession : IMethod<DestroySessionRes>
{
public long session_id;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -2,61 +2,64 @@
namespace TL
{
/// <summary>Object describes the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessage"/></para></summary>
public abstract class DecryptedMessageBase : IObject
#pragma warning disable IDE1006, CS1574
/// <summary>Object describes the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessage"/></para> <para>Derived classes: <see cref="DecryptedMessage"/>, <see cref="DecryptedMessageService"/></para></summary>
public abstract partial class DecryptedMessageBase : IObject
{
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
public virtual uint FFlags { get; }
public virtual uint FFlags => default;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public virtual long RandomId { get; }
public virtual long RandomId => default;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary>
public virtual int Ttl { get; }
public virtual int Ttl => default;
/// <summary>Message text</summary>
public virtual string Message { get; }
public virtual string Message => default;
/// <summary>Media content</summary>
public virtual DecryptedMessageMedia Media { get; }
public virtual DecryptedMessageMedia Media => default;
/// <summary>Message <a href="https://corefork.telegram.org/api/entities">entities</a> for styled text (parameter added in layer 45)</summary>
public virtual MessageEntity[] Entities { get; }
public virtual MessageEntity[] Entities => default;
/// <summary>Specifies the ID of the inline bot that generated the message (parameter added in layer 45)</summary>
public virtual string ViaBotName { get; }
public virtual string ViaBotName => default;
/// <summary>Random message ID of the message this message replies to (parameter added in layer 45)</summary>
public virtual long ReplyToRandom { get; }
public virtual long ReplyToRandom => default;
/// <summary>Random group ID, assigned by the author of message.<br/>Multiple encrypted messages with a photo attached and with the same group ID indicate an <a href="https://corefork.telegram.org/api/files#albums-grouped-media">album or grouped media</a> (parameter added in layer 45)</summary>
public virtual long Grouped { get; }
public virtual byte[] RandomBytes { get; }
public virtual DecryptedMessageAction Action { get; }
public virtual long Grouped => default;
/// <summary>Random bytes, removed in layer 17.</summary>
public virtual byte[] RandomBytes => default;
public virtual DecryptedMessageAction Action => default;
}
/// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para></summary>
/// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para> <para>Derived classes: <see cref="DecryptedMessageMediaPhoto"/>, <see cref="DecryptedMessageMediaVideo"/>, <see cref="DecryptedMessageMediaGeoPoint"/>, <see cref="DecryptedMessageMediaContact"/>, <see cref="DecryptedMessageMediaDocument"/>, <see cref="DecryptedMessageMediaAudio"/>, <see cref="DecryptedMessageMediaExternalDocument"/>, <see cref="DecryptedMessageMediaVenue"/>, <see cref="DecryptedMessageMediaWebPage"/></para></summary>
/// <remarks>a <see langword="null"/> value means <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaEmpty">decryptedMessageMediaEmpty</a></remarks>
public abstract class DecryptedMessageMedia : IObject
public abstract partial class DecryptedMessageMedia : IObject
{
public virtual string MimeType { get; }
public virtual string MimeType => default;
internal virtual (long size, byte[] key, byte[] iv) SizeKeyIV { get => default; set => throw new WTelegram.WTException("Incompatible DecryptedMessageMedia"); }
}
/// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para></summary>
public abstract class DecryptedMessageAction : IObject { }
/// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para> <para>Derived classes: <see cref="DecryptedMessageActionSetMessageTTL"/>, <see cref="DecryptedMessageActionReadMessages"/>, <see cref="DecryptedMessageActionDeleteMessages"/>, <see cref="DecryptedMessageActionScreenshotMessages"/>, <see cref="DecryptedMessageActionFlushHistory"/>, <see cref="DecryptedMessageActionResend"/>, <see cref="DecryptedMessageActionNotifyLayer"/>, <see cref="DecryptedMessageActionTyping"/>, <see cref="DecryptedMessageActionRequestKey"/>, <see cref="DecryptedMessageActionAcceptKey"/>, <see cref="DecryptedMessageActionAbortKey"/>, <see cref="DecryptedMessageActionCommitKey"/>, <see cref="DecryptedMessageActionNoop"/></para></summary>
public abstract partial class DecryptedMessageAction : IObject { }
/// <summary>Indicates the location of a photo, will be deprecated soon <para>See <a href="https://corefork.telegram.org/type/FileLocation"/></para></summary>
public abstract class FileLocationBase : IObject
/// <summary>Indicates the location of a photo, will be deprecated soon <para>See <a href="https://corefork.telegram.org/type/FileLocation"/></para> <para>Derived classes: <see cref="FileLocationUnavailable"/>, <see cref="FileLocation"/></para></summary>
public abstract partial class FileLocationBase : IObject
{
/// <summary>Server volume</summary>
public virtual long VolumeId { get; }
/// <summary>File ID</summary>
public virtual int LocalId { get; }
/// <summary>Checksum to access the file</summary>
public virtual long Secret { get; }
/// <summary>Volume ID</summary>
public virtual long VolumeId => default;
/// <summary>Local ID</summary>
public virtual int LocalId => default;
/// <summary>Secret</summary>
public virtual long Secret => default;
}
namespace Layer8
{
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x1F814F1F)]
public class DecryptedMessage : DecryptedMessageBase
public sealed partial class DecryptedMessage : DecryptedMessageBase
{
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id;
/// <summary>Random bytes, removed in layer 17.</summary>
public byte[] random_bytes;
/// <summary>Message text</summary>
public string message;
@ -69,20 +72,23 @@ namespace TL
public override string Message => message;
/// <summary>Media content</summary>
public override DecryptedMessageMedia Media => media;
/// <summary>Random bytes, removed in layer 17.</summary>
public override byte[] RandomBytes => random_bytes;
}
/// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary>
[TLDef(0xAA48327D)]
public class DecryptedMessageService : DecryptedMessageBase
public sealed partial class DecryptedMessageService : DecryptedMessageBase
{
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public long random_id;
/// <summary>Random bytes, removed in Layer 17.</summary>
public byte[] random_bytes;
/// <summary>Action relevant to the service message</summary>
public DecryptedMessageAction action;
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public override long RandomId => random_id;
/// <summary>Random bytes, removed in Layer 17.</summary>
public override byte[] RandomBytes => random_bytes;
/// <summary>Action relevant to the service message</summary>
public override DecryptedMessageAction Action => action;
@ -90,7 +96,7 @@ namespace TL
/// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary>
[TLDef(0x32798A8C)]
public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaPhoto : DecryptedMessageMedia
{
/// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary>
public byte[] thumb;
@ -114,7 +120,7 @@ namespace TL
}
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x4CEE6EF3)]
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia
{
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
public byte[] thumb;
@ -139,7 +145,7 @@ namespace TL
}
/// <summary>GeoPoint attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaGeoPoint"/></para></summary>
[TLDef(0x35480A59)]
public class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia
{
/// <summary>Latitude of point</summary>
public double lat;
@ -148,7 +154,7 @@ namespace TL
}
/// <summary>Contact attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaContact"/></para></summary>
[TLDef(0x588A0A97)]
public class DecryptedMessageMediaContact : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaContact : DecryptedMessageMedia
{
/// <summary>Phone number</summary>
public string phone_number;
@ -161,7 +167,7 @@ namespace TL
}
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0xB095434B)]
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
{
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
public byte[] thumb;
@ -169,6 +175,7 @@ namespace TL
public int thumb_w;
/// <summary>Thumbnail height</summary>
public int thumb_h;
/// <summary>File name, moved to <c>attributes</c> in Layer 45.</summary>
public string file_name;
/// <summary>File MIME-type</summary>
public string mime_type;
@ -186,7 +193,7 @@ namespace TL
}
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
[TLDef(0x6080758F)]
public class DecryptedMessageMediaAudio : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaAudio : DecryptedMessageMedia
{
/// <summary>Audio duration in seconds</summary>
public int duration;
@ -202,42 +209,42 @@ namespace TL
/// <summary>Setting of a message lifetime after reading. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionSetMessageTTL"/></para></summary>
[TLDef(0xA1733AEC)]
public class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction
public sealed partial class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction
{
/// <summary>Lifetime in seconds</summary>
public int ttl_seconds;
}
/// <summary>Messages marked as read. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionReadMessages"/></para></summary>
[TLDef(0x0C4F40BE)]
public class DecryptedMessageActionReadMessages : DecryptedMessageAction
public sealed partial class DecryptedMessageActionReadMessages : DecryptedMessageAction
{
/// <summary>List of message IDs</summary>
public long[] random_ids;
}
/// <summary>Deleted messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionDeleteMessages"/></para></summary>
[TLDef(0x65614304)]
public class DecryptedMessageActionDeleteMessages : DecryptedMessageAction
public sealed partial class DecryptedMessageActionDeleteMessages : DecryptedMessageAction
{
/// <summary>List of deleted message IDs</summary>
public long[] random_ids;
}
/// <summary>A screenshot was taken. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionScreenshotMessages"/></para></summary>
[TLDef(0x8AC1F475)]
public class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction
public sealed partial class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction
{
/// <summary>List of affected message ids (that appeared on the screenshot)</summary>
public long[] random_ids;
}
/// <summary>The entire message history has been deleted. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionFlushHistory"/></para></summary>
[TLDef(0x6719E45C)]
public class DecryptedMessageActionFlushHistory : DecryptedMessageAction { }
public sealed partial class DecryptedMessageActionFlushHistory : DecryptedMessageAction { }
}
namespace Layer23
{
/// <summary>Image description. <para>See <a href="https://corefork.telegram.org/constructor/photoSize"/></para></summary>
[TLDef(0x77BFB61B)]
public partial class PhotoSize : PhotoSizeBase
public sealed partial class PhotoSize : PhotoSizeBase
{
/// <summary><a href="https://corefork.telegram.org/api/files#image-thumbnail-types">Thumbnail type »</a></summary>
public string type;
@ -254,7 +261,7 @@ namespace TL
}
/// <summary>Description of an image and its content. <para>See <a href="https://corefork.telegram.org/constructor/photoCachedSize"/></para></summary>
[TLDef(0xE9A734FA)]
public partial class PhotoCachedSize : PhotoSizeBase
public sealed partial class PhotoCachedSize : PhotoSizeBase
{
/// <summary>Thumbnail type</summary>
public string type;
@ -272,23 +279,23 @@ namespace TL
/// <summary>User is uploading a video. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadVideoAction"/></para></summary>
[TLDef(0x92042FF7)]
public class SendMessageUploadVideoAction : SendMessageAction { }
public sealed partial class SendMessageUploadVideoAction : SendMessageAction { }
/// <summary>User is uploading a voice message. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadAudioAction"/></para></summary>
[TLDef(0xE6AC8A6F)]
public class SendMessageUploadAudioAction : SendMessageAction { }
public sealed partial class SendMessageUploadAudioAction : SendMessageAction { }
/// <summary>User is uploading a photo. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadPhotoAction"/></para></summary>
[TLDef(0x990A3C1A)]
public class SendMessageUploadPhotoAction : SendMessageAction { }
public sealed partial class SendMessageUploadPhotoAction : SendMessageAction { }
/// <summary>User is uploading a file. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadDocumentAction"/></para></summary>
[TLDef(0x8FAEE98E)]
public class SendMessageUploadDocumentAction : SendMessageAction { }
public sealed partial class SendMessageUploadDocumentAction : SendMessageAction { }
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
[TLDef(0xFB0A5727)]
public class DocumentAttributeSticker : DocumentAttribute { }
public sealed partial class DocumentAttributeSticker : DocumentAttribute { }
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
[TLDef(0x5910CCCB)]
public class DocumentAttributeVideo : DocumentAttribute
public sealed partial class DocumentAttributeVideo : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
@ -299,7 +306,7 @@ namespace TL
}
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
[TLDef(0x051448E5)]
public class DocumentAttributeAudio : DocumentAttribute
public sealed partial class DocumentAttributeAudio : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
@ -307,7 +314,7 @@ namespace TL
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x204D3878)]
public class DecryptedMessage : DecryptedMessageBase
public sealed partial class DecryptedMessage : DecryptedMessageBase
{
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id;
@ -329,7 +336,7 @@ namespace TL
}
/// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary>
[TLDef(0x73164160)]
public class DecryptedMessageService : DecryptedMessageBase
public sealed partial class DecryptedMessageService : DecryptedMessageBase
{
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public long random_id;
@ -344,7 +351,7 @@ namespace TL
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x524A415D)]
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia
{
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
public byte[] thumb;
@ -374,7 +381,7 @@ namespace TL
}
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
[TLDef(0x57E0A9CB)]
public class DecryptedMessageMediaAudio : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaAudio : DecryptedMessageMedia
{
/// <summary>Audio duration in seconds</summary>
public int duration;
@ -394,7 +401,7 @@ namespace TL
}
/// <summary>Non-e2e documented forwarded from non-secret chat <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaExternalDocument"/></para></summary>
[TLDef(0xFA95B0DD)]
public class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
{
/// <summary>Document ID</summary>
public long id;
@ -419,7 +426,7 @@ namespace TL
/// <summary>Request for the other party in a Secret Chat to automatically resend a contiguous range of previously sent messages, as explained in <a href="https://corefork.telegram.org/api/end-to-end/seq_no">Sequence number is Secret Chats</a>. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionResend"/></para></summary>
[TLDef(0x511110B0)]
public class DecryptedMessageActionResend : DecryptedMessageAction
public sealed partial class DecryptedMessageActionResend : DecryptedMessageAction
{
/// <summary><c>out_seq_no</c> of the first message to be resent, with correct parity</summary>
public int start_seq_no;
@ -428,21 +435,21 @@ namespace TL
}
/// <summary>A notification stating the API layer that is used by the client. You should use your current layer and take notice of the layer used on the other side of a conversation when sending messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNotifyLayer"/></para></summary>
[TLDef(0xF3048883)]
public class DecryptedMessageActionNotifyLayer : DecryptedMessageAction
public sealed partial class DecryptedMessageActionNotifyLayer : DecryptedMessageAction
{
/// <summary>Layer number, must be <strong>17</strong> or higher (this constructor was introduced in Layer 17.</summary>
public int layer;
}
/// <summary>User is preparing a message: typing, recording, uploading, etc. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionTyping"/></para></summary>
[TLDef(0xCCB27641)]
public class DecryptedMessageActionTyping : DecryptedMessageAction
public sealed partial class DecryptedMessageActionTyping : DecryptedMessageAction
{
/// <summary>Type of action</summary>
public SendMessageAction action;
}
/// <summary>Request rekeying, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionRequestKey"/></para></summary>
[TLDef(0xF3C9611B)]
public class DecryptedMessageActionRequestKey : DecryptedMessageAction
public sealed partial class DecryptedMessageActionRequestKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
@ -451,7 +458,7 @@ namespace TL
}
/// <summary>Accept new key <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAcceptKey"/></para></summary>
[TLDef(0x6FE1735B)]
public class DecryptedMessageActionAcceptKey : DecryptedMessageAction
public sealed partial class DecryptedMessageActionAcceptKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
@ -462,14 +469,14 @@ namespace TL
}
/// <summary>Abort rekeying <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAbortKey"/></para></summary>
[TLDef(0xDD05EC6B)]
public class DecryptedMessageActionAbortKey : DecryptedMessageAction
public sealed partial class DecryptedMessageActionAbortKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
}
/// <summary>Commit new key, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionCommitKey"/></para></summary>
[TLDef(0xEC2E0B9B)]
public class DecryptedMessageActionCommitKey : DecryptedMessageAction
public sealed partial class DecryptedMessageActionCommitKey : DecryptedMessageAction
{
/// <summary>Exchange ID, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long exchange_id;
@ -478,11 +485,11 @@ namespace TL
}
/// <summary>NOOP action <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNoop"/></para></summary>
[TLDef(0xA82FDD63)]
public class DecryptedMessageActionNoop : DecryptedMessageAction { }
public sealed partial class DecryptedMessageActionNoop : DecryptedMessageAction { }
/// <summary>Sets the layer number for the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageLayer"/></para></summary>
[TLDef(0x1BE31789)]
public class DecryptedMessageLayer : IObject
public sealed partial class DecryptedMessageLayer : IObject
{
/// <summary>Set of random bytes to prevent content recognition in short encrypted messages.<br/>Clients are required to check that there are at least 15 random bytes included in each message. Messages with less than 15 random bytes must be ignored.<br/>Parameter moved here from <see cref="DecryptedMessage"/> in Layer 17.</summary>
public byte[] random_bytes;
@ -498,40 +505,40 @@ namespace TL
/// <summary>File is currently unavailable. <para>See <a href="https://corefork.telegram.org/constructor/fileLocationUnavailable"/></para></summary>
[TLDef(0x7C596B46)]
public class FileLocationUnavailable : FileLocationBase
public sealed partial class FileLocationUnavailable : FileLocationBase
{
/// <summary>Server volume</summary>
/// <summary>Volume ID</summary>
public long volume_id;
/// <summary>File ID</summary>
/// <summary>Local ID</summary>
public int local_id;
/// <summary>Checksum to access the file</summary>
/// <summary>Secret</summary>
public long secret;
/// <summary>Server volume</summary>
/// <summary>Volume ID</summary>
public override long VolumeId => volume_id;
/// <summary>File ID</summary>
/// <summary>Local ID</summary>
public override int LocalId => local_id;
/// <summary>Checksum to access the file</summary>
/// <summary>Secret</summary>
public override long Secret => secret;
}
/// <summary>File location. <para>See <a href="https://corefork.telegram.org/constructor/fileLocation"/></para></summary>
[TLDef(0x53D69076)]
public class FileLocation : FileLocationBase
public sealed partial class FileLocation : FileLocationBase
{
/// <summary>Number of the data center holding the file</summary>
/// <summary>DC ID</summary>
public int dc_id;
/// <summary>Server volume</summary>
/// <summary>Volume ID</summary>
public long volume_id;
/// <summary>File ID</summary>
/// <summary>Local ID</summary>
public int local_id;
/// <summary>Checksum to access the file</summary>
/// <summary>Secret</summary>
public long secret;
/// <summary>Server volume</summary>
/// <summary>Volume ID</summary>
public override long VolumeId => volume_id;
/// <summary>File ID</summary>
/// <summary>Local ID</summary>
public override int LocalId => local_id;
/// <summary>Checksum to access the file</summary>
/// <summary>Secret</summary>
public override long Secret => secret;
}
}
@ -540,7 +547,7 @@ namespace TL
{
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
[TLDef(0xDED218E0)]
public class DocumentAttributeAudio : DocumentAttribute
public sealed partial class DocumentAttributeAudio : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
@ -555,7 +562,7 @@ namespace TL
{
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
[TLDef(0x3A556302)]
public class DocumentAttributeSticker : DocumentAttribute
public sealed partial class DocumentAttributeSticker : DocumentAttribute
{
/// <summary>Alternative emoji representation of sticker</summary>
public string alt;
@ -565,7 +572,7 @@ namespace TL
/// <summary>Message entity representing a <a href="https://corefork.telegram.org/api/mentions">user mention</a>: for <em>creating</em> a mention use <see cref="InputMessageEntityMentionName"/>. <para>See <a href="https://corefork.telegram.org/constructor/messageEntityMentionName"/></para></summary>
[TLDef(0x352DCA58, inheritBefore = true)]
public class MessageEntityMentionName : MessageEntityMention
public sealed partial class MessageEntityMentionName : MessageEntity
{
/// <summary>Identifier of the user that was mentioned</summary>
public int user_id;
@ -573,9 +580,9 @@ namespace TL
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x36B091DE)]
public class DecryptedMessage : DecryptedMessageBase
public sealed partial class DecryptedMessage : DecryptedMessageBase
{
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
public Flags flags;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id;
@ -624,7 +631,7 @@ namespace TL
/// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary>
[TLDef(0xF1FA8D78)]
public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaPhoto : DecryptedMessageMedia
{
/// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary>
public byte[] thumb;
@ -650,7 +657,7 @@ namespace TL
}
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x970C8C0E)]
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia
{
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
public byte[] thumb;
@ -682,7 +689,7 @@ namespace TL
}
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0x7AFE8AE2)]
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
{
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
public byte[] thumb;
@ -710,7 +717,7 @@ namespace TL
}
/// <summary>Venue <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVenue"/></para></summary>
[TLDef(0x8A0DF56F)]
public class DecryptedMessageMediaVenue : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaVenue : DecryptedMessageMedia
{
/// <summary>Latitude of venue</summary>
public double lat;
@ -720,14 +727,14 @@ namespace TL
public string title;
/// <summary>Address</summary>
public string address;
/// <summary>Venue provider: currently only "foursquare" needs to be supported</summary>
/// <summary>Venue provider: currently only "foursquare" and "gplaces" (Google Places) need to be supported</summary>
public string provider;
/// <summary>Venue ID in the provider's database</summary>
public string venue_id;
}
/// <summary>Webpage preview <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaWebPage"/></para></summary>
[TLDef(0xE50511D8)]
public class DecryptedMessageMediaWebPage : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaWebPage : DecryptedMessageMedia
{
/// <summary>URL of webpage</summary>
public string url;
@ -738,16 +745,36 @@ namespace TL
{
/// <summary>User is uploading a round video <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadRoundAction"/></para></summary>
[TLDef(0xBB718624)]
public class SendMessageUploadRoundAction : SendMessageAction { }
public sealed partial class SendMessageUploadRoundAction : SendMessageAction { }
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
[TLDef(0x0EF02CE6)]
public sealed partial class DocumentAttributeVideo : DocumentAttribute
{
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
public Flags flags;
/// <summary>Duration in seconds</summary>
public int duration;
/// <summary>Video width</summary>
public int w;
/// <summary>Video height</summary>
public int h;
[Flags] public enum Flags : uint
{
/// <summary>Whether this is a round video</summary>
round_message = 0x1,
}
}
}
namespace Layer73
{
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x91CC4674)]
public class DecryptedMessage : DecryptedMessageBase
public sealed partial class DecryptedMessage : DecryptedMessageBase
{
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
public Flags flags;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id;
@ -770,7 +797,6 @@ namespace TL
{
/// <summary>Field <see cref="reply_to_random_id"/> has a value</summary>
has_reply_to_random_id = 0x8,
/// <summary>Whether this is a silent message (no notification triggered)</summary>
silent = 0x20,
/// <summary>Field <see cref="entities"/> has a value</summary>
has_entities = 0x80,
@ -804,13 +830,17 @@ namespace TL
}
namespace Layer101
{ }
{
/// <summary>Message entity representing a block quote. <para>See <a href="https://corefork.telegram.org/constructor/messageEntityBlockquote"/></para></summary>
[TLDef(0x020DF5D0)]
public sealed partial class MessageEntityBlockquote : MessageEntity { }
}
namespace Layer143
{
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0x6ABD9782)]
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
{
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
public byte[] thumb;

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@ namespace TL
}
partial class InputPeerChat
{
/// <summary>⚠ Only for small private Chat. Chat groups of type Channel must use InputPeerChannel. See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/README.md#terminology">Terminology</see> in README</summary>
/// <summary>⚠ <b>This type is only for basic Chat</b>. See <see href="https://wiz0u.github.io/WTelegramClient/#terminology">Terminology</see> in the README to understand what this means<br/>Chat groups of type Channel must use <see cref="InputPeerChannel"/>.</summary>
/// <param name="chat_id">Chat identifier</param>
public InputPeerChat(long chat_id) => this.chat_id = chat_id;
internal InputPeerChat() { }
@ -86,16 +86,36 @@ namespace TL
else
return new InputChatUploadedPhoto { file = this, flags = InputChatUploadedPhoto.Flags.has_file };
}
/// <summary>Random file identifier created by the client</summary>
public abstract long ID { get; set; }
/// <summary>Number of parts saved</summary>
public abstract int Parts { get; set; }
/// <summary>Full name of the file</summary>
public abstract string Name { get; set; }
}
partial class InputFile
{
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => new InputEncryptedFileUploaded { id = id, parts = parts, md5_checksum = md5_checksum, key_fingerprint = key_fingerprint };
public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => new InputSecureFileUploaded { id = id, parts = parts, md5_checksum = md5_checksum, file_hash = file_hash, secret = secret };
public override long ID { get => id; set => id = value; }
public override int Parts { get => parts; set => parts = value; }
public override string Name { get => name; set => name = value; }
}
partial class InputFileBig
{
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => new InputEncryptedFileBigUploaded { id = id, parts = parts, key_fingerprint = key_fingerprint };
public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => new InputSecureFileUploaded { id = id, parts = parts, file_hash = file_hash, secret = secret };
public override long ID { get => id; set => id = value; }
public override int Parts { get => parts; set => parts = value; }
public override string Name { get => name; set => name = value; }
}
partial class InputFileStoryDocument // apparently this is used only in InputMediaUploadedDocument.file
{
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => throw new NotSupportedException();
public override InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret) => throw new NotSupportedException();
public override long ID { get => 0; set => throw new NotSupportedException(); }
public override int Parts { get => 0; set => throw new NotSupportedException(); }
public override string Name { get => null; set => throw new NotSupportedException(); }
}
partial class InputMediaUploadedDocument
@ -105,7 +125,16 @@ namespace TL
{
file = inputFile;
mime_type = mimeType;
if (inputFile.Name is string filename) attributes = new[] { new DocumentAttributeFilename { file_name = filename } };
if (inputFile.Name is string filename) attributes = [new DocumentAttributeFilename { file_name = filename }];
}
public InputMediaUploadedDocument(InputFileBase inputFile, string mimeType, params DocumentAttribute[] attribs)
{
file = inputFile;
mime_type = mimeType;
if (inputFile.Name is string filename && !attribs.Any(a => a is DocumentAttributeFilename))
attributes = [.. attribs, new DocumentAttributeFilename { file_name = filename }];
else
attributes = attribs;
}
}
@ -169,12 +198,24 @@ namespace TL
/// <summary>An estimation of the number of days ago the user was last seen (Online=0, Recently=1, LastWeek=5, LastMonth=20, LongTimeAgo=150)</summary>
public TimeSpan LastSeenAgo => status?.LastSeenAgo ?? TimeSpan.FromDays(150);
public bool IsBot => (flags & Flags.bot) != 0;
public IEnumerable<string> ActiveUsernames
{
get
{
if (username != null)
yield return username;
if (usernames != null)
foreach (var un in usernames)
if (un.flags.HasFlag(Username.Flags.active))
yield return un.username;
}
}
}
/// <remarks>a <c>null</c> value means <a href="https://corefork.telegram.org/constructor/userStatusEmpty">userStatusEmpty</a> = last seen a long time ago, more than a month (or blocked/deleted users)</remarks>
partial class UserStatus { internal abstract TimeSpan LastSeenAgo { get; } }
partial class UserStatusOnline { internal override TimeSpan LastSeenAgo => TimeSpan.Zero; }
partial class UserStatusOffline { internal override TimeSpan LastSeenAgo => DateTime.UtcNow - new DateTime((was_online + 62135596800L) * 10000000, DateTimeKind.Utc); }
partial class UserStatusOffline { internal override TimeSpan LastSeenAgo => DateTime.UtcNow - was_online; }
/// <remarks>covers anything between 1 second and 2-3 days</remarks>
partial class UserStatusRecently { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(1); }
/// <remarks>between 2-3 and seven days</remarks>
@ -186,6 +227,9 @@ namespace TL
{
/// <summary>Is this chat among current user active chats?</summary>
public abstract bool IsActive { get; }
/// <summary>Is this chat a broadcast channel?</summary>
public virtual bool IsChannel => false;
public bool IsGroup => !IsChannel;
public virtual string MainUsername => null;
public abstract ChatPhoto Photo { get; }
/// <summary>returns true if you're banned of any of these rights</summary>
@ -220,18 +264,30 @@ namespace TL
partial class Channel
{
public override bool IsActive => (flags & Flags.left) == 0;
public override string MainUsername => username ?? usernames?.FirstOrDefault(u => u.flags.HasFlag(Username.Flags.active))?.username;
public override bool IsChannel => (flags & Flags.broadcast) != 0;
public override string MainUsername => username ?? usernames?.FirstOrDefault(un => un.flags.HasFlag(Username.Flags.active))?.username;
public override ChatPhoto Photo => photo;
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => ((banned_rights?.flags ?? 0) & flags) != 0 || ((default_banned_rights?.flags ?? 0) & flags) != 0;
public override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash);
public static implicit operator InputChannel(Channel channel) => new(channel.id, channel.access_hash);
public override string ToString() => (flags.HasFlag(Flags.broadcast) ? "Channel " : "Group ") + (MainUsername is string uname ? '@' + uname : $"\"{title}\"");
public bool IsChannel => (flags & Flags.broadcast) != 0;
public bool IsGroup => (flags & Flags.broadcast) == 0;
public IEnumerable<string> ActiveUsernames
{
get
{
if (username != null)
yield return username;
if (usernames != null)
foreach (var un in usernames)
if (un.flags.HasFlag(Username.Flags.active))
yield return un.username;
}
}
}
partial class ChannelForbidden
{
public override bool IsActive => false;
public override bool IsChannel => (flags & Flags.broadcast) != 0;
public override ChatPhoto Photo => null;
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => true;
public override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash);
@ -248,9 +304,10 @@ namespace TL
partial class ChatParticipantAdmin { public override bool IsAdmin => true; }
partial class ChatParticipantsBase { public abstract ChatParticipantBase[] Participants { get; }}
partial class ChatParticipantsForbidden { public override ChatParticipantBase[] Participants => Array.Empty<ChatParticipantBase>(); }
partial class ChatParticipantsForbidden { public override ChatParticipantBase[] Participants => []; }
partial class ChatParticipants { public override ChatParticipantBase[] Participants => participants; }
partial class MessageBase { public MessageReplyHeader ReplyHeader => ReplyTo as MessageReplyHeader; }
partial class MessageEmpty { public override string ToString() => "(no message)"; }
partial class Message { public override string ToString() => $"{(from_id ?? peer_id)?.ID}> {message} {media}"; }
partial class MessageService { public override string ToString() => $"{(from_id ?? peer_id)?.ID} [{action.GetType().Name[13..]}]"; }
@ -270,6 +327,7 @@ namespace TL
correct_answers = results.results?.Where(pav => pav.flags.HasFlag(PollAnswerVoters.Flags.correct)).Select(pav => pav.option).ToArray(),
flags = (results.results != null ? InputMediaPoll.Flags.has_correct_answers : 0) | (results.solution != null ? InputMediaPoll.Flags.has_solution : 0) }; }
partial class MessageMediaDice { public override InputMedia ToInputMedia() => new InputMediaDice { emoticon = emoticon }; }
partial class MessageMediaWebPage { public override InputMedia ToInputMedia() => new InputMediaWebPage { flags = (InputMediaWebPage.Flags)((int)flags & 3), url = webpage.Url }; }
partial class PhotoBase
{
@ -289,7 +347,9 @@ namespace TL
protected override InputPhoto ToInputPhoto() => new() { id = id, access_hash = access_hash, file_reference = file_reference };
public InputPhotoFileLocation ToFileLocation() => ToFileLocation(LargestPhotoSize);
public InputPhotoFileLocation ToFileLocation(PhotoSizeBase photoSize) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = photoSize.Type };
public InputPhotoFileLocation ToFileLocation(VideoSize videoSize) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = videoSize.type };
public PhotoSizeBase LargestPhotoSize => sizes.Aggregate((agg, next) => (long)next.Width * next.Height > (long)agg.Width * agg.Height ? next : agg);
public VideoSize LargestVideoSize => video_sizes?.OfType<VideoSize>().DefaultIfEmpty().Aggregate((agg, next) => (long)next.w * next.h > (long)agg.w * agg.h ? next : agg);
}
partial class PhotoSizeBase
@ -355,6 +415,13 @@ namespace TL
public static implicit operator InputGeoPoint(GeoPoint geo) => new() { lat = geo.lat, lon = geo.lon, accuracy_radius = geo.accuracy_radius, flags = (InputGeoPoint.Flags)geo.flags };
}
partial class InputNotifyPeerBase
{
public static implicit operator InputNotifyPeerBase(InputPeer peer) => new InputNotifyPeer { peer = peer };
public static implicit operator InputNotifyPeerBase(ChatBase chat) => new InputNotifyPeer { peer = chat };
public static implicit operator InputNotifyPeerBase(UserBase user) => new InputNotifyPeer { peer = user };
}
partial class WallPaperBase { public static implicit operator InputWallPaperBase(WallPaperBase wp) => wp.ToInputWallPaper();
protected abstract InputWallPaperBase ToInputWallPaper(); }
partial class WallPaper { protected override InputWallPaperBase ToInputWallPaper() => new InputWallPaper { id = id, access_hash = access_hash }; }
@ -384,25 +451,28 @@ namespace TL
public abstract Update[] UpdateList { get; }
public virtual Dictionary<long, User> Users => NoUsers;
public virtual Dictionary<long, ChatBase> Chats => NoChats;
private static readonly Dictionary<long, User> NoUsers = new();
private static readonly Dictionary<long, ChatBase> NoChats = new();
private static readonly Dictionary<long, User> NoUsers = [];
private static readonly Dictionary<long, ChatBase> NoChats = [];
public virtual (long mbox_id, int pts, int pts_count) GetMBox() => default;
}
partial class UpdatesCombined
{
public override Update[] UpdateList => updates;
public override Dictionary<long, User> Users => users;
public override Dictionary<long, ChatBase> Chats => chats;
public override (long mbox_id, int pts, int pts_count) GetMBox() => (-2, seq, seq - seq_start + 1);
}
partial class Updates
{
public override Update[] UpdateList => updates;
public override Dictionary<long, User> Users => users;
public override Dictionary<long, ChatBase> Chats => chats;
public override (long mbox_id, int pts, int pts_count) GetMBox() => (-2, seq, 1);
}
partial class UpdatesTooLong { public override Update[] UpdateList => Array.Empty<Update>(); }
partial class UpdateShort { public override Update[] UpdateList => new[] { update }; }
partial class UpdateShortSentMessage { public override Update[] UpdateList => Array.Empty<Update>(); }
partial class UpdateShortMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage
partial class UpdatesTooLong { public override Update[] UpdateList => []; }
partial class UpdateShort { public override Update[] UpdateList => [update]; }
partial class UpdateShortSentMessage { public override Update[] UpdateList => []; }
partial class UpdateShortMessage { public override Update[] UpdateList => [ new UpdateNewMessage
{
message = new Message
{
@ -412,8 +482,8 @@ namespace TL
peer_id = new PeerUser { user_id = user_id },
fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period
}, pts = pts, pts_count = pts_count
} }; }
partial class UpdateShortChatMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage
} ]; }
partial class UpdateShortChatMessage { public override Update[] UpdateList => [ new UpdateNewMessage
{
message = new Message
{
@ -423,7 +493,7 @@ namespace TL
peer_id = new PeerChat { chat_id = chat_id },
fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period
}, pts = pts, pts_count = pts_count
} }; }
} ]; }
partial class InputEncryptedChat { public static implicit operator int(InputEncryptedChat chat) => chat.chat_id;
public static implicit operator InputEncryptedChat(EncryptedChatBase chat) => new() { chat_id = chat.ID, access_hash = chat.AccessHash }; }
@ -455,10 +525,11 @@ namespace TL
partial class Document
{
public override long ID => id;
public override string ToString() => Filename is string filename ? base.ToString() + ": " + filename : base.ToString();
public override string ToString() => $"{Filename ?? $"Document {mime_type}"} {size:N0} bytes";
public string Filename => GetAttribute<DocumentAttributeFilename>()?.file_name;
protected override InputDocument ToInputDocument() => new() { id = id, access_hash = access_hash, file_reference = file_reference };
public InputDocumentFileLocation ToFileLocation(PhotoSizeBase thumbSize = null) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = thumbSize?.Type };
public InputDocumentFileLocation ToFileLocation(VideoSize videoSize) => new() { id = id, access_hash = access_hash, file_reference = file_reference, thumb_size = videoSize.type };
public PhotoSizeBase LargestThumbSize => thumbs?.Aggregate((agg, next) => (long)next.Width * next.Height > (long)agg.Width * agg.Height ? next : agg);
public T GetAttribute<T>() where T : DocumentAttribute => attributes.OfType<T>().FirstOrDefault();
}
@ -498,7 +569,10 @@ namespace TL
partial class MessageEntity
{
public string Type { get { var name = GetType().Name; return name[(name.IndexOf("MessageEntity") + 13)..]; } }
public string Type {
get { var name = GetType().Name; return name[(name.IndexOf("MessageEntity") + 13)..]; }
set { if (value != Type) throw new NotSupportedException("Can't change Type. You need to create a new instance of the right TL.MessageEntity* subclass"); }
}
public int Offset { get => offset; set => offset = value; }
public int Length { get => length; set => length = value; }
}
@ -517,8 +591,10 @@ namespace TL
public static implicit operator InputPeer(Contacts_ResolvedPeer resolved) => resolved?.UserOrChat.ToInputPeer();
/// <returns>A <see cref="TL.User"/>, or <see langword="null"/> if the username was for a channel</returns>
public User User => peer is PeerUser pu ? users[pu.user_id] : null;
/// <returns>A <see cref="Channel"/> or <see cref="ChannelForbidden"/>, or <see langword="null"/> if the username was for a user</returns>
/// <returns>A <see cref="TL.Channel"/> or <see cref="TL.ChannelForbidden"/>, or <see langword="null"/> if the username was for a user</returns>
public ChatBase Chat => peer is PeerChannel or PeerChat ? chats[peer.ID] : null;
/// <returns>A <see cref="TL.Channel"/>, or <see langword="null"/> if the username was for a user or for a forbidden channel</returns>
public Channel Channel => peer is PeerChannel pc ? chats[pc.channel_id] as Channel : null;
}
partial class Updates_ChannelDifferenceBase
@ -527,13 +603,15 @@ namespace TL
public abstract Update[] OtherUpdates { get; }
public abstract bool Final { get; }
public abstract int Timeout { get; }
public abstract int Pts { get; }
}
partial class Updates_ChannelDifferenceEmpty
{
public override MessageBase[] NewMessages => Array.Empty<MessageBase>();
public override Update[] OtherUpdates => Array.Empty<Update>();
public override MessageBase[] NewMessages => [];
public override Update[] OtherUpdates => [];
public override bool Final => flags.HasFlag(Flags.final);
public override int Timeout => timeout;
public override int Pts => pts;
}
partial class Updates_ChannelDifference
{
@ -541,6 +619,7 @@ namespace TL
public override Update[] OtherUpdates => other_updates;
public override bool Final => flags.HasFlag(Flags.final);
public override int Timeout => timeout;
public override int Pts => pts;
}
partial class Updates_ChannelDifferenceTooLong
{
@ -548,6 +627,7 @@ namespace TL
public override Update[] OtherUpdates => null;
public override bool Final => flags.HasFlag(Flags.final);
public override int Timeout => timeout;
public override int Pts => dialog is Dialog d ? d.pts : 0;
}
partial class ChannelParticipantBase
@ -581,7 +661,7 @@ namespace TL
partial class ChannelAdminLogEventsFilter
{
public static implicit operator ChannelAdminLogEventsFilter(Flags flags) => new() { flags = flags };
public static implicit operator ChannelAdminLogEventsFilter(Flags flags) => flags == 0 ? null : new() { flags = flags };
}
partial class InputMessage
@ -592,6 +672,8 @@ namespace TL
partial class InputDialogPeerBase
{
public static implicit operator InputDialogPeerBase(InputPeer peer) => new InputDialogPeer { peer = peer };
public static implicit operator InputDialogPeerBase(ChatBase chat) => new InputDialogPeer { peer = chat };
public static implicit operator InputDialogPeerBase(UserBase user) => new InputDialogPeer { peer = user };
}
partial class SecureFile
@ -601,7 +683,19 @@ namespace TL
}
partial class JsonObjectValue { public override string ToString() => $"{HttpUtility.JavaScriptStringEncode(key, true)}:{value}"; }
partial class JSONValue { public abstract object ToNative(); }
partial class JSONValue { public abstract object ToNative();
private static JsonObjectValue FromJsonProperty(System.Text.Json.JsonProperty p) => new() { key = p.Name, value = FromJsonElement(p.Value) };
public static JSONValue FromJsonElement(System.Text.Json.JsonElement elem) => elem.ValueKind switch
{
System.Text.Json.JsonValueKind.True or
System.Text.Json.JsonValueKind.False => new JsonBool { value = elem.GetBoolean() },
System.Text.Json.JsonValueKind.Object => new JsonObject { value = [.. elem.EnumerateObject().Select(FromJsonProperty)] },
System.Text.Json.JsonValueKind.Array => new JsonArray { value = [.. elem.EnumerateArray().Select(FromJsonElement)] },
System.Text.Json.JsonValueKind.String => new JsonString { value = elem.GetString() },
System.Text.Json.JsonValueKind.Number => new JsonNumber { value = elem.GetDouble() },
_ => new JsonNull(),
};
}
partial class JsonNull { public override object ToNative() => null; public override string ToString() => "null"; }
partial class JsonBool { public override object ToNative() => value; public override string ToString() => value ? "true" : "false"; }
partial class JsonNumber { public override object ToNative() => value; public override string ToString() => value.ToString(CultureInfo.InvariantCulture); }
@ -615,7 +709,7 @@ namespace TL
sb.Append(i == 0 ? "" : ",").Append(value[i]);
return sb.Append(']').ToString();
}
public object[] ToNativeArray() => value.Select(v => v.ToNative()).ToArray();
public object[] ToNativeArray() => [.. value.Select(v => v.ToNative())];
public override object ToNative()
{
if (value.Length == 0) return Array.Empty<object>();
@ -663,6 +757,20 @@ namespace TL
}
}
partial class Theme { public static implicit operator InputTheme(Theme theme) => new() { id = theme.id, access_hash = theme.access_hash }; }
partial class GroupCallBase { public static implicit operator InputGroupCall(GroupCallBase call) => new() { id = call.ID, access_hash = call.AccessHash }; }
partial class Theme { public static implicit operator InputTheme(Theme theme) => new() { id = theme.id, access_hash = theme.access_hash }; }
partial class MessageReplyHeader { public int TopicID => flags.HasFlag(Flags.forum_topic) ? flags.HasFlag(Flags.has_reply_to_top_id) ? reply_to_top_id : reply_to_msg_id : 0; }
partial class GroupCallBase { public static implicit operator InputGroupCall(GroupCallBase call) => new() { id = call.ID, access_hash = call.AccessHash }; }
partial class EmojiStatusBase { public virtual long DocumentId => 0; }
partial class EmojiStatus { public override long DocumentId => document_id; }
partial class EmojiStatusCollectible{ public override long DocumentId => document_id; }
partial class ForumTopicBase { public virtual string Title => null; }
partial class ForumTopic { public override string Title => title; }
partial class RequestedPeer { public abstract long ID { get; } }
partial class RequestedPeerUser { public override long ID => user_id; }
partial class RequestedPeerChat { public override long ID => chat_id; }
partial class RequestedPeerChannel { public override long ID => channel_id; }
}

233
src/TL.cs
View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
@ -7,46 +8,62 @@ using System.Reflection;
using System.Security.Cryptography;
using System.Text;
#pragma warning disable IDE1006 // Naming Styles
namespace TL
{
#if MTPG
public interface IObject { void WriteTL(BinaryWriter writer); }
#else
public interface IObject { }
public interface IMethod<ReturnType> : IObject { }
#endif
public interface IMethod<out ReturnType> : IObject { }
public interface IPeerResolver { IPeerInfo UserOrChat(Peer peer); }
[AttributeUsage(AttributeTargets.Class)]
public class TLDefAttribute : Attribute
public sealed class TLDefAttribute(uint ctorNb) : Attribute
{
public readonly uint CtorNb;
public TLDefAttribute(uint ctorNb) => CtorNb = ctorNb;
public readonly uint CtorNb = ctorNb;
public bool inheritBefore;
}
[AttributeUsage(AttributeTargets.Field)]
public class IfFlagAttribute : Attribute
public sealed class IfFlagAttribute(int bit) : Attribute
{
public readonly int Bit;
public IfFlagAttribute(int bit) => Bit = bit;
public readonly int Bit = bit;
}
public class RpcException : WTelegram.WTException
public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message)
{
public readonly int Code;
public readonly int Code = code;
/// <summary>The value of X in the message, -1 if no variable X was found</summary>
public readonly int X;
public RpcException(int code, string message, int x = -1) : base(message) { Code = code; X = x; }
public readonly int X = x;
public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); }
}
public class ReactorError : IObject
public sealed partial class ReactorError : IObject
{
public Exception Exception;
public void WriteTL(BinaryWriter writer) => throw new NotSupportedException();
}
public static class Serialization
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static byte[] ToBytes<T>(this T obj) where T : IObject
{
using var ms = new MemoryStream(384);
using var writer = new BinaryWriter(ms);
writer.WriteTLObject(obj);
return ms.ToArray();
}
public static void WriteTLObject<T>(this BinaryWriter writer, T obj) where T : IObject
{
if (obj == null) { writer.WriteTLNull(typeof(T)); return; }
#if MTPG
obj.WriteTL(writer);
#else
var type = obj.GetType();
var tlDef = type.GetCustomAttribute<TLDefAttribute>();
var ctorNb = tlDef.CtorNb;
@ -64,14 +81,18 @@ namespace TL
if (field.Name == "flags") flags = (uint)value;
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
}
#endif
}
public static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0)
{
if (ctorNb == 0) ctorNb = reader.ReadUInt32();
if (ctorNb == Layer.GZipedCtor)
using (var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress)))
return ReadTLObject(gzipReader);
#if MTPG
if (!Layer.Table.TryGetValue(ctorNb, out var ctor))
throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}");
return ctor?.Invoke(reader);
#else
if (ctorNb == Layer.GZipedCtor) return (IObject)reader.ReadTLGzipped(typeof(IObject));
if (!Layer.Table.TryGetValue(ctorNb, out var type))
throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}");
if (type == null) return null; // nullable ctor (class meaning is associated with null)
@ -91,6 +112,18 @@ namespace TL
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
}
return (IObject)obj;
#endif
}
public static IMethod<X> ReadTLMethod<X>(this BinaryReader reader)
{
uint ctorNb = reader.ReadUInt32();
if (!Layer.Methods.TryGetValue(ctorNb, out var ctor))
throw new WTelegram.WTException($"Cannot find method for ctor #{ctorNb:x}");
var method = ctor?.Invoke(reader);
if (method is IMethod<bool> && typeof(X) == typeof(object))
method = new BoolMethod { query = method };
return (IMethod<X>)method;
}
internal static void WriteTLValue(this BinaryWriter writer, object value, Type valueType)
@ -115,16 +148,16 @@ namespace TL
if (type.IsArray)
if (value is byte[] bytes)
writer.WriteTLBytes(bytes);
else if (value is _Message[] messages)
writer.WriteTLMessages(messages);
else
writer.WriteTLVector((Array)value);
else if (value is IObject tlObject)
WriteTLObject(writer, tlObject);
else if (value is List<_Message> messages)
writer.WriteTLMessages(messages);
else if (value is Int128 int128)
writer.Write(int128);
else if (value is Int256 int256)
writer.Write(int256);
else if (value is IObject tlObject)
WriteTLObject(writer, tlObject);
else if (type.IsEnum) // needed for Mono (enums in generic types are seen as TypeCode.Object)
writer.Write((uint)value);
else
@ -179,6 +212,26 @@ namespace TL
}
}
internal static void WriteTLMessages(this BinaryWriter writer, List<_Message> messages)
{
writer.Write(messages.Count);
foreach (var msg in messages)
{
writer.Write(msg.msg_id);
writer.Write(msg.seqno);
var patchPos = writer.BaseStream.Position;
writer.Write(0); // patched below
if ((msg.seqno & 1) != 0)
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}");
else
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}");
writer.WriteTLObject(msg.body);
writer.BaseStream.Position = patchPos;
writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field
writer.Seek(0, SeekOrigin.End);
}
}
internal static void WriteTLVector(this BinaryWriter writer, Array array)
{
writer.Write(Layer.VectorCtor);
@ -190,24 +243,40 @@ namespace TL
writer.WriteTLValue(array.GetValue(i), elementType);
}
internal static void WriteTLMessages(this BinaryWriter writer, _Message[] messages)
internal static void WriteTLRawVector(this BinaryWriter writer, Array array, int elementSize)
{
writer.Write(messages.Length);
foreach (var msg in messages)
var startPos = writer.BaseStream.Position;
int count = array.Length;
var elementType = array.GetType().GetElementType();
for (int i = count - 1; i >= 0; i--)
{
writer.Write(msg.msg_id);
writer.Write(msg.seqno);
var patchPos = writer.BaseStream.Position;
writer.Write(0); // patched below
writer.WriteTLObject(msg.body);
if ((msg.seqno & 1) != 0)
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}");
else
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}");
writer.BaseStream.Position = patchPos;
writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field
writer.Seek(0, SeekOrigin.End);
writer.BaseStream.Position = startPos + i * elementSize;
writer.WriteTLValue(array.GetValue(i), elementType);
}
writer.BaseStream.Position = startPos;
writer.Write(count);
writer.BaseStream.Position = startPos + count * elementSize + 4;
}
internal static List<T> ReadTLRawVector<T>(this BinaryReader reader, uint ctorNb)
{
int count = reader.ReadInt32();
var list = new List<T>(count);
for (int i = 0; i < count; i++)
list.Add((T)reader.ReadTLObject(ctorNb));
return list;
}
internal static T[] ReadTLVector<T>(this BinaryReader reader)
{
var elementType = typeof(T);
if (reader.ReadUInt32() is not Layer.VectorCtor and uint ctorNb)
throw new WTelegram.WTException($"Cannot deserialize {elementType.Name}[] with ctor #{ctorNb:x}");
int count = reader.ReadInt32();
var array = new T[count];
for (int i = 0; i < count; i++)
array[i] = (T)reader.ReadTLValue(elementType);
return array;
}
internal static Array ReadTLVector(this BinaryReader reader, Type type)
@ -241,27 +310,29 @@ namespace TL
internal static Dictionary<long, T> ReadTLDictionary<T>(this BinaryReader reader) where T : class, IPeerInfo
{
uint ctorNb = reader.ReadUInt32();
var elementType = typeof(T);
if (ctorNb != Layer.VectorCtor)
throw new WTelegram.WTException($"Cannot deserialize Vector<{elementType.Name}> with ctor #{ctorNb:x}");
throw new WTelegram.WTException($"Cannot deserialize Vector<{typeof(T).Name}> with ctor #{ctorNb:x}");
int count = reader.ReadInt32();
var dict = new Dictionary<long, T>(count);
for (int i = 0; i < count; i++)
{
var value = (T)reader.ReadTLValue(elementType);
dict[value.ID] = value is UserEmpty ? null : value;
var obj = reader.ReadTLObject();
if (obj is T value) dict[value.ID] = value;
else if (obj is UserEmpty ue) dict[ue.id] = null;
else throw new InvalidCastException($"ReadTLDictionary got '{obj?.GetType().Name}' instead of '{typeof(T).Name}'");
}
return dict;
}
internal static void WriteTLStamp(this BinaryWriter writer, DateTime datetime)
=> writer.Write(datetime == DateTime.MaxValue ? int.MaxValue : (int)(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L));
=> writer.Write((int)Math.Min(Math.Max(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L, 0), int.MaxValue));
internal static DateTime ReadTLStamp(this BinaryReader reader)
internal static DateTime ReadTLStamp(this BinaryReader reader) => reader.ReadInt32() switch
{
int unixstamp = reader.ReadInt32();
return unixstamp == int.MaxValue ? DateTime.MaxValue : new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc);
}
<= 0 => default,
int.MaxValue => DateTime.MaxValue,
int unixstamp => new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc)
};
internal static void WriteTLString(this BinaryWriter writer, string str)
{
@ -318,6 +389,19 @@ namespace TL
writer.Write(0); // null arrays/strings are serialized as empty
}
internal static object ReadTLGzipped(this BinaryReader reader, Type type)
{
using var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress));
return gzipReader.ReadTLValue(type);
}
internal static bool ReadTLBool(this BinaryReader reader) => reader.ReadUInt32() switch
{
0x997275b5 => true,
0xbc799737 => false,
var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}")
};
#if DEBUG
private static void ShouldntBeHere() => System.Diagnostics.Debugger.Break();
#else
@ -329,13 +413,13 @@ namespace TL
{
public byte[] raw;
public Int128(System.IO.BinaryReader reader) => raw = reader.ReadBytes(16);
public Int128(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[16]);
public Int128(BinaryReader reader) => raw = reader.ReadBytes(16);
public Int128(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[16]);
public static bool operator ==(Int128 left, Int128 right) { for (int i = 0; i < 16; i++) if (left.raw[i] != right.raw[i]) return false; return true; }
public static bool operator !=(Int128 left, Int128 right) { for (int i = 0; i < 16; i++) if (left.raw[i] != right.raw[i]) return true; return false; }
public override bool Equals(object obj) => obj is Int128 other && this == other;
public override int GetHashCode() => BitConverter.ToInt32(raw, 0);
public override string ToString() => Convert.ToHexString(raw);
public override readonly bool Equals(object obj) => obj is Int128 other && this == other;
public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0);
public override readonly string ToString() => Convert.ToHexString(raw);
public static implicit operator byte[](Int128 int128) => int128.raw;
}
@ -343,48 +427,69 @@ namespace TL
{
public byte[] raw;
public Int256(System.IO.BinaryReader reader) => raw = reader.ReadBytes(32);
public Int256(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[32]);
public Int256(BinaryReader reader) => raw = reader.ReadBytes(32);
public Int256(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[32]);
public static bool operator ==(Int256 left, Int256 right) { for (int i = 0; i < 32; i++) if (left.raw[i] != right.raw[i]) return false; return true; }
public static bool operator !=(Int256 left, Int256 right) { for (int i = 0; i < 32; i++) if (left.raw[i] != right.raw[i]) return true; return false; }
public override bool Equals(object obj) => obj is Int256 other && this == other;
public override int GetHashCode() => BitConverter.ToInt32(raw, 0);
public override string ToString() => Convert.ToHexString(raw);
public override readonly bool Equals(object obj) => obj is Int256 other && this == other;
public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0);
public override readonly string ToString() => Convert.ToHexString(raw);
public static implicit operator byte[](Int256 int256) => int256.raw;
}
public sealed partial class UpdateAffectedMessages : Update // auto-generated for OnOwnUpdates in case of such API call result
{
public long mbox_id;
public int pts;
public int pts_count;
public override (long, int, int) GetMBox() => (mbox_id, pts, pts_count);
#if MTPG
public override void WriteTL(BinaryWriter writer) => throw new NotSupportedException();
#endif
}
// Below TL types are commented "parsed manually" from https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/Resources/tl/mtproto.tl
[TLDef(0x7A19CB76)] //RSA_public_key#7a19cb76 n:bytes e:bytes = RSAPublicKey
public class RSAPublicKey : IObject
public sealed partial class RSAPublicKey : IObject
{
public byte[] n;
public byte[] e;
}
[TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult
public class RpcResult : IObject
public sealed partial class RpcResult : IObject
{
public long req_msg_id;
public object result;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")]
[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message
public class _Message
public sealed partial class _Message(long msgId, int seqno, IObject obj) : IObject
{
public _Message(long msgId, int seqNo, IObject obj) { msg_id = msgId; seqno = seqNo; body = obj; }
public long msg_id;
public int seqno;
public long msg_id = msgId;
public int seqno = seqno;
public int bytes;
public IObject body;
public IObject body = obj;
}
[TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer
public class MsgContainer : IObject { public _Message[] messages; }
public sealed partial class MsgContainer : IObject { public List<_Message> messages; }
[TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy
public class MsgCopy : IObject { public _Message orig_message; }
public sealed partial class MsgCopy : IObject { public _Message orig_message; }
[TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object
public class GzipPacked : IObject { public byte[] packed_data; }
public sealed partial class GzipPacked : IObject { public byte[] packed_data; }
public sealed class Null<X> : IObject
{
public readonly static Null<X> Instance = new();
public void WriteTL(BinaryWriter writer) => writer.WriteTLNull(typeof(X));
}
public sealed class BoolMethod : IMethod<object>
{
public IObject query;
public void WriteTL(BinaryWriter writer) => query.WriteTL(writer);
}
}

View file

@ -11,14 +11,13 @@ using System.Threading.Tasks;
namespace WTelegram
{
class TlsStream : Helpers.IndirectStream
internal sealed class TlsStream(Stream innerStream) : Helpers.IndirectStream(innerStream)
{
public TlsStream(Stream innerStream) : base(innerStream) { }
private int _tlsFrameleft;
private readonly byte[] _tlsSendHeader = new byte[] { 0x17, 0x03, 0x03, 0, 0 };
private readonly byte[] _tlsSendHeader = [0x17, 0x03, 0x03, 0, 0];
private readonly byte[] _tlsReadHeader = new byte[5];
static readonly byte[] TlsServerHello3 = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03 };
static readonly byte[] TlsClientPrefix = new byte[] { 0x14, 0x03, 0x03, 0x00, 0x01, 0x01 };
static readonly byte[] TlsServerHello3 = [0x14, 0x03, 0x03, 0x00, 0x01, 0x01, 0x17, 0x03, 0x03];
static readonly byte[] TlsClientPrefix = [0x14, 0x03, 0x03, 0x00, 0x01, 0x01];
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct)
{
@ -85,39 +84,43 @@ namespace WTelegram
throw new WTException("TLS Handshake failed");
}
static readonly byte[] TlsClientHello1 = new byte[11] {
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 };
static readonly byte[] TlsClientHello1 = [ // https://tls13.xargs.org/#client-hello/annotated
0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03 ];
// digest[32]
// 0x20
// random[32]
// 0x00, 0x20, grease(0) GREASE are two identical bytes ending with nibble 'A'
static readonly byte[] TlsClientHello2 = new byte[34] {
// 0x00, 0x20
// grease(0) GREASE are two identical bytes ending with nibble 'A'
static readonly byte[] TlsClientHello2 = [
0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30, 0xcc, 0xa9,
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0x01, 0x00, 0x01, 0x93 };
// grease(2), 0x00, 0x00, 0x00, 0x00
// len { len { 0x00 len { domain } } } len is 16-bit big-endian length of the following block of data
static readonly byte[] TlsClientHello3 = new byte[101] {
0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08,
0x4A, 0x4A, // = grease(4)
0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x23, 0x00, 0x00,
0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31,
0x2e, 0x31, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00,
0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06,
0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, 0x2b, 0x00, 0x29,
0x4A, 0x4A, // = grease(4)
0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20 };
// random[32] = public key
static readonly byte[] TlsClientHello4 = new byte[35] {
0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a,
0x6A, 0x6A, // = grease(6)
0x03, 0x04, 0x03, 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
0x3A, 0x3A, // = grease(3)
0x00, 0x01, 0x00, 0x00, 0x15 };
0xcc, 0xa8, 0xc0, 0x13, 0xc0, 0x14, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35,
0x01, 0x00, 0x01, 0x93 ];
// grease(2)
// 0x00, 0x00
static readonly byte[] TlsClientHello3 = [
// 0x00, 0x00, len { len { 0x00 len { domain } } } len is 16-bit big-endian length of the following block of data
0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x4A, 0x4A/*=grease(4)*/, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
0x00, 0x0b, 0x00, 0x02, 0x01, 0x00,
0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x05, 0x01, 0x08, 0x06, 0x06, 0x01,
0x00, 0x10, 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, 0x31,
0x00, 0x12, 0x00, 0x00,
0x00, 0x17, 0x00, 0x00,
0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x02,
0x00, 0x23, 0x00, 0x00,
0x00, 0x2b, 0x00, 0x07, 0x06, 0x6A, 0x6A/*=grease(6)*/, 0x03, 0x04, 0x03, 0x03,
0x00, 0x2d, 0x00, 0x02, 0x01, 0x01,
0x00, 0x33, 0x00, 0x2b, 0x00, 0x29, 0x4A, 0x4A/*=grease(4)*/, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, /* random[32] */
0x44, 0x69, 0x00, 0x05, 0x00, 0x03, 0x02, 0x68, 0x32,
0xff, 0x01, 0x00, 0x01, 0x00,
];
// grease(3)
static readonly byte[] TlsClientHello4 = [
0x00, 0x01, 0x00, 0x00, 0x15 ];
// len { padding } padding with NUL bytes to reach 517 bytes
static byte[] TlsClientHello(byte[] key, byte[] domain)
{
int dlen = domain.Length;
var greases = new byte[7];
Encryption.RNG.GetBytes(greases);
for (int i = 0; i < 7; i++) greases[i] = (byte)((greases[i] & 0xF0) + 0x0A);
@ -130,19 +133,54 @@ namespace WTelegram
buffer[78] = buffer[79] = greases[0];
TlsClientHello2.CopyTo(buffer, 80);
buffer[114] = buffer[115] = greases[2];
buffer[121] = (byte)(dlen + 5);
buffer[123] = (byte)(dlen + 3);
buffer[126] = (byte)dlen;
domain.CopyTo(buffer, 127);
TlsClientHello3.CopyTo(buffer, 127 + dlen);
buffer[142 + dlen] = buffer[143 + dlen] = greases[4];
buffer[219 + dlen] = buffer[220 + dlen] = greases[4];
Encryption.RNG.GetBytes(buffer, 228 + dlen, 32); // public key
buffer[228 + dlen + 31] &= 0x7F; // must be positive
TlsClientHello4.CopyTo(buffer, 260 + dlen);
buffer[271 + dlen] = buffer[272 + dlen] = greases[6];
buffer[288 + dlen] = buffer[289 + dlen] = greases[3];
buffer[296 + dlen] = (byte)(220 - dlen);
int dlen = domain.Length;
var server_name = new byte[dlen + 9];
server_name[3] = (byte)(dlen + 5);
server_name[5] = (byte)(dlen + 3);
server_name[8] = (byte)dlen;
domain.CopyTo(server_name, 9);
var key_share = new byte[47];
Array.Copy(TlsClientHello3, 105, key_share, 0, 15);
key_share[6] = key_share[7] = greases[4];
Encryption.RNG.GetBytes(key_share, 15, 32); // public key
key_share[46] &= 0x7F; // must be positive
var random = new Random();
var permutations = new ArraySegment<byte>[15];
for (var i = 0; i < permutations.Length; i++)
{
var j = random.Next(0, i + 1);
if (i != j) permutations[i] = permutations[j];
permutations[j] = i switch
{
0 => new(server_name),
1 => new(TlsClientHello3, 0, 9),
2 => PatchGrease(TlsClientHello3[9..23], 6, greases[4]),
3 => new(TlsClientHello3, 23, 6),
4 => new(TlsClientHello3, 29, 22),
5 => new(TlsClientHello3, 51, 18),
6 => new(TlsClientHello3, 69, 4),
7 => new(TlsClientHello3, 73, 4),
8 => new(TlsClientHello3, 77, 7),
9 => new(TlsClientHello3, 84, 4),
10 => PatchGrease(TlsClientHello3[88..99], 5, greases[6]),
11 => new(TlsClientHello3, 99, 6),
12 => new(key_share),
13 => new(TlsClientHello3, 120, 9),
_ => new(TlsClientHello3, 129, 5),
};
}
int offset = 118;
foreach (var perm in permutations)
{
Array.Copy(perm.Array, perm.Offset, buffer, offset, perm.Count);
offset += perm.Count;
}
buffer[offset++] = buffer[offset++] = greases[3];
TlsClientHello4.CopyTo(buffer, offset);
buffer[offset + 6] = (byte)(510 - offset);
// patch-in digest with timestamp
using var hmac = new HMACSHA256(key);
@ -152,6 +190,12 @@ namespace WTelegram
BinaryPrimitives.WriteInt32LittleEndian(digest.AsSpan(28), stamp);
digest.CopyTo(buffer, 11);
return buffer;
static ArraySegment<byte> PatchGrease(byte[] buffer, int offset, byte grease)
{
buffer[offset] = buffer[offset + 1] = grease;
return new(buffer);
}
}
}
}

610
src/UpdateManager.cs Normal file
View file

@ -0,0 +1,610 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TL;
namespace WTelegram
{
public class UpdateManager
{
/// <summary>Collected info about Users <i>(only if using the default collector)</i></summary>
public readonly Dictionary<long, User> Users;
/// <summary>Collected info about Chats <i>(only if using the default collector)</i></summary>
public readonly Dictionary<long, ChatBase> Chats;
/// <summary>Timout to detect lack of updates and force refetch them</summary>
public TimeSpan InactivityThreshold { get; set; } = TimeSpan.FromMinutes(15);
/// <summary>Logging callback (defaults to WTelegram.Helpers.Log ; can be null for performance)</summary>
public Action<int, string> Log { get; set; } = Helpers.Log;
/// <summary>Current set of update states (for saving and later resume)</summary>
public ImmutableDictionary<long, MBoxState> State
{
get
{
_sem.Wait();
try { return _local.ToImmutableDictionary(); }
finally { _sem.Release(); }
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")]
public sealed class MBoxState { public int pts { get; set; } public long access_hash { get; set; } }
private readonly Client _client;
private readonly Func<Update, Task> _onUpdate;
private readonly IPeerCollector _collector;
private readonly bool _reentrant;
private readonly TaskScheduler _scheduler;
private readonly SemaphoreSlim _sem = new(1);
private readonly List<(Update update, UpdatesBase updates, bool own, DateTime stamp)> _pending = [];
private readonly Dictionary<long, MBoxState> _local; // -2 for seq/date, -1 for qts, 0 for common pts, >0 for channel pts
private const int L_SEQ = -2, L_QTS = -1, L_PTS = 0;
private const long UndefinedSeqDate = 3155378975999999999L; // DateTime.MaxValue.Ticks
private static readonly TimeSpan HalfSec = new(TimeSpan.TicksPerSecond / 2);
private Task _recoveringGaps;
private DateTime _lastUpdateStamp = DateTime.UtcNow;
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
/// <param name="client">the WTelegram Client to manage</param>
/// <param name="onUpdate">Event to be called on sequential individual update</param>
/// <param name="state">(optional) Resume session by recovering all updates that occured since this state</param>
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
public UpdateManager(Client client, Func<Update, Task> onUpdate, IDictionary<long, MBoxState> state = null, IPeerCollector collector = null, bool reentrant = false)
{
_client = client;
_onUpdate = onUpdate;
_collector = collector ?? new Services.CollectorPeer(Users = [], Chats = []);
_scheduler = SynchronizationContext.Current == null ? TaskScheduler.Current : TaskScheduler.FromCurrentSynchronizationContext();
if (state == null || state.Count < 3)
_local = new() { [L_SEQ] = new() { access_hash = UndefinedSeqDate }, [L_QTS] = new(), [L_PTS] = new() };
else
_local = state as Dictionary<long, MBoxState> ?? new Dictionary<long, MBoxState>(state);
_reentrant = reentrant;
client.OnOther += OnOther;
client.OnUpdates += u => OnUpdates(u, false);
client.OnOwnUpdates += u => OnUpdates(u, true);
}
private async Task OnOther(IObject obj)
{
switch (obj)
{
case Pong when DateTime.UtcNow - _lastUpdateStamp > InactivityThreshold:
if (_local[L_PTS].pts != 0) await ResyncState();
break;
case User user when user.flags.HasFlag(User.Flags.self):
_collector.Collect([user]);
goto newSession;
case NewSessionCreated when _client.User != null:
newSession:
await Task.Delay(HalfSec); // let the opportunity to call DropPendingUpdates/StopResync before a big resync
if (_local[L_PTS].pts != 0) await ResyncState();
else await ResyncState(await _client.Updates_GetState());
break;
case Updates_State state:
await ResyncState(state);
break;
}
}
private async Task ResyncState(Updates_State state = null)
{
if (state != null) state.qts = 0; // for some reason Updates_GetState returns an invalid qts, so better consider we have no qts.
else state = new() { qts = int.MaxValue };
await _sem.WaitAsync();
try
{
var local = _local[L_PTS];
Log?.Invoke(2, $"Got Updates_State {local.pts}->{state.pts}, date={new DateTime(_local[L_SEQ].access_hash, DateTimeKind.Utc)}->{state.date}, seq={_local[L_SEQ].pts}->{state.seq}");
if (local.pts == 0 || local.pts >= state.pts && _local[L_SEQ].pts >= state.seq && _local[L_QTS].pts >= state.qts)
await HandleDifference(null, null, state, null);
else if (await GetDifference(L_PTS, state.pts, local))
await ApplyFilledGaps();
}
finally { _sem.Release(); }
}
private async Task OnUpdates(UpdatesBase updates, bool own)
{
RaiseCollect(updates.Users, updates.Chats);
await _sem.WaitAsync();
try
{
await HandleUpdates(updates, own);
}
finally { _sem.Release(); }
}
private async Task HandleUpdates(UpdatesBase updates, bool own)
{
var now = _lastUpdateStamp = DateTime.UtcNow;
var updateList = updates.UpdateList;
if (updates is UpdateShortSentMessage sent)
updateList = [new UpdateNewMessage { pts = sent.pts, pts_count = sent.pts_count, message = new Message {
flags = (Message.Flags)sent.flags,
id = sent.id, date = sent.date, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
} }];
else if (updates is UpdateShortMessage usm && !_collector.HasUser(usm.user_id))
RaiseCollect(await _client.Updates_GetDifference(usm.pts - usm.pts_count, usm.date, 0));
else if (updates is UpdateShortChatMessage uscm && (!_collector.HasUser(uscm.from_id) || !_collector.HasChat(uscm.chat_id)))
RaiseCollect(await _client.Updates_GetDifference(uscm.pts - uscm.pts_count, uscm.date, 0));
bool ptsChanged = false, gotUPts = false;
int seq = 0;
try
{
if (updates is UpdatesTooLong)
{
var local_pts = _local[L_PTS];
ptsChanged = await GetDifference(L_PTS, local_pts.pts, local_pts);
return;
}
foreach (var update in updateList)
{
if (update == null) continue;
var (mbox_id, pts, pts_count) = update.GetMBox();
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
MBoxState local = null;
if (pts != 0)
{
local = _local.GetOrCreate(mbox_id);
if (mbox_id > 0 && local.access_hash == 0)
if (updates.Chats.TryGetValue(mbox_id, out var chat) && chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min))
local.access_hash = channel.access_hash;
var diff = local.pts + pts_count - pts;
if (diff > 0 && pts_count != 0) // the update was already applied, and must be ignored.
{
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} ignored {ExtendedLog(update)}");
continue;
}
if (diff < 0) // there's an update gap that must be filled.
{
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} pending {ExtendedLog(update)}");
_pending.Add((update, updates, own, now + HalfSec));
_recoveringGaps ??= Task.Delay(HalfSec).ContinueWith(RecoverGaps, _scheduler);
continue;
}
// the update can be applied.
}
Log?.Invoke(1, $"({mbox_id,10}, {local?.pts,6}+{pts_count}->{pts,-6}) {update,-30} applied {ExtendedLog(update)}");
if (mbox_id == L_SEQ && update is UpdatePtsChanged) gotUPts = true;
if (pts_count > 0 && pts != 0)
{
ptsChanged = true;
if (mbox_id == L_SEQ)
seq = pts;
else if (pts_count != 0)
local.pts = pts;
}
if (!own) await RaiseUpdate(update);
}
}
finally
{
if (seq > 0) // update local_seq & date after the updates were applied
{
var local_seq = _local[L_SEQ];
local_seq.pts = seq;
local_seq.access_hash = updates.Date.Ticks;
}
if (gotUPts) ptsChanged = await GetDifference(L_PTS, _local[L_PTS].pts = 1, _local[L_PTS]);
if (ptsChanged) await ApplyFilledGaps();
}
}
private async Task<int> ApplyFilledGaps()
{
if (_pending.Count != 0) Log?.Invoke(2, $"Trying to apply {_pending.Count} pending updates after filled gaps");
int removed = 0;
for (int i = 0; i < _pending.Count; )
{
var (update, updates, own, _) = _pending[i];
var (mbox_id, pts, pts_count) = update.GetMBox();
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
var local = _local[mbox_id];
var diff = local.pts + pts_count - pts;
if (diff < 0)
++i; // there's still a gap, skip it
else
{
_pending.RemoveAt(i);
++removed;
if (diff > 0) // the update was already applied, remove & ignore
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} obsolete {ExtendedLog(update)}");
else
{
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} applied now {ExtendedLog(update)}");
// the update can be applied.
local.pts = pts;
if (mbox_id == L_SEQ) local.access_hash = updates.Date.Ticks;
if (!own) await RaiseUpdate(update);
i = 0; // rescan pending updates from start
}
}
}
return removed;
}
private async Task RecoverGaps(Task _) // https://corefork.telegram.org/api/updates#recovering-gaps
{
await _sem.WaitAsync();
try
{
_recoveringGaps = null;
if (_pending.Count == 0) return;
Log?.Invoke(2, $"Trying to recover gaps for {_pending.Count} pending updates");
var now = DateTime.UtcNow;
while (_pending.Count != 0)
{
var (update, updates, own, stamp) = _pending[0];
if (stamp > now)
{
_recoveringGaps = Task.Delay(stamp - now).ContinueWith(RecoverGaps, _scheduler);
return;
}
var (mbox_id, pts, pts_count) = update.GetMBox();
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
var local = _local[mbox_id];
bool getDiffSuccess = false;
if (local.pts == 0)
Log?.Invoke(2, $"({mbox_id,10}, new +{pts_count}->{pts,-6}) {update,-30} First appearance of MBox {ExtendedLog(update)}");
else if (local.access_hash == -1) // no valid access_hash for this channel, so just raise this update
Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} No access_hash to recover {ExtendedLog(update)}");
else if (local.pts + pts_count - pts >= 0)
getDiffSuccess = true;
else
{
Log?.Invoke(1, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} Calling GetDifference {ExtendedLog(update)}");
getDiffSuccess = await GetDifference(mbox_id, pts, local);
}
if (!getDiffSuccess) // no getDiff => just raise received pending updates in order
{
local.pts = pts - pts_count;
for (int i = 1; i < _pending.Count; i++) // find lowest pending pts-pts_count for this mbox
{
var pending = _pending[i];
var mbox = pending.update.GetMBox();
if (mbox.pts == 0) mbox = pending.updates.GetMBox();
if (mbox.mbox_id == mbox_id) local.pts = Math.Min(local.pts, mbox.pts - mbox.pts_count);
}
}
if (await ApplyFilledGaps() == 0)
{
Log?.Invoke(3, $"({mbox_id,10}, {local.pts,6}+{pts_count}->{pts,-6}) {update,-30} forcibly removed!");
_pending.RemoveAt(0);
local.pts = pts;
if (!own) await RaiseUpdate(update);
}
}
}
finally { _sem.Release(); }
}
public async Task StopResync()
{
await _sem.WaitAsync();
try
{
foreach (var local in _local.Values)
local.pts = 0;
_pending.Clear();
}
finally { _sem.Release(); }
}
private async Task<InputChannel> GetInputChannel(long channel_id, MBoxState local)
{
if (channel_id <= 0) return null;
if (local?.access_hash is not null and not 0)
return new InputChannel(channel_id, local.access_hash);
var inputChannel = new InputChannel(channel_id, 0);
try
{
var mc = await _client.Channels_GetChannels(inputChannel);
if (mc.chats.TryGetValue(channel_id, out var chat) && chat is Channel channel)
inputChannel.access_hash = channel.access_hash;
}
catch (Exception)
{
inputChannel.access_hash = -1; // no valid access_hash available
}
local ??= _local[channel_id] = new();
local.access_hash = inputChannel.access_hash;
return inputChannel;
}
private async Task<bool> GetDifference(long mbox_id, int expected_pts, MBoxState local)
{
try
{
moreDiffNeeded:
if (mbox_id <= 0)
{
Log?.Invoke(0, $"Local states {string.Join(" ", _local.Select(l => $"{l.Key}:{l.Value.pts}"))}");
var local_seq = _local[L_SEQ];
var diff = await _client.Updates_GetDifference(_local[L_PTS].pts, qts: _local[L_QTS].pts,
date: new DateTime(local_seq.access_hash, DateTimeKind.Utc));
Log?.Invoke(1, $"{diff.GetType().Name[8..]}: {diff.NewMessages.Length} msg, {diff.OtherUpdates.Length} upd, pts={diff.State?.pts}, date={diff.State?.date}, seq={diff.State?.seq}, msgIDs={string.Join(" ", diff.NewMessages.Select(m => m.ID))}");
switch (diff)
{
case Updates_Difference ud:
await HandleDifference(ud.new_messages, ud.new_encrypted_messages, ud.state,
new UpdatesCombined { updates = ud.other_updates, users = ud.users, chats = ud.chats,
date = ud.state.date, seq_start = local_seq.pts + 1, seq = ud.state.seq });
break;
case Updates_DifferenceSlice uds:
await HandleDifference(uds.new_messages, uds.new_encrypted_messages, uds.intermediate_state,
new UpdatesCombined { updates = uds.other_updates, users = uds.users, chats = uds.chats,
date = uds.intermediate_state.date, seq_start = local_seq.pts + 1, seq = uds.intermediate_state.seq });
goto moreDiffNeeded;
case Updates_DifferenceTooLong udtl:
_local[L_PTS].pts = udtl.pts;
goto moreDiffNeeded;
case Updates_DifferenceEmpty ude:
local_seq.pts = ude.seq;
local_seq.access_hash = ude.date.Ticks;
_lastUpdateStamp = DateTime.UtcNow;
break;
}
}
else
{
var channel = await GetInputChannel(mbox_id, local);
if (channel.access_hash == -1) return false;
try
{
var diff = await _client.Updates_GetChannelDifference(channel, null, local.pts);
Log?.Invoke(1, $"{diff.GetType().Name[8..]}({mbox_id}): {diff.NewMessages.Length} msg, {diff.OtherUpdates.Length} upd, pts={diff.Pts}, msgIDs={string.Join(" ", diff.NewMessages.Select(m => m.ID))}");
switch (diff)
{
case Updates_ChannelDifference ucd:
local.pts = ucd.pts;
await HandleDifference(ucd.new_messages, null, null,
new UpdatesCombined { updates = ucd.other_updates, users = ucd.users, chats = ucd.chats });
if (!ucd.flags.HasFlag(Updates_ChannelDifference.Flags.final)) goto moreDiffNeeded;
break;
case Updates_ChannelDifferenceTooLong ucdtl:
if (ucdtl.dialog is Dialog dialog) local.pts = dialog.pts;
await HandleDifference(ucdtl.messages, null, null,
new UpdatesCombined { updates = null, users = ucdtl.users, chats = ucdtl.chats });
break;
case Updates_ChannelDifferenceEmpty ucde:
local.pts = ucde.pts;
break;
}
}
catch (RpcException ex) when (ex.Message is "CHANNEL_PRIVATE" or "CHANNEL_INVALID")
{
local.access_hash = -1; // access_hash is no longer valid
throw;
}
}
return true;
}
catch (Exception ex)
{
Log?.Invoke(4, $"GetDifference({mbox_id}, {local.pts}->{expected_pts}) raised {ex}");
if (ex.Message == "PERSISTENT_TIMESTAMP_INVALID") // oh boy, we're lost!
if (mbox_id <= 0)
await HandleDifference(null, null, await _client.Updates_GetState(), null);
else if ((await _client.Channels_GetFullChannel(await GetInputChannel(mbox_id, local))).full_chat is ChannelFull full)
local.pts = full.pts;
}
finally
{
if (local.pts < expected_pts) local.pts = expected_pts;
}
return false;
}
private async Task HandleDifference(MessageBase[] new_messages, EncryptedMessageBase[] enc_messages, Updates_State state, UpdatesCombined updates)
{
if (updates != null)
RaiseCollect(updates.users, updates.chats);
try
{
int updatesCount = updates?.updates.Length ?? 0;
if (updatesCount != 0)
for (int i = 0; i < updates.updates.Length; i++)
{
var update = updates.updates[i];
if (update is UpdateMessageID or UpdateStoryID)
{
await RaiseUpdate(update);
updates.updates[i] = null;
--updatesCount;
}
}
if (new_messages?.Length > 0)
{
var update = state == null ? new UpdateNewChannelMessage() : new UpdateNewMessage() { pts = state.pts, pts_count = 1 };
foreach (var msg in new_messages)
{
if (_pending.Any(p => p is { own: true, update: UpdateNewMessage { message: { Peer.ID: var peer_id, ID: var msg_id } } }
&& peer_id == msg.Peer.ID && msg_id == msg.ID))
continue;
update.message = msg;
await RaiseUpdate(update);
}
}
if (enc_messages?.Length > 0)
{
var update = new UpdateNewEncryptedMessage();
if (state != null) update.qts = state.qts;
foreach (var msg in enc_messages)
{
if (_pending.Any(p => p is { own: true, update: UpdateNewEncryptedMessage { message: { ChatId: var chat_id, RandomId: var random_id } } }
&& chat_id == msg.ChatId && random_id == msg.RandomId))
continue;
update.message = msg;
await RaiseUpdate(update);
}
}
if (updatesCount != 0)
{
// try to remove matching pending OwnUpdates from this updates list (starting from most-recent)
for (int p = _pending.Count - 1, u = updates.updates.Length; p >= 0 && u > 0; p--)
{
if (_pending[p].own == false) continue;
var updateP = _pending[p].update;
var (mbox_idP, ptsP, pts_countP) = updateP.GetMBox();
if (ptsP == 0) (mbox_idP, ptsP, pts_countP) = _pending[p].updates.GetMBox();
Type updatePtype = null;
while (--u >= 0)
{
var update = updates.updates[u];
if (update == null) continue;
var (mbox_id, pts, pts_count) = update.GetMBox();
if (pts == 0) (mbox_id, pts, pts_count) = updates.GetMBox();
if (mbox_idP == mbox_id && ptsP <= pts)
{
updatePtype ??= updateP.GetType();
if (updatePtype == (update is UpdateDeleteMessages ? typeof(UpdateAffectedMessages) : update.GetType()))
{
updates.updates[u] = null;
--updatesCount;
break;
}
}
}
}
if (updatesCount != 0)
await HandleUpdates(updates, false);
}
}
finally
{
if (state != null)
{
_local[L_PTS].pts = state.pts;
_local[L_QTS].pts = state.qts;
var local_seq = _local[L_SEQ];
local_seq.pts = state.seq;
local_seq.access_hash = state.date.Ticks;
}
}
}
private void RaiseCollect(Updates_DifferenceBase diff)
{
if (diff is Updates_DifferenceSlice uds)
RaiseCollect(uds.users, uds.chats);
else if (diff is Updates_Difference ud)
RaiseCollect(ud.users, ud.chats);
}
private void RaiseCollect(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
{
try
{
foreach (var chat in chats.Values)
if (chat is Channel channel && !channel.flags.HasFlag(Channel.Flags.min))
if (_local.TryGetValue(channel.id, out var local))
local.access_hash = channel.access_hash;
_collector.Collect(users.Values);
_collector.Collect(chats.Values);
}
catch (Exception ex)
{
Log?.Invoke(4, $"Collect({users?.Count},{chats?.Count}) raised {ex}");
}
}
private async Task RaiseUpdate(Update update)
{
try
{
var task = _onUpdate(update);
if (!_reentrant) await task;
}
catch (Exception ex)
{
Log?.Invoke(4, $"onUpdate({update?.GetType().Name}) raised {ex}");
}
}
private static string ExtendedLog(Update update) => update switch
{
UpdateNewMessage unm => $"| msgID={unm.message.ID}",
UpdateEditMessage uem => $"| msgID={uem.message.ID}",
UpdateDeleteMessages udm => $"| count={udm.messages.Length}",
_ => null
};
/// <summary>Load latest dialogs states, checking for missing updates</summary>
/// <param name="dialogs">structure returned by Messages_Get*Dialogs calls</param>
/// <param name="fullLoadNewChans">Dangerous! Load full history of unknown new channels as updates</param>
public async Task LoadDialogs(Messages_Dialogs dialogs, bool fullLoadNewChans = false)
{
await _sem.WaitAsync();
try
{
foreach (var dialog in dialogs.dialogs.OfType<Dialog>())
{
if (dialog.peer is not PeerChannel pc) continue;
var local = _local.GetOrCreate(pc.channel_id);
if (dialogs.chats.TryGetValue(pc.channel_id, out var chat) && chat is Channel channel)
local.access_hash = channel.access_hash;
if (local.pts is 0)
if (fullLoadNewChans) local.pts = 1;
else local.pts = dialog.pts;
if (local.pts < dialog.pts)
{
Log?.Invoke(1, $"LoadDialogs {pc.channel_id} has {local.pts} < {dialog.pts} ({dialog.folder_id})");
await GetDifference(pc.channel_id, dialog.pts, local);
}
}
}
finally { _sem.Release(); }
}
/// <summary>Save the current state of the manager to JSON file</summary>
/// <param name="statePath">File path to write</param>
/// <remarks>Note: This does not save the content of collected Users/Chats dictionaries</remarks>
public void SaveState(string statePath)
=> System.IO.File.WriteAllText(statePath, System.Text.Json.JsonSerializer.Serialize(State, Helpers.JsonOptions));
public static Dictionary<long, MBoxState> LoadState(string statePath) => !System.IO.File.Exists(statePath) ? null
: System.Text.Json.JsonSerializer.Deserialize<Dictionary<long, MBoxState>>(System.IO.File.ReadAllText(statePath), Helpers.JsonOptions);
/// <summary>returns a <see cref="User"/> or <see cref="ChatBase"/> for the given Peer <i>(only if using the default collector)</i></summary>
public IPeerInfo UserOrChat(Peer peer) => peer?.UserOrChat(Users, Chats);
}
public interface IPeerCollector
{
void Collect(IEnumerable<User> users);
void Collect(IEnumerable<ChatBase> chats);
bool HasUser(long id);
bool HasChat(long id);
}
}
namespace TL
{
using WTelegram;
[EditorBrowsable(EditorBrowsableState.Never)]
public static class UpdateManagerExtensions
{
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
/// <param name="onUpdate">Event to be called on sequential individual update</param>
/// <param name="statePath">Resume session by recovering all updates that occured since the state saved in this file</param>
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, string statePath, IPeerCollector collector = null, bool reentrant = false)
=> new(client, onUpdate, UpdateManager.LoadState(statePath), collector, reentrant);
/// <summary>Manager ensuring that you receive Telegram updates in correct order, without missing any</summary>
/// <param name="onUpdate">Event to be called on sequential individual update</param>
/// <param name="state">(optional) Resume session by recovering all updates that occured since this state</param>
/// <param name="collector">Custom users/chats collector. By default, those are collected in properties Users/Chats</param>
/// <param name="reentrant"><see langword="true"/> if your <paramref name="onUpdate"/> method can be called again even when last async call didn't return yet</param>
public static UpdateManager WithUpdateManager(this Client client, Func<TL.Update, Task> onUpdate, IDictionary<long, UpdateManager.MBoxState> state = null, IPeerCollector collector = null, bool reentrant = false)
=> new(client, onUpdate, state, collector, reentrant);
}
}

View file

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net5.0;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<RootNamespace>WTelegram</RootNamespace>
<Deterministic>true</Deterministic>
@ -11,26 +11,31 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageId>WTelegramClient</PackageId>
<Version>0.0.0</Version>
<Authors>Wizou</Authors>
<Description>Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 158&#10;&#10;Release Notes:&#10;$(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A"))</Description>
<Copyright>Copyright © Olivier Marcoux 2021-2023</Copyright>
<VersionPrefix>0.0.0</VersionPrefix>
<VersionSuffix>layer.220</VersionSuffix>
<Description>Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 220
Release Notes:
$(ReleaseNotes)</Description>
<Copyright>Copyright © Olivier Marcoux 2021-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/wiz0u/WTelegramClient</PackageProjectUrl>
<PackageProjectUrl>https://wiz0u.github.io/WTelegramClient</PackageProjectUrl>
<PackageIcon>logo.png</PackageIcon>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/wiz0u/WTelegramClient.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>Telegram;MTProto;Client;Api;UserBot;TLSharp</PackageTags>
<PackageTags>Telegram;MTProto;Client;Api;UserBot</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>$(ReleaseNotes.Replace("|", "%0D%0A").Replace(" - ","%0D%0A- ").Replace(" ", "%0D%0A%0D%0A"))</PackageReleaseNotes>
<NoWarn>IDE0079;0419;1573;1591;NETSDK1138</NoWarn>
<DefineConstants>TRACE;OBFUSCATION</DefineConstants>
<PackageReleaseNotes>$(ReleaseNotes)</PackageReleaseNotes>
<NoWarn>NETSDK1138;CS0419;CS1573;CS1591</NoWarn>
<DefineConstants>TRACE;OBFUSCATION;MTPG</DefineConstants>
<!--<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>-->
</PropertyGroup>
<ItemGroup>
<None Include="..\.github\dev.yml" Link="Data\dev.yml" />
<None Include="..\.github\release.yml" Link="Data\release.yml" />
<None Include="..\.github\workflows\dev.yml" Link="Data\dev.yml" />
<None Include="..\.github\workflows\release.yml" Link="Data\release.yml" />
<None Include="..\EXAMPLES.md" Link="Data\EXAMPLES.md" />
<None Include="..\FAQ.md" Link="Data\FAQ.md" />
<None Include="..\README.md" Link="Data\README.md" Pack="true" PackagePath="\" />
@ -41,11 +46,19 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>-->
<ItemGroup Condition="$(DefineConstants.Contains('MTPG'))">
<ProjectReference Include="..\generator\MTProtoGenerator.csproj" OutputItemType="Analyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IndexRange" Version="1.0.2" />
<PackageReference Include="IndexRange" Version="1.0.3" />
<PackageReference Include="System.Memory" Version="4.5.5" />
<PackageReference Include="System.Text.Json" Version="6.0.5" />
<PackageReference Include="System.Text.Json" Version="6.0.10" />
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net5.0'">
<PackageReference Include="System.Collections.Immutable" Version="6.0.0" />
</ItemGroup>
</Project>