Compare commits

...

679 commits

Author SHA1 Message Date
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
Wizou 7c7a2a0625 Telegram suddenly removed Messages_GetAllChats method 🤦🏻‍♂ So here it is back, as a helper based on GetAllDialogs 2023-04-24 22:03:03 +02:00
Wizou ee2f0bfee1 added Document.GetAttribute<> helpers, setters on Input* classes and more 2023-04-23 14:35:08 +02:00
Wizou acb7ff1d74 Layer 158: chat wallpapers, enhanced chat folders/lists & switch-inline, edit bot infos, ... 2023-04-21 18:02:13 +02:00
Wizou 24a46206e1 api doc 2023-04-21 16:33:04 +02:00
Wizou 22e64ea3ee added some helpers 2023-04-21 16:31:57 +02:00
Wizou d53dc5f07c Support IDictionary in CollectUsersChats (#137) 2023-04-09 14:14:56 +02:00
wiz0u 0c6a8dd0a9
Create autolock.yml 2023-04-08 18:46:18 +02:00
Wizou 2f79411fce Support IDictionary in CollectUsersChats (closes #137)
Renamed UserID => UserId due to discrepancy
2023-04-08 16:32:19 +02:00
Wizou ddfa095f1a api doc 2023-04-08 16:26:33 +02:00
Wizou 9af6404eff Now throwing WTException. Class hierarchy is TL.RpcException : WTelegram.WTException : ApplicationException 2023-04-02 13:44:23 +02:00
Wizou 621a88bb9f Fix cache_time type 2023-04-02 00:06:26 +02:00
Wizou 81870b2de1 API Layer 156: some email login stuff 2023-03-26 17:59:15 +02:00
Wizou b307534078 api doc 2023-03-24 16:57:54 +01:00
Wizou 2f3106fe69 MainUsername property on IPeerInfo 2023-03-16 13:43:18 +01:00
Wizou fd9177f805 API Layer 155: timestamp of reactions 2023-03-09 22:41:07 +01:00
Wizou b63829393e Don't raise ReactorError for alt DCs 2023-03-09 22:32:57 +01:00
Wizou c646cac738 API Layer 154: bot app/webview stuff, modifying stickers... 2023-03-08 20:47:10 +01:00
Wizou 22ea4c6de8 update doc 2023-03-08 20:10:09 +01:00
Wizou 86796ebf0c Correctly dispose session store on ctor exception (#128) 2023-02-26 17:09:45 +01:00
Wizou 5d0fd6452f Added helpers AnalyzeInviteLink & GetMessageByLink 2023-02-17 19:31:01 +01:00
Wizou 514015639d ToString use MainUsername rather than username 2023-02-15 18:42:52 +01:00
Wizou 7948dbd8e3 Remove deprecated CollectAccessHash system 2023-02-14 11:14:17 +01:00
Wizou 08a0802ed3 Small change to support Firebase SMS 2023-02-13 11:32:45 +01:00
Wizou b5d7ef311d Small change to support Firebase SMS 2023-02-13 11:29:02 +01:00
Wizou bf7207fa7d Support for Firebase SMS 2023-02-05 15:43:49 +01:00
Wizou f86291117f API Layer 152: emoji pfp, autosave medias, auto-translations, media permissions, Firebase... 2023-02-04 10:36:19 +01:00
Wizou 553934c5ad add GetAllDialogs to WinForms example app 2023-01-26 14:42:50 +01:00
Wizou 66d8b75463 deprecate the experimental CollectAccessHash system 2023-01-12 01:37:12 +01:00
Wizou 8f10df8849 made Peer.UserOrChat as protected internal to be user-overridable 2023-01-09 13:22:35 +01:00
Wizou 014f563b89 added Channel.MainUsername helper | simplified GetAllChats example 2023-01-07 13:22:40 +01:00
Wizou 750dbef33b default value for offset_topic arg 2023-01-06 13:29:33 +01:00
Wizou d858411a87 demonstrate doc.Filename in Program_DownloadSavedMedia 2023-01-06 13:28:58 +01:00
Wizou 8098f36932 API Layer 151: media spoiler flag, personal profile photo, UpdateUser... 2022-12-29 22:33:28 +01:00
Wizou 7fa4051e99 Changed the ordering of optional parameters: Moved bool parameters to the end of parameter list 2022-12-29 22:28:58 +01:00
Wizou 389f110cfb Helper simplified method for Channels_GetAdminLog 2022-12-19 14:14:17 +01:00
Wizou e7ec282ac1 Signal wrong use of some params[] methods 2022-12-12 10:07:31 +01:00
Wizou aa9c4b532c Improved documentation 2022-12-12 10:07:07 +01:00
Wizou eb2577beed Upgrade to layer 150: Anti-Spam, web/contact tokens, chat TTL, hide topics, Fragment login 2022-12-07 13:36:49 +01:00
Wizou 231bf632e2 Custom emoji Markdown/Html syntax updated for tdlib compatibility
- Markdown: ![x](tg://emoji?id=...)
- Html: <tg-emoji emoji-id="...">
Previous syntaxes are still accepted
Also, changed Github examples links with ts=4
2022-12-05 20:32:32 +01:00
Wizou 42be950896 Alt DC disconnection logged as simple warning 2022-12-02 01:42:39 +01:00
Wizou 76d75a6035 implicit Input* operators for Wallpaper, EncryptedChat, PhoneCall, Theme, GroupCall 2022-11-26 23:10:06 +01:00
Wizou bd629e7384 doc updates 2022-11-26 15:16:04 +01:00
Wizou a8d6656c05 Using Github pages. Added User.IsBot helper 2022-11-26 14:16:52 +01:00
Wizou 898523346b build fix | Upgrade to layer 149: More Topics stuff 2022-11-23 14:28:25 +01:00
Wizou 11238550d3 Upgrade to layer 149: More Topics stuff 2022-11-23 14:18:07 +01:00
Wizou fd593b429a InputPeer.ID & MessageBase.ToString helpers.
Publish pre-releases on nuget now
2022-11-20 17:15:57 +01:00
Wizou adf6134911 Doc/Rationalize user/chat generic helpers 2022-11-19 02:03:48 +01:00
Wizou 2047154d26 doc 2022-11-15 16:20:00 +01:00
Wizou 61510465d2 Fix immediate crash 🤦🏻‍♂️ 2022-11-13 00:25:56 +01:00
Wizou fe5773ce29 Add release notes in Description too 2022-11-12 20:02:46 +01:00
Wizou ba523f7d21 Releasing 3.1.1 2022-11-12 19:40:34 +01:00
Wizou 6b3fcdb967 Trying to add ReleaseNotes in nuget package 2022-11-11 01:47:37 +01:00
Wizou a038be87af Improved Secret Chats:
Support layer 144 : big documents, silent
(layer not yet supported by other clients)
List of ISecretChat with detailed properties
Fix PFS issue losing a message...
2022-11-11 00:33:29 +01:00
Wizou 8fa00a8cc6 Support for bare methods in session 2022-11-08 17:06:16 +01:00
Wizou d9b137d41c added User.MainUsername to help with multiple usernames 2022-11-05 23:01:38 +01:00
Wizou fd42d3e6df Upgrade to layer 148: Topics, Usernames, Sticker keywords...
+ support for flags2 has_fields
2022-11-01 19:26:40 +01:00
Wizou b902b33558 updated docs (and reordered Examples.md) 2022-11-01 18:44:01 +01:00
Wizou d64c5c0c1e closes #103: files incorrectly padded to nearest 16 bytes 2022-10-26 17:33:06 +02:00
Wizou 49969e46cf xmldoc 2022-10-26 15:14:29 +02:00
Wizou 1a3cde4241 Replaced *Default & *None structures with null 2022-10-26 14:26:22 +02:00
Wizou cc9cf16f8a ToInputChatPhoto helper 2022-10-25 20:07:43 +02:00
Wizou 517fab89bb ReadHistory helper 2022-10-25 10:34:53 +02:00
Wizou 7e9d010392 documentation 2022-10-24 11:51:33 +02:00
Wizou d916342f1c Program_SecretChats timerstamp name for saved media 2022-10-21 19:54:12 +02:00
Wizou b9587e3997 Added SecretChats.DownloadFile and media.MimeType to simplify download & decryption 2022-10-20 23:12:43 +02:00
Wizou 82d852e071 optimized PQFactorize 2022-10-16 11:49:16 +02:00
Wizou d18b3853e1 Various minor improvements 2022-10-14 11:20:21 +02:00
Wizou 2e84061b4d Login: warning on phone_number mismatch 2022-10-13 10:07:46 +02:00
Wizou 88d54eb5a6 make SendAlbumAsync accept list too 2022-10-12 23:25:15 +02:00
Wizou fdc05e5514 Login failed: CancelCode but don't make things worse 2022-10-10 21:55:56 +02:00
Wizou 2cee5b8e6e update doc 2022-10-10 13:56:41 +02:00
Wizou 9c14a17af1 Released 3.0.0 2022-10-08 15:49:11 +02:00
Wizou 3fdc7bc1ad update documentation before release 2022-10-08 15:35:10 +02:00
Wizou e4b2cdd2c1 Fix ReactorError during InvokeBare 2022-10-08 15:06:36 +02:00
Wizou e51ea2441e DownloadFileAsync support for !CanSeek streams (like AES_IGE_Stream) 2022-10-07 11:24:08 +02:00
Wizou f22990cb58 Support for Encrypted Files/Medias 2022-10-07 03:00:57 +02:00
Wizou c6adeb1f31 Secret Chats example 2022-10-05 18:36:43 +02:00
Wizou b05d238c94 Secret Chats protocol implementation 2022-10-05 02:17:04 +02:00
Wizou 79097cdf8d some more optimizations 2022-10-04 00:52:44 +02:00
Wizou a45cd0f44e various optimizations 2022-10-01 13:56:43 +02:00
Wizou 3784ad00ad TL abstract => virtual props 2022-09-29 11:53:41 +02:00
Wizou ec0285077e xmldoc of recent layers 2022-09-25 19:21:00 +02:00
Wizou f3f1b37b85 _saltChangeCounter is now in seconds 2022-09-24 15:34:31 +02:00
Wizou 10c159b2d3 implicit operator InputDialogPeer from InputPeer 2022-09-24 15:30:43 +02:00
Wizou 280bc3c411 Alternative/simplified constructor & login method 2022-09-20 17:30:32 +02:00
Wizou 9523ca4036 Support premium emojies in Html/Markdown helpers 2022-09-19 22:28:12 +02:00
Wizou faf8ab3fd0 Upgrade to layer 146: Invoice extended media 2022-09-14 18:33:33 +02:00
Wizou 1ed10d99af Support for login email registration (still experimental) 2022-09-14 18:29:07 +02:00
Wizou 11a9ca8631 more 'params' arguments 2022-09-14 18:22:52 +02:00
Wizou b1649839d9 Support multiple Test DC connections 2022-09-11 15:34:38 +02:00
Wizou 26942d33f2 No changes, documentation 2022-09-10 18:23:01 +02:00
Wizou a071c993d5 MessageMedia.ToInputMedia helper 2022-09-04 15:24:59 +02:00
Wizou 3f3ff4cb9b Upgrade to layer 145: Emoji/reactions/stickerset stuff, email verification 2022-09-02 23:47:51 +02:00
Wizou 222d24c9a6 updated Intellisense / doc 2022-09-02 23:02:44 +02:00
Wizou 983c9a4c6b Safety check on bareRpc <-> unencrypted 2022-08-30 15:15:43 +02:00
Wizou ace31a3213 Reset _bareRpc too 2022-08-29 02:37:30 +02:00
Wizou 97bd76cf0f Minor code cleaning 2022-08-13 01:20:53 +02:00
Wizou f1448ac517 minor documentation update 2022-08-12 21:19:10 +02:00
Wizou 9b7e4293d8 Removed compatibility with legacy (pre-2.0.0) session files 2022-08-11 19:39:18 +02:00
Wizou 46c3cf3d9a Fix crash on null OnUpdate 2022-08-06 13:06:46 +02:00
Wizou ee0a777685 Added to layer 144: Album covers 2022-08-04 00:20:42 +02:00
Wizou 3e1506d0a7 Throw exception if calling API without connecting first. 2022-08-01 19:06:31 +02:00
Wizou 668b19e3e8 Renamed Update event to OnUpdate, returning Task
(to gracefully handle async exceptions)
2022-07-29 15:24:18 +02:00
Wizou 6977641b2d Upgrade to layer 144: Premium gifts, custom emoji stickers... 2022-07-29 02:21:05 +02:00
Wizou cf53520e02 minor clarifications 2022-07-29 02:19:25 +02:00
Wizou 5743942a7f Notify ReactorError before (not after) throwing exception on pending APIs 2022-07-24 15:03:13 +02:00
Wizou 000c35b256 SendAlbumAsync now returns all the Message[] (#75) 2022-07-24 11:14:08 +02:00
Wizou 1299e27cab Added method to DisableUpdates 2022-07-12 01:31:18 +02:00
Wizou 4f1a6610aa Option to prevent logout on failed resume 2022-07-08 01:15:37 +02:00
Wizou 39b46d71f0 moving code around Client.Helpers.cs 2022-07-04 12:55:00 +02:00
Wizou 08f58e3d0a Fixed "ignoring old msg_id" warnings on session start 2022-07-01 12:18:13 +02:00
Wizou 1ffbca1b51 coherent null behaviour for UserOrChat helpers 2022-06-25 17:05:27 +02:00
Wizou 4d8c0843d9 revoke_history always true for DeleteChatUser helpers (same behavior with channels) 2022-06-23 01:51:55 +02:00
Wizou aad40cf5df parameters MessagesFilter filter are optional 2022-06-23 01:49:07 +02:00
Wizou f620514759 fix issue with MTProxy and media dc_id 2022-06-19 18:08:19 +02:00
Wizou b1c8d225f2 implicit InputStickerSet from string shortName 2022-06-17 15:12:50 +02:00
Wizou 3f84e10f7f Heroku example: ignore our own outgoing messages 2022-06-15 19:16:11 +02:00
Wizou 823a414839 Fix possible NullRef with _bareRpc 2022-06-15 12:21:22 +02:00
Wizou 8898308d9c Upgrade to layer 143: Premium features (long file_size, transcribed audio, chat mgmt...), advanced invoice/purchase, bot attach menu... 2022-06-15 01:58:44 +02:00
Wizou 33a2fb02c1 minor changes 2022-06-14 00:58:51 +02:00
Wizou 6aef50db85 Retry wrong password/verification_code up to MaxCodePwdAttempts times 2022-05-31 14:28:37 +02:00
Wizou 4a9652dc7a added DeleteMessages helper 2022-05-21 02:32:15 +02:00
Wizou a24a24fc06 Releasing 2.4.1 2022-05-20 14:59:27 +02:00
Wizou 2a250ab39f added GetMessages helper and InputMessageID implicit operator 2022-05-20 14:54:07 +02:00
Wizou 5e66d562df Last arg of API methods can be params for simplicity 2022-05-20 14:04:54 +02:00
Wizou 8369832723 new dev nuget link 2022-05-19 22:42:59 +02:00
Wizou a8d2dfcfa1 Improve security by preventing replay attacks 2022-05-19 01:32:22 +02:00
Wizou 3be28f0fbd minor doc changes 2022-05-15 00:57:28 +02:00
Wizou 90ce527f31 Upgrade to layer 142: some various stuff 2022-05-15 00:36:28 +02:00
Wizou 9753b2b385 UpdateShortMessage.UpdateList correctly handle Flags.out_ 2022-05-13 23:16:48 +02:00
Wizou 5465474505 Try to wait for clean reactorTask completion on Dispose/Reset 2022-05-09 23:43:06 +02:00
Wizou ec65e66673 small updates to FAQ#tlsharp 2022-05-07 22:55:08 +02:00
Wizou 332b784384 minor doc update 2022-05-07 22:13:20 +02:00
Habeeb 0570fa05c6
Update README.md (#53)
Spelling corrections.
2022-04-29 22:43:45 +02:00
Wizou de0f34e8c5 handle TAKEOUT_INIT_DELAY_X 2022-04-26 21:13:23 +02:00
Wizou 5c2a66d8ce Program_Heroku: added support for SESSION_NAME env var 2022-04-23 01:34:21 +02:00
Wizou 61dd83a162 mark Auth_* methods as obsolete. call Auth_CancelCode on exception 2022-04-22 23:07:43 +02:00
Wizou 9d18eefe32 FAQ: fix nuget feed url 2022-04-19 14:30:07 +02:00
Wizou a53610ccb9 call OnUpdate(signUpRequired) instead of OnUpdate(TOS) only 2022-04-19 13:39:33 +02:00
Wizou 02c5b4137a Fix NullRef when RPC result is a nullable TL type 2022-04-18 22:07:04 +02:00
Wizou 9aa97d341a Upgrade to layer 140: Ringtones/Sounds, Custom bot menu, ... 2022-04-13 16:02:02 +02:00
Wizou 0f928d4992 Move ParseX logic out of TL.MTProto 2022-04-13 15:53:06 +02:00
Wizou 796b49546e Provide X number (if any) and generic message in RpcException. Retry only once on -503 error. 2022-04-11 12:08:17 +02:00
Wizou e6a1dbb24d No code change. Just moving methods around 2022-04-06 18:38:54 +02:00
Wizou 05752863bd Various minor stuff 2022-04-01 21:48:28 +02:00
Wizou 10e0e08bbb Examples for EntitiesTo* helpers 2022-03-30 15:17:28 +02:00
Wizou 2af9763a81 moved VPS doc to FAQ 2022-03-29 17:04:58 +02:00
Wizou 63faf9a070 updated EXAMPLES.md for Program_Heroku 2022-03-28 13:24:37 +02:00
Wizou 74f22a2f5c added Heroku examples with database store 2022-03-28 12:41:28 +02:00
Wizou 5c5b8032b9 Fix race condition on pendingRpcs adding/pulling 2022-03-27 22:29:48 +02:00
Wizou a54cc92618 ctor for Input User/Channel with mandatory access_hash parameter 2022-03-27 22:26:57 +02:00
Wizou c2f228f7de ctor for Input User/Channel with mandatory access_hash parameter 2022-03-27 12:18:43 +02:00
Wizou 67da1da8c0 added xmldoc for these helpers 2022-03-23 17:33:23 +01:00
Wizou 8c5fe45c44 added EntitiesToMarkdown & EntitiesToHtml helpers 2022-03-23 17:04:17 +01:00
Wizou 073056c079 added InputUserBase.UserId helper property 2022-03-23 13:50:43 +01:00
Wizou b31c9b4366 Split TL.Schema.cs and TL.Helpers.cs 2022-03-21 21:25:30 +01:00
Wizou f339fe1160 lock sessionStore while updating/writing buffer to store
(useful to avoid buffer copy for custom stores)
2022-03-20 13:09:25 +01:00
Wizou b31aa55c34 updated API docs 2022-03-18 03:53:19 +01:00
Wizou 8fbd564c11 explain 2FA in ReadMe 2022-03-10 14:55:32 +01:00
Wizou d0d63547b4 cancellationToken for Channels_GetAllParticipants 2022-03-10 14:39:44 +01:00
Wizou 07fcb2d9e4 Upgrade to layer 139 : RTMP groupcalls, video stickerset, ... 2022-03-10 14:35:42 +01:00
Wizou 78d7e250f3 added Messages_GetAllDialogs. UserOrChat(null) returns null 2022-02-27 22:06:13 +01:00
Wizou f3a55385ab updated web doc + FAQ TLSharp 2022-02-26 05:22:41 +01:00
Wizou a178d4be6f easier access to Document filename 2022-02-24 17:12:52 +01:00
Wizou f282d270ae Retry API call once on error -503 Timeout 2022-02-24 16:44:27 +01:00
Wizou 722a8313b0 Updated XML doc comments from corefork 2022-02-23 23:50:52 +01:00
Wizou 902a37443f Remove dependencies on Microsoft.Bcl.HashCode & System.Formats.Asn1 2022-02-22 11:50:55 +01:00
Wizou bdbf17aa07 Remove dependencies on Microsoft.Bcl.HashCode & System.Formats.Asn1 2022-02-19 03:30:50 +01:00
Wizou 74bba5721e Updated docs. Negative PingInterval 2022-02-14 15:17:15 +01:00
Wizou 4e07c03a0b CollectUsersChats helper for your dictionaries (min-aware). CollectAccessHash won't collect 'min' access_hash. 2022-02-14 02:02:13 +01:00
Wizou 0667d36ed8 updated example Programs.cs 2022-02-13 03:15:23 +01:00
Wizou 34f05f5947 releasing 2.1.1 2022-02-13 03:00:17 +01:00
Wizou 6646e85e78 more optional parameters. Messages_Search<> helper 2022-02-13 02:50:10 +01:00
Wizou 7570732a3f call CollectField only if CollectAccessHash 2022-02-11 18:05:12 +01:00
Wizou 3fe9002f2e Remove OLDKEY/MTPROTO1 code 2022-02-11 02:43:48 +01:00
Wizou 2982e09c9b use static variables in Program_ListenUpdates 2022-02-10 02:28:32 +01:00
Wizou 9b52cad74d Channels_GetAllParticipants: added logs 2022-02-09 23:20:21 +01:00
Wizou 4ebddba95d Channels_GetAllParticipants: restore original name 2022-02-09 22:58:26 +01:00
Wizou 08484fff41 optimized Channels_GetAllParticipantsSlow. Because of Telegram FLOOD_WAITs, no need for parallel queries 2022-02-09 22:27:55 +01:00
Wizou 5e5e51102f renamed Channels_GetAllParticipantsSlow. use clever alphabet and detect lying subcounts to fetch more entries. (WIP) 2022-02-09 22:17:19 +01:00
Wizou e36ea252fb adding ASPnet_webapp.zip 2022-02-08 18:49:39 +01:00
Wizou 403969356f Fix InvalidCast when reading an enum vector 2022-02-07 22:06:10 +01:00
Wizou 8bbb753c32 fix (uint) unboxing on fields "int flags" (improved) 2022-02-06 23:22:51 +01:00
Wizou bedb44582e fix (uint) unboxing on fields "int flags" 2022-02-06 14:16:00 +01:00
Wizou 272a89f562 Released 2.0.2 2022-02-04 22:33:05 +01:00
Wizou 1b1243a758 fix #27: compatibility with Mono/Android 2022-02-04 02:51:14 +01:00
Wizou 72fba55407 Upgrade to layer 138 2022-02-03 16:55:16 +01:00
Wizou e1023ecae6 renamed longitude long_ to lon. UserOrChat now returns null instead of throwing if peer unknown (shouldn't happen anyway). UserOrChat on UpdatesBase 2022-01-29 16:47:47 +01:00
Wizou cdba0f7088 optimize session writes 2022-01-27 17:34:16 +01:00
Wizou 176809a5be Support for custom sessionStore 2022-01-26 22:00:52 +01:00
Wizou 12850182ff fix #25: possible NullReferenceException with Document.LargestThumbSize
fix undisposed FileStream on broken session file.
2022-01-26 11:08:33 +01:00
Wizou 582027e800 some renaming 2022-01-25 23:16:28 +01:00
Wizou 733d106265 store api_id in session file. optional session_key (=api_hash by default) as encryption key 2022-01-25 16:37:09 +01:00
Wizou 6ff0dc40ed Example for SendAlbumAsync 2022-01-24 22:52:18 +01:00
Wizou 28803412c1 FAQ about "connection shutdown" 2022-01-23 14:42:35 +01:00
Wizou 8e962178f9 Improved session file writes 2022-01-23 01:28:10 +01:00
Wizou 44ee933b03 webdoc "only simple chat" for 4 more methods 2022-01-21 19:04:33 +01:00
Wizou f6e14c7499 Maintain AddressFamily when migrating DCs 2022-01-21 15:22:21 +01:00
Wizou f711948da7 factorize IndirectStream 2022-01-21 01:40:10 +01:00
Wizou 3730cdc7f0 Added helper SendAlbumAsync, and implicit operators/constructors for media 2022-01-20 02:44:43 +01:00
Wizou 411fcad556 Upgrade to layer 137 2022-01-19 21:31:07 +01:00
Wizou 8d70f241ad ChatFull Participants helpers 2022-01-18 15:24:04 +01:00
Wizou 79224551e6 Prevent reactor reconnect if Dispose() was called 2022-01-17 16:54:34 +01:00
Wizou 0ed77ef984 Support GZipped array in RpcResult 2022-01-17 15:06:29 +01:00
Wizou cba347dc89 clone TcpHandler & PingInterval for secondary DCs 2022-01-13 14:22:52 +01:00
Wizou 6882e85fe1 TLS public key must be positive 2022-01-13 03:06:22 +01:00
Wizou 145ade4414 fix issue #21
Exception "An item with the same key has already been added" in Messages_GetHistory
2022-01-12 14:25:32 +01:00
Wizou c93481189a added HtmlToEntities and HtmlText.Escape 2022-01-12 02:54:59 +01:00
Wizou 0dffccc047 adding WinForms_app.zip 2022-01-12 00:37:55 +01:00
Wizou 83b4e8a4e7 clean dispose of TlsStream 2022-01-11 04:42:41 +01:00
Wizou d6d656c8fe Support for TLS MTProxy 2022-01-11 04:14:23 +01:00
Wizou 16258cb5ba minor doc 2022-01-07 23:10:18 +01:00
Wizou 6bdb0b9cc7 reduce allocations for encryption 2022-01-07 01:14:16 +01:00
Wizou 0c1785596d various minor stuff 2022-01-07 00:24:47 +01:00
Wizou e6f1087c54 minor doc 2022-01-05 09:44:47 +01:00
Wizou 7967f9a16c layer doc about reactions 2022-01-03 19:18:40 +01:00
Wizou 5addcea0a2 MTProxy example 2022-01-03 18:50:16 +01:00
Wizou 28f099ed1e Transport obfuscation and MTProxy support 2022-01-03 18:15:32 +01:00
Wizou 88d2491db9 MarkdownToEntities: spoiler with ||
see https://core.telegram.org/bots/api/#markdownv2-style
2021-12-31 12:10:28 +01:00
Wizou 024c5ba705 More helpers to generically manage Chats & Channels 2021-12-31 08:38:41 +01:00
Wizou e3b7d17d2b Handle BadMsgNotification 16/17 by resynchronizing server time 2021-12-30 23:45:28 +01:00
Wizou 4739c9f539 Fix risk of having only WTelegram.session.tmp if killing the program in the middle of Delete/Move
(that's what File.Replace was trying to avoid!!)
2021-12-30 23:43:00 +01:00
Wizou a7fcbf60fa MarkdownToEntities allows spoiler with ~~ 2021-12-30 17:50:43 +01:00
Wizou 6df9f76cae Examples for changing 2FA password, and sending a message reaction 2021-12-30 17:38:07 +01:00
Wizou e91c20ba8e Added GetFullChat helper 2021-12-30 17:37:25 +01:00
Wizou dced397c15 Released 1.8.3 2021-12-30 12:45:52 +01:00
Wizou b4a8f9f280 Upgrade to layer 136 2021-12-30 12:14:41 +01:00
Wizou 86ac336691 DownloadFileAsync supports PhotoStrippedSize 2021-12-28 12:12:38 +01:00
Wizou 9171268d19 DownloadProfilePhotoAsync supports mini thumb 2021-12-28 09:38:22 +01:00
Wizou 72b28e97ba added DownloadProfilePhotoAsync helper 2021-12-28 06:43:33 +01:00
Wizou 791dc88ea0 improved documentation for newbies 2021-12-26 18:28:10 +01:00
Wizou 51a89bc6a1 change Dictionary of UserBase into Dictionary of User 2021-12-25 03:20:22 +01:00
Wizou 2881155f8b documentation 2021-12-24 07:21:02 +01:00
Wizou 27f62b7537 Fun with stickers, dice and animated emojies
and flags
2021-12-23 01:38:59 +01:00
Wizou 1396eebca1 Provide computation for new 2FA passwords 2021-12-22 18:25:38 +01:00
Wizou fa10998618 Examples: Add/Invite/Remove someone in a chat 2021-12-22 05:02:43 +01:00
Wizou 35cd3b682e More helpers 2021-12-22 04:46:01 +01:00
Wizou a4f6d00bd0 fix InformationalVersion 2021-12-21 02:28:35 +01:00
Wizou 45d6e330bc renamed CallAsync as Invoke 2021-12-16 14:51:47 +01:00
Wizou 8fe0c086bb Added contacts list & GUI examples 2021-12-16 14:41:43 +01:00
Wizou 9cc164b9ec updated ReadMe 2021-12-15 20:55:24 +01:00
Wizou 6e4b90710a Added static metnod InputCheckPassword 2021-12-14 13:39:43 +01:00
Wizou 97c6de4cbb Support the request to resend the verification_code through alternate methods. 2021-12-13 15:28:06 +01:00
Rinat Abzalov 62dad83370 Fix System.IO.IOException: 'Unable to remove the file to be replaced. ' #10
Based on: 00416bc92f
2021-12-13 14:07:17 +01:00
Wizou 15c3fda05d helper TotalCount on Messages_DialogsBase 2021-12-12 20:38:13 +01:00
Wizou 8efa248aef Added LastSeenAgo
and other helpers
2021-12-10 17:21:39 +01:00
Wizou 624f45bd05 Config("user_id") represents a long 2021-12-10 14:19:28 +01:00
Wizou 275ece196b comments 2021-12-09 08:43:20 +01:00
Wizou 4b4965a335 added xNetStandard proxy example 2021-12-07 17:16:02 +01:00
Wizou f289b9e2e5 Correctly handle RPC Error for methods returning bool.
Also don't hang CallAsync on ReadRpcResult deserializing error
2021-12-07 16:35:23 +01:00
Wizou 95d9135bd0 Configurable PingInterval to support slow connections 2021-12-07 15:26:53 +01:00
Wizou f6eb1aa6fd fix never-ending loop in Channels_GetAllParticipants 2021-12-07 15:07:35 +01:00
Wizou 73ac9384e4 config.yml for issue_template 2021-12-07 03:36:28 +01:00
Wizou 1812908d5b config.yml for issue_template 2021-12-07 03:31:18 +01:00
Wizou 4195876b8f docs: more anchors 2021-12-07 02:32:19 +01:00
Wizou d0c783ec23 FAQ update 2021-12-07 02:06:24 +01:00
Wizou 0351ad027f added Channels_GetAllParticipants helper 2021-12-07 00:34:57 +01:00
Wizou d22628918c Rewrote the FAQ about access_hash 2021-12-06 18:05:12 +01:00
Wizou 934ee9bae4 GUI compatibility: Detach interactive Config calls from MainThread 2021-12-05 11:47:52 +01:00
Wizou 409cf25619 Fix 444 trying to download from a media_only DC with unnegociated AuthKey 2021-12-05 07:21:30 +01:00
Wizou e1132f653b Handle BadMsgNotification 32/33 by renewing session 2021-12-04 00:14:15 +01:00
Wizou bafe3c56bd make example programs static 2021-12-03 10:16:01 +01:00
Wizou 65a4b779c1 later rejoin for background 2FA ValidityChecks 2021-12-01 16:24:30 +01:00
Wizou 5e04a51e54 Fix 2FA issue under .NET Fw 4.7.2
minor doc updates
2021-12-01 15:50:35 +01:00
Wizou 12ea8ac1bb released 1.7.3 2021-11-27 03:29:19 +01:00
Wizou 8bfdedae1f Upgrade to layer 135 2021-11-27 03:03:04 +01:00
Wizou ff796775eb some more helpers 2021-11-27 02:57:50 +01:00
Wizou e7b9ea93cd Upgrade to layer 135 2021-11-27 02:56:42 +01:00
Wizou 934895a81c Gracefully handle a disconnection from an idle secondary DC
(don't reconnect)
2021-11-21 00:43:39 +01:00
Wizou e8a98a5799 FAQ: added "How to not get banned" 2021-11-20 12:54:49 +01:00
Wizou 7fdd05a714 new FAQ 2021-11-15 17:17:11 +01:00
Wizou 27ab343ad0 Added Example for Help_GetAppConfig / sending Dice 2021-11-14 15:35:41 +01:00
Wizou f901319ca4 simplified Help_GetAppConfig access to Json values 2021-11-13 15:09:28 +01:00
Wizou 7ed21d5af4 FloodRetryThreshold, ProgressCallback, don't call Updates_GetState on alt DCs upon reconnection 2021-11-12 20:50:39 +01:00
Wizou c0b10e82f4 small update to ReadMe 2021-11-12 06:10:26 +01:00
Wizou c157fba5e4 Move TL methods in Extensions class and TL.Methods namespace. Remove partial modifiers when possible 2021-11-10 17:26:40 +01:00
Wizou 30f20fad0e [rollback] Use records for TL methods
It was a nice idea and it worked but it made the lib 3x larger just for sugar syntax in generated code
2021-11-10 02:17:08 +01:00
Wizou 9e4d67ed86 Use records for TL methods 2021-11-10 01:20:54 +01:00
Wizou 2b95b235f3 Added IPeerResolver 2021-11-09 23:23:16 +01:00
Wizou d557b4fc91 inheritAfter => inheritBefore (improved perf) 2021-11-09 15:01:59 +01:00
Wizou f0ae44c088 More helpers that simplify the handling of Updates 2021-11-09 03:00:03 +01:00
Wizou 382e78cccc Added some helpers. SendMessage/Media return the transmitted Message. Fixed minor issues 2021-11-09 01:43:27 +01:00
Wizou dbcb6814ff renamed ITLObject to IObject 2021-11-07 16:52:58 +01:00
Wizou fd9ec2eaed Get rid of ITLFunction lambda writer and use declared ITLMethod<Ret> classes instead 2021-11-07 16:50:59 +01:00
Wizou d0be053707 We don't need to store full User in session file anymore 2021-11-07 09:09:15 +01:00
Wizou a06be4e096 fix ReadMe for nuget.org 2021-11-06 05:52:44 +01:00
Wizou 14e2437097 Add xmldoc for all public members 2021-11-06 05:22:33 +01:00
Wizou cf2072e830 add eventual [deactivated] in Chat.ToString 2021-11-05 04:37:21 +01:00
Wizou b872e58e28 use Users_GetUsers instead of Updates_GetState in login so we update our User info 2021-11-04 20:10:44 +01:00
Wizou 42348166f0 reorder csproj properties 2021-11-04 03:28:01 +01:00
Wizou 4418f562cb Log precise version of lib 2021-11-04 03:10:03 +01:00
Wizou ec6fb6c0c0 Try to use .NET 6.0 RC2 SDK and embed README.md in nuget package 2021-11-04 02:41:04 +01:00
Wizou 248b7d7f5a renamed TL.Helpers.cs to match namespace 2021-11-04 02:21:12 +01:00
Wizou 3bd1f1bb22 Moved "Possible error codes" to <summary> as they are not displayed correctly with <exception> under VS 2019 2021-11-04 02:09:20 +01:00
Wizou e7637011e1 xmldoc for RpcException possible errors and missing type descriptions 2021-11-03 18:20:54 +01:00
Wizou a197573258 Complete XML documentation of the Telegram API ! 2021-11-03 03:53:48 +01:00
Wizou 3ad36f3e56 Fix BadMsgNotification 17 for PC with not precise system clock
Add logging examples
2021-11-02 01:47:14 +01:00
Wizou 2c99e21234 add some debug logging 2021-11-01 15:44:40 +01:00
Wizou ba72e67c56 update layer version 2021-10-31 18:37:17 +01:00
Wizou 3d561ff63e added Markdown.Escape
improved MarkdownToEntities a bit
2021-10-31 18:28:25 +01:00
Wizou 7616625ac3 Upgrade to layer 134 2021-10-31 03:55:03 +01:00
Wizou 8d8465fa64 precision on tg://user?id= 2021-10-31 02:56:51 +01:00
Wizou e615f83db6 added MarkdownToEntities 2021-10-31 02:40:10 +01:00
Wizou b94ec0abca clarification on Config null behavior 2021-10-29 23:27:23 +02:00
Wizou df2fe83ad2 small improvements to Login methods 2021-10-28 22:48:43 +02:00
Wizou 36e7d4ddbc Reenable incoming Updates after an exception in Reactor 2021-10-28 12:31:34 +02:00
Wizou 668138fd78 various markdown updates 2021-10-28 04:59:41 +02:00
Wizou 984c241a09 Support for proxy 2021-10-25 02:40:15 +02:00
Wizou 391c7f96f0 auto-generate properties helpers for fields shared among derived classes 2021-10-23 03:36:46 +02:00
Wizou c9ccaf2d17 More helpers (reordered), notably UserOrChat 2021-10-23 01:37:50 +02:00
Wizou 08ba766e5c Helpers for Contacts_ResolvedPeer. Fix some examples 2021-10-22 19:33:17 +02:00
Wizou af79bfa873 IPeerInfo, IsActive and Dialogs helpers 2021-10-22 15:26:46 +02:00
Wizou 2cf3f939a8 corrected first 2 examples 2021-10-20 19:49:25 +02:00
Wizou e3965addf1 releasing 1.4.x 2021-10-20 19:20:15 +02:00
Wizou 718e96a763 chats and users fields are now serialized as Dictionary for easier access 2021-10-20 19:12:50 +02:00
Wizou 5e2ddf41f6 A null config value for "password" will also show a console prompt 2021-10-20 13:08:10 +02:00
Wizou a473475e11 Removed unused _sem in Session 2021-10-20 00:24:50 +02:00
Wizou c3dcd2a367 Released 1.3.1 2021-10-17 23:53:57 +02:00
Wizou 8ea714d6d4 Releasing 1.x 2021-10-17 23:43:47 +02:00
Wizou f5b108dc9b Fix compatibility issues with .NET Fw 4.7 2021-10-17 23:35:14 +02:00
Wizou 2ea6ede0a0 Added EXAMPLES.md and explanation on TG Terminology 2021-10-17 03:22:49 +02:00
Wizou 4b1ae1c5e0 Decode max timestamp (= forever) as DateTime.MaxValue 2021-10-17 03:18:36 +02:00
Wizou 80edb184bc Removed DisplayName in favor of ToString() for User classes 2021-10-17 03:16:51 +02:00
Wizou 480697d329 Fix wrong type for MessageBase.ID 2021-10-16 02:01:49 +02:00
Wizou c5c8b49331 Simplify library usage even more 2021-10-15 04:24:34 +02:00
Wizou 98a95376f3 Improved examples documentation 2021-10-13 00:27:40 +02:00
Wizou 37ce6524e9 updated ci version 2021-10-12 02:07:54 +02:00
Wizou f296e6b36d Added Examples/Program_GetAllChats 2021-10-11 18:29:24 +02:00
Wizou 609e8a6a2d Added Examples\Program_CollectAccessHash 2021-10-11 16:11:37 +02:00
Wizou 4f9fbfc12c A null config value for "verification_code" will show a console prompt. This allows Environment.GetEnvironmentVariable to be used directly as config callback. 2021-10-11 14:44:49 +02:00
Wizou 87a85bec4b added Examples\Program_DownloadSavedMedia.cs 2021-10-06 08:21:42 +02:00
Wizou d96902a614 Parallelize upload/download of file parts 2021-10-06 07:54:20 +02:00
Wizou 2520a57f20 ToString for SendMessageAction 2021-10-06 02:41:54 +02:00
Wizou b01eed2042 DC disconnect bug fix 2021-10-01 02:44:56 +02:00
Wizou e7c5d2eb27 Better IPv6 support/fallback. Support RpcError in response to an API returning an array 2021-10-01 02:22:26 +02:00
Wizou d72f7a6398 Ready to release v1.0.0 2021-09-30 20:36:52 +02:00
Wizou d7b4afb1ee More nullable ctors (*NotModified) 2021-09-30 04:00:38 +02:00
Wizou d4c373d95f Nullable ctor: Empty classes are now converted into/from a null value. This simplifies the hierarchy 2021-09-30 03:40:08 +02:00
Wizou 52fb2a7831 Fix SHA corruption due to concurrent use of static instance 2021-09-29 04:38:39 +02:00
Wizou 8610f7e809 Handle AUTH_RESTART & PHONE_CODE_INVALID during login 2021-09-29 02:51:48 +02:00
Wizou da5098e8d5 MULTIPLE-CONNECTION! First version that implement parallel active connections to DCs (through Client instances dependent of the main Client instance)
Also improved on:
- reconnection/retry/resent strategy
- start of multiple parallel downloads triggering a new DC connection
2021-09-28 16:12:20 +02:00
Wizou 66757ccd0b fix MT issue in session.Save 2021-09-27 17:08:22 +02:00
Wizou cb35ec3799 tcs use RunContinuationsAsynchronously (much better) 2021-09-27 17:07:56 +02:00
Wizou 78a0a47c50 Fix serialization issue with null string. DownloadFileAsync for documents now returns the MIME type (more accurate) 2021-09-27 03:25:28 +02:00
Wizou 6770f66270 Fix issue with session file corruption 2021-09-27 03:20:58 +02:00
Wizou 2ff0de667d More class inheritance 2021-09-27 00:39:30 +02:00
Wizou e3b70fda19 BREAKING CHANGE: Introducing inheritAfter which allows for better/simplified (but possibly changed) class hierarchy generation.
Also improved layers class generation.
2021-09-26 05:07:17 +02:00
Wizou e6c5d94d9d Generator: Clean code for generation of abstract classes 2021-09-26 01:19:32 +02:00
Wizou c789308d1e Avoid unnecessary Auth_ExportAuthorization. More reliable DownloadFileAsync 2021-09-25 19:43:39 +02:00
Wizou ca1c1ce8de TL Helpers for MessageBase + Example of use of Messages_GetDialogs 2021-09-25 02:13:06 +02:00
Wizou 0e88280d90 Monitor excessive salt changes in a better way 2021-09-24 13:21:35 +02:00
Wizou c570f3d8d0 support TL bare vectors 2021-09-24 12:29:04 +02:00
Wizou be4d1aca6b added DownloadFileAsync helpers 2021-09-23 13:13:36 +02:00
Wizou 0b3180917f minor improvements 2021-09-23 09:42:35 +02:00
Wizou f1ebafcf09 TL Photo helpers 2021-09-23 09:29:19 +02:00
Wizou 35786ef02a Support multi-DC sessions 2021-09-23 09:27:52 +02:00
Wizou fe7bc6f61c Optimized reactor frame handling 2021-09-23 05:37:00 +02:00
Wizou 223d8984cf Serialize User to Json inside session file to prevent future breaking changes 2021-09-18 07:26:06 +02:00
Wizou f6cc00068a Fix "Invalid frame_len" when downloading files 2021-09-18 02:40:58 +02:00
Wizou 3918362743 Fix "Non-negative number" issue with large bytes fields 2021-09-18 02:30:12 +02:00
Wizou e0556af742 Convert empty type hierarchies to enums 2021-09-18 02:11:23 +02:00
Wizou 701795efe9 Start keep-alive pings only after authorization key 2021-09-17 15:13:38 +02:00
Wizou f66872cb32 Fix unencrypted message sending issue after switch to Intermediate protocol 2021-09-17 15:09:12 +02:00
Wizou 37974c70b7 Convert TL functions to extension methods 2021-09-17 04:53:02 +02:00
Wizou 77a44a86a6 Upgrade to layer 133 2021-09-17 04:38:42 +02:00
Wizou c727f75380 Upgrade to layer 133 (lots of changes, all IDs are now long). You'll need to delete your WTelegram.session files 2021-09-17 03:58:52 +02:00
Wizou 661b5223ac Switch to MTProto Intermediate transport protocol (more lightweight/adequate for TCP) 2021-09-17 03:44:52 +02:00
Wizou a403a462db fetch Updates_GetState on session resuming to subscribe to new updates 2021-09-17 03:25:27 +02:00
Wizou 184a133dce Add Keep-Alive system (will also send pending acks) 2021-09-17 03:12:23 +02:00
Wizou b17349bd75 Added auto-reconnect system, hoping it will help with connection shutdown issues
README: added Troubleshooting guide
2021-09-16 04:47:15 +02:00
Wizou dcd384ed27 Update callback is now void (or async void)
Added ListenUpdates example
2021-09-05 01:08:16 +02:00
Wizou 3b8ec9d910 Constructor's updateHandler parameter is now an Update event 2021-09-03 00:06:48 +02:00
Wizou c5e9e228a7 Updated README about 2nd run prompts.
Validation of logged-in user can be skipped with user_id = -1
(use it cautiously, if you're sure you're not changing user between sessions)
2021-09-02 00:39:06 +02:00
Wizou 832e2143f5 Fix issue with SHA computation under .NET Framework 2021-09-02 00:00:54 +02:00
Wizou cd6d813845 Prevent logging reactor exceptions if expected due to cancellation 2021-09-01 23:11:18 +02:00
Wizou 67285c1c08 Remember the good primes to prevent verifying them twice 2021-09-01 23:03:04 +02:00
Wizou 3701ba6f72 Fix warning: The predefined type "RuntimeHelpers" is defined in multiple assemblies 2021-08-30 16:13:52 +02:00
Wizou 4174b21a83 Experimental collection of id/access_hash pairs 2021-08-30 01:31:08 +02:00
Wizou 0d5546bee5 Improve nuget properties 2021-08-29 23:15:34 +02:00
Wizou ce41af2f84 Validation of logged-in user can also be done by its user_id 2021-08-27 22:44:43 +02:00
Wizou ef93dda3ac Validate preexisting logged-in user. Force reauthorization after AUTH_RESTART 2021-08-27 14:14:24 +02:00
Wizou ccb48e3b3d Logon => Login 2021-08-26 17:46:45 +02:00
Wizou faf24bfb2a UserAuthIfNeeded => LogonUserIfNeeded (+ more robust) 2021-08-25 15:32:25 +02:00
Wizou d4cb4f59d5 Moved UpdateCrc32 to Compat
Added some xml comment
2021-08-25 12:53:36 +02:00
Wizou 166a35f732 added BotAuthIfNeeded 2021-08-24 17:24:46 +02:00
Wizou 53ee143a1d Removed dependency on Crc32.NET 2021-08-24 10:49:32 +02:00
Wizou 593463f46b Upgrade to layer 131
not available as JSON on official website https://core.telegram.org/schema
but found as TL files at https://github.com/telegramdesktop/tdesktop/tree/dev/Telegram/Resources/tl
2021-08-20 14:45:39 +02:00
Wizou 524cb71a65 FLOOD_WAIT_X throw exception if wait is more than 60 seconds 2021-08-20 03:41:00 +02:00
Wizou 2930bd2f9c Added .github folder 2021-08-20 03:20:20 +02:00
Wizou d61234cabc Make MillerRabinIterations configurable for slow devices 2021-08-20 02:33:43 +02:00
Wizou f2a1dbc20d Implement Telegram protocol safety checks 2021-08-20 02:13:58 +02:00
Wizou e205244cf7 Implement RSA_PAD algo for new public keys 2021-08-19 09:28:56 +02:00
Wizou 8aa07011e9 JSON serialization through JSONValue.ToString() 2021-08-19 06:56:55 +02:00
Wizou 6f77e828db Added new package logo from @MrVeil303 (resized 128x128 as recommended by Nuget) 2021-08-18 00:12:33 +02:00
Wizou d35de0f3e2 Version now 0.9.* 2021-08-16 23:08:16 +02:00
Wizou 2a3c64d3d6 fix build 2021-08-16 22:52:33 +02:00
Wizou 866c562d81 Add compatibility with .NET Standard 2.0 (multi-target) 2021-08-16 22:30:45 +02:00
Wizou 2bced387a1 Add helper for Peer ID 2021-08-16 05:56:39 +02:00
Wizou cc83944985 Better handle bare requests. Handle more service messages. 2021-08-14 15:15:41 +02:00
Wizou 39f03ed78f Support for FLOOD_WAIT_X 2021-08-14 08:55:30 +02:00
Wizou 70f9a61e17 updated readme badges 2021-08-13 17:32:07 +02:00
Wizou e01caba162 Fix issue with actual RpcResult in MsgContainer ; Parallelize upload of file parts 2021-08-13 07:06:44 +02:00
Wizou 897b61747a Reactor system for parallelization of requests 2021-08-13 00:28:34 +02:00
Wizou 5e6421d76e simplify ITLFunction<X> into ITLFunction 2021-08-12 12:37:56 +02:00
Wizou 83c0991770 XML comments: add URLs to API pages 2021-08-12 11:01:15 +02:00
Wizou 424e582446 README.md 2021-08-10 14:51:16 +02:00
Wizou 21caecd164 MTProto 2.0 2021-08-10 14:40:41 +02:00
Wizou ba90b47831 Added 2FA support 2021-08-10 08:25:37 +02:00
Wizou 3d540cdb8f Make TL classes partial so we can extend them with helpers in Helpers.TL.cs 2021-08-10 03:12:33 +02:00
wiz0u 9e7d85ec1d
Update README.md for new TL methods 2021-08-09 14:57:22 +02:00
wiz0u 6fb86b7fac
+ chat badge 2021-08-09 14:48:49 +02:00
wiz0u 67fb06f357 release.yml: deploy & notify in stages 2021-08-09 14:43:16 +02:00
wiz0u b697d4b5ea Fix YAML syntax 2021-08-09 14:36:05 +02:00
Wizou 041cee1438 don't trigger on pr 2021-08-09 14:22:33 +02:00
Wizou c4faf3934f ci: don't trigger on pr
release: Telegram notice
2021-08-09 14:13:32 +02:00
Wizou 674130e079 TL Functions are now direct Client methods ; Renamed/rationalized serialization methods 2021-08-09 11:41:50 +02:00
[-_-Mr_Veil-_-] 00211e5b5c Migrate to .NET Standard 2.0 2021-08-08 17:35:05 +03:00
Wizou 8ae3c2a283 ReadMe 2021-08-08 14:58:49 +02:00
Wizou d9efe4fae0 Add MIT license 2021-08-08 11:09:44 +02:00
Wizou ac41931cdf fix feed 2021-08-08 10:34:31 +02:00
Wizou fcf5a8dfbb 0.7.0 released 2021-08-08 10:32:32 +02:00
Wizou ca42bf406c renamed YAML files 2021-08-08 10:19:25 +02:00
45 changed files with 47526 additions and 9460 deletions

2
.github/FUNDING.yml vendored Normal file
View file

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

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,4 @@
contact_links:
- name: You have a question about the Telegram API or how to do something with WTelegramClient?
url: https://stackoverflow.com/questions/ask?tags=c%23+wtelegramclient+telegram-api
about: The answer to your question can be helpful to the community so it's better to ask them on StackOverflow --->

63
.github/dev.yml vendored Normal file
View file

@ -0,0 +1,63 @@
pr: none
trigger:
branches:
include: [ master ]
paths:
exclude: [ '.github', '*.md', 'Examples' ]
name: 4.3.2-dev.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
Release_Notes: $[replace(variables['Build.SourceVersionMessage'], '"', '''''')]
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: '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'
- 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'

67
.github/release.yml vendored Normal file
View file

@ -0,0 +1,67 @@
pr: none
trigger: none
name: 4.3.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
Release_Notes: $[replace(variables['releaseNotes'], '"', '''''')]
stages:
- stage: publish
jobs:
- job: publish
steps:
- checkout: self
persistCredentials: true
- task: UseDotNet@2
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
version: '9.x'
includePreviewVersions: true
- 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'
nuGetFeedType: 'external'
publishFeedCredentials: 'nuget.org'
- script: |
git tag $(Build.BuildNumber)
git push --tags
workingDirectory: $(Build.SourcesDirectory)
displayName: Git Tag
- 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)\"}"
}
waitForCompletion: 'false'

24
.github/workflows/autolock.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: 'Auto-Lock Issues'
on:
schedule:
- cron: '17 2 * * 1'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
discussions: write
concurrency:
group: lock-threads
jobs:
action:
runs-on: ubuntu-latest
steps:
- 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

624
EXAMPLES.md Normal file
View file

@ -0,0 +1,624 @@
# Example programs using WTelegramClient
For these examples to work as a fully-functional Program.cs, be sure to start with these lines:
```csharp
using System;
using System.Linq;
using TL;
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
await client.LoginUserIfNeeded();
```
In this case, environment variables are used for configuration so make sure to
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.
In real production code, you might want to properly test the success of each operation or handle exceptions,
and avoid calling the same methods (like `Messages_GetAllChats`) repetitively.
➡️ Use Ctrl-F to search this page for the example matching your needs
WTelegramClient covers 100% of Telegram Client API, much more than the examples below: check the [full API methods list](https://corefork.telegram.org/methods)!
More examples can also be found in the [Examples folder](https://github.com/wiz0u/WTelegramClient/tree/master/Examples) and in answers to [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient).
<a name="logging"></a>
## Change logging settings
By default, WTelegramClient logs are displayed on the Console screen.
If you are not in a Console app or don't want the logs on screen, you can redirect them as you prefer:
```csharp
// • Log to file in replacement of default Console screen logging, using this static variable:
static StreamWriter WTelegramLogs = new StreamWriter("WTelegram.log", true, Encoding.UTF8) { AutoFlush = true };
...
WTelegram.Helpers.Log = (lvl, str) => WTelegramLogs.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{"TDIWE!"[lvl]}] {str}");
// • Log to VS Output debugging pane in addition (+=) to the default Console screen logging:
WTelegram.Helpers.Log += (lvl, str) => System.Diagnostics.Debug.WriteLine(str);
// • In ASP.NET service, you will typically send logs to an ILogger:
WTelegram.Helpers.Log = (lvl, str) => _logger.Log((LogLevel)lvl, str);
// • Disable logging (⛔️𝗗𝗢𝗡'𝗧 𝗗𝗢 𝗧𝗛𝗜𝗦 as you won't be able to diagnose any upcoming problem):
WTelegram.Helpers.Log = (lvl, str) => { };
```
The `lvl` argument correspond to standard [LogLevel values](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel#fields)
<a name="msg-by-name"></a>
## Send a message to someone by @username
```csharp
var resolved = await client.Contacts_ResolveUsername("JsonDumpBot"); // username without the @
await client.SendMessageAsync(resolved, "/start");
```
*Note: This also works if the @username points to a channel/group, but you must already have joined that channel before sending a message to it.
If the username is invalid/unused, the API call raises an RpcException.*
<a name="markdown"></a>
<a name="html"></a>
## Convert message to/from HTML or Markdown, and send it to ourself (Saved Messages)
```csharp
// HTML-formatted text:
var text = $"Hello <u>dear <b>{HtmlText.Escape(client.User.first_name)}</b></u>\n" +
"Enjoy this <code>userbot</code> written with <a href=\"https://github.com/wiz0u/WTelegramClient\">WTelegramClient</a>";
var entities = client.HtmlToEntities(ref text);
var sent = await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
// if you need to convert a sent/received Message to HTML: (easier to store)
text = client.EntitiesToHtml(sent.message, sent.entities);
```
```csharp
// Markdown-style text:
var text2 = $"Hello __dear *{Markdown.Escape(client.User.first_name)}*__\n" +
"Enjoy this `userbot` written with [WTelegramClient](https://github.com/wiz0u/WTelegramClient)";
var entities2 = client.MarkdownToEntities(ref text2);
var sent2 = await client.SendMessageAsync(InputPeer.Self, text2, entities: entities2);
// if you need to convert a sent/received Message to Markdown: (easier to store)
text2 = client.EntitiesToMarkdown(sent2.message, sent2.entities);
```
See [HTML formatting style](https://core.telegram.org/bots/api/#html-style) and [MarkdownV2 formatting style](https://core.telegram.org/bots/api/#markdownv2-style) for details.
*Note: For the `tg://user?id=` notation to work, you need to pass the _users dictionary in arguments ([see below](#collect-users-chats))*
<a name="list-dialogs"></a>
## List all dialogs (chats/groups/channels/user chat) we are currently in
```csharp
var dialogs = await client.Messages_GetAllDialogs();
foreach (Dialog dialog in dialogs.dialogs)
{
switch (dialogs.UserOrChat(dialog))
{
case User user when user.IsActive: Console.WriteLine("User " + user); break;
case ChatBase chat when chat.IsActive: Console.WriteLine(chat); break;
}
//var latestMsg = dialogs.messages.FirstOrDefault(m => m.Peer.ID == dialog.Peer.ID && m.ID == dialog.TopMessage);
}
```
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#L18).
- To retrieve the dialog information about a specific [peer](README.md#terminology), use `client.Messages_GetPeerDialogs(inputPeer)`
<a name="list-chats"></a>
## List all chats (groups/channels NOT users) that we joined and send a message to one
```csharp
var chats = await client.Messages_GetAllChats();
foreach (var (id, chat) in chats.chats)
if (chat.IsActive)
Console.WriteLine($"{id} : {chat}");
Console.Write("Choose a chat ID to send a message to: ");
long chatId = long.Parse(Console.ReadLine());
await client.SendMessageAsync(chats.chats[chatId], "Hello, World");
```
Notes:
- This list does not include discussions with other users. For this, you need to use [Messages_GetAllDialogs](#list-dialogs).
- 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#L31)
<a name="list-members"></a>
## List the members from a chat
For a basic Chat: *(see Terminology in [ReadMe](README.md#terminology))*
```csharp
var chatFull = await client.Messages_GetFullChat(1234567890); // the chat we want
foreach (var (id, user) in chatFull.users)
Console.WriteLine(user);
```
For a Channel/Group:
```csharp
var chats = await client.Messages_GetAllChats();
var channel = (Channel)chats.chats[1234567890]; // the channel we want
for (int offset = 0; ;)
{
var participants = await client.Channels_GetParticipants(channel, null, offset);
foreach (var (id, user) in participants.users)
Console.WriteLine(user);
offset += participants.participants.Length;
if (offset >= participants.count || participants.participants.Length == 0) break;
}
```
For big Channel/Group, Telegram servers might limit the number of members you can obtain with the normal above method.
In this case, you can use the following helper method, but it can take several minutes to complete:
```csharp
var chats = await client.Messages_GetAllChats();
var channel = (Channel)chats.chats[1234567890]; // the channel we want
var participants = await client.Channels_GetAllParticipants(channel);
```
You can use specific filters, for example to list only the channel owner/admins:
```csharp
var participants = await client.Channels_GetParticipants(channel, filter: new ChannelParticipantsAdmins());
foreach (var participant in participants.participants) // This is the better way to enumerate the result
{
var user = participants.users[participant.UserID];
if (participant is ChannelParticipantCreator cpc) Console.WriteLine($"{user} is the owner '{cpc.rank}'");
else if (participant is ChannelParticipantAdmin cpa) Console.WriteLine($"{user} is admin '{cpa.rank}'");
}
```
*Note: It is not possible to list only the Deleted Accounts. Those will be automatically removed by Telegram from your group after a while*
<a name="history"></a>
## Fetch all messages (history) from a chat/user
```csharp
var chats = await client.Messages_GetAllChats();
InputPeer peer = chats.chats[1234567890]; // the chat (or User) we want
for (int offset_id = 0; ;)
{
var messages = await client.Messages_GetHistory(peer, offset_id);
if (messages.Messages.Length == 0) break;
foreach (var msgBase in messages.Messages)
{
var from = messages.UserOrChat(msgBase.From ?? msgBase.Peer); // from can be User/Chat/Channel
if (msgBase is Message msg)
Console.WriteLine($"{from}> {msg.message} {msg.media}");
else if (msgBase is MessageService ms)
Console.WriteLine($"{from} [{ms.action.GetType().Name[13..]}]");
}
offset_id = messages.Messages[^1].ID;
}
```
Notes:
- `peer` can also be a User, obtained through methods like [`Messages_GetAllDialogs`](#list-dialogs)
- To stop at a specific msg ID, use Messages_GetHistory `min_id` argument. For example, `min_id: dialog.read_inbox_max_id`
- To mark the message history as read, use: `await client.ReadHistory(peer);`
<a name="updates"></a>
## Monitor all Telegram events happening for the user
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#L21).
<a name="monitor-msg"></a>
## Monitor new messages being posted in chats in real-time
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 `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.
See also [explanation below](#message-user) to extract user/chat info from messages.
<a name="download"></a>
## Downloading photos, medias, files
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#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
```csharp
const int ChatId = 1234567890; // the chat we want
const string Filepath = @"C:\...\photo.jpg";
var chats = await client.Messages_GetAllChats();
InputPeer peer = chats.chats[ChatId];
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
// Photo 1 already on Telegram: latest photo found in the user's Saved Messages
var history = await client.Messages_GetHistory(InputPeer.Self);
PhotoBase photoFromTelegram = history.Messages.OfType<Message>().Select(m => m.media).OfType<MessageMediaPhoto>().First().photo;
// Photo 2 uploaded now from our computer:
var uploadedFile = await client.UploadFileAsync(@"C:\Pictures\flower.jpg");
// Photo 3 specified by external url:
const string photoUrl = "https://picsum.photos/310/200.jpg";
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");
```
*Note: Don't mix Photos and file Documents in your album, it doesn't work well*
<a name="forward"></a><a name="copy"></a>
## Forward or copy a message to another chat
```csharp
// Determine which chats and message to forward/copy
var chats = await client.Messages_GetAllChats();
var from_chat = chats.chats[1234567890]; // source chat
var to_chat = chats.chats[1234567891]; // destination chat
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.ForwardMessagesAsync(from_chat, [msg.ID], to_chat);
// • Copy the message without the "Forwarded" header (only the source message id is necessary)
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);
```
<a name="schedule-msg"></a>
## Schedule a message to be sent to a chat
```csharp
var chats = await client.Messages_GetAllChats();
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
```csharp
// • List all stickerSets the user has added to his account
var allStickers = await client.Messages_GetAllStickers();
foreach (var stickerSet in allStickers.sets)
Console.WriteLine($"Pack {stickerSet.short_name} contains {stickerSet.count} stickers");
//if you need details on each: var sticketSetDetails = await client.Messages_GetStickerSet(stickerSet);
// • Send a random sticker from the user's favorites stickers
var favedStickers = await client.Messages_GetFavedStickers();
var stickerDoc = favedStickers.stickers[new Random().Next(favedStickers.stickers.Length)];
await client.SendMessageAsync(InputPeer.Self, null, stickerDoc);
// • Send a specific sticker given the stickerset shortname and emoticon
var friendlyPanda = await client.Messages_GetStickerSet("Friendly_Panda");
var laughId = friendlyPanda.packs.First(p => p.emoticon == "😂").documents[0];
var laughDoc = friendlyPanda.documents.First(d => d.ID == laughId);
await client.SendMessageAsync(InputPeer.Self, null, laughDoc);
// • Send a GIF from an internet URL
await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDocumentExternal
{ url = "https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif" });
// • Send a GIF stored on the computer (you can save inputFile for later reuse)
var inputFile = await client.UploadFileAsync(@"C:\Pictures\Rotating_earth_(large).gif");
await client.SendMediaAsync(InputPeer.Self, null, inputFile);
// • Send a random dice/game-of-chance effect from the list of available "dices", see https://core.telegram.org/api/dice
var appConfig = await client.Help_GetAppConfig();
var emojies_send_dice = appConfig.config["emojies_send_dice"] as string[];
var dice_emoji = emojies_send_dice[new Random().Next(emojies_send_dice.Length)];
var diceMsg = await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDice { emoticon = dice_emoji });
Console.WriteLine("Dice result:" + ((MessageMediaDice)diceMsg.media).value);
// • Send an animated emoji with full-screen animation, see https://core.telegram.org/api/animated-emojis#emoji-reactions
// Please note that on Telegram Desktop, you won't view the effect from the sender user's point of view
// You can view the effect if you're on Telegram Android, or if you're the receiving user (instead of Self)
var msg = await client.SendMessageAsync(InputPeer.Self, "🎉");
await Task.Delay(5000); // wait for classic animation to finish
await client.SendMessageAsync(InputPeer.Self, "and now, full-screen animation on the above emoji");
// trigger the full-screen animation,
var typing = await client.Messages_SetTyping(InputPeer.Self, new SendMessageEmojiInteraction {
emoticon = "🎉", msg_id = msg.id,
interaction = new DataJSON { data = @"{""v"":1,""a"":[{""t"":0.0,""i"":1}]}" }
});
await Task.Delay(5000);
```
<a name="reaction"></a><a name="pinned"></a><a name="custom_emoji"></a>
## Fun with custom emojies and reactions on pinned messages
```csharp
// • Sending a message with custom emojies in Markdown to ourself:
var text = "Vicksy says Hi! ![👋](tg://emoji?id=5190875290439525089)";
var entities = client.MarkdownToEntities(ref text, premium: true);
await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
// also available in HTML: <tg-emoji emoji-id="5190875290439525089">👋</tg-emoji>
// • Fetch all available standard emoji reactions
var all_emoji = await client.Messages_GetAvailableReactions();
var chats = await client.Messages_GetAllChats();
var chat = chats.chats[1234567890]; // the chat we want
// • Check reactions available in this chat
var full = await client.GetFullChat(chat);
Reaction reaction = full.full_chat.AvailableReactions switch
{
ChatReactionsSome some => some.reactions[0], // only some reactions are allowed => pick the first
ChatReactionsAll all => // all reactions are allowed in this chat
all.flags.HasFlag(ChatReactionsAll.Flags.allow_custom) && client.User.flags.HasFlag(TL.User.Flags.premium)
? new ReactionCustomEmoji { document_id = 5190875290439525089 } // we can use custom emoji reactions here
: new ReactionEmoji { emoticon = all_emoji.reactions[0].reaction }, // else, pick the first standard emoji reaction
_ => null // reactions are not allowed in this chat
};
if (reaction == null) return;
// • Send the selected reaction on the last 2 pinned messages
var messages = await client.Messages_Search<InputMessagesFilterPinned>(chat, limit: 2);
foreach (var msg in messages.Messages)
await client.Messages_SendReaction(chat, msg.ID, reaction: new[] { reaction });
```
*Note: you can find custom emoji document IDs via API methods like [Messages_GetFeaturedEmojiStickers](https://corefork.telegram.org/methods#working-with-custom-animated-emojis) or inspecting messages entities. Access hash is not required*
<a name="join-channel"></a>
## Join a channel/group by their public name or invite link
* For a public channel/group `@channelname`
If you have a link of the form `https://t.me/channelname`, you need to extract the `channelname` from the URL.
You can resolve the channel/group username and join it like this:
```csharp
var resolved = await client.Contacts_ResolveUsername("channelname"); // without the @
if (resolved.Chat is Channel channel)
await client.Channels_JoinChannel(channel);
```
* For a private channel/group/chat, you need to have an invite link
Telegram invite links can typically have two forms: `https://tme/joinchat/HASH` or `https://tme/+HASH` *(note the '+' prefix here)*
To use them, you need to extract the `HASH` part from the URL and then you can use those two methods:
```csharp
var chatInvite = await client.Messages_CheckChatInvite("HASH"); // optional: get information before joining
await client.Messages_ImportChatInvite("HASH"); // join the channel/group
// Note: This works also with HASH invite links from public channel/group
```
`CheckChatInvite` can return [3 different types of invitation object](https://corefork.telegram.org/type/ChatInvite)
You can also use helper methods `AnalyzeInviteLink` and `GetMessageByLink` to more easily fetch information from links.
<a name="add-members"></a>
## Add/Invite/Remove someone in a chat
```csharp
var chats = await client.Messages_GetAllChats();
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:
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);
var invite = (ChatInviteExported)mcf.full_chat.ExportedInvite;
await client.SendMessageAsync(user, "Join our group with this link: " + invite.link);
// • Create a new invite link for the chat/channel, and send it to the user
var invite = (ChatInviteExported)await client.Messages_ExportChatInvite(chat, title: "MyLink");
await client.SendMessageAsync(user, "Join our group with this link: " + invite.link);
// • Revoke then delete that invite link (when you no longer need it)
await client.Messages_EditExportedChatInvite(chat, invite.link, revoked: true);
await client.Messages_DeleteExportedChatInvite(chat, invite.link);
// • Remove the user from a Chat/Channel/Group:
await client.DeleteChatUser(chat, user);
```
<a name="msg-by-phone"></a>
## Send a message to someone by phone number
```csharp
var contacts = await client.Contacts_ImportContacts(new[] { new InputPhoneContact { phone = "+PHONENUMBER" } });
if (contacts.imported.Length > 0)
await client.SendMessageAsync(contacts.users[contacts.imported[0].user_id], "Hello!");
```
*Note: Don't use this method too much. To prevent spam, Telegram may restrict your ability to add new phone numbers or ban your account.*
<a name="contacts"></a>
## Retrieve the current user's contacts list
There are two different methods. Here is the simpler one:
```csharp
var contacts = await client.Contacts_GetContacts();
foreach (User contact in contacts.users.Values)
Console.WriteLine($"{contact} {contact.phone}");
```
<a name="takeout"></a>
The second method uses the more complex GDPR export, **takeout session** system.
Here is an example on how to implement it:
```csharp
using TL.Methods; // methods as structures, for Invoke* calls
var takeout = await client.Account_InitTakeoutSession(contacts: true);
var finishTakeout = new Account_FinishTakeoutSession();
try
{
var savedContacts = await client.InvokeWithTakeout(takeout.id, new Contacts_GetSaved());
foreach (SavedPhoneContact contact in savedContacts)
Console.WriteLine($"{contact.first_name} {contact.last_name} {contact.phone}, added on {contact.date}");
finishTakeout.flags = Account_FinishTakeoutSession.Flags.success;
}
finally
{
await client.InvokeWithTakeout(takeout.id, finishTakeout);
}
```
<a name="collect-access-hash"></a>
<a name="collect-users-chats"></a>
<a name="user-or-chat"></a>
## 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.OnUpdates`.
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
private Dictionary<long, User> _users = new();
private Dictionary<long, ChatBase> _chats = new();
...
var dialogs = await client.Messages_GetAllDialogs();
dialogs.CollectUsersChats(_users, _chats);
private async Task OnUpdates(UpdatesBase updates)
{
updates.CollectUsersChats(_users, _chats);
...
}
// example of UserOrChat usage:
var firstPeer = dialogs.UserOrChat(dialogs.dialogs[0].Peer);
if (firstPeer is User firstUser) Console.WriteLine($"First dialog is with user {firstUser}");
else if (firstPeer is ChatBase firstChat) Console.WriteLine($"First dialog is {firstChat}");
```
*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/):
```csharp
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
client.TcpHandler = async (address, port) =>
{
var proxy = new Socks5ProxyClient(ProxyHost, ProxyPort, ProxyUsername, ProxyPassword);
//var proxy = xNet.Socks5ProxyClient.Parse("host:port:username:password");
return proxy.CreateConnection(address, port);
};
await client.LoginUserIfNeeded();
```
<a name="mtproxy"></a>
MTProxy (MTProto proxy) can be used to prevent ISP blocking Telegram servers, through the `client.MTProxyUrl` property:
```csharp
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/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*
<a name="2FA"></a>
## Change 2FA password
```csharp
const string old_password = "password"; // current password if any (unused otherwise)
const string new_password = "new_password"; // or null to disable 2FA
var accountPwd = await client.Account_GetPassword();
var password = accountPwd.current_algo == null ? null : await WTelegram.Client.InputCheckPassword(accountPwd, old_password);
accountPwd.current_algo = null; // makes InputCheckPassword generate a new password
var new_password_hash = new_password == null ? null : await WTelegram.Client.InputCheckPassword(accountPwd, new_password);
await client.Account_UpdatePasswordSettings(password, new Account_PasswordInputSettings
{
flags = Account_PasswordInputSettings.Flags.has_new_algo,
new_password_hash = new_password_hash?.A,
new_algo = accountPwd.new_algo,
hint = "new password hint",
});
```
<a name="database"></a><a name="sessionStore"></a><a name="customStore"></a>
## Store session data to database or elsewhere, instead of files
If you don't want to store session data into files *(for example if your VPS Hosting doesn't allow that)*, or just for easier management,
you can choose to store the session data somewhere else, like in a database.
The WTelegram.Client constructor takes an optional `sessionStore` parameter to allow storing sessions in an alternate manner.
Use it to pass a custom Stream-derived class that will **read** (first initial call to Length & Read) and **store** (subsequent Writes) session data to database.
You can find an example for such custom session store in [Examples/Program_Heroku.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_Heroku.cs?ts=4#L61)
<a name="e2e"></a><a name="secrets"></a>
## Send/receive end-to-end encrypted messages & files in Secret Chats
This can be done easily using the helper class `WTelegram.SecretChats` offering methods to manage/encrypt/decrypt secret chats & encrypted messages/files.
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).

BIN
Examples/ASPnet_webapp.zip Normal file

Binary file not shown.

View file

@ -0,0 +1,55 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using TL;
namespace WTelegramClientTest
{
static class Program_DownloadSavedMedia
{
// 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)");
var cts = new CancellationTokenSource();
await using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
var user = await client.LoginUserIfNeeded();
client.OnUpdates += Client_OnUpdates;
Console.ReadKey();
cts.Cancel();
async Task Client_OnUpdates(UpdatesBase 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
if (message.peer_id.ID != user.ID)
continue; // if it's not in the "Saved messages" chat, ignore it
if (message.media is MessageMediaDocument { document: Document document })
{
var filename = document.Filename; // use document original filename, or build a name from document ID & MIME type:
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, progress: (p, t) => cts.Token.ThrowIfCancellationRequested());
Console.WriteLine("Download finished");
}
else if (message.media is MessageMediaPhoto { photo: Photo photo })
{
var filename = $"{photo.id}.jpg";
Console.WriteLine("Downloading " + filename);
using var fileStream = File.Create(filename);
var type = await client.DownloadFileAsync(photo, fileStream);
fileStream.Close(); // necessary for the renaming
Console.WriteLine("Download finished");
if (type is not Storage_FileType.unknown and not Storage_FileType.partial)
File.Move(filename, $"{photo.id}.{type}", true); // rename extension
}
}
}
}
}
}

View file

@ -0,0 +1,58 @@
using System;
using System.Threading.Tasks;
using TL;
namespace WTelegramClientTest
{
static class Program_GetAllChats
{
// 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 > 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");
if (what == "api_hash") return Environment.GetEnvironmentVariable("api_hash");
if (what == "phone_number") return Environment.GetEnvironmentVariable("phone_number");
if (what == "verification_code") return null; // let WTelegramClient ask the user with a console prompt
if (what == "first_name") return "John"; // if sign-up is required
if (what == "last_name") return "Doe"; // if sign-up is required
if (what == "password") return "secret!"; // if user has enabled 2FA
return null;
}
static async Task Main(string[] _)
{
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})");
var chats = await client.Messages_GetAllChats(); // chats = groups/channels (does not include users dialogs)
Console.WriteLine("This user has joined the following:");
foreach (var (id, chat) in chats.chats)
switch (chat)
{
case Chat smallgroup when smallgroup.IsActive:
Console.WriteLine($"{id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members");
break;
case Channel channel when channel.IsChannel:
Console.WriteLine($"{id}: Channel {channel.username}: {channel.title}");
//Console.WriteLine($" → access_hash = {channel.access_hash:X}");
break;
case Channel group: // no broadcast flag => it's a big group, also called supergroup or megagroup
Console.WriteLine($"{id}: Group {group.username}: {group.title}");
//Console.WriteLine($" → access_hash = {group.access_hash:X}");
break;
}
Console.Write("Type a chat ID to send a message: ");
long chatId = long.Parse(Console.ReadLine());
var target = chats.chats[chatId];
Console.WriteLine($"Sending a message in chat {chatId}: {target.Title}");
// Next line implicitely creates an adequate InputPeer from ChatBase: (with the access_hash if these is one)
InputPeer peer = target;
await client.SendMessageAsync(peer, "Hello, World");
}
}
}

139
Examples/Program_Heroku.cs Normal file
View file

@ -0,0 +1,139 @@
using Npgsql;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using TL;
// This is an example userbot designed to run on a Heroku account with a PostgreSQL database for session storage
// This userbot simply answer "Pong" when someone sends him a "Ping" private message (or in Saved Messages)
// To use/install/deploy this userbot ➡️ follow the steps at the end of this file
// When run locally, close the window or type ALT-F4 to exit cleanly and save session (similar to Heroku SIGTERM)
namespace WTelegramClientTest
{
static class Program_Heroku
{
static WTelegram.Client Client;
static User My;
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[] _)
{
var exit = new SemaphoreSlim(0);
AppDomain.CurrentDomain.ProcessExit += (s, e) => exit.Release(); // detect SIGTERM to exit gracefully
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);
await using (Client)
{
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();
dialogs.CollectUsersChats(Users, Chats);
await exit.WaitAsync();
}
}
private static async Task Client_OnUpdates(UpdatesBase updates)
{
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
{
Console.WriteLine(update.GetType().Name);
if (update is UpdateNewMessage { message: Message { peer_id: PeerUser { user_id: var user_id } } msg }) // private message
if (!msg.flags.HasFlag(Message.Flags.out_)) // ignore our own outgoing messages
if (Users.TryGetValue(user_id, out var user))
{
Console.WriteLine($"New message from {user}: {msg.message}");
if (msg.message.Equals("Ping", StringComparison.OrdinalIgnoreCase))
await Client.SendMessageAsync(user, "Pong");
}
}
}
}
#region PostgreSQL session store
class PostgreStore : Stream
{
private readonly NpgsqlConnection _sql;
private readonly string _sessionName;
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>
public PostgreStore(string databaseUrl, string sessionName = null)
{
_sessionName = sessionName ?? "Heroku";
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))
create.ExecuteNonQuery();
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql);
using var rdr = cmd.ExecuteReader();
if (rdr.Read())
_dataLen = (_data = rdr[0] as byte[]).Length;
}
protected override void Dispose(bool disposing)
{
_sql.Dispose();
}
public override int Read(byte[] buffer, int offset, int count)
{
Array.Copy(_data, 0, buffer, offset, count);
return count;
}
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock()
{
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;
public override long Position { get => 0; set { } }
public override bool CanSeek => false;
public override bool CanRead => true;
public override bool CanWrite => true;
public override long Seek(long offset, SeekOrigin origin) => 0;
public override void SetLength(long value) { }
public override void Flush() { }
}
#endregion
}
/******************************************************************************************************************************
HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- From your Heroku.com account dashboard, create a new app
- Navigate to the app Resources and add the add-on "Heroku Postgres"
- Navigate to the app Settings, click Reveal Config Vars and save the Heroku git URL and the value of DATABASE_URL
- Add a new var named "api_hash" with your api hash obtained from https://my.telegram.org/apps
- Add a new var named "phone_number" with the phone_number of the user this userbot will manage
- Scroll down to Buildpacks and add this URL: https://github.com/jincod/dotnetcore-buildpack.git
- 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 > 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"
- You can now commit & push your git sources to Heroku, they will be compiled and deployed/run as a web app
- The userbot should work fine for 1 minute, but it will be taken down because it is not a web app. Let's fix that
- Navigate to the app Resources, copy the line starting with: web cd $HOME/.......
- Back in Visual Studio (or Explorer), at the root of the repository create a Procfile text file (without extension)
- Paste inside the line you copied, and replace the initial "web" with "worker:" (don't forget the colon)
- Commit and push the Git changes to Heroku. Wait until the deployment is complete.
- Verify that the Resources "web" line has changed to "worker" and is enabled (use the pencil icon if necessary)
- Now your userbot should be running 24/7.
- To prevent AUTH_KEY_DUPLICATED issues, set a SESSION_NAME env variable in your local VS project with a value like "PC"
DISCLAIMER: I'm not affiliated nor expert with Heroku, so if you have any problem with the above I might not be able to help
******************************************************************************************************************************/

View file

@ -0,0 +1,72 @@
using System;
using System.Threading.Tasks;
using TL;
namespace WTelegramClientTest
{
static class Program_ListenUpdates
{
static WTelegram.Client Client;
static WTelegram.UpdateManager Manager;
static User My;
// 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);
await using (Client)
{
Manager = Client.WithUpdateManager(Client_OnUpdate/*, "Updates.state"*/);
My = await Client.LoginUserIfNeeded();
// 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(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(Update update)
{
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 HandleMessage(MessageBase messageBase, bool edit = false)
{
if (edit) Console.Write("(Edit): ");
switch (messageBase)
{
case Message m: Console.WriteLine($"{Peer(m.from_id) ?? m.post_author} in {Peer(m.peer_id)}> {m.message}"); break;
case MessageService ms: Console.WriteLine($"{Peer(ms.from_id)} in {Peer(ms.peer_id)} [{ms.action.GetType().Name[13..]}]"); break;
}
return Task.CompletedTask;
}
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

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using TL;
using WTelegram;
namespace WTelegramClientTest
{
static class Program_SecretChats
{
static Client Client;
static SecretChats Secrets;
static ISecretChat ActiveChat; // the secret chat currently selected
static readonly Dictionary<long, User> Users = [];
static readonly Dictionary<long, ChatBase> Chats = [];
// 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);
Client = new Client(Environment.GetEnvironmentVariable);
Secrets = new SecretChats(Client, "Secrets.bin");
AppDomain.CurrentDomain.ProcessExit += (s, e) => { Secrets.Dispose(); Client.Dispose(); };
SelectActiveChat();
Client.OnUpdates += Client_OnUpdates;
var myself = await Client.LoginUserIfNeeded();
Users[myself.id] = myself;
Console.WriteLine($"We are logged-in as {myself}");
var dialogs = await Client.Messages_GetAllDialogs(); // load the list of users/chats
dialogs.CollectUsersChats(Users, Chats);
Console.WriteLine(@"Available commands:
/request <UserID> Initiate Secret Chat with user (see /users)
/discard [delete] Terminate active secret chat [and delete history]
/select <ChatID> Select another Secret Chat
/photo filename.jpg Send a JPEG photo
/read Mark active discussion as read
/users List collected users and their IDs
Type a command, or a message to send to the active secret chat:");
do
{
try
{
var line = Console.ReadLine();
if (line.StartsWith('/'))
{
if (line == "/discard delete") { await Secrets.Discard(ActiveChat.ChatId, true); SelectActiveChat(); }
else if (line == "/discard") { await Secrets.Discard(ActiveChat.ChatId, false); SelectActiveChat(); }
else if (line == "/read") await Client.Messages_ReadEncryptedHistory(ActiveChat.Peer, DateTime.UtcNow);
else if (line == "/users") foreach (var user in Users.Values) Console.WriteLine($"{user.id,-10} {user}");
else if (line.StartsWith("/select ")) SelectActiveChat(int.Parse(line[8..]));
else if (line.StartsWith("/request "))
if (Users.TryGetValue(long.Parse(line[9..]), out var user))
SelectActiveChat(await Secrets.Request(user));
else
Console.WriteLine("User not found");
else if (line.StartsWith("/photo "))
{
var media = new TL.Layer46.DecryptedMessageMediaPhoto { caption = line[7..] };
var file = await Secrets.UploadFile(File.OpenRead(line[7..]), media);
var sent = await Secrets.SendMessage(ActiveChat.ChatId, new TL.Layer73.DecryptedMessage { random_id = Helpers.RandomLong(),
media = media, flags = TL.Layer73.DecryptedMessage.Flags.has_media }, file: file);
}
else Console.WriteLine("Unrecognized command");
}
else if (ActiveChat == null) Console.WriteLine("No active secret chat");
else await Secrets.SendMessage(ActiveChat.ChatId, new TL.Layer73.DecryptedMessage { message = line, random_id = Helpers.RandomLong() });
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
} while (true);
}
private static async Task Client_OnUpdates(UpdatesBase updates)
{
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
switch (update)
{
case UpdateEncryption ue: // Change in Secret Chat status
await Secrets.HandleUpdate(ue);
break;
case UpdateNewEncryptedMessage unem: // Encrypted message or service message:
if (unem.message.ChatId != ActiveChat?.ChatId) SelectActiveChat(unem.message.ChatId);
foreach (var msg in Secrets.DecryptMessage(unem.message))
{
if (msg.Media != null && unem.message is EncryptedMessage { file: EncryptedFile ef })
{
int slash = msg.Media.MimeType?.IndexOf('/') ?? 0; // quick & dirty conversion from MIME type to file extension
var filename = $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.{(slash > 0 ? msg.Media.MimeType[(slash + 1)..] : "bin")}";
Console.WriteLine($"{unem.message.ChatId}> {msg.Message} [attached file downloaded to {filename}]");
using var output = File.Create(filename);
await Secrets.DownloadFile(ef, msg.Media, output);
}
else if (msg.Action == null) Console.WriteLine($"{unem.message.ChatId}> {msg.Message}");
else Console.WriteLine($"{unem.message.ChatId}> Service Message {msg.Action.GetType().Name[22..]}");
}
break;
case UpdateEncryptedChatTyping:
case UpdateEncryptedMessagesRead:
//Console.WriteLine(update.GetType().Name);
break;
}
}
private static void SelectActiveChat(int newActiveChat = 0)
{
ActiveChat = Secrets.Chats.FirstOrDefault(sc => newActiveChat == 0 || sc.ChatId == newActiveChat);
Console.WriteLine("Active secret chat ID: " + ActiveChat?.ChatId);
}
}
}

BIN
Examples/WinForms_app.zip Normal file

Binary file not shown.

362
FAQ.md Normal file
View file

@ -0,0 +1,362 @@
# FAQ
Before asking questions, make sure to **[read through the ReadMe first](README.md)**,
take a look at the [example programs](EXAMPLES.md) or [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient),
and refer to the [API method list](https://corefork.telegram.org/methods) for the full range of Telegram services available in this library.
➡️ Use Ctrl-F to search this page for the information you seek
<a name="remove-logs"></a>
## 1. How to remove the Console logs?
Writing the library logs to the Console is the default behavior of the `WTelegram.Helpers.Log` delegate.
You can change the delegate with the `+=` operator to **also** write them somewhere else, or with the `=` operator to prevent them from being printed to screen and instead write them somewhere (file, logger, ...).
In any case, it is not recommended to totally ignore those logs because you wouldn't be able to diagnose a problem after it happens.
Read the [example about logging settings](EXAMPLES.md#logging) for how to write logs to a file.
<a name="multiple-users"></a>
## 2. How to handle multiple user accounts
The WTelegram.session file contains the authentication keys negociated for the current user.
You could switch the current user via an `Auth_Logout` followed by a `LoginUserIfNeeded` but that would require the user to sign in with a verification_code each time.
Instead, if you want to deal with multiple users from the same machine, the recommended solution is to have a different session file for each user.
This can be done by having your Config callback reply with a different filename (or folder) for "**session_pathname**" for each user.
This way, you can keep separate session files (each with their authentication keys) for each user.
If you need to manage these user accounts in parallel, you can create multiple instances of WTelegram.Client,
and give them a Config callback that will select a different session file ;
for example: `new WTelegram.Client(what => Config(what, "session42"))`
Also please note that the session files are encrypted with your api_hash (or session_key), so if you change it, the existing session files can't be read anymore.
Your api_id/api_hash represents your application, and shouldn't change with each user account the application will manage.
<a name="GUI"></a>
<a name="ASPNET"></a>
## 3. How to use the library in a WinForms, WPF or ASP.NET application
The library should work without a problem in such applications.
The difficulty might be in your Config callback when the user must enter the verification code or password, as you can't use `Console.ReadLine` here.
For GUI apps, an easy solution is to call `Interaction.InputBox("Enter verification code")` instead.
This might require adding a reference *(and `using`)* to the Microsoft.VisualBasic assembly.
A more complex solution requires the use of a `ManualResetEventSlim` that you will wait for in Config callback,
and when the user has provided the verification_code through your app, you "set" the event to release your Config callback so it can return the code.
Another solution is to use the [alternative login method](README.md#alternative-simplified-configuration--login),
calling `client.Login(...)` as the user provides the requested configuration elements.
You can download such full example apps [for WinForms](Examples/WinForms_app.zip) and [for ASP.NET](Examples/ASPnet_webapp.zip)
<a name="access-hash"></a>
## 4. How to use IDs and access_hash? Why the error `CHANNEL_INVALID` or `USER_ID_INVALID`?
⚠️ In Telegram Client API *(contrary to Bot API)*, you **cannot** interact with channels/users/etc. with only their IDs.
You also need to obtain their `access_hash` which is specific to the resource you want to access AND to the currently logged-in user.
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 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...).
You obtain the `access_hash` through TL **description structures** like `Channel`, `User`, `Photo`, `Document` that you receive through updates
or when you query them through API methods like `Messages_GetAllChats`, `Messages_GetAllDialogs`, `Contacts_ResolveUsername`, etc...
You can use the [`UserOrChat` and `CollectUsersChats` methods](EXAMPLES.md#collect-users-chats) to help you in obtaining/collecting
the description structures you receive via API calls or updates.
Once you obtained the description structure, there are 2 methods for building your `Input...` request structure:
* **Recommended:** Just pass that description structure you already have, in place of the `Input...` argument, it will work!
*The implicit conversion operators on base classes like `ChatBase/UserBase` will create the `Input...` structure for you automatically.*
* Alternatively, you can manually create the `Input...` structure yourself by extracting the `access_hash` from the description structure
*Note: An `access_hash` obtained from a User/Channel structure with flag `min` may not be usable for most requests. See [Min constructors](https://core.telegram.org/api/min).*
<a name="dev-versions"></a>
## 5. I need to test a feature that has been recently developed but seems not available in my program
The developmental versions of the library are now available as **pre-release** on Nuget (with `-dev` in the version number)
So make sure you tick the checkbox "Include prerelease" in Nuget manager and/or navigate to the Versions list then select the highest `x.x.x-dev.x` version to install in your program.
<a name="wrong-server"></a>
## 6. Telegram asks me to signup (firstname, lastname) even for an existing account
This happens when you connect to Telegram Test servers instead of Production servers.
On these separate test servers, all created accounts and chats are periodically deleted, so you shouldn't use them under normal circumstances.
You can verify this is your issue by looking at [WTelegram logs](EXAMPLES.md#logging) on the line `Connected to (Test) DC x...`
This wrong-server problem typically happens when you use WTelegramClient Github source project in your application in DEBUG builds.
It is **not recommended** to use WTelegramClient in source code form.
Instead, you should use the Nuget manager to **install package WTelegramClient** into your application.
*And remember to delete the WTelegram.session file to force a new login on the correct server.*
If you use the Github source project in an old .NET Framework 4.x or .NET Core x.x application, you may also experience the following error
> System.TypeInitializationException (FileNotFoundException for "System.Text.Json Version=5.0.0.0 ...")
To fix this, you should also switch to using the [WTelegramClient Nuget package](https://www.nuget.org/packages/WTelegramClient) as it will install the required dependencies for it to work.
<a name="abuse"></a>
## 7. I get errors FLOOD_WAIT_X or PEER_FLOOD, PHONE_NUMBER_BANNED, USER_DEACTIVATED_BAN. I can't import phone numbers.
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.
An account that was restricted due to reported spam might receive PEER_FLOOD errors. Read [Telegram Spam FAQ](https://telegram.org/faq_spam) to learn more.
If you think your phone number was banned from Telegram for a wrong reason, you may try to contact [recover@telegram.org](mailto:recover@telegram.org), explaining what you were doing.
In any case, WTelegramClient is not responsible for the bad usage of the library and we are not affiliated to Telegram teams, so there is nothing we can do.
<a name="prevent-ban"></a>
## 8. How to NOT get banned from Telegram?
**Do not share publicly your app's ID and hash!** They cannot be regenerated and are bound to your Telegram account.
From the [official documentation](https://core.telegram.org/api/obtaining_api_id):
> Note that all API client libraries are strictly monitored to prevent abuse.
> If you use the Telegram API for flooding, spamming, faking subscriber and view counters of channels, you **will be banned forever**.
> Due to excessive abuse of the Telegram API, **all accounts that sign up or log in using unofficial Telegram clients are automatically
> put under observation** to avoid violations of the [Terms of Service](https://core.telegram.org/api/terms).
Here are some advices from [another similar library](https://github.com/gotd/td/blob/main/.github/SUPPORT.md#how-to-not-get-banned):
1. This client is unofficial, Telegram treats such clients suspiciously, especially fresh ones.
2. Use regular bots instead of userbots whenever possible.
3. If you still want to automate things with a user, use it passively (i.e. receive more than sending).
4. When using it with a user:
* Do not use QR code login, this will result in permaban.
* Do it with extreme care.
* Do not use VoIP numbers.
* Do not abuse, spam or use it for other suspicious activities.
* Implement a rate limiting system.
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 `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.
9. When creating a new API ID/Hash, I recommend you use your own phone number with long history of normal Telegram usage, rather than a brand new phone number with short history.
In particular, DON'T create an API ID/Hash for every phone numbers you will control. One API ID/Hash represents your application, which can be used to control several user accounts.
10. If you actually do use the library to spam, scam, or other stuff annoying to everybody, GTFO and don't cry that you got banned using WTelegramClient. Some people don't seem to realize by themselves that what they plan to do with the library is actually negative for the community and are surprised that they got caught.
We don't support such use of the library, and will not help people asking for support if we suspect them of mass-user manipulation.
11. If your client displays Telegram channels to your users, you have to support and display [official sponsored messages](https://core.telegram.org/api/sponsored-messages).
<a name="chat-id"></a>
## 9. Why the error `CHAT_ID_INVALID`?
Most chat groups you see are likely of type `Channel`, not `Chat`.
This difference is important to understand. Please [read about the Terminology in ReadMe](README.md#terminology).
You typically get the error `CHAT_ID_INVALID` when you try to call API methods designed specifically for a `Chat`, with the ID of a `Channel`.
All API methods taking a `long chat_id` as a direct method parameter are for Chats and cannot be used with Channels.
There is probably another method achieving the same result but specifically designed for Channels, and it will have a similar name, but beginning with `Channels_` ...
However, note that those Channel-compatible methods will require an `InputChannel` or `InputPeerChannel` object as argument instead of a simple channel ID.
That object must be created with both fields `channel_id` and `access_hash` correctly filled. You can read more about this in [FAQ #4](#access-hash).
<a name="chats-chats"></a>
<a name="chat-not-found"></a>
## 10. `chats.chats[id]` fails. My chats list is empty or does not contain the chat I'm looking for.
There can be several reasons why `chats.chats` doesn't contain the chat you expect:
- You're searching for a user instead of a chat ID.
Private messages with a user are not called "chats". See [Terminology in ReadMe](README.md#terminology).
To obtain the list of users (as well as chats and channels) the logged-in user is currenly engaged in a discussion with, you should [use the API method `Messages_GetAllDialogs`](EXAMPLES.md#list-dialogs)
- The currently logged-in user account has not joined this particular chat.
API method [`Messages_GetAllChats`](https://corefork.telegram.org/method/messages.getAllChats) will only return those chat groups/channels the user is in, not all Telegram chat groups.
If you're looking for other Telegram groups/channels/users, try API methods [`Contacts_ResolveUsername`](EXAMPLES.md#msg-by-name) or `Contacts_Search`
- You're trying to use a Bot API (or TDLib) numerical ID, like -1001234567890
Telegram Client API don't use these kind of IDs for chats. Remove the -100 prefix and try again with the rest (1234567890).
- the `chats.chats` dictionary is empty.
This is the case if you are logged-in as a brand new user account (that hasn't join any chat groups/channels)
or if you are connected to a Test DC (a Telegram datacenter server for tests) instead of Production DC
([read FAQ #6](#wrong-server) for more)
To help determine if `chats.chats` is empty or does not contain a certain chat, you should [dump the chat list to the screen](EXAMPLES.md#list-chats)
or simply use a debugger: Place a breakpoint after the `Messages_GetAllChats` call, run the program up to there, and use a Watch pane to display the content of the chats.chats dictionary.
<a name="shutdown"></a>
## 11. I get "Connection shut down" errors in my logs
There are various reasons why you may get this error. Here are the explanation and how to solve it:
1) On secondary DCs *(DC used to download files)*, a Connection shut down is considered "normal"
Your main DC is the one WTelegramClient connects to during login. Secondary DC connections are established and maintained when you download files.
The DC number for an operation or error is indicated with a prefix like "2>" on the log line.
If Telegram servers decide to shutdown this secondary connection, it's not an issue, WTelegramClient will re-establish the connection later if necessary.
2) Occasional connection shutdowns on the main DC should be caught by WTelegramClient and the reactor should automatically reconnect to the DC
*(up to `MaxAutoReconnects` times)*.
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 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 (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.
In this case, you can use the `PingInterval` property to increase the delay between pings *(for example 300 seconds instead of 60)*.
5) If you're using an [MTProxy](EXAMPLES.md#proxy), some of them are known to be quite unstable. You may want to try switching to another MTProxy that is more stable.
<a name="TLSharp"></a>
## 12. How to migrate from TLSharp? How to sign-in/sign-up/register account properly?
First, make sure you read the [ReadMe documentation](README.md) completely, it contains essential information and a quick tutorial to easily understand how to correctly use the library.
WTelegramClient approach is much more simpler and secure than TLSharp.
All client APIs have dedicated async methods that you can call like this: `await client.Method_Name(param1, param2, ...)`
See the [full method list](https://core.telegram.org/methods) (just replace the dot with an underscore in the names)
A session file is created or resumed automatically on startup, and maintained up-to-date automatically throughout the session.
That session file is incompatible with TLSharp so you cannot reuse a TLSharp .dat file. You'll need to create a new session.
To fight against the reselling of fake user accounts, we don't support the import/export of session files from external sources.
**DON'T** call methods Auth_SendCode/SignIn/SignUp/... because all the login phase is handled automatically by calling `await client.LoginUserIfNeeded()` after creating the client.
Your Config callback just need to provide the various login answers if they are needed (see [ReadMe](README.md) and [FAQ #4](#GUI)).
In particular, it will detect and handle automatically and properly the various login cases/particularity like:
* Login not necessary (when a session is resumed with an already logged-in user)
* Logout required (if you want to change the logged-in user)
* 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 (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...
<a name="heroku"></a><a name="vps"></a><a name="host"></a>
## 13. How to host my userbot online?
If you need your userbot to run 24/7, you would typically design your userbot as a Console program, compiled for Linux or Windows,
and hosted online on any [VPS Hosting](https://www.google.com/search?q=vps+hosting) (Virtual Private Server).
Pure WebApp hosts might not be adequate as they will recycle (stop) your app if there is no incoming HTTP requests.
There are many cheap VPS Hosting offers available, for example Heroku:
See [Examples/Program_Heroku.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_Heroku.cs?ts=4#L9) for such an implementation and the steps to host/deploy it.
<a name="secrets"></a>
## 14. Secret Chats implementation details
The following choices were made while implementing Secret Chats in WTelegramClient:
- It may not support remote antique Telegram clients *(prior to 2018, still using insecure MTProto 1.0)*
- It doesn't store outgoing messages *(so if remote client was offline for a week and ask us to resend old messages, we will send void data)*
- It doesn't store incoming messages on disk *(it's up to you if you want to store them)*
- If you pass `DecryptMessage` parameter `fillGaps: true` *(default)*, incoming messages are ensured to be delivered to you in correct order.
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 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.
<a name="compile"></a>
## 15. The example codes don't compile on my machine
The snippets of example codes found in the [ReadMe](README.md) or [Examples](EXAMPLES.md) pages were written for .NET 5 / C# 9 minimum.
If you're having compiler problem on code constructs such as `using`, `foreach`, `[^1]` or about "Deconstruct",
that typically means you're still using an obsolete version of .NET (Framework 4.x or Core)
Here are the recommended actions to fix your problem:
- Create a new project for .NET 6+ (in Visual Studio 2019 or more recent):
- Select File > New > Project
- Search for "C# Console"
- Select the **Console App**, but NOT Console App (.NET Framework) !
- On the framework selection page, choose .NET 6.0 or more recent
- Now you can start developing for WTelegramClient 🙂
- If you don't want to target a recent version of .NET, you can upgrade your existing project to use the latest version of the C# language:
- Close Visual Studio
- Edit your *.csproj file **with Notepad**
- Within the first `<PropertyGroup>`, add the following line:
`<LangVersion>latest</LangVersion>`
- Save, close Notepad and reopen your project in Visual Studio
- If you still have issues on some `foreach` constructs, add this class somewhere in your project:
```csharp
static class Extensions
{
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
{
key = tuple.Key;
value = tuple.Value;
}
}
```
Also, remember to add a `using TL;` at the top of your files to have access to all the Telegram API methods.
<a name="troubleshoot"></a>
# Troubleshooting guide
Here is a list of common issues and how to fix them so that your program work correctly:
1) Are you using the Nuget package or the library source code?
It is not recommended to copy/compile the source code of the library for a normal usage.
When built in DEBUG mode, the source code connects to Telegram test servers (see also [FAQ #6](#wrong-server)).
So you can either:
- **Recommended:** Use the [official Nuget package](https://www.nuget.org/packages/WTelegramClient)
- Build your code in RELEASE mode
- Modify your config callback to reply to "server_address" with the IP address of Telegram production servers (as found on your API development tools)
2) Did you call `Login` or `LoginUserIfNeeded` succesfully?
If you don't complete authentication as a user (or bot), you have access to a very limited subset of Telegram APIs.
Make sure your calls succeed and don't throw an exception.
3) Did you use `await` with every Client methods?
This library is completely Task-based. You should learn, understand and use the [asynchronous model of C# programming](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) before proceeding further.
Using `.Result` or `.Wait()` can lead to deadlocks.
4) Is your program ending immediately instead of waiting for Updates?
Your program must be running/waiting continuously in order for the background Task to receive and process the Updates.
So make sure your main program doesn't end immediately or dispose the client too soon (via `using`?).
For a console program, this is typical done by waiting for a key or some close event.
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

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

225
README.md
View file

@ -1,114 +1,209 @@
# WTelegramClient
### _Telegram client library written 100% in C# and .NET Core_
[![API Layer](https://img.shields.io/badge/API_Layer-216-blueviolet)](https://corefork.telegram.org/methods)
[![NuGet version](https://img.shields.io/nuget/v/WTelegramClient?color=00508F)](https://www.nuget.org/packages/WTelegramClient/)
[![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)
## How to use
## *_Telegram Client API library written 100% in C# and .NET_*
:warning: This library relies on asynchronous C# programming (`async/await`) so make sure you are familiar with this before proceeding.
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.
After installing WTelegramClient through Nuget, your first Console program will be as simple as:
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.
# How to use
After installing WTelegramClient through [Nuget](https://www.nuget.org/packages/WTelegramClient/), your first Console program will be as simple as:
```csharp
static Task Main(string[] _)
static async Task Main(string[] _)
{
using var client = new WTelegram.Client();
await client.ConnectAsync();
var user = await client.UserAuthIfNeeded();
Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})");
var myself = await client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {myself} (id {myself.id})");
}
```
When run, this will prompt you interactively for your App **api_id** and **api_hash** (that you obtain through Telegram's [API development tools](https://my.telegram.org/apps) page) and try to connect to Telegram servers.
When run, this will prompt you interactively for your App **api_hash** and **api_id** (that you obtain through Telegram's
[API development tools](https://my.telegram.org/apps) page) and try to connect to Telegram servers.
Those api hash/id represent your application and one can be used for handling many user accounts.
Then it will attempt to sign-in as a user for which you must enter the **phone_number** and the **verification_code** that will be sent to this user (for example through SMS or another Telegram client app the user is connected to).
Then it will attempt to sign-in *(login)* as a user for which you must enter the **phone_number** and the **verification_code**
that will be sent to this user (for example through SMS, Email, or another Telegram client app the user is connected to).
If the verification succeeds but the phone number is unknown to Telegram, the user might be prompted to sign-up (accepting the Terms of Service) and enter their **first_name** and **last_name**.
If the verification succeeds but the phone number is unknown to Telegram, the user might be prompted to sign-up
*(register their account by accepting the Terms of Service)* and provide their **first_name** and **last_name**.
If the account already exists and has enabled two-step verification (2FA) a **password** might be required.
In some case, Telegram may request that you associate an **email** with your account for receiving login verification codes,
you may skip this step by leaving **email** empty, otherwise the email address will first receive an **email_verification_code**.
All these login scenarios are handled automatically within the call to `LoginUserIfNeeded`.
And that's it, you now have access to the full range of Telegram services, mainly through calls to `await client.CallAsync(...)`
After login, you now have access to the **[full range of Telegram Client APIs](https://corefork.telegram.org/methods)**.
All those API methods require `using TL;` namespace and are called with an underscore instead of a dot in the method name, like this: `await client.Method_Name(...)`
# Saved session
If you run this program again, you will notice that the previous prompts are gone and you are automatically logged-on and ready to go.
If you run this program again, you will notice that only **api_hash** is requested, the other prompts are gone and you are automatically logged-on and ready to go.
This is because WTelegramClient saves (typically in the encrypted file **bin\WTelegram.session**) its state and the authentication keys that were negociated with Telegram so that you needn't sign-in again every time.
This is because WTelegramClient saves (typically in the encrypted file **bin\WTelegram.session**) its state
and the authentication keys that were negotiated with Telegram so that you needn't sign-in again every time.
That file path is configurable, and under various circumstances (changing user or server address) you may want to change it or simply delete the existing session file in order to restart the authentification process.
That file path is configurable (**session_pathname**), and under various circumstances *(changing user or server address, write permissions)*
you may want to change it or simply delete the existing session file in order to restart the authentification process.
# Non-interactive configuration
Your next step will probably be to provide a configuration to the client so that the required elements (in bold above) are not prompted through the Console but answered by your program.
Your next step will probably be to provide a configuration to the client so that the required elements are not prompted through the Console but answered by your program.
For that you need to write a method that will provide the answer, and pass it on the constructor:
To do this, you need to write a method that will provide the answers, and pass it on the constructor:
```csharp
static string Config(string what)
{
if (what == "api_id") return "YOUR_API_ID";
if (what == "api_hash") return "YOUR_API_HASH";
if (what == "phone_number") return "+12025550156";
if (what == "verification_code") { Console.Write("Code: "); return Console.ReadLine(); }
if (what == "first_name") return "John"; // if sign-up is required
if (what == "last_name") return "Doe"; // if sign-up is required
return null;
switch (what)
{
case "api_id": return "YOUR_API_ID";
case "api_hash": return "YOUR_API_HASH";
case "phone_number": return "+12025550156";
case "verification_code": Console.Write("Code: "); return Console.ReadLine();
case "first_name": return "John"; // if sign-up is required
case "last_name": return "Doe"; // if sign-up is required
case "password": return "secret!"; // if user has enabled 2FA
default: return null; // let WTelegramClient decide the default config
}
}
...
using var client = new WTelegram.Client(Config);
```
There are other configuration items that are queried to your method but returning `null` let WTelegramClient choose a default adequate value.
Those shown above are the only ones that have no default values and should be provided by your method.
The configuration items shown above are the only ones that have no default values and are required to be provided by your method.
Returning `null` for verification_code or password will show a prompt for console apps, or an error otherwise
*(see [FAQ #3](https://wiz0u.github.io/WTelegramClient/FAQ#GUI) for WinForms)*
Returning `""` for verification_code requests the resending of the code through another system (SMS or Call).
The constructor also takes another optional delegate parameter that will be called for any other Update and other information/status/service messages that Telegram sends unsollicited, independently of your API requests.
Another simple approach is to pass `Environment.GetEnvironmentVariable` as the config callback and define the configuration items as environment variables
*(undefined variables get the default `null` behavior)*.
Finally, if you want to redirect the library logs to your logger instead of the Console, you can install a delegate in the `WTelegram.Helpers.Log` static property.
Its `int` argument is the log severity, compatible with the classic [LogLevel enum](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel)
Its `int` argument is the log severity, compatible with the [LogLevel enum](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel).
# Alternative simplified configuration & login
Since version 3.0.0, a new approach to login/configuration has been added. Some people might find it easier to deal with:
```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)
{
while (client.User == null)
switch (await client.Login(loginInfo)) // returns which config is needed to continue login
{
case "verification_code": Console.Write("Code: "); loginInfo = Console.ReadLine(); break;
case "name": loginInfo = "John Doe"; break; // if sign-up is required (first/last_name)
case "password": loginInfo = "secret!"; break; // if user has enabled 2FA
default: loginInfo = null; break;
}
Console.WriteLine($"We are logged-in as {client.User} (id {client.User.id})");
}
```
With this method, you can choose in some cases to interrupt the login loop via a `return` instead of `break`, and resume it later
by calling `DoLogin(requestedCode)` again once you've obtained the requested code/password/etc...
See [WinForms example](https://wiz0u.github.io/WTelegramClient/Examples/WinForms_app.zip) and [ASP.NET example](https://wiz0u.github.io/WTelegramClient/Examples/ASPnet_webapp.zip)
# Example of API call
:information_source: The Telegram API makes extensive usage of base and derived classes, so be ready to use the various syntaxes C# offer to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
> 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` )
To find which derived classes are available for a given base class, the fastest is to check our [TL.Schema.cs](src/TL.Schema.cs) source file as they are listed in groups.
All the Telegram API classes/methods are fully documented through Intellisense: Place your mouse over a class/method name,
or start typing the call arguments to see a tooltip displaying their description, the list of derived classes and a web link to the official API page.
The Telegram [API object classes](https://core.telegram.org/schema) are defined in the `TL` namespace, and the request classes ([API functions](https://core.telegram.org/methods)) usable with `client.CallAsync` are under the `TL.Fn` static class.
The Telegram [API object classes](https://corefork.telegram.org/schema) are defined in the `TL` namespace,
and the [API functions](https://corefork.telegram.org/methods) are available as async methods of `Client`.
Below is an example of calling the [messages.getAllChats](https://core.telegram.org/method/messages.getAllChats) API function and enumerating the various groups/channels the user is in:
Below is an example of calling the [messages.getAllChats](https://corefork.telegram.org/method/messages.getAllChats) API function,
enumerating the various groups/channels the user is in, and then using `client.SendMessageAsync` helper function to easily send a message:
```csharp
using TL;
...
var chatsBase = await client.CallAsync(new Fn.Messages_GetAllChats { });
if (chatsBase is not Messages_Chats { chats: var chats }) throw new Exception("hu?");
var chats = await client.Messages_GetAllChats();
Console.WriteLine("This user has joined the following:");
foreach (var chat in chats)
switch (chat)
{
case Chat smallgroup when (smallgroup.flags & Chat.Flags.deactivated) == 0:
Console.WriteLine($"{smallgroup.id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members");
break;
case Channel channel when (channel.flags & Channel.Flags.broadcast) != 0:
Console.WriteLine($"{channel.id}: Channel {channel.username}: {channel.title}");
break;
case Channel group:
Console.WriteLine($"{group.id}: Group {group.username}: {group.title}");
break;
}
foreach (var (id, chat) in chats.chats)
if (chat.IsActive)
Console.WriteLine($"{id,10}: {chat}");
Console.Write("Type a chat ID to send a message: ");
long chatId = long.Parse(Console.ReadLine());
var target = chats.chats[chatId];
Console.WriteLine($"Sending a message in chat {chatId}: {target.Title}");
await client.SendMessageAsync(target, "Hello, World");
```
➡️ You can find lots of useful code snippets in [EXAMPLES](https://wiz0u.github.io/WTelegramClient/EXAMPLES)
and more detailed programs in the [Examples subdirectory](https://github.com/wiz0u/WTelegramClient/tree/master/Examples).
➡️ Check [the FAQ](https://wiz0u.github.io/WTelegramClient/FAQ#compile) if example codes don't compile correctly on your machine, or other troubleshooting.
<a name="terminology"></a>
# Terminology in Telegram Client API
In the API, Telegram uses some terms/classnames that can be confusing as they differ from the terms shown to end-users:
- `Channel`: A (large or public) chat group *(sometimes called [supergroup](https://corefork.telegram.org/api/channel#supergroups))*,
or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (the `broadcast` flag differentiate those)
- `Chat`: A private [basic chat group](https://corefork.telegram.org/api/channel#basic-groups) with less than 200 members
(it may be migrated to a supergroup `Channel` with a new ID when it gets bigger or public, in which case the old `Chat` will still exist but will be `deactivated`)
**⚠️ Most chat groups you see are really of type `Channel`, not `Chat`!**
- **chats**: In plural or general meaning, it means either `Chat` or `Channel` *(therefore, no private user discussions)*
- `Peer`: Either a `Chat`, a `Channel` or a `User`
- **Dialog**: Status of chat with a `Peer` *(draft, last message, unread count, pinned...)*. It represents each line from your Telegram chat list.
- **Access Hash**: Telegram requires you to provide a specific `access_hash` for users, channels, and other resources before interacting with them.
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
An invalid API request can result in a RpcException being raised, reflecting the [error code and status text](https://core.telegram.org/api/errors) of the problem.
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)
Beyond CallAsync, the Client class offers a few other methods to simplify the sending of files, medias or messages.
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 override include: **session_pathname, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code**
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`
# Development status
The library is already well usable for many scenarios involving automated steps based on API requests/responses.
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**
Here are the main expected developments:
- [x] Encrypt session file
- [x] Support SignUp of unregistered users
- [x] Improve code Generator (import of TL-schema JSONs)
- [ ] Improve Nuget deployment experience (debug symbols? XML documentation?)
- [ ] Convert API functions classes to real methods and serialize structures without using Reflection
- [ ] Separate task/thread for reading/handling update messages independently from CallAsync
- [ ] Support MTProto 2.0
- [ ] Support users with 2FA enabled
- [ ] Support secret chats end-to-end encryption & PFS
- [ ] Support all service messages
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).
------------
[![Build Status](https://dev.azure.com/wiz0u/WTelegramClient/_apis/build/status/wiz0u.WTelegramClient?branchName=master)](https://dev.azure.com/wiz0u/WTelegramClient/_build/latest?definitionId=7&branchName=master)
I've added several useful converters, implicit cast or helper properties to various API objects so that they are more easy to manipulate.
Beyond the TL async methods, the Client class offers a few other methods to simplify the sending/receiving of files, medias or messages,
as well as generic handling of chats/channels.
This library works best with **.NET 5.0+** (faster, no dependencies) and is also available for **.NET Standard 2.0** (.NET Framework 4.6.1+ & .NET Core 2.0+) and **Xamarin/Mono.Android**
# Library uses and limitations
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](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
It has been tested in a Console app, [in Windows Forms](https://wiz0u.github.io/WTelegramClient/Examples/WinForms_app.zip),
[in ASP.NET webservice](https://wiz0u.github.io/WTelegramClient/Examples/ASPnet_webapp.zip), and in Xamarin/Android.
Don't use this library for Spam or Scam. Respect Telegram [Terms of Service](https://telegram.org/tos)
as well as the [API Terms of Service](https://core.telegram.org/api/terms) or you might get banned from Telegram servers.
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://buymeacoffee.com/wizou) ❤ This will help the project keep going.
© 2021-2025 Olivier Marcoux

View file

@ -1,31 +0,0 @@
trigger:
- master
name: 0.7.0-alpha.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
includesymbols: true
versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber'
buildProperties: 'NoWarn="1573;1591;0419";Version=$(Build.BuildNumber)'
# buildProperties: 'NoWarn="1573;1591;0419";AllowedOutputExtensionsInPackageBuildOutputFolder=".dll;.xml;.pdb"'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.*upkg'
publishPackageMetadata: true
nuGetFeedType: 'internal'
publishVstsFeed: 'wiz0u'

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>

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -1,28 +0,0 @@
trigger: none
name: 0.7.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
packagesToPack: '**/*.csproj'
includesymbols: true
versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber'
buildProperties: 'NoWarn="1573;1591;0419";Version=$(Build.BuildNumber)'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.*upkg'
nuGetFeedType: 'external'
publishFeedCredentials: 'nuget.org'

950
src/Client.Helpers.cs Normal file
View file

@ -0,0 +1,950 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using TL;
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
namespace WTelegram
{
partial class Client
{
/// <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 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 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)
{
var client = await GetClientForDC(-_dcSession.DcID, true);
using (stream)
{
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 = 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 (read < FilePartSize && bytesLeft != 0) throw new WTException($"Failed to fully read stream ({read},{bytesLeft})");
async Task SavePart(int file_part, byte[] bytes)
{
try
{
if (isBig)
await client.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
else
await client.Upload_SaveFilePart(file_id, file_part, bytes);
lock (tasks) { transmitted += bytes.Length; tasks.Remove(file_part); }
progress?.Invoke(transmitted, length);
}
catch (Exception)
{
abort = true;
throw;
}
finally
{
_parallelTransfers.Release();
}
}
}
Task[] remainingTasks;
lock (tasks) remainingTasks = [.. tasks.Values];
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
: 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="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 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="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 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 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="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 uploadedFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
{
mimeType ??= Path.GetExtension(uploadedFile.Name)?.ToLowerInvariant() switch
{
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
".mp4" => "video",
".gif" => "image/gif",
".webp" => "image/webp",
".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 = 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);
}
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="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, LinkPreview preview = LinkPreview.BelowText)
{
UpdatesBase updates;
long random_id = Helpers.RandomLong();
if (media == null)
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: 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)
{
switch (update)
{
case UpdateMessageID updMsgId when updMsgId.random_id == random_id: msgId = updMsgId.id; break;
case UpdateNewMessage { message: Message message } when message.id == msgId: return message;
case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg;
}
}
return null;
}
/// <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>
/// <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 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, bool videoUrlAsFile = false)
{
System.Net.Http.HttpClient httpClient = null;
int i = 0, length = medias.Count;
var multiMedia = new InputSingleMedia[length];
var random_id = Helpers.RandomLong();
foreach (var media in medias)
{
var ism = multiMedia[i] = new InputSingleMedia { random_id = random_id + i, media = media };
i++;
retry:
switch (ism.media)
{
case InputMediaUploadedPhoto imup:
var mmp = (MessageMediaPhoto)await this.Messages_UploadMedia(peer, imup);
ism.media = mmp.photo;
break;
case InputMediaUploadedDocument imud:
var mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imud);
ism.media = mmd.document;
break;
case InputMediaPhotoExternal impe:
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();
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
return await UploadFileAsync(stream, filename);
}
}
}
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: 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)
{
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;
}
/// <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 },
InputPeerUser ipu => new PeerUser { user_id = ipu.user_id },
InputPeerChat ipc => new PeerChat { chat_id = ipc.chat_id },
InputPeerChannel ipch => new PeerChannel { channel_id = ipch.channel_id },
InputPeerUserFromMessage ipufm => new PeerUser { user_id = ipufm.user_id },
InputPeerChannelFromMessage ipcfm => new PeerChannel { channel_id = ipcfm.channel_id },
_ => null,
};
/// <summary>Download a 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="photoSize">A specific size/version of the photo, or <see langword="null"/> to download the largest version of the 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, PhotoSizeBase photoSize = null, ProgressCallback progress = null)
{
if (photoSize is PhotoStrippedSize psp)
return InflateStrippedThumb(outputStream, psp.bytes) ? Storage_FileType.jpeg : 0;
photoSize ??= photo.LargestPhotoSize;
var fileLocation = photo.ToFileLocation(photoSize);
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>
/// <param name="thumbSize">A specific size/version of the document thumbnail to download, or <see langword="null"/> to download the document itself</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<string> DownloadFileAsync(Document document, Stream outputStream, PhotoSizeBase thumbSize = null, ProgressCallback progress = null)
{
if (thumbSize is PhotoStrippedSize psp)
return InflateStrippedThumb(outputStream, psp.bytes) ? "image/jpeg" : null;
var fileLocation = document.ToFileLocation(thumbSize);
var fileType = await DownloadFileAsync(fileLocation, outputStream, document.dc_id, thumbSize?.FileSize ?? document.size, progress);
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>
/// <param name="dc_id">(optional) DC on which the file is stored</param>
/// <param name="fileSize">(optional) Expected file size</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>The file type</returns>
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);
using var writeSem = new SemaphoreSlim(1);
bool canSeek = outputStream.CanSeek;
long streamStartPos = canSeek ? outputStream.Position : 0;
long fileOffset = 0, maxOffsetSeen = 0;
long transmitted = 0;
var tasks = new Dictionary<long, Task>();
progress?.Invoke(0, fileSize);
bool abort = false;
while (!abort)
{
await _parallelTransfers.WaitAsync();
var task = LoadPart(fileOffset);
lock (tasks) tasks[fileOffset] = task;
if (dc_id == 0) { await task; dc_id = client._dcSession.DcID; }
if (!canSeek) await task;
fileOffset += FilePartSize;
if (fileSize != 0 && fileOffset >= fileSize)
{
if (await task != ((fileSize - 1) % FilePartSize) + 1)
throw new WTException("Downloaded file size does not match expected file size");
break;
}
async Task<int> LoadPart(long offset)
{
Upload_FileBase fileBase;
try
{
fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize);
}
catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X")
{
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")
{
abort = true;
return 0;
}
catch (Exception)
{
abort = true;
throw;
}
finally
{
_parallelTransfers.Release();
}
if (fileBase is not Upload_File fileData)
throw new WTException("Upload_GetFile returned unsupported " + fileBase?.GetType().Name);
if (fileData.bytes.Length != FilePartSize) abort = true;
if (fileData.bytes.Length != 0)
{
fileType = fileData.type;
await writeSem.WaitAsync();
try
{
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);
}
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)
{
abort = true;
throw;
}
finally
{
writeSem.Release();
}
}
lock (tasks) tasks.Remove(offset);
return fileData.bytes.Length;
}
}
Task[] remainingTasks;
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);
return fileType;
}
/// <summary>Download the profile photo for a given peer into the outputStream</summary>
/// <param name="peer">User, Chat or Channel</param>
/// <param name="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
/// <param name="big">Whether to download the high-quality version of the picture</param>
/// <param name="miniThumb">Whether to extract the embedded very low-res thumbnail (synchronous, no actual download needed)</param>
/// <returns>The file type of the photo, or 0 if no photo available</returns>
public async Task<Storage_FileType> DownloadProfilePhotoAsync(IPeerInfo peer, Stream outputStream, bool big = false, bool miniThumb = false)
{
int dc_id;
long photo_id;
byte[] stripped_thumb;
switch (peer)
{
case User user:
if (user.photo == null) return 0;
dc_id = user.photo.dc_id;
photo_id = user.photo.photo_id;
stripped_thumb = user.photo.stripped_thumb;
break;
case ChatBase { Photo: var photo }:
if (photo == null) return 0;
dc_id = photo.dc_id;
photo_id = photo.photo_id;
stripped_thumb = photo.stripped_thumb;
break;
default:
return 0;
}
if (miniThumb && !big)
return InflateStrippedThumb(outputStream, stripped_thumb) ? Storage_FileType.jpeg : 0;
var fileLocation = new InputPeerPhotoFileLocation { peer = peer.ToInputPeer(), photo_id = photo_id };
if (big) fileLocation.flags |= InputPeerPhotoFileLocation.Flags.big;
return await DownloadFileAsync(fileLocation, outputStream, dc_id);
}
private static bool InflateStrippedThumb(Stream outputStream, byte[] stripped_thumb)
{
if (stripped_thumb == null || stripped_thumb.Length <= 3 || stripped_thumb[0] != 1)
return false;
var header = Helpers.StrippedThumbJPG;
outputStream.Write(header, 0, 164);
outputStream.WriteByte(stripped_thumb[1]);
outputStream.WriteByte(0);
outputStream.WriteByte(stripped_thumb[2]);
outputStream.Write(header, 167, header.Length - 167);
outputStream.Write(stripped_thumb, 3, stripped_thumb.Length - 3);
outputStream.WriteByte(0xff);
outputStream.WriteByte(0xd9);
return true;
}
/// <summary>Get all chats, channels and supergroups</summary>
public async Task<Messages_Chats> Messages_GetAllChats()
{
var dialogs = await Messages_GetAllDialogs();
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>
/// <param name="folder_id"><a href="https://corefork.telegram.org/api/folders#peer-folders">Peer folder ID, for more info click here</a></param>
/// <returns>See <a href="https://corefork.telegram.org/constructor/messages.dialogs"/></returns>
public async Task<Messages_Dialogs> Messages_GetAllDialogs(int? folder_id = null)
{
var dialogs = await this.Messages_GetDialogs(folder_id: folder_id);
switch (dialogs)
{
case Messages_DialogsSlice mds:
var dialogList = new List<DialogBase>();
var messageList = new List<MessageBase>();
int skip = 0;
while (dialogs.Dialogs.Length > skip)
{
dialogList.AddRange(skip == 0 ? dialogs.Dialogs : dialogs.Dialogs[skip..]);
messageList.AddRange(dialogs.Messages);
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];
mds.messages = [.. messageList];
return mds;
case Messages_Dialogs md: return md;
default: throw new WTException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
}
}
/// <summary>Helper method that tries to fetch all participants from a Channel (beyond Telegram server-side limitations)</summary>
/// <param name="channel">The channel to query</param>
/// <param name="includeKickBan">Also fetch the kicked/banned members?</param>
/// <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 <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 = [], users = [] };
var user_ids = new HashSet<long>();
var participants = new List<ChannelParticipantBase>();
var mcf = await this.Channels_GetFullChannel(channel);
result.count = mcf.full_chat.ParticipantsCount;
if (result.count > 2000 && ((Channel)mcf.chats[channel.ChannelId]).IsChannel)
Helpers.Log(2, "Fetching all participants on a big channel can take several minutes...");
await GetWithFilter(new ChannelParticipantsAdmins());
await GetWithFilter(new ChannelParticipantsBots());
await GetWithFilter(new ChannelParticipantsSearch { q = "" }, (f, c) => new ChannelParticipantsSearch { q = f.q + c }, alphabet1);
if (includeKickBan)
{
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];
return result;
async Task GetWithFilter<T>(T filter, Func<T, char, T> recurse = null, string alphabet = null) where T : ChannelParticipantsFilter
{
Channels_ChannelParticipants ccp;
int maxCount = 0;
for (int offset = 0; ;)
{
cancellationToken.ThrowIfCancellationRequested();
ccp = await this.Channels_GetParticipants(channel, filter, offset, 1024, 0);
if (ccp.count > maxCount) maxCount = ccp.count;
foreach (var kvp in ccp.chats) result.chats[kvp.Key] = kvp.Value;
foreach (var kvp in ccp.users) result.users[kvp.Key] = kvp.Value;
lock (participants)
foreach (var participant in ccp.participants)
if (user_ids.Add(participant.UserId))
participants.Add(participant);
offset += ccp.participants.Length;
if (offset >= ccp.count || ccp.participants.Length == 0) break;
}
Helpers.Log(0, $"GetParticipants({(filter as ChannelParticipantsSearch)?.q}) returned {ccp.count}/{maxCount}.\tAccumulated count: {participants.Count}");
if (recurse != null && (ccp.count < maxCount - 100 || ccp.count == 200 || ccp.count == 1000))
foreach (var c in alphabet)
await GetWithFilter(recurse(filter, c), recurse, c == 'А' ? alphabet : alphabet2);
}
}
/// <summary>Helper simplified method: Get the admin log of a <a href="https://corefork.telegram.org/api/channel">channel/supergroup</a> <para>See <a href="https://corefork.telegram.org/method/channels.getAdminLog"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403 (<a href="https://corefork.telegram.org/method/channels.getAdminLog#possible-errors">details</a>)</para></summary>
/// <param name="channel">Channel</param>
/// <param name="q">Search query, can be empty</param>
/// <param name="events_filter">Event filter</param>
/// <param name="admin">Only show events from this admin</param>
public async Task<Channels_AdminLogResults> Channels_GetAdminLog(InputChannelBase channel, ChannelAdminLogEventsFilter.Flags events_filter = 0, string q = null, InputUserBase admin = null)
{
var admins = admin == null ? null : new[] { admin };
var result = await this.Channels_GetAdminLog(channel, q, events_filter: events_filter, admins: admins);
var resultFull = result;
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;
}
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<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),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// <summary>Generic helper: Kick a user from a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/channels.editBanned"/><br/> and <a href="https://corefork.telegram.org/method/messages.deleteChatUser"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="user">User to be removed</param>
public Task<UpdatesBase> DeleteChatUser(InputPeer peer, InputUser user) => peer switch
{
InputPeerChat chat => this.Messages_DeleteChatUser(chat.chat_id, user, true),
InputPeerChannel channel => this.Channels_EditBanned(channel, user, new ChatBannedRights { flags = ChatBannedRights.Flags.view_messages }),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// <summary>Generic helper: Leave a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.deleteChatUser"/><br/> and <a href="https://corefork.telegram.org/method/channels.leaveChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel to leave</param>
public Task<UpdatesBase> LeaveChat(InputPeer peer) => peer switch
{
InputPeerChat chat => this.Messages_DeleteChatUser(chat.chat_id, InputUser.Self, true),
InputPeerChannel channel => this.Channels_LeaveChannel(channel),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// <summary>Generic helper: Make a user admin in a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.editChatAdmin"/><br/> and <a href="https://corefork.telegram.org/method/channels.editAdmin"/> [bots: ✓]</para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="user">The user to make admin</param>
/// <param name="is_admin">Whether to make them admin</param>
public async Task<UpdatesBase> EditChatAdmin(InputPeer peer, InputUserBase user, bool is_admin)
{
switch (peer)
{
case InputPeerChat chat:
await this.Messages_EditChatAdmin(chat.chat_id, user, is_admin);
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)0x1E8BF : 0 }, null);
default:
throw new ArgumentException(OnlyChatChannel);
}
}
/// <summary>Generic helper: Change the photo of a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.editChatPhoto"/><br/> and <a href="https://corefork.telegram.org/method/channels.editPhoto"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="photo">New photo</param>
public Task<UpdatesBase> EditChatPhoto(InputPeer peer, InputChatPhotoBase photo) => peer switch
{
InputPeerChat chat => this.Messages_EditChatPhoto(chat.chat_id, photo),
InputPeerChannel channel => this.Channels_EditPhoto(channel, photo),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// <summary>Generic helper: Edit the name of a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.editChatTitle"/><br/> and <a href="https://corefork.telegram.org/method/channels.editTitle"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">Chat/Channel</param>
/// <param name="title">New name</param>
public Task<UpdatesBase> EditChatTitle(InputPeer peer, string title) => peer switch
{
InputPeerChat chat => this.Messages_EditChatTitle(chat.chat_id, title),
InputPeerChannel channel => this.Channels_EditTitle(channel, title),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// <summary>Get full info about a Chat or Channel [bots: ✓] <para>See <a href="https://corefork.telegram.org/method/messages.getFullChat"/><br/> and <a href="https://corefork.telegram.org/method/channels.getFullChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
/// <param name="peer">Chat/Channel</param>
public Task<Messages_ChatFull> GetFullChat(InputPeer peer) => peer switch
{
InputPeerChat chat => this.Messages_GetFullChat(chat.chat_id),
InputPeerChannel channel => this.Channels_GetFullChannel(channel),
_ => throw new ArgumentException(OnlyChatChannel),
};
/// <summary>Generic helper: Delete a Chat or Channel <para>See <a href="https://corefork.telegram.org/method/messages.deleteChat"/><br/> and <a href="https://corefork.telegram.org/method/channels.deleteChannel"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403,406</para></summary>
/// <param name="peer">Chat/Channel to delete</param>
public async Task<UpdatesBase> DeleteChat(InputPeer peer)
{
switch (peer)
{
case InputPeerChat chat:
await this.Messages_DeleteChat(chat.chat_id);
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);
default:
throw new ArgumentException(OnlyChatChannel);
}
}
/// <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>
public Task<Messages_MessagesBase> GetMessages(InputPeer peer, params InputMessage[] id)
=> peer is InputPeerChannel channel ? this.Channels_GetMessages(channel, id) : this.Messages_GetMessages(id);
/// <summary>Generic helper: Delete messages by IDs [bots: ✓]<br/>Messages are deleted for all participants <para>See <a href="https://corefork.telegram.org/method/messages.deleteMessages"/><br/> and <a href="https://corefork.telegram.org/method/channels.deleteMessages"/></para> <para>Possible <see cref="RpcException"/> codes: 400,403</para></summary>
/// <param name="peer">User/Chat/Channel</param>
/// <param name="id">IDs of messages to delete</param>
public Task<Messages_AffectedMessages> DeleteMessages(InputPeer peer, params int[] id)
=> peer is InputPeerChannel channel ? this.Channels_DeleteMessages(channel, id) : this.Messages_DeleteMessages(id, true);
/// <summary>Generic helper: Marks message history as read. <para>See <a href="https://corefork.telegram.org/method/messages.readHistory"/><br/> and <a href="https://corefork.telegram.org/method/channels.readHistory"/></para> <para>Possible <see cref="RpcException"/> codes: 400</para></summary>
/// <param name="peer">User/Chat/Channel</param>
/// <param name="max_id">If a positive value is passed, only messages with identifiers less or equal than the given one will be marked read</param>
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[] UrlSeparator = ['?', '#', '/'];
/// <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, IDictionary<long, ChatBase> chats = null)
{
int start = url.IndexOf("//");
start = url.IndexOf('/', start + 2) + 1;
int end = url.IndexOfAny(UrlSeparator, start);
if (end == -1) end = url.Length;
if (start == 0 || end == start) throw new ArgumentException("Invalid URL");
string hash;
if (url[start] == '+')
hash = url[(start + 1)..end];
else if (string.Compare(url, start, "joinchat/", 0, 9, StringComparison.OrdinalIgnoreCase) == 0)
hash = url[(end + 1)..];
else
{
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);
if (join)
try
{
var res = await this.Messages_ImportChatInvite(hash);
if (res.Chats.Values.FirstOrDefault() is ChatBase chat) return chat;
}
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "INVITE_REQUEST_SENT") { }
switch (chatInvite)
{
case ChatInviteAlready cia: return cia.chat;
case ChatInvitePeek cip: return cip.chat;
case ChatInvite ci:
ChatPhoto chatPhoto = null;
if (ci.photo is Photo photo)
{
var stripped_thumb = photo.sizes.OfType<PhotoStrippedSize>().FirstOrDefault()?.bytes;
chatPhoto = new ChatPhoto
{
dc_id = photo.dc_id,
photo_id = photo.id,
stripped_thumb = stripped_thumb,
flags = (stripped_thumb != null ? ChatPhoto.Flags.has_stripped_thumb : 0) |
(photo.flags.HasFlag(Photo.Flags.has_video_sizes) ? ChatPhoto.Flags.has_video : 0),
};
}
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,
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 = 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 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 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);
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]);
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
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}");
}
}
}
}

File diff suppressed because it is too large Load diff

121
src/Compat.cs Normal file
View file

@ -0,0 +1,121 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.ComponentModel;
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 partial class Compat
{
internal static BigInteger BigEndianInteger(byte[] value) => new(value, true, true);
internal static IPEndPoint IPEndPoint_Parse(string addr) => IPEndPoint.Parse(addr);
}
}
#else // Compatibility shims for methods missing in netstandard2.0:
namespace WTelegram
{
static partial class Compat
{
internal static BigInteger BigEndianInteger(byte[] value)
{
var data = new byte[value.Length + 1];
value.CopyTo(data, 1);
Array.Reverse(data);
return new BigInteger(data);
}
internal static byte[] ToByteArray(this BigInteger bigInteger, bool isUnsigned, bool isBigEndian)
{
if (!isBigEndian || !isUnsigned) throw new ArgumentException("Unexpected parameters to ToByteArray");
var result = bigInteger.ToByteArray();
if (result[^1] == 0) result = result[0..^1];
Array.Reverse(result);
return result;
}
internal static long GetBitLength(this BigInteger bigInteger)
{
var bytes = bigInteger.ToByteArray();
var length = bytes.LongLength * 8L;
int lastByte = bytes[^1];
while ((lastByte & 0x80) == 0) { length--; lastByte = (lastByte << 1) + 1; }
return length;
}
public static V GetValueOrDefault<K, V>(this IReadOnlyDictionary<K, V> dictionary, K key, V defaultValue = default)
=> dictionary.TryGetValue(key, out V value) ? value : defaultValue;
public static void Deconstruct<K, V>(this KeyValuePair<K, V> kvp, out K key, out V value) { key = kvp.Key; value = kvp.Value; }
internal static IPEndPoint IPEndPoint_Parse(string addr)
{
int colon = addr.LastIndexOf(':');
return new IPEndPoint(IPAddress.Parse(addr[0..colon]), int.Parse(addr[(colon + 1)..]));
}
internal static void ImportFromPem(this RSA rsa, string pem)
{
var header = pem.IndexOf("-----BEGIN RSA PUBLIC KEY-----");
var footer = pem.IndexOf("-----END RSA PUBLIC KEY-----");
if (header == -1 || footer <= header) throw new ArgumentException("Invalid RSA Public Key");
byte[] bytes = System.Convert.FromBase64String(pem[(header+30)..footer]);
if (bytes.Length != 270 || BinaryPrimitives.ReadInt64BigEndian(bytes) != 0x3082010A02820101 || bytes[265] != 0x02 || bytes[266] != 0x03)
throw new ArgumentException("Unrecognized sequence in RSA Public Key");
rsa.ImportParameters(new RSAParameters { Modulus = bytes[8..265], Exponent = bytes[267..270] });
}
}
}
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
namespace System.Runtime.CompilerServices
{
internal static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, Range range)
{
if (array == null) throw new ArgumentNullException();
var (offset, length) = range.GetOffsetAndLength(array.Length);
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);
return dest;
}
}
[EditorBrowsable(EditorBrowsableState.Never)]
internal class IsExternalInit { }
}
#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

@ -4,31 +4,42 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TL;
using static WTelegram.Compat;
namespace WTelegram
{
internal static class Encryption
public static class Encryption
{
internal static readonly RNGCryptoServiceProvider RNG = new();
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = new();
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = [];
internal static readonly RandomNumberGenerator RNG = RandomNumberGenerator.Create();
internal static readonly Aes AesECB = Aes.Create();
internal static async Task CreateAuthorizationKey(Client client, Session session)
static Encryption()
{
if (PublicKeys.Count == 0) LoadDefaultPublicKey();
AesECB.Mode = CipherMode.ECB;
AesECB.Padding = PaddingMode.Zeros;
if (AesECB.BlockSize != 128) throw new WTException("AES Blocksize is not 16 bytes");
}
internal static async Task CreateAuthorizationKey(Client client, Session.DCSession session)
{
if (PublicKeys.Count == 0) LoadDefaultPublicKeys();
var sha1 = SHA1.Create();
var sha256 = SHA256.Create();
//1)
var reqPQ = new Fn.ReqPQ() { nonce = new Int128(RNG) };
await client.SendAsync(reqPQ, false);
var nonce = new TL.Int128(RNG);
var resPQ = await client.ReqPqMulti(nonce);
//2)
var reply = await client.RecvInternalAsync();
if (reply is not ResPQ resPQ) throw new ApplicationException($"Expected ResPQ but got {reply.GetType().Name}");
if (resPQ.nonce != reqPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (resPQ.nonce != nonce) throw new WTException("Nonce mismatch");
var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey);
if (fingerprint == 0) throw new ApplicationException("Couldn't match any server_public_key_fingerprints");
if (fingerprint == 0) throw new WTException("Couldn't match any server_public_key_fingerprints");
var publicKey = PublicKeys[fingerprint];
Helpers.Log(2, $"Selected public key with fingerprint {fingerprint:X}");
//3)
long retry_id = 0;
@ -36,256 +47,524 @@ namespace WTelegram
ulong p = Helpers.PQFactorize(pq);
ulong q = pq / p;
//4)
var new_nonce = new Int256(RNG);
var reqDHparams = MakeReqDHparam(fingerprint, PublicKeys[fingerprint], new PQInnerData
var pqInnerData = new PQInnerDataDc
{
pq = resPQ.pq,
p = Helpers.ToBigEndian(p),
q = Helpers.ToBigEndian(q),
nonce = resPQ.nonce,
nonce = nonce,
server_nonce = resPQ.server_nonce,
new_nonce = new_nonce,
});
await client.SendAsync(reqDHparams, false);
new_nonce = new Int256(RNG),
dc = session.DataCenter?.id ?? 0
};
if (client.TLConfig?.test_mode == true) pqInnerData.dc += 10000;
if (session.DataCenter?.flags.HasFlag(DcOption.Flags.media_only) == true) pqInnerData.dc = -pqInnerData.dc;
byte[] encrypted_data = null;
{
//4.1) RSA_PAD(data, server_public_key)
using var clearStream = new MemoryStream(256);
using var writer = new BinaryWriter(clearStream);
byte[] aes_key = new byte[32], zero_iv = new byte[32];
var n = BigEndianInteger(publicKey.n);
do
{
RNG.GetBytes(aes_key);
clearStream.Position = 0;
clearStream.Write(aes_key, 0, 32); // write aes_key as prefix for initial Sha256 computation
writer.WriteTLObject(pqInnerData);
int clearLength = (int)clearStream.Position - 32; // length before padding
if (clearLength > 144) throw new WTException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, 32 + clearLength, 192 - clearLength);
sha256.ComputeHash(clearBuffer, 0, 32 + 192).CopyTo(clearBuffer, 224); // append Sha256
Array.Reverse(clearBuffer, 32, 192);
var aes_encrypted = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(32, 224), aes_key, zero_iv, true);
var hash_aes = sha256.ComputeHash(aes_encrypted);
for (int i = 0; i < 32; i++) // prefix aes_encrypted with temp_key_xor
clearBuffer[i] = (byte)(aes_key[i] ^ hash_aes[i]);
aes_encrypted.CopyTo(clearBuffer, 32);
var x = BigEndianInteger(clearBuffer);
if (x < n) // if good result, encrypt with RSA key:
encrypted_data = BigInteger.ModPow(x, BigEndianInteger(publicKey.e), n).To256Bytes();
} while (encrypted_data == null); // otherwise, repeat the steps
}
var serverDHparams = await client.ReqDHParams(pqInnerData.nonce, pqInnerData.server_nonce, pqInnerData.p, pqInnerData.q, fingerprint, encrypted_data);
//5)
reply = await client.RecvInternalAsync();
if (reply is not ServerDHParams serverDHparams) throw new ApplicationException($"Expected ServerDHParams but got {reply.GetType().Name}");
var localTime = DateTimeOffset.UtcNow;
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new ApplicationException("not server_DH_params_ok");
if (serverDHparamsOk.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, new_nonce);
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(sha1, resPQ.server_nonce, pqInnerData.new_nonce);
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
using var encryptedReader = new BinaryReader(new MemoryStream(answer));
var answerHash = encryptedReader.ReadBytes(20);
var answerObj = Schema.DeserializeValue(encryptedReader, typeof(object));
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new ApplicationException("not server_DH_inner_data");
long padding = encryptedReader.BaseStream.Length - encryptedReader.BaseStream.Position;
if (padding >= 16) throw new ApplicationException("Too much pad");
if (!Enumerable.SequenceEqual(SHA1.HashData(answer.AsSpan(20..^(int)padding)), answerHash))
throw new ApplicationException("Answer SHA1 mismatch");
if (serverDHinnerData.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var g_a = new BigInteger(serverDHinnerData.g_a, true, true);
var dh_prime = new BigInteger(serverDHinnerData.dh_prime, true, true);
ValidityChecks(dh_prime, serverDHinnerData.g);
Helpers.Log(1, $"Server time: {serverDHinnerData.server_time} UTC");
session.ServerTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
using var answerReader = new BinaryReader(new MemoryStream(answer));
var answerHash = answerReader.ReadBytes(20);
var answerObj = answerReader.ReadTLObject();
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new WTException("not server_DH_inner_data");
long padding = answerReader.BaseStream.Length - answerReader.BaseStream.Position;
if (padding >= 16) throw new WTException("Too much pad");
if (!Enumerable.SequenceEqual(sha1.ComputeHash(answer, 20, answer.Length - (int)padding - 20), answerHash))
throw new WTException("Answer SHA1 mismatch");
if (serverDHinnerData.nonce != nonce) throw new WTException("Nonce mismatch");
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
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");
//6)
var bData = new byte[256];
RNG.GetBytes(bData);
var b = new BigInteger(bData, true, true);
var salt = new byte[256];
RNG.GetBytes(salt);
var b = BigEndianInteger(salt);
var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime);
var setClientDHparams = MakeClientDHparams(tmp_aes_key, tmp_aes_iv, new ClientDHInnerData
CheckGoodGaAndGb(g_a, dh_prime);
CheckGoodGaAndGb(g_b, dh_prime);
var clientDHinnerData = new ClientDHInnerData
{
nonce = resPQ.nonce,
nonce = nonce,
server_nonce = resPQ.server_nonce,
retry_id = retry_id,
g_b = g_b.ToByteArray(true, true)
});
await client.SendAsync(setClientDHparams, false);
};
{
using var clearStream = new MemoryStream(384);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream);
writer.WriteTLObject(clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int paddingToAdd = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + paddingToAdd);
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, paddingToAdd);
sha1.ComputeHash(clearBuffer, 20, clearLength - 20).CopyTo(clearBuffer, 0);
encrypted_data = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(0, clearLength + paddingToAdd), tmp_aes_key, tmp_aes_iv, true);
}
var setClientDHparamsAnswer = await client.SetClientDHParams(clientDHinnerData.nonce, clientDHinnerData.server_nonce, encrypted_data);
//7)
var gab = BigInteger.ModPow(g_a, b, dh_prime);
var authKey = gab.ToByteArray(true, true);
//8)
var authKeyHash = SHA1.HashData(authKey);
var authKeyHash = sha1.ComputeHash(authKey);
retry_id = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash); // (auth_key_aux_hash)
//9)
reply = await client.RecvInternalAsync();
if (reply is not SetClientDHParamsAnswer setClientDHparamsAnswer) throw new ApplicationException($"Expected SetClientDHParamsAnswer but got {reply.GetType().Name}");
if (setClientDHparamsAnswer is not DHGenOk) throw new ApplicationException("not dh_gen_ok");
if (setClientDHparamsAnswer.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (setClientDHparamsAnswer.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
if (setClientDHparamsAnswer is not DhGenOk dhGenOk) throw new WTException("not dh_gen_ok");
if (dhGenOk.nonce != nonce) throw new WTException("Nonce mismatch");
if (dhGenOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch");
var expected_new_nonceN = new byte[32 + 1 + 8];
new_nonce.raw.CopyTo(expected_new_nonceN, 0);
pqInnerData.new_nonce.raw.CopyTo(expected_new_nonceN, 0);
expected_new_nonceN[32] = 1;
Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash)
if (!Enumerable.SequenceEqual(setClientDHparamsAnswer.new_nonce_hashN.raw, SHA1.HashData(expected_new_nonceN).Skip(4)))
throw new ApplicationException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
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(new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.Save();
static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(Int128 server_nonce, Int256 new_nonce)
{
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
using var sha1 = new SHA1Managed();
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.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.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]
return (tmp_aes_key, tmp_aes_iv);
}
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.OldSalt = session.Salt;
}
private static void ValidityChecks(BigInteger p, int g)
public static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(SHA1 sha1, TL.Int128 server_nonce, Int256 new_nonce)
{
//TODO: check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime)
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)
{
Helpers.Log(2, "Verifying encryption key safety... (this should happen only once per DC)");
// check that 2^2047 <= p < 2^2048
if (p.GetBitLength() != 2048) throw new ApplicationException("p is not 2048-bit number");
if (p.GetBitLength() != 2048) throw new WTException("p is not 2048-bit number");
// check that g generates a cyclic subgroup of prime order (p - 1) / 2, i.e. is a quadratic residue mod p.
BigInteger mod_r;
if (g switch
{
2 => p % 8 != 7,
3 => p % 3 != 2,
4 => false,
5 => (mod_r = p % 5) != 1 && mod_r != 4,
6 => (mod_r = p % 24) != 19 && mod_r != 23,
7 => (mod_r = p % 7) != 3 && mod_r != 5 && mod_r != 6,
5 => (int)(p % 5) is not 1 and not 4,
6 => (int)(p % 24) is not 19 and not 23,
7 => (int)(p % 7) is not 3 and not 5 and not 6,
_ => true,
})
throw new ApplicationException("Bad prime mod 4g");
//TODO: check that g, g_a and g_b are greater than 1 and less than dh_prime - 1.
throw new WTException("Bad prime mod 4g");
// check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime)
if (SafePrimes.Contains(p)) return;
if (!p.IsProbablePrime()) throw new WTException("p is not a prime number");
if (!((p - 1) / 2).IsProbablePrime()) throw new WTException("(p - 1) / 2 is not a prime number");
SafePrimes.Add(p);
}
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,
0x05, 0x70, 0xED, 0x5E, 0xFF, 0xA9, 0x7F, 0xF8, 0xA0, 0xBE, 0x3E, 0xE8, 0x15, 0xFC, 0x18, 0xE4,
0xE4, 0x9A, 0x5B, 0xEF, 0x8F, 0x92, 0xA3, 0x9C, 0xFF, 0xD6, 0xB0, 0x65, 0xC4, 0x6B, 0x9C, 0x16,
0x8D, 0x17, 0xB1, 0x2D, 0x58, 0x46, 0xDD, 0xB9, 0xB4, 0x65, 0x59, 0x0D, 0x95, 0xED, 0x17, 0xFD,
0x54, 0x47, 0x28, 0xF1, 0x0E, 0x4E, 0x14, 0xB3, 0x14, 0x2A, 0x4B, 0xA8, 0xD8, 0x74, 0xBA, 0x0D,
0x41, 0x6B, 0x0F, 0x6B, 0xB5, 0x53, 0x27, 0x16, 0x7E, 0x90, 0x51, 0x10, 0x81, 0x95, 0xA6, 0xA4,
0xA4, 0xF9, 0x7C, 0xE6, 0xBE, 0x60, 0x90, 0x3A, 0x4F, 0x3C, 0x8E, 0x37, 0x9B, 0xFA, 0x08, 0x07,
0x88, 0x49, 0xCC, 0xC8, 0x4A, 0x1D, 0xCD, 0x5B, 0x1D, 0x94, 0x2A, 0xBB, 0x96, 0xFE, 0x77, 0x24,
0x64, 0x5F, 0x59, 0x8C, 0xAF, 0x8F, 0xF1, 0x54, 0x84, 0x32, 0x69, 0x29, 0x51, 0x46, 0x97, 0xDC,
0xAB, 0x13, 0x6B, 0x6B, 0xFE, 0xD4, 0x8C, 0xC6, 0x5A, 0x70, 0x58, 0x94, 0xF6, 0x51, 0xFD, 0x20,
0x37, 0x7C, 0xCE, 0x4C, 0xD4, 0xAE, 0x43, 0x95, 0x13, 0x25, 0xC9, 0x0A, 0x6E, 0x6F, 0x33, 0xFA,
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)
{
// check that g, g_a and g_b are greater than 1 and less than dh_prime - 1.
// We recommend checking that g_a and g_b are between 2^{2048-64} and dh_prime - 2^{2048-64} as well.
if (g.GetBitLength() < 2048 - 64 || (dh_prime - g).GetBitLength() < 2048 - 64)
throw new WTException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}");
}
private static Fn.ReqDHParams MakeReqDHparam(long publicKey_fingerprint, RSAPublicKey publicKey, PQInnerData pqInnerData)
{
// the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(255);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
Schema.Serialize(writer, pqInnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
if (clearLength > 255) throw new ApplicationException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, 255 - clearLength);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer); // patch with SHA1
var encrypted_data = BigInteger.ModPow(new BigInteger(clearBuffer, true, true), // encrypt with RSA key
new BigInteger(publicKey.e, true, true), new BigInteger(publicKey.n, true, true)).ToByteArray(true, true);
return new Fn.ReqDHParams
{
nonce = pqInnerData.nonce,
server_nonce = pqInnerData.server_nonce,
p = pqInnerData.p,
q = pqInnerData.q,
public_key_fingerprint = publicKey_fingerprint,
encrypted_data = encrypted_data
};
}
private static Fn.SetClientDHParams MakeClientDHparams(byte[] tmp_aes_key, byte[] tmp_aes_iv, ClientDHInnerData clientDHinnerData)
{
// the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(384);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
Schema.Serialize(writer, clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int padding = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + padding);
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, padding);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer);
var encrypted_data = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(0, clearLength + padding), tmp_aes_key, tmp_aes_iv, true);
return new Fn.SetClientDHParams
{
nonce = clientDHinnerData.nonce,
server_nonce = clientDHinnerData.server_nonce,
encrypted_data = encrypted_data
};
}
/// <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();
using var sha1 = SHA1.Create();
rsa.ImportFromPem(pem);
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 };
var bareData = Schema.Serialize(publicKey).AsSpan(4); // bare serialization
var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(SHA1.HashData(bareData).AsSpan(12)); // 64 lower-order bits of SHA1
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}");
}
private static void LoadDefaultPublicKey() // fingerprint C3B42B026CE86B21
private static void LoadDefaultPublicKeys()
{
// Production Public Key (D09D1D85DE64FD85)
LoadPublicKey(@"-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
MIIBCgKCAQEA6LszBcC1LGzyr992NzE0ieY+BSaOW622Aa9Bd4ZHLl+TuFQ4lo4g
5nKaMBwK/BIb9xUfg0Q29/2mgIR6Zr9krM7HjuIcCzFvDtr+L0GQjae9H0pRB2OO
62cECs5HKhT5DZ98K33vmWiLowc621dQuwKWSQKjWf50XYFw42h21P2KXUGyp2y/
+aEyZ+uVgLLQbRA1dEjSDZ2iGRy12Mk5gpYc397aYp438fsJoHIgJ2lgMv5h7WY9
t6N/byY9Nw9p21Og3AoXSL2q/2IJ1WRUhebgAdGVMlV1fkuOQoEzR7EdpqtQD9Cs
5+bfo3Nhmcyvk5ftB0WkJ9z6bNZ7yxrP8wIDAQAB
-----END RSA PUBLIC KEY-----");
// Test Public Key (B25898DF208D2603)
LoadPublicKey(@"-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAyMEdY1aR+sCR3ZSJrtztKTKqigvO/vBfqACJLZtS7QMgCGXJ6XIR
yy7mx66W0/sOFa7/1mAZtEoIokDP3ShoqF4fVNb6XeqgQfaUHd8wJpDWHcR2OFwv
plUUI1PLTktZ9uW2WE23b+ixNwJjJGwBDJPQEQFBE+vfmH0JP503wr5INS1poWg/
j25sIWeYPHYeOrFp/eXaqhISP6G+q2IeTaWTXpwZj4LzXq5YOpk4bYEQ6mvRq7D1
aHWfYmlEGepfaYR8Q0YqvvhYtMte3ITnuSJs171+GDqpdKcSwHnd6FudwGO4pcCO
j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
-----END RSA PUBLIC KEY-----");
}
internal static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, byte[] authKey, byte[] clearSha1)
public static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
{
// first, construct AES key & IV
int x = encrypt ? 0 : 8;
byte[] aes_key = new byte[32], aes_iv = new byte[32];
using var sha1 = new SHA1Managed();
sha1.TransformBlock(clearSha1, 4, 16, null, 0); // msgKey
sha1.TransformFinalBlock(authKey, x, 32); // authKey[x:32]
var sha1_a = sha1.Hash;
sha1.TransformBlock(authKey, 32 + x, 16, null, 0); // authKey[32+x:16]
sha1.TransformBlock(clearSha1, 4, 16, null, 0); // msgKey
sha1.TransformFinalBlock(authKey, 48 + x, 16); // authKey[48+x:16]
var sha1_b = sha1.Hash;
sha1.TransformBlock(authKey, 64 + x, 32, null, 0); // authKey[64+x:32]
sha1.TransformFinalBlock(clearSha1, 4, 16); // msgKey
var sha1_c = sha1.Hash;
sha1.TransformBlock(clearSha1, 4, 16, null, 0); // msgKey
sha1.TransformFinalBlock(authKey, 96 + x, 32); // authKey[96+x:32]
var sha1_d = sha1.Hash;
Array.Copy(sha1_a, 0, aes_key, 0, 8);
Array.Copy(sha1_b, 8, aes_key, 8, 12);
Array.Copy(sha1_c, 4, aes_key, 20, 12);
Array.Copy(sha1_a, 8, aes_iv, 0, 12);
Array.Copy(sha1_b, 0, aes_iv, 12, 8);
Array.Copy(sha1_c, 16, aes_iv, 20, 4);
Array.Copy(sha1_d, 0, aes_iv, 24, 8);
sha256.TransformBlock(msgKey, msgKeyOffset, 16, null, 0); // msgKey
sha256.TransformFinalBlock(authKey, x, 36); // authKey[x:36]
var sha256_a = sha256.Hash;
sha256.Initialize();
sha256.TransformBlock(authKey, 40 + x, 36, null, 0); // authKey[40+x:36]
sha256.TransformFinalBlock(msgKey, msgKeyOffset, 16); // msgKey
var sha256_b = sha256.Hash;
sha256.Initialize();
Array.Copy(sha256_a, 0, aes_key, 0, 8);
Array.Copy(sha256_b, 8, aes_key, 8, 16);
Array.Copy(sha256_a, 24, aes_key, 24, 8);
Array.Copy(sha256_b, 0, aes_iv, 0, 8);
Array.Copy(sha256_a, 8, aes_iv, 8, 16);
Array.Copy(sha256_b, 24, aes_iv, 24, 8);
return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt);
}
private 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)
{
using var aes = Aes.Create();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.Zeros;
if (aes.BlockSize != 128) throw new ApplicationException("AES Blocksize is not 16 bytes");
if (input.Length % 16 != 0) throw new ApplicationException("intput size not divisible by 16");
if (input.Length % 16 != 0) throw new WTException("AES_IGE input size not divisible by 16");
// code adapted from PHP implementation found at https://mgp25.com/AESIGE/
using var aesCrypto = encrypt ? AesECB.CreateEncryptor(aes_key, null) : AesECB.CreateDecryptor(aes_key, null);
var output = new byte[input.Length];
var xPrev = aes_iv.AsSpan(encrypt ? 16 : 0, 16);
var yPrev = aes_iv.AsSpan(encrypt ? 0 : 16, 16);
var aesCrypto = encrypt ? aes.CreateEncryptor(aes_key, null) : aes.CreateDecryptor(aes_key, null);
using (aesCrypto)
var prevBytes = (byte[])aes_iv.Clone();
var span = MemoryMarshal.Cast<byte, long>(input);
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;)
{
byte[] yXOR = new byte[16];
for (int i = 0; i < input.Length; i += 16)
{
var x = input.Slice(i, 16);
var y = output.AsSpan(i, 16);
for (int j = 0; j < 16; j++)
yXOR[j] = (byte)(x[j] ^ yPrev[j]);
var yFinal = aesCrypto.TransformFinalBlock(yXOR, 0, 16);
for (int j = 0; j < 16; j++)
y[j] = (byte)(yFinal[j] ^ xPrev[j]);
xPrev = x;
yPrev = y;
}
sout[i] = span[i] ^ prev[0]; sout[i + 1] = span[i + 1] ^ prev[1];
aesCrypto.TransformBlock(output, i * 8, 16, output, i * 8);
prev[0] = sout[i] ^= prev[2]; prev[1] = sout[i + 1] ^= prev[3];
prev[2] = span[i++]; prev[3] = span[i++];
}
return output;
}
#if OBFUSCATION
public sealed class AesCtr(byte[] key, byte[] ivec) : IDisposable
{
readonly ICryptoTransform _encryptor = AesECB.CreateEncryptor(key, null);
readonly byte[] _ecount = new byte[16];
int _num;
public void Dispose() => _encryptor.Dispose();
public void EncryptDecrypt(Span<byte> buffer)
{
for (int i = 0; i < buffer.Length; i++)
{
if (_num == 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;
}
}
}
// see https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation
internal static (AesCtr, AesCtr, byte[]) InitObfuscation(byte[] secret, byte protocolId, int dcId)
{
byte[] preamble = new byte[64];
do
RNG.GetBytes(preamble, 0, 58);
while (preamble[0] == 0xef ||
BinaryPrimitives.ReadUInt32LittleEndian(preamble) is 0x44414548 or 0x54534f50 or 0x20544547 or 0x4954504f or 0x02010316 or 0xdddddddd or 0xeeeeeeee ||
BinaryPrimitives.ReadInt32LittleEndian(preamble.AsSpan(4)) == 0);
preamble[62] = preamble[56]; preamble[63] = preamble[57];
preamble[56] = preamble[57] = preamble[58] = preamble[59] = protocolId;
preamble[60] = (byte)dcId; preamble[61] = (byte)(dcId >> 8);
byte[] recvKey = preamble[8..40], recvIV = preamble[40..56];
Array.Reverse(preamble, 8, 48);
byte[] sendKey = preamble[8..40], sendIV = preamble[40..56];
if (secret != null)
{
using var sha256 = SHA256.Create();
sha256.TransformBlock(sendKey, 0, 32, null, 0);
sha256.TransformFinalBlock(secret, 0, 16);
sendKey = sha256.Hash;
sha256.Initialize();
sha256.TransformBlock(recvKey, 0, 32, null, 0);
sha256.TransformFinalBlock(secret, 0, 16);
recvKey = sha256.Hash;
}
var sendCtr = new AesCtr(sendKey, sendIV);
var recvCtr = new AesCtr(recvKey, recvIV);
var encrypted = (byte[])preamble.Clone();
sendCtr.EncryptDecrypt(encrypted);
for (int i = 56; i < 64; i++)
preamble[i] = encrypted[i];
return (sendCtr, recvCtr, preamble);
}
#endif
internal static async Task<InputCheckPasswordSRP> Check2FA(Account_Password accountPassword, Func<Task<string>> getPassword)
{
if (accountPassword.current_algo is not PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow algo)
if (accountPassword.current_algo == null && (algo = accountPassword.new_algo as PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow) != null)
{
int salt1len = algo.salt1.Length;
Array.Resize(ref algo.salt1, salt1len + 32);
RNG.GetBytes(algo.salt1, salt1len, 32);
}
else
throw new WTException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name);
var g = new BigInteger(algo.g);
var p = BigEndianInteger(algo.p);
var validTask = Task.Run(() => CheckGoodPrime(p, algo.g));
System.Threading.Thread.Sleep(100);
Helpers.Log(3, $"This account has enabled 2FA. A password is needed. {accountPassword.hint}");
var passwordBytes = Encoding.UTF8.GetBytes(await getPassword());
using var sha256 = SHA256.Create();
sha256.TransformBlock(algo.salt1, 0, algo.salt1.Length, null, 0);
sha256.TransformBlock(passwordBytes, 0, passwordBytes.Length, null, 0);
sha256.TransformFinalBlock(algo.salt1, 0, algo.salt1.Length);
var hash = sha256.Hash;
sha256.Initialize();
sha256.TransformBlock(algo.salt2, 0, algo.salt2.Length, null, 0);
sha256.TransformBlock(hash, 0, 32, null, 0);
sha256.TransformFinalBlock(algo.salt2, 0, algo.salt2.Length);
hash = sha256.Hash;
#if NETCOREAPP2_0_OR_GREATER
using var derive = new Rfc2898DeriveBytes(hash, algo.salt1, 100000, HashAlgorithmName.SHA512);
var pbkdf2 = derive.GetBytes(64);
#else
var pbkdf2 = PBKDF2_SHA512(hash, algo.salt1, 100000, 64);
#endif
sha256.Initialize();
sha256.TransformBlock(algo.salt2, 0, algo.salt2.Length, null, 0);
sha256.TransformBlock(pbkdf2, 0, 64, null, 0);
sha256.TransformFinalBlock(algo.salt2, 0, algo.salt2.Length);
var x = BigEndianInteger(sha256.Hash);
var v = BigInteger.ModPow(g, x, p);
if (accountPassword.current_algo == null) // we're computing a new password
{
await validTask;
return new InputCheckPasswordSRP { A = v.To256Bytes() };
}
var g_b = BigEndianInteger(accountPassword.srp_B);
var g_b_256 = g_b.To256Bytes();
var g_256 = g.To256Bytes();
sha256.Initialize();
sha256.TransformBlock(algo.p, 0, 256, null, 0);
sha256.TransformFinalBlock(g_256, 0, 256);
var k = BigEndianInteger(sha256.Hash);
var k_v = (k * v) % p;
var a = BigEndianInteger(new Int256(RNG).raw);
var g_a = BigInteger.ModPow(g, a, p);
var g_a_256 = g_a.To256Bytes();
sha256.Initialize();
sha256.TransformBlock(g_a_256, 0, 256, null, 0);
sha256.TransformFinalBlock(g_b_256, 0, 256);
var u = BigEndianInteger(sha256.Hash);
var t = (g_b - k_v) % p; //(positive modulo, if the result is negative increment by p)
if (t.Sign < 0) t += p;
var s_a = BigInteger.ModPow(t, a + u * x, p);
sha256.Initialize();
var k_a = sha256.ComputeHash(s_a.To256Bytes());
hash = sha256.ComputeHash(algo.p);
var h2 = sha256.ComputeHash(g_256);
for (int i = 0; i < 32; i++) hash[i] ^= h2[i];
var hs1 = sha256.ComputeHash(algo.salt1);
var hs2 = sha256.ComputeHash(algo.salt2);
sha256.Initialize();
sha256.TransformBlock(hash, 0, 32, null, 0);
sha256.TransformBlock(hs1, 0, 32, null, 0);
sha256.TransformBlock(hs2, 0, 32, null, 0);
sha256.TransformBlock(g_a_256, 0, 256, null, 0);
sha256.TransformBlock(g_b_256, 0, 256, null, 0);
sha256.TransformFinalBlock(k_a, 0, 32);
var m1 = sha256.Hash;
await validTask;
return new InputCheckPasswordSRP { A = g_a_256, M1 = m1, srp_id = accountPassword.srp_id };
}
#if !NETCOREAPP2_0_OR_GREATER
// adapted from https://github.com/dotnet/aspnetcore/blob/main/src/DataProtection/Cryptography.KeyDerivation/src/PBKDF2/ManagedPbkdf2Provider.cs
private static byte[] PBKDF2_SHA512(byte[] password, byte[] salt, int iterationCount, int numBytesRequested)
{
// PBKDF2 is defined in NIST SP800-132, Sec. 5.3: http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf
byte[] retVal = new byte[numBytesRequested];
int numBytesWritten = 0;
int numBytesRemaining = numBytesRequested;
// For each block index, U_0 := Salt || block_index
byte[] saltWithBlockIndex = new byte[checked(salt.Length + sizeof(uint))];
Buffer.BlockCopy(salt, 0, saltWithBlockIndex, 0, salt.Length);
using var hashAlgorithm = new HMACSHA512(password);
for (uint blockIndex = 1; numBytesRemaining > 0; blockIndex++)
{
// write the block index out as big-endian
saltWithBlockIndex[^4] = (byte)(blockIndex >> 24);
saltWithBlockIndex[^3] = (byte)(blockIndex >> 16);
saltWithBlockIndex[^2] = (byte)(blockIndex >> 8);
saltWithBlockIndex[^1] = (byte)blockIndex;
// U_1 = PRF(U_0) = PRF(Salt || block_index)
// T_blockIndex = U_1
byte[] U_iter = hashAlgorithm.ComputeHash(saltWithBlockIndex); // this is U_1
byte[] T_blockIndex = U_iter;
for (int iter = 1; iter < iterationCount; iter++)
{
U_iter = hashAlgorithm.ComputeHash(U_iter);
for (int j = U_iter.Length - 1; j >= 0; j--)
T_blockIndex[j] ^= U_iter[j];
// At this point, the 'U_iter' variable actually contains U_{iter+1} (due to indexing differences).
}
// At this point, we're done iterating on this block, so copy the transformed block into retVal.
int numBytesToCopy = Math.Min(numBytesRemaining, T_blockIndex.Length);
Buffer.BlockCopy(T_blockIndex, 0, retVal, numBytesWritten, numBytesToCopy);
numBytesWritten += numBytesToCopy;
numBytesRemaining -= numBytesToCopy;
}
return retVal; // retVal := T_1 || T_2 || ... || T_n, where T_n may be truncated to meet the desired output length
}
#endif
}
internal sealed class AES_IGE_Stream : Helpers.IndirectStream
{
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); }
}
public override long Length => base.Length + 15 & ~15;
public override bool CanSeek => false;
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count)
{
count = _innerStream.Read(buffer, offset, count);
if (count == 0) return 0;
Process(buffer, offset, count);
if (ContentLength.HasValue && _innerStream.Position == _innerStream.Length)
return count - (int)(_innerStream.Position - ContentLength.Value);
return count + 15 & ~15;
}
public override void Write(byte[] buffer, int offset, int count)
{
Process(buffer, offset, count);
if (ContentLength.HasValue && _innerStream.Position + count > ContentLength)
count -= (int)(_innerStream.Position + count - ContentLength.Value);
_innerStream.Write(buffer, offset, count);
}
public void Process(byte[] buffer, int offset, int count)
{
count = count + 15 & ~15;
var span = MemoryMarshal.Cast<byte, long>(buffer.AsSpan(offset, count));
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);
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

@ -1,380 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace WTelegram
{
public class Generator
{
//TODO: generate BinaryReader/Writer serialization directly to avoid using Reflection
//TODO: generate partial class with methods for functions instead of exposing request classes
readonly Dictionary<int, string> ctorToTypes = new();
readonly HashSet<string> allTypes = new();
readonly Dictionary<int, Dictionary<string, TypeInfo>> typeInfosByLayer = new();
Dictionary<string, TypeInfo> typeInfos;
int currentLayer;
string tabIndent;
public async Task FromWeb()
{
Console.WriteLine("Fetch web pages...");
//using var http = new HttpClient();
//var html = await http.GetStringAsync("https://core.telegram.org/api/layers");
var html = await Task.FromResult("#layer-121");
currentLayer = int.Parse(Regex.Match(html, @"#layer-(\d+)").Groups[1].Value);
//await File.WriteAllBytesAsync("TL.MTProto.json", await http.GetByteArrayAsync("https://core.telegram.org/schema/mtproto-json"));
//await File.WriteAllBytesAsync("TL.Schema.json", await http.GetByteArrayAsync("https://core.telegram.org/schema/json"));
//await File.WriteAllBytesAsync("TL.Secret.json", await http.GetByteArrayAsync("https://core.telegram.org/schema/end-to-end-json"));
FromJson("TL.MTProto.json", "TL.MTProto.cs", @"TL.Table.cs");
FromJson("TL.Schema.json", "TL.Schema.cs", @"TL.Table.cs");
FromJson("TL.Secret.json", "TL.Secret.cs", @"TL.Table.cs");
}
public void FromJson(string jsonPath, string outputCs, string tableCs = null)
{
Console.WriteLine("Parsing " + jsonPath);
var schema = JsonSerializer.Deserialize<SchemaJson>(File.ReadAllText(jsonPath));
using var sw = File.CreateText(outputCs);
sw.WriteLine("// This file is (mainly) generated automatically using the Generator class");
sw.WriteLine("using System;");
sw.WriteLine();
sw.WriteLine("namespace TL");
sw.Write("{");
tabIndent = "\t";
var layers = schema.constructors.GroupBy(c => c.layer).OrderBy(g => g.Key);
foreach (var layer in layers)
{
typeInfos = typeInfosByLayer.GetOrCreate(layer.Key);
if (layer.Key != 0)
{
sw.WriteLine();
sw.WriteLine("\tnamespace Layer" + layer.Key);
sw.Write("\t{");
tabIndent += "\t";
}
string layerPrefix = layer.Key == 0 ? "" : $"Layer{layer.Key}.";
foreach (var ctor in layer)
{
if (ctorToTypes.ContainsKey(ctor.ID)) continue;
if (ctor.type is "Bool" or "Vector t") continue;
var structName = CSharpName(ctor.predicate);
ctorToTypes[ctor.ID] = layerPrefix + structName;
var typeInfo = typeInfos.GetOrCreate(ctor.type);
if (ctor.ID == 0x5BB8E511) { ctorToTypes[ctor.ID] = structName = ctor.predicate = ctor.type = "_Message"; }
if (typeInfo.ReturnName == null) typeInfo.ReturnName = CSharpName(ctor.type);
typeInfo.Structs.Add(ctor);
if (structName == typeInfo.ReturnName) typeInfo.SameName = ctor;
}
foreach (var (name, typeInfo) in typeInfos)
{
if (allTypes.Contains(typeInfo.ReturnName)) { typeInfo.NeedAbstract = -2; continue; }
if (typeInfo.SameName == null)
{
typeInfo.NeedAbstract = -1;
if (typeInfo.Structs.Count > 1)
{
List<Param> fakeCtorParams = new();
while (typeInfo.Structs[0].@params.Length > fakeCtorParams.Count)
{
fakeCtorParams.Add(typeInfo.Structs[0].@params[fakeCtorParams.Count]);
if (!typeInfo.Structs.All(ctor => HasPrefix(ctor, fakeCtorParams)))
{
fakeCtorParams.RemoveAt(fakeCtorParams.Count - 1);
break;
}
}
if (fakeCtorParams.Count > 0)
{
typeInfo.Structs.Insert(0, typeInfo.SameName = new Constructor
{ id = null, @params = fakeCtorParams.ToArray(), predicate = typeInfo.ReturnName, type = typeInfo.ReturnName });
typeInfo.NeedAbstract = fakeCtorParams.Count;
}
}
}
else if (typeInfo.Structs.Count > 1)
{
typeInfo.NeedAbstract = typeInfo.SameName.@params.Length;
foreach (var ctor in typeInfo.Structs)
{
if (ctor == typeInfo.SameName) continue;
if (!HasPrefix(ctor, typeInfo.SameName.@params)) { typeInfo.NeedAbstract = -1; typeInfo.ReturnName += "Base"; break; }
}
}
}
foreach (var typeInfo in typeInfos.Values)
WriteTypeInfo(sw, typeInfo, jsonPath, layerPrefix, false);
if (layer.Key != 0)
{
sw.WriteLine("\t}");
tabIndent = tabIndent[1..];
}
}
if (typeInfosByLayer[0]["Message"].SameName.ID == 0x5BB8E511) typeInfosByLayer[0].Remove("Message");
sw.WriteLine();
var methods = new List<TypeInfo>();
if (schema.methods.Length != 0)
{
typeInfos = typeInfosByLayer[0];
sw.WriteLine("\tpublic static partial class Fn // ---functions---");
sw.Write("\t{");
tabIndent = "\t\t";
foreach (var method in schema.methods)
{
var typeInfo = new TypeInfo { ReturnName = method.type };
typeInfo.Structs.Add(new Constructor { id = method.id, @params = method.@params, predicate = method.method, type = method.type });
methods.Add(typeInfo);
WriteTypeInfo(sw, typeInfo, jsonPath, "", true);
}
sw.WriteLine("\t}");
}
sw.WriteLine("}");
if (tableCs != null) UpdateTable(jsonPath, tableCs, methods);
}
void WriteTypeInfo(StreamWriter sw, TypeInfo typeInfo, string definedIn, string layerPrefix, bool isMethod)
{
var parentClass = typeInfo.NeedAbstract != 0 ? typeInfo.ReturnName : "ITLObject";
var genericType = typeInfo.ReturnName.Length == 1 ? $"<{typeInfo.ReturnName}>" : null;
if (isMethod) parentClass = $"ITLFunction<{MapType(typeInfo.ReturnName, "")}>";
bool needNewLine = true;
if (typeInfo.NeedAbstract == -1 && allTypes.Add(layerPrefix + parentClass))
{
needNewLine = false;
sw.WriteLine();
sw.WriteLine($"{tabIndent}public abstract class {parentClass} : ITLObject {{ }}");
}
int skipParams = 0;
foreach (var ctor in typeInfo.Structs)
{
string className = CSharpName(ctor.predicate) + genericType;
//if (typeInfo.ReturnName == "SendMessageAction") System.Diagnostics.Debugger.Break();
if (layerPrefix != "" && className == parentClass) { className += "_"; ctorToTypes[ctor.ID] = layerPrefix + className; }
if (!allTypes.Add(layerPrefix + className)) continue;
if (needNewLine) { needNewLine = false; sw.WriteLine(); }
if (ctor.id == null)
sw.Write($"{tabIndent}public abstract class {className} : ITLObject");
else
{
sw.Write($"{tabIndent}[TLDef(0x{ctor.ID:X8})] //{ctor.predicate}#{ctor.ID:x8} ");
if (genericType != null) sw.Write($"{{{typeInfo.ReturnName}:Type}} ");
foreach (var parm in ctor.@params) sw.Write($"{parm.name}:{parm.type} ");
sw.WriteLine($"= {ctor.type}");
sw.Write($"{tabIndent}public class {className} : ");
sw.Write(skipParams == 0 && typeInfo.NeedAbstract > 0 ? "ITLObject" : parentClass);
}
var parms = ctor.@params.Skip(skipParams).ToArray();
if (parms.Length == 0)
{
sw.WriteLine(" { }");
continue;
}
var hasFlagEnum = parms.Any(p => p.type.StartsWith("flags."));
bool multiline = hasFlagEnum || parms.Length > 1;
if (multiline)
{
sw.WriteLine();
sw.WriteLine(tabIndent + "{");
}
else
sw.Write(" { ");
if (hasFlagEnum)
{
var list = new SortedList<int, string>();
foreach (var parm in parms)
{
if (!parm.type.StartsWith("flags.") || !parm.type.EndsWith("?true")) continue;
var mask = 1 << int.Parse(parm.type[6..parm.type.IndexOf('?')]);
if (!list.ContainsKey(mask)) list[mask] = MapName(parm.name);
}
foreach (var parm in parms)
{
if (!parm.type.StartsWith("flags.") || parm.type.EndsWith("?true")) continue;
var mask = 1 << int.Parse(parm.type[6..parm.type.IndexOf('?')]);
if (list.ContainsKey(mask)) continue;
var name = MapName("has_" + parm.name);
if (list.Values.Contains(name)) name += "_field";
list[mask] = name;
}
string line = tabIndent + "\t[Flags] public enum Flags { ";
foreach (var (mask, name) in list)
{
var str = $"{name} = 0x{mask:X}, ";
if (line.Length + str.Length + tabIndent.Length * 3 >= 134) { sw.WriteLine(line); line = tabIndent + "\t\t"; }
line += str;
}
sw.WriteLine(line.TrimEnd(',', ' ') + " }");
}
foreach (var parm in parms)
{
if (parm.type.EndsWith("?true")) continue;
if (multiline) sw.Write(tabIndent + "\t");
if (parm.type == "#")
sw.Write($"public {(hasFlagEnum ? "Flags" : "int")} {parm.name};");
else
{
if (parm.type.StartsWith("flags."))
{
int qm = parm.type.IndexOf('?');
sw.Write($"[IfFlag({parm.type[6..qm]})] public {MapType(parm.type[(qm + 1)..], parm.name)} {MapName(parm.name)};");
}
else
sw.Write($"public {MapType(parm.type, parm.name)} {MapName(parm.name)};");
}
if (multiline) sw.WriteLine();
}
if (ctorNeedClone.Contains(className))
sw.WriteLine($"{tabIndent}\tpublic {className} Clone() => ({className})MemberwiseClone();");
if (multiline)
sw.WriteLine(tabIndent + "}");
else
sw.WriteLine(" }");
skipParams = typeInfo.NeedAbstract;
}
string MapName(string name) => name switch
{
"out" => "out_",
"static" => "static_",
"long" => "long_",
"default" => "default_",
"public" => "public_",
"params" => "params_",
"private" => "private_",
_ => name
};
string MapType(string type, string name)
{
if (type.StartsWith("Vector<", StringComparison.OrdinalIgnoreCase))
return MapType(type[7..^1], name) + "[]";
else if (type == "Bool")
return "bool";
else if (type == "bytes")
return "byte[]";
else if (type == "int128")
return "Int128";
else if (type == "int256")
return "Int256";
else if (type == "Object")
return "ITLObject";
else if (type == "!X")
return "ITLFunction<X>";
else if (typeInfos.TryGetValue(type, out var typeInfo))
return typeInfo.ReturnName;
else if (type == "int")
{
var name2 = '_' + name + '_';
if (name2.EndsWith("_date_") || name2.EndsWith("_time_") || name2 == "_expires_" || name2 == "_now_" || name2.StartsWith("_valid_"))
return "DateTime";
else
return "int";
}
else if (type == "string")
return name.StartsWith("md5") ? "byte[]" : "string";
else
return type;
}
}
void UpdateTable(string jsonPath, string tableCs, List<TypeInfo> methods)
{
var myTag = $"\t\t\t// from {Path.GetFileNameWithoutExtension(jsonPath)}:";
var seen_ids = new HashSet<int>();
using (var sr = new StreamReader(tableCs))
using (var sw = new StreamWriter(tableCs + ".new"))
{
string line;
while ((line = sr.ReadLine()) != null)
{
if (currentLayer != 0 && line.StartsWith("\t\tpublic const int Layer"))
sw.WriteLine($"\t\tpublic const int Layer = {currentLayer};\t\t\t\t\t// fetched {DateTime.UtcNow}");
else
sw.WriteLine(line);
if (line == myTag)
{
foreach (var ctor in ctorToTypes)
if (seen_ids.Add(ctor.Key))
sw.WriteLine($"\t\t\t[0x{ctor.Key:X8}] = typeof({ctor.Value}),");
while ((line = sr.ReadLine()) != null)
if (line.StartsWith("\t\t\t// "))
break;
sw.WriteLine(line);
}
else if (line.StartsWith("\t\t\t[0x"))
seen_ids.Add(int.Parse(line[6..14], System.Globalization.NumberStyles.HexNumber));
}
}
File.Replace(tableCs + ".new", tableCs, null);
}
static readonly HashSet<string> ctorNeedClone = new() { /*"User"*/ };
private static bool HasPrefix(Constructor ctor, IList<Param> prefixParams)
{
if (ctor.@params.Length < prefixParams.Count) return false;
for (int i = 0; i < prefixParams.Count; i++)
if (ctor.@params[i].name != prefixParams[i].name || ctor.@params[i].type != prefixParams[i].type)
return false;
return true;
}
private static string CSharpName(string name)
{
name = char.ToUpper(name[0]) + name[1..];
int i;
while ((i = name.IndexOf('_')) > 0)
name = name[..i] + char.ToUpper(name[i + 1]) + name[(i + 2)..];
while ((i = name.IndexOf('.')) > 0)
name = name[..i] + '_' + char.ToUpper(name[i + 1]) + name[(i + 2)..];
return name;
}
class TypeInfo
{
public string ReturnName;
public Constructor SameName;
public List<Constructor> Structs = new();
internal int NeedAbstract; // 0:no, -1:create auto, n:use first generated constructor and skip n params
}
#pragma warning disable IDE1006 // Naming Styles
public class SchemaJson
{
public Constructor[] constructors { get; set; }
public Method[] methods { get; set; }
}
public class Constructor
{
public string id { get; set; }
public string predicate { get; set; }
public Param[] @params { get; set; }
public string type { get; set; }
public int layer { get; set; }
public int ID => int.Parse(id);
}
public class Param
{
public string name { get; set; }
public string type { get; set; }
}
public class Method
{
public string id { get; set; }
public string method { get; set; }
public Param[] @params { get; set; }
public string type { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles
}
}

View file

@ -1,19 +1,37 @@
using System;
using System.Collections.Generic;
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
{
/// <summary>Callback for logging a line (string) with its associated <see href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel">severity level</see> (int)</summary>
public static Action<int, string> Log { get; set; } = DefaultLogger;
public static readonly System.Text.Json.JsonSerializerOptions JsonOptions = new(System.Text.Json.JsonSerializerDefaults.Web) { IncludeFields = true, WriteIndented = true };
/// <summary>For serializing indented Json with fields included</summary>
public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true,
#if NET8_0_OR_GREATER
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? null : WTelegramContext.Default,
#endif
IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull };
public static V GetOrCreate<K, V>(this Dictionary<K, V> dictionary, K key) where V : new()
=> dictionary.TryGetValue(key, out V value) ? value : dictionary[key] = new V();
private static readonly ConsoleColor[] LogLevelToColor = new[] { ConsoleColor.DarkGray, ConsoleColor.DarkCyan, ConsoleColor.Cyan,
ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue };
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];
@ -21,24 +39,48 @@ namespace WTelegram
Console.ResetColor();
}
public static V GetOrCreate<K, V>(this Dictionary<K, V> dictionary, K key) where V : new()
=> dictionary.TryGetValue(key, out V value) ? value : dictionary[key] = new V();
/// <summary>Get a cryptographic random 64-bit value</summary>
public static long RandomLong()
{
Span<long> span = stackalloc long[1];
System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes(span));
return span[0];
#if NETCOREAPP2_1_OR_GREATER
long value = 0;
System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes(
System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref value, 1)));
return value;
#else
var span = new byte[8];
Encryption.RNG.GetBytes(span);
return BitConverter.ToInt64(span, 0);
#endif
}
public static byte[] ToBigEndian(ulong value) // variable-size buffer
public static async Task<int> FullReadAsync(this Stream stream, byte[] buffer, int length, CancellationToken ct)
{
int i;
var temp = value;
for (i = 1; (temp >>= 8) != 0; i++);
for (int offset = 0; offset < length;)
{
#pragma warning disable CA1835
var read = await stream.ReadAsync(buffer, offset, length - offset, ct);
#pragma warning restore CA1835
if (read == 0) return offset;
offset += read;
}
return length;
}
internal static byte[] ToBigEndian(ulong value) // variable-size buffer
{
int i = 1;
for (ulong temp = value; (temp >>= 8) != 0; ) i++;
var result = new byte[i];
while (--i >= 0) { result[i] = (byte)value; value >>= 8; }
for (; --i >= 0; value >>= 8)
result[i] = (byte)value;
return result;
}
public static ulong FromBigEndian(byte[] bytes) // variable-size buffer
internal static ulong FromBigEndian(byte[] bytes) // variable-size buffer
{
if (bytes.Length > 8) throw new ArgumentException($"expected bytes length <= 8 but got {bytes.Length}");
ulong result = 0;
@ -47,7 +89,16 @@ namespace WTelegram
return result;
}
internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L90
internal static byte[] To256Bytes(this BigInteger bi)
{
var bigEndian = bi.ToByteArray(true, true);
if (bigEndian.Length == 256) return bigEndian;
var result = new byte[256];
bigEndian.CopyTo(result, 256 - bigEndian.Length);
return result;
}
internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L103
{
if (pq < 2) return 1;
var random = new Random();
@ -61,26 +112,16 @@ namespace WTelegram
for (int j = 1; j < lim; j++)
{
iter++;
ulong a = x;
ulong b = x;
ulong c = q;
// c += a * b
while (b != 0)
// x = (q + x * x) % pq
ulong res = q, a = x;
while (x != 0)
{
if ((b & 1) != 0)
{
c += a;
if (c >= pq)
c -= pq;
}
a += a;
if (a >= pq)
a -= pq;
b >>= 1;
if ((x & 1) != 0)
res = (res + a) % pq;
a = (a + a) % pq;
x >>= 1;
}
x = c;
x = res;
ulong z = x < y ? pq + x - y : x - y;
g = gcd(z, pq);
if (g != 1)
@ -100,33 +141,133 @@ namespace WTelegram
}
return g;
static ulong gcd(ulong a, ulong b)
static ulong gcd(ulong left, ulong right)
{
if (a == 0) return b;
if (b == 0) return a;
int shift = 0;
while ((a & 1) == 0 && (b & 1) == 0)
while (right != 0)
{
a >>= 1;
b >>= 1;
shift++;
}
while (true)
{
while ((a & 1) == 0)
a >>= 1;
while ((b & 1) == 0)
b >>= 1;
if (a > b)
a -= b;
else if (b > a)
b -= a;
else
return a << shift;
ulong num = left % right;
left = right;
right = num;
}
return left;
}
}
public static int MillerRabinIterations { get; set; } = 64; // 64 is OpenSSL default for 2048-bits numbers
/// <summary>MillerRabin primality test</summary>
/// <param name="n">The number to check for primality</param>
public static bool IsProbablePrime(this BigInteger n)
{
var n_minus_one = n - BigInteger.One;
if (n_minus_one.Sign <= 0) return false;
int s;
var d = n_minus_one;
for (s = 0; d.IsEven; s++) d >>= 1;
var bitLen = n.GetBitLength();
var randomBytes = new byte[bitLen / 8 + 1];
var lastByteMask = (byte)((1 << (int)(bitLen % 8)) - 1);
BigInteger a;
if (MillerRabinIterations < 15) // 15 is the minimum recommended by Telegram
Log(3, $"MillerRabinIterations ({MillerRabinIterations}) is below the minimal level of safety (15)");
for (int i = 0; i < MillerRabinIterations; i++)
{
do
{
Encryption.RNG.GetBytes(randomBytes);
randomBytes[^1] &= lastByteMask; // we don't want more bits than necessary
a = new BigInteger(randomBytes);
}
while (a < 3 || a >= n_minus_one);
a--;
var x = BigInteger.ModPow(a, d, n);
if (x.IsOne || x == n_minus_one) continue;
int r;
for (r = s - 1; r > 0; r--)
{
x = BigInteger.ModPow(x, 2, n);
if (x.IsOne) return false;
if (x == n_minus_one) break;
}
if (r == 0) return false;
}
return true;
}
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,
0x3c, 0x7b, 0x58, 0x5d, 0x49, 0x64, 0x91, 0x80, 0x99, 0x96, 0x8f, 0x80, 0x8c, 0x8a, 0xa0, 0xb4, 0xe6, 0xc3,
0xa0, 0xaa, 0xda, 0xad, 0x8a, 0x8c, 0xc8, 0xff, 0xcb, 0xda, 0xee, 0xf5, 0xff, 0xff, 0xff, 0x9b, 0xc1, 0xff,
0xff, 0xff, 0xfa, 0xff, 0xe6, 0xfd, 0xff, 0xf8, 0xff, 0xdb, 0x00, 0x43, 0x01, 0x2b, 0x2d, 0x2d, 0x3c, 0x35,
0x3c, 0x76, 0x41, 0x41, 0x76, 0xf8, 0xa5, 0x8c, 0xa5, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x00, 0x00, 0x00, 0x03, 0x01, 0x22, 0x00,
0x02, 0x11, 0x01, 0x03, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04, 0x03, 0x05, 0x05,
0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06,
0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52,
0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53,
0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75,
0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,
0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,
0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6,
0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4,
0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03, 0x04, 0x07, 0x05,
0x04, 0x04, 0x00, 0x01, 0x02, 0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41,
0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33,
0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74,
0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94,
0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,
0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,
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()
{
var os = System.Runtime.InteropServices.RuntimeInformation.OSDescription;
int space = os.IndexOf(' ') + 1, dot = os.IndexOf('.');
return os[(os.IndexOf(' ', space) < 0 ? 0 : space)..(dot < 0 ? os.Length : dot)];
}
internal static string GetAppVersion()
=> (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0";
public class IndirectStream(Stream innerStream) : Stream
{
public long? ContentLength;
protected readonly Stream _innerStream = innerStream;
public override bool CanRead => _innerStream.CanRead;
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 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();
}
}
public class WTException : ApplicationException
{
public WTException(string message) : base(message) { }
public WTException(string message, Exception innerException) : base(message, innerException) { }
}
}

626
src/SecretChats.cs Normal file
View file

@ -0,0 +1,626 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TL;
using static WTelegram.Compat;
using static WTelegram.Encryption;
namespace WTelegram
{
public interface ISecretChat
{
int ChatId { get; }
long RemoteUserId { get; }
InputEncryptedChat Peer { get; }
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 = [];
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;
/// <summary>Instantiate a Secret Chats manager</summary>
/// <param name="client">The Telegram client</param>
/// <param name="filename">File path to load/save secret chats keys/status (optional)</param>
public SecretChats(Client client, string filename = null)
{
this.client = client;
if (filename != null)
{
storage = File.Open(filename, FileMode.OpenOrCreate);
if (storage.Length != 0) Load(storage);
OnChanged = () => { storage.SetLength(0); Save(storage); };
}
}
public void Dispose() { OnChanged?.Invoke(); storage?.Dispose(); sha256.Dispose(); sha1.Dispose(); }
public List<ISecretChat> Chats => [.. chats.Values];
public bool IsChatActive(int chat_id) => !(chats.GetValueOrDefault(chat_id)?.flags.HasFlag(SecretChat.Flags.requestChat) ?? true);
public void Save(Stream output)
{
using var writer = new BinaryWriter(output, Encoding.UTF8, true);
writer.Write(0);
writer.WriteTLObject(dh);
writer.Write(chats.Count);
foreach (var chat in chats.Values)
writer.WriteTLObject(chat);
}
public void Load(Stream input)
{
using var reader = new BinaryReader(input, Encoding.UTF8, true);
if (reader.ReadInt32() != 0) throw new WTException("Unrecognized Secrets format");
dh = (Messages_DhConfig)reader.ReadTLObject();
if (dh?.p != null) dh_prime = BigEndianInteger(dh.p);
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
var chat = (SecretChat)reader.ReadTLObject();
if (chat.authKey?.Length > 0) chat.key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.authKey).AsSpan(12));
chats[chat.ChatId] = chat;
}
}
/// <summary>Terminate the secret chat</summary>
/// <param name="chat_id">Secret Chat ID</param>
/// <param name="delete_history">Whether to delete the entire chat history for the other user as well</param>
public async Task Discard(int chat_id, bool delete_history = false)
{
if (chats.TryGetValue(chat_id, out var chat))
{
chats.Remove(chat_id);
chat.Discarded();
}
try
{
await client.Messages_DiscardEncryption(chat_id, delete_history);
}
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "ENCRYPTION_ALREADY_DECLINED") { }
}
private async Task<byte[]> UpdateDHConfig()
{
var mdhcb = await client.Messages_GetDhConfig(dh?.version ?? 0, 256);
if (mdhcb is Messages_DhConfigNotModified { random: var random })
_ = dh ?? throw new WTException("DhConfigNotModified on zero version");
else if (mdhcb is Messages_DhConfig dhc)
{
var p = BigEndianInteger(dhc.p);
CheckGoodPrime(p, dhc.g);
(dh, dh_prime, random, dh.random) = (dhc, p, dhc.random, null);
}
else throw new WTException("Unexpected DHConfig response: " + mdhcb?.GetType().Name);
if (random.Length != 256) throw new WTException("Invalid DHConfig random");
var salt = new byte[256];
RNG.GetBytes(salt);
for (int i = 0; i < 256; i++) salt[i] ^= random[i];
return salt;
}
/// <summary>Initiate a secret chat with the given user.<br/>(chat must be acknowledged by remote user before being active)</summary>
/// <param name="user">The remote user</param>
/// <returns>Secret Chat ID</returns>
/// <exception cref="WTException"></exception>
public async Task<int> Request(InputUserBase user)
{
int chat_id;
do chat_id = (int)Helpers.RandomLong(); while (chats.ContainsKey(chat_id));
var chat = chats[chat_id] = new SecretChat
{
flags = SecretChat.Flags.requestChat | SecretChat.Flags.originator,
peer = { chat_id = chat_id },
participant_id = user.UserId ?? 0,
salt = await UpdateDHConfig(),
out_seq_no = 1,
};
var a = BigEndianInteger(chat.salt);
var g_a = BigInteger.ModPow(dh.g, a, dh_prime);
CheckGoodGaAndGb(g_a, dh_prime);
var ecb = await client.Messages_RequestEncryption(user, chat_id, g_a.To256Bytes());
if (ecb is not EncryptedChatWaiting ecw || ecw.id != chat_id || ecw.participant_id != chat.participant_id)
throw new WTException("Invalid " + ecb?.GetType().Name);
chat.peer.access_hash = ecw.access_hash;
return chat_id;
}
/// <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>
public async Task<bool> HandleUpdate(UpdateEncryption update, bool? acceptChatRequests = true)
{
try
{
if (chats.TryGetValue(update.chat.ID, out var chat))
{
if (update.chat is EncryptedChat ec && chat.flags.HasFlag(SecretChat.Flags.requestChat)) // remote accepted our request
{
var a = BigEndianInteger(chat.salt);
var g_b = BigEndianInteger(ec.g_a_or_b);
CheckGoodGaAndGb(g_b, dh_prime);
var gab = BigInteger.ModPow(g_b, a, dh_prime);
chat.flags &= ~SecretChat.Flags.requestChat;
SetAuthKey(chat, gab.To256Bytes());
if (ec.key_fingerprint != chat.key_fingerprint) throw new WTException("Invalid fingerprint on accepted secret chat");
if (ec.access_hash != chat.peer.access_hash || ec.participant_id != chat.participant_id) throw new WTException("Invalid peer on accepted secret chat");
await SendNotifyLayer(chat);
return true;
}
else if (update.chat is EncryptedChatDiscarded ecd)
{
chats.Remove(chat.ChatId);
chat.Discarded();
return true;
}
Helpers.Log(3, $"Unexpected {update.chat.GetType().Name} for secret chat {chat.ChatId}");
return false;
}
else if (update.chat is EncryptedChatRequested ecr) // incoming request
{
switch (acceptChatRequests)
{
case null: return false;
case false: await client.Messages_DiscardEncryption(ecr.id, false); return true;
case true:
var salt = await UpdateDHConfig();
var b = BigEndianInteger(salt);
var g_b = BigInteger.ModPow(dh.g, b, dh_prime);
var g_a = BigEndianInteger(ecr.g_a);
CheckGoodGaAndGb(g_a, dh_prime);
CheckGoodGaAndGb(g_b, dh_prime);
var gab = BigInteger.ModPow(g_a, b, dh_prime);
chat = chats[ecr.id] = new SecretChat
{
flags = 0,
peer = { chat_id = ecr.id, access_hash = ecr.access_hash },
participant_id = ecr.admin_id,
in_seq_no = -1,
};
SetAuthKey(chat, gab.To256Bytes());
var ecb = await client.Messages_AcceptEncryption(chat.peer, g_b.ToByteArray(true, true), chat.key_fingerprint);
if (ecb is not EncryptedChat ec || ec.id != ecr.id || ec.access_hash != ecr.access_hash ||
ec.admin_id != ecr.admin_id || ec.key_fingerprint != chat.key_fingerprint)
throw new WTException("Inconsistent accepted secret chat");
await SendNotifyLayer(chat);
return true;
}
}
else if (update.chat is EncryptedChatDiscarded) // unknown chat discarded
return true;
Helpers.Log(3, $"Unexpected {update.chat.GetType().Name} for unknown secret chat {update.chat.ID}");
return false;
}
catch
{
await Discard(update.chat.ID);
throw;
}
finally
{
OnChanged?.Invoke();
}
}
private void SetAuthKey(SecretChat chat, byte[] key)
{
chat.authKey = key;
chat.key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(key).AsSpan(12));
chat.exchange_id = 0;
chat.key_useCount = 0;
chat.key_created = DateTime.UtcNow;
}
private async Task SendNotifyLayer(SecretChat chat)
{
await SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
action = new TL.Layer23.DecryptedMessageActionNotifyLayer { layer = Layer.SecretChats } });
if (chat.remoteLayer < Layer.MTProto2) chat.remoteLayer = Layer.MTProto2;
}
/// <summary>Encrypt and send a message on a secret chat</summary>
/// <remarks>You would typically pass an instance of <see cref="TL.Layer73.DecryptedMessage"/> or <see cref="TL.Layer23.DecryptedMessageService"/> that you created and filled
/// <br/>Remember to fill <c>random_id</c> with <see cref="WTelegram.Helpers.RandomLong"/>, and the <c>flags</c> field if necessary</remarks>
/// <param name="chatId">Secret Chat ID</param>
/// <param name="msg">The pre-filled <see cref="TL.Layer73.DecryptedMessage">DecryptedMessage</see> or <see cref="TL.Layer23.DecryptedMessageService">DecryptedMessageService </see> to send</param>
/// <param name="silent">Send encrypted message without a notification</param>
/// <param name="file">Optional file attachment. See method <see cref="UploadFile">UploadFile</see></param>
/// <returns>Confirmation of sent message</returns>
public async Task<Messages_SentEncryptedMessage> SendMessage(int chatId, DecryptedMessageBase msg, bool silent = false, InputEncryptedFileBase file = null)
{
if (!chats.TryGetValue(chatId, out var chat)) throw new WTException("Secret chat not found");
try
{
var dml = new TL.Layer23.DecryptedMessageLayer
{
layer = Math.Min(chat.remoteLayer, Layer.SecretChats),
random_bytes = new byte[15],
in_seq_no = chat.in_seq_no < 0 ? chat.in_seq_no + 2 : chat.in_seq_no,
out_seq_no = chat.out_seq_no,
message = msg
};
//Debug.WriteLine($">\t\t\t\t{dml.in_seq_no}\t{dml.out_seq_no}");
var result = await SendMessage(chat, dml, silent, file);
chat.out_seq_no += 2;
return result;
}
finally
{
OnChanged?.Invoke();
}
}
private async Task<Messages_SentEncryptedMessage> SendMessage(SecretChat chat, TL.Layer23.DecryptedMessageLayer dml, bool silent = false, InputEncryptedFileBase file = null)
{
RNG.GetBytes(dml.random_bytes);
int x = 8 - (int)(chat.flags & SecretChat.Flags.originator);
using var memStream = new MemoryStream(1024);
using var writer = new BinaryWriter(memStream);
using var clearStream = new MemoryStream(1024);
using var clearWriter = new BinaryWriter(clearStream);
clearWriter.Write(chat.authKey, 88 + x, 32);
clearWriter.Write(0); // int32 message_data_length (to be patched)
clearWriter.WriteTLObject(dml); // bytes message_data
int clearLength = (int)clearStream.Length - 32; // length before padding (= 4 + message_data_length)
int padding = (0x7FFFFFF0 - clearLength) % 16;
padding += random.Next(2, 16) * 16; // MTProto 2.0 padding must be between 12..1024 with total length divisible by 16
clearStream.SetLength(32 + clearLength + padding);
byte[] clearBuffer = clearStream.GetBuffer();
BinaryPrimitives.WriteInt32LittleEndian(clearBuffer.AsSpan(32), clearLength - 4); // patch message_data_length
RNG.GetBytes(clearBuffer, 32 + clearLength, padding);
var msgKeyLarge = sha256.ComputeHash(clearBuffer, 0, 32 + clearLength + padding);
const int msgKeyOffset = 8; // msg_key = middle 128-bits of SHA256(authkey_part+plaintext+padding)
byte[] encrypted_data = EncryptDecryptMessage(clearBuffer.AsSpan(32, clearLength + padding), true, x, chat.authKey, msgKeyLarge, msgKeyOffset, sha256);
writer.Write(chat.key_fingerprint); // int64 key_fingerprint
writer.Write(msgKeyLarge, msgKeyOffset, 16); // int128 msg_key
writer.Write(encrypted_data); // bytes encrypted_data
var data = memStream.ToArray();
CheckPFS(chat);
if (file != null)
return await client.Messages_SendEncryptedFile(chat.peer, dml.message.RandomId, data, file, silent);
else if (dml.message is TL.Layer23.DecryptedMessageService or TL.Layer8.DecryptedMessageService)
return await client.Messages_SendEncryptedService(chat.peer, dml.message.RandomId, data);
else
return await client.Messages_SendEncrypted(chat.peer, dml.message.RandomId, data, silent);
}
private IObject Decrypt(SecretChat chat, byte[] data, int dataLen)
{
if (dataLen < 32) // authKeyId+msgKey+(length+ctorNb)
throw new WTException($"Encrypted packet too small: {data.Length}");
var authKey = chat.authKey;
long authKeyId = BinaryPrimitives.ReadInt64LittleEndian(data);
if (authKeyId == chat.key_fingerprint)
if (!chat.flags.HasFlag(SecretChat.Flags.commitKey)) CheckPFS(chat);
else { chat.flags &= ~SecretChat.Flags.commitKey; Array.Clear(chat.salt, 0, chat.salt.Length); }
else if (chat.flags.HasFlag(SecretChat.Flags.commitKey) && authKeyId == BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12))) authKey = chat.salt;
else throw new WTException($"Received a packet encrypted with unexpected key {authKeyId:X}");
int x = (int)(chat.flags & SecretChat.Flags.originator);
byte[] decrypted_data = EncryptDecryptMessage(data.AsSpan(24, dataLen - 24), false, x, authKey, data, 8, sha256);
var length = BinaryPrimitives.ReadInt32LittleEndian(decrypted_data);
var success = length >= 4 && length <= decrypted_data.Length - 4;
if (success)
{
sha256.Initialize();
sha256.TransformBlock(authKey, 88 + x, 32, null, 0);
sha256.TransformFinalBlock(decrypted_data, 0, decrypted_data.Length);
if (success = data.AsSpan(8, 16).SequenceEqual(sha256.Hash.AsSpan(8, 16)))
if (decrypted_data.Length - 4 - length is < 12 or > 1024) throw new WTException($"Invalid MTProto2 padding length: {decrypted_data.Length - 4}-{length}");
else if (chat.remoteLayer < Layer.MTProto2) chat.remoteLayer = Layer.MTProto2;
}
if (!success) throw new WTException("Could not decrypt message");
if (length % 4 != 0) throw new WTException($"Invalid message_data_length: {length}");
using var reader = new BinaryReader(new MemoryStream(decrypted_data, 4, length));
return reader.ReadTLObject();
}
/// <summary>Decrypt an encrypted message obtained in <see cref="UpdateNewEncryptedMessage"/></summary>
/// <param name="msg">Encrypted <see cref="UpdateNewEncryptedMessage.message"/></param>
/// <param name="fillGaps">If messages are missing or received in wrong order, automatically request to resend missing messages</param>
/// <returns>An array of <see cref="TL.Layer73.DecryptedMessage">DecryptedMessage</see> or <see cref="TL.Layer23.DecryptedMessageService">DecryptedMessageService </see> from various TL.LayerXX namespaces.<br/>
/// You can use the generic properties to access their fields
/// <para>May return an empty array if msg was already previously received or is not the next message in sequence.
/// <br/>May return multiple messages if missing messages are finally received (using <paramref name="fillGaps"/> = true)</para></returns>
/// <exception cref="WTException"></exception>
public ICollection<DecryptedMessageBase> DecryptMessage(EncryptedMessageBase msg, bool fillGaps = true)
{
if (!chats.TryGetValue(msg.ChatId, out var chat)) throw new WTException("Secret chat not found");
try
{
var obj = Decrypt(chat, msg.Bytes, msg.Bytes.Length);
if (obj is not TL.Layer23.DecryptedMessageLayer dml) throw new WTException("Decrypted object is not DecryptedMessageLayer");
if (dml.random_bytes.Length < 15) throw new WTException("Not enough random_bytes");
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 []; // already received message
var pendingMsgSeqNo = chat.pendingMsgs.Keys;
if (fillGaps && dml.out_seq_no > chat.in_seq_no + 2)
{
var lastPending = pendingMsgSeqNo.LastOrDefault();
if (lastPending == 0) lastPending = chat.in_seq_no;
chat.pendingMsgs[dml.out_seq_no] = dml;
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 [];
}
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 [];
else return [dml.message];
else // we have pendingMsgs completing the sequence in order
{
var list = new List<DecryptedMessageBase>();
if (!HandleAction(chat, dml.message.Action))
list.Add(dml.message);
do
{
dml = chat.pendingMsgs.Values[0];
chat.pendingMsgs.RemoveAt(0);
chat.in_seq_no += 2;
if (!HandleAction(chat, dml.message.Action))
list.Add(dml.message);
} while (pendingMsgSeqNo.Count != 0 && pendingMsgSeqNo[0] == chat.in_seq_no + 2);
return list;
}
}
catch (Exception)
{
_ = Discard(msg.ChatId);
throw;
}
finally
{
OnChanged?.Invoke();
}
}
private bool HandleAction(SecretChat chat, DecryptedMessageAction action)
{
switch (action)
{
case TL.Layer23.DecryptedMessageActionNotifyLayer dmanl:
chat.remoteLayer = dmanl.layer;
return true;
case TL.Layer23.DecryptedMessageActionResend resend:
Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> Resend {resend.start_seq_no}-{resend.end_seq_no}");
var msgSvc = new TL.Layer23.DecryptedMessageService { action = new TL.Layer23.DecryptedMessageActionNoop() };
var dml = new TL.Layer23.DecryptedMessageLayer
{
layer = Math.Min(chat.remoteLayer, Layer.SecretChats),
random_bytes = new byte[15],
in_seq_no = chat.in_seq_no,
message = msgSvc
};
for (dml.out_seq_no = resend.start_seq_no; dml.out_seq_no <= resend.end_seq_no; dml.out_seq_no += 2)
{
msgSvc.random_id = Helpers.RandomLong();
_ = SendMessage(chat, dml);
}
return true;
case TL.Layer23.DecryptedMessageActionNoop:
Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> Noop");
return true;
case TL.Layer23.DecryptedMessageActionRequestKey:
case TL.Layer23.DecryptedMessageActionAcceptKey:
case TL.Layer23.DecryptedMessageActionCommitKey:
case TL.Layer23.DecryptedMessageActionAbortKey:
Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> PFS {action.GetType().Name[22..]}");
HandlePFS(chat, action);
return true;
}
return false;
}
private async void CheckPFS(SecretChat chat)
{
if (++chat.key_useCount < ThresholdPFS && chat.key_created >= DateTime.UtcNow.AddDays(-7)) return;
if (chat.key_useCount < ThresholdPFS) chat.key_useCount = ThresholdPFS;
if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != 0)
if (chat.key_useCount < ThresholdPFS * 2) return;
else { Helpers.Log(4, "SC{(short)chat.ChatId:X4}> PFS Failure"); _ = Discard(chat.ChatId); return; }
try
{
chat.flags |= SecretChat.Flags.renewKey;
Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> PFS RenewKey");
await Task.Delay(100);
chat.salt = new byte[256];
RNG.GetBytes(chat.salt);
var a = BigEndianInteger(chat.salt);
var g_a = BigInteger.ModPow(dh.g, a, dh_prime);
CheckGoodGaAndGb(g_a, dh_prime);
chat.exchange_id = Helpers.RandomLong();
await SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
action = new TL.Layer23.DecryptedMessageActionRequestKey { exchange_id = chat.exchange_id, g_a = g_a.To256Bytes() } });
}
catch (Exception ex)
{
Helpers.Log(4, "Error in CheckRenewKey: " + ex);
chat.flags &= ~SecretChat.Flags.renewKey;
}
}
private async void HandlePFS(SecretChat chat, DecryptedMessageAction action)
{
try
{
switch (action)
{
case TL.Layer23.DecryptedMessageActionRequestKey request:
switch (chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey))
{
case SecretChat.Flags.renewKey: // Concurrent Re-Keying
if (chat.exchange_id > request.exchange_id) return; // we won, ignore the smaller exchange_id RequestKey
chat.flags &= ~SecretChat.Flags.renewKey;
if (chat.exchange_id == request.exchange_id) // equal => silent abort both re-keing
{
Array.Clear(chat.salt, 0, chat.salt.Length);
chat.exchange_id = 0;
return;
}
break; // we lost, process with the larger exchange_id RequestKey
case 0: break;
default: throw new WTException("Invalid RequestKey");
}
var g_a = BigEndianInteger(request.g_a);
var salt = new byte[256];
RNG.GetBytes(salt);
var b = BigEndianInteger(salt);
var g_b = BigInteger.ModPow(dh.g, b, dh_prime);
CheckGoodGaAndGb(g_a, dh_prime);
CheckGoodGaAndGb(g_b, dh_prime);
var gab = BigInteger.ModPow(g_a, b, dh_prime);
chat.flags |= SecretChat.Flags.acceptKey;
chat.salt = gab.To256Bytes();
chat.exchange_id = request.exchange_id;
var key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12));
await SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
action = new TL.Layer23.DecryptedMessageActionAcceptKey { exchange_id = request.exchange_id, g_b = g_b.To256Bytes(), key_fingerprint = key_fingerprint } });
break;
case TL.Layer23.DecryptedMessageActionAcceptKey accept:
if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.renewKey)
throw new WTException("Invalid AcceptKey");
if (accept.exchange_id != chat.exchange_id)
throw new WTException("AcceptKey: exchange_id mismatch");
var a = BigEndianInteger(chat.salt);
g_b = BigEndianInteger(accept.g_b);
CheckGoodGaAndGb(g_b, dh_prime);
gab = BigInteger.ModPow(g_b, a, dh_prime);
var authKey = gab.To256Bytes();
key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(authKey).AsSpan(12));
if (accept.key_fingerprint != key_fingerprint)
throw new WTException("AcceptKey: key_fingerprint mismatch");
_ = SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
action = new TL.Layer23.DecryptedMessageActionCommitKey { exchange_id = accept.exchange_id, key_fingerprint = accept.key_fingerprint } });
chat.salt = chat.authKey; // A may only discard the previous key after a message encrypted with the new key has been received.
SetAuthKey(chat, authKey);
chat.flags = chat.flags & ~SecretChat.Flags.renewKey | SecretChat.Flags.commitKey;
break;
case TL.Layer23.DecryptedMessageActionCommitKey commit:
if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.acceptKey)
throw new WTException("Invalid RequestKey");
key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12));
if (commit.exchange_id != chat.exchange_id | commit.key_fingerprint != key_fingerprint)
throw new WTException("CommitKey: data mismatch");
chat.flags &= ~SecretChat.Flags.acceptKey;
authKey = chat.authKey;
SetAuthKey(chat, chat.salt);
Array.Clear(authKey, 0, authKey.Length); // the old key must be securely discarded
await SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(),
action = new TL.Layer23.DecryptedMessageActionNoop() });
break;
case TL.Layer23.DecryptedMessageActionAbortKey abort:
if ((chat.flags & (SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) == 0 ||
chat.flags.HasFlag(SecretChat.Flags.commitKey) || abort.exchange_id != chat.exchange_id)
return;
chat.flags &= ~(SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey);
Array.Clear(chat.salt, 0, chat.salt.Length);
chat.exchange_id = 0;
break;
}
}
catch (Exception ex)
{
Helpers.Log(4, $"Error handling {action}: {ex}");
_ = Discard(chat.ChatId);
}
}
/// <summary>Upload a file to Telegram in encrypted form</summary>
/// <param name="stream">Content of the file to upload. This method close/dispose the stream</param>
/// <param name="media">The associated media structure that will be updated with file size and the random AES key/iv</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>the uploaded file info that should be passed to method <see cref="SendMessage">SendMessage</see></returns>
public async Task<InputEncryptedFileBase> UploadFile(Stream stream, DecryptedMessageMedia media, Client.ProgressCallback progress = null)
{
byte[] aes_key = new byte[32], aes_iv = new byte[32];
RNG.GetBytes(aes_key);
RNG.GetBytes(aes_iv);
media.SizeKeyIV = (stream.Length, aes_key, aes_iv);
using var md5 = MD5.Create();
md5.TransformBlock(aes_key, 0, 32, null, 0);
var res = md5.TransformFinalBlock(aes_iv, 0, 32);
long fingerprint = BinaryPrimitives.ReadInt64LittleEndian(md5.Hash);
fingerprint ^= fingerprint >> 32;
using var ige = new AES_IGE_Stream(stream, aes_key, aes_iv, true);
var inputFile = await client.UploadFileAsync(ige, null, progress);
return inputFile.ToInputEncryptedFile((int)fingerprint);
}
/// <summary>Download and decrypt an encrypted file from Telegram Secret Chat into the outputStream</summary>
/// <param name="encryptedFile">The encrypted file to download &amp; decrypt</param>
/// <param name="media">The associated message media structure</param>
/// <param name="outputStream">Stream to write the decrypted file content to. This method does not close/dispose the stream</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>The mime type of the decrypted file, <see langword="null"/> if unknown</returns>
public async Task<string> DownloadFile(EncryptedFile encryptedFile, DecryptedMessageMedia media, Stream outputStream, Client.ProgressCallback progress = null)
{
var (size, key, iv) = media.SizeKeyIV;
if (key == null || iv == null) throw new ArgumentException("Media has no information about encrypted file", nameof(media));
using var md5 = MD5.Create();
md5.TransformBlock(key, 0, 32, null, 0);
var res = md5.TransformFinalBlock(iv, 0, 32);
long fingerprint = BinaryPrimitives.ReadInt64LittleEndian(md5.Hash);
fingerprint ^= fingerprint >> 32;
if (encryptedFile.key_fingerprint != (int)fingerprint) throw new WTException("Encrypted file fingerprint mismatch");
using var decryptStream = new AES_IGE_Stream(outputStream, size, key, iv);
var fileLocation = encryptedFile.ToFileLocation();
await client.DownloadFileAsync(fileLocation, decryptStream, encryptedFile.dc_id, encryptedFile.size, progress);
return media.MimeType;
}
}
}

565
src/Services.cs Normal file
View file

@ -0,0 +1,565 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using WTelegram;
namespace TL
{
public static class Services
{
public sealed partial class CollectorPeer(IDictionary<long, User> _users, IDictionary<long, ChatBase> _chats) : Peer, IPeerCollector
{
public override long ID => 0;
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)
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;
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)0x5DAFE000;
const User.Flags2 updated_flags2 = (User.Flags2)0x711;
// 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 (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 > 0)
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, 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");
[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
{
/// <summary>Converts a <a href="https://core.telegram.org/bots/api/#markdownv2-style">Markdown text</a> into the (plain text + entities) format used by Telegram messages</summary>
/// <param name="_">not used anymore, you can pass null</param>
/// <param name="text">[in] The Markdown text<br/>[out] The same (plain) text, stripped of all Markdown notation</param>
/// <param name="premium">Generate premium entities if any</param>
/// <param name="users">Dictionary used for <c>tg://user?id=</c> notation</param>
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="Client.SendMessageAsync">SendMessageAsync</see> or <see cref="Client.SendMediaAsync">SendMediaAsync</see></returns>
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 (offset = 0; offset < sb.Length;)
{
switch (sb[offset])
{
case '\r': sb.Remove(offset, 1); break;
case '\\': sb.Remove(offset++, 1); break;
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);
ProcessEntity<MessageEntityUnderline>();
}
else
ProcessEntity<MessageEntityItalic>();
break;
case '|':
if (inCode == 0 && offset + 1 < sb.Length && sb[offset + 1] == '|')
{
sb.Remove(offset, 1);
ProcessEntity<MessageEntitySpoiler>();
}
else
offset++;
break;
case '`':
int count = entities.Count;
if (offset + 2 < sb.Length && sb[offset + 1] == '`' && sb[offset + 2] == '`')
{
int len = 3;
if (entities.FindLast(e => e.length == -1) is MessageEntityPre pre)
pre.length = offset - pre.offset;
else
{
while (offset + len < sb.Length && !char.IsWhiteSpace(sb[offset + len]))
len++;
entities.Add(new MessageEntityPre { offset = offset, length = -1, language = sb.ToString(offset + 3, len - 3) });
if (sb[offset + len] == '\n') len++;
}
sb.Remove(offset, len);
}
else
ProcessEntity<MessageEntityCode>();
if (entities.Count > count) inCode++; else inCode--;
break;
case '>' when inCode == 0 && offset == 0 || sb[offset - 1] == '\n':
sb.Remove(offset, 1);
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 (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)
{
textUrl.length = offset - textUrl.offset;
int offset2 = offset + 2;
while (offset2 < sb.Length)
{
char c = sb[offset2++];
if (c == '\\') sb.Remove(offset2 - 1, 1);
else if (c == ')') break;
}
textUrl.url = sb.ToString(offset + 2, offset2 - offset - 3);
if (textUrl.url.StartsWith("tg://user?id=") && long.TryParse(textUrl.url[13..], out var id) && users?.GetValueOrDefault(id)?.access_hash is long hash)
entities[lastIndex] = new InputMessageEntityMentionName { offset = textUrl.offset, length = textUrl.length, user_id = new InputUser(id, hash) };
else if ((textUrl.url.StartsWith("tg://emoji?id=") || textUrl.url.StartsWith("emoji?id=")) && long.TryParse(textUrl.url[(textUrl.url.IndexOf('=') + 1)..], out id))
if (premium) entities[lastIndex] = new MessageEntityCustomEmoji { offset = textUrl.offset, length = textUrl.length, document_id = id };
else entities.RemoveAt(lastIndex);
sb.Remove(offset, offset2 - offset);
break;
}
}
offset++;
break;
default: offset++; break;
}
void ProcessEntity<T>() where T : MessageEntity, new()
{
sb.Remove(offset, 1);
if (entities.LastOrDefault(e => e.length == -1) is T prevEntity)
if (offset == prevEntity.offset)
entities.Remove(prevEntity);
else
prevEntity.length = offset - prevEntity.offset;
else
entities.Add(new T { offset = offset, length = -1 });
}
}
if (lastBlockQuote != null) CloseBlockQuote();
HtmlText.FixUps(sb, entities);
text = sb.ToString();
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>
/// <param name="client">Client, used only for getting current user ID in case of <c>InputMessageEntityMentionName+InputUserSelf</c></param>
/// <param name="message">The plain text, typically obtained from <see cref="Message.message"/></param>
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="Message.entities"/></param>
/// <param name="premium">Convert premium entities (might lead to non-standard markdown)</param>
/// <returns>The message text with MarkdownV2 formattings</returns>
public static string EntitiesToMarkdown(this Client client, string message, MessageEntity[] entities, bool premium = false)
{
if (entities == null || entities.Length == 0) return Escape(message);
var closings = new List<(int offset, string md)>();
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;
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))
{
var closing = (nextEntity.offset + nextEntity.length, md);
if (md[0] is '[' or '!')
{
if (nextEntity is MessageEntityTextUrl metu)
closing.md = $"]({metu.url.Replace("\\", "\\\\").Replace(")", "\\)").Replace(">", "%3E")})";
else if (nextEntity is MessageEntityMentionName memn)
closing.md = $"](tg://user?id={memn.user_id})";
else if (nextEntity is InputMessageEntityMentionName imemn)
closing.md = $"](tg://user?id={imemn.user_id.UserId ?? client.UserId})";
else if (nextEntity is MessageEntityCustomEmoji mecu)
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));
closings.Insert(index, closing);
if (i > 0 && md[0] == '_' && sb[i - 1] == '_') md = '\r' + md;
sb.Insert(i, md); i += md.Length;
}
}
switch (lastCh = sb[i])
{
case '_': case '*': case '~': case '#': case '+': case '-': case '=': case '.': case '!':
case '[': case ']': case '(': case ')': case '{': case '}': case '>': case '|': case '\\':
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()
{
[typeof(MessageEntityBold)] = "*",
[typeof(MessageEntityItalic)] = "_",
[typeof(MessageEntityCode)] = "`",
[typeof(MessageEntityPre)] = "```",
[typeof(MessageEntityTextUrl)] = "[",
[typeof(MessageEntityMentionName)] = "[",
[typeof(InputMessageEntityMentionName)] = "[",
[typeof(MessageEntityUnderline)] = "__",
[typeof(MessageEntityStrike)] = "~",
[typeof(MessageEntitySpoiler)] = "||",
[typeof(MessageEntityCustomEmoji)] = "![",
[typeof(MessageEntityBlockquote)] = ">",
};
/// <summary>Insert backslashes in front of Markdown reserved characters</summary>
/// <param name="text">The text to escape</param>
/// <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++)
{
switch (text[index])
{
case '_': case '*': case '~': case '`': case '#': case '+': case '-': case '=': case '.': case '!':
case '[': case ']': case '(': case ')': case '{': case '}': case '>': case '|': case '\\':
sb ??= new StringBuilder(text, text.Length + 32);
sb.Insert(index + added++, '\\');
break;
}
}
return sb?.ToString() ?? text;
}
}
public static class HtmlText
{
/// <summary>Converts an <a href="https://core.telegram.org/bots/api/#html-style">HTML-formatted text</a> into the (plain text + entities) format used by Telegram messages</summary>
/// <param name="_">not used anymore, you can pass null</param>
/// <param name="text">[in] The HTML-formatted text<br/>[out] The same (plain) text, stripped of all HTML tags</param>
/// <param name="premium">Generate premium entities if any</param>
/// <param name="users">Dictionary used for <c>tg://user?id=</c> notation</param>
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="Client.SendMessageAsync">SendMessageAsync</see> or <see cref="Client.SendMediaAsync">SendMediaAsync</see></returns>
public static MessageEntity[] HtmlToEntities(this Client _, ref string text, bool premium = false, IReadOnlyDictionary<long, User> users = null)
{
var entities = new List<MessageEntity>();
var sb = new StringBuilder(text);
int end;
for (int offset = 0; offset < sb.Length;)
{
char c = sb[offset];
if (c == '&')
{
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);
}
else
offset = end;
}
else if (c == '<')
{
for (end = ++offset; end < sb.Length; end++)
if (sb[end] == '>') break;
if (end >= sb.Length) break;
bool closing = sb[offset] == '/';
var tag = closing ? sb.ToString(offset + 1, end - offset - 1) : sb.ToString(offset, end - offset);
sb.Remove(--offset, end + 1 - offset);
switch (tag)
{
case "b": case "strong": ProcessEntity<MessageEntityBold>(); break;
case "i": case "em": ProcessEntity<MessageEntityItalic>(); break;
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)
{
if (tag == "a")
{
var prevEntity = entities.LastOrDefault(e => e.length == -1);
if (prevEntity is InputMessageEntityMentionName or MessageEntityTextUrl)
prevEntity.length = offset - prevEntity.offset;
}
}
else if ((tag[^1] == '"' && tag.StartsWith("a href=\""))
|| (tag[^1] == '\'' && tag.StartsWith("a href='")))
{
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[^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 emoji-id='")))
entities.Add(new MessageEntityCustomEmoji { offset = offset, length = -1, document_id = long.Parse(tag[(tag.IndexOf('=') + 2)..^1]) });
break;
}
void ProcessEntity<T>() where T : MessageEntity, new()
{
if (!closing)
entities.Add(new T { offset = offset, length = -1 });
else if (entities.LastOrDefault(e => e.length == -1) is T prevEntity)
prevEntity.length = offset - prevEntity.offset;
}
}
else
offset++;
}
FixUps(sb, entities);
text = sb.ToString();
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>
/// <param name="client">Client, used only for getting current user ID in case of <c>InputMessageEntityMentionName+InputUserSelf</c></param>
/// <param name="message">The plain text, typically obtained from <see cref="Message.message"/></param>
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="Message.entities"/></param>
/// <param name="premium">Convert premium entities</param>
/// <returns>The message text with HTML formatting tags</returns>
public static string EntitiesToHtml(this Client client, string message, MessageEntity[] entities, bool premium = false)
{
if (entities == null || entities.Length == 0) return Escape(message);
var closings = new List<(int offset, string tag)>();
var sb = new StringBuilder(message);
int entityIndex = 0;
var nextEntity = entities[entityIndex];
for (int offset = 0, i = 0; ; offset++, i++)
{
while (closings.Count != 0 && offset == closings[0].offset)
{
var tag = closings[0].tag;
sb.Insert(i, tag); i += tag.Length;
closings.RemoveAt(0);
}
if (i == sb.Length) break;
for (; offset == nextEntity?.offset; nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null)
{
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=\"{Escape(metu.url)}\">";
else if (nextEntity is MessageEntityMentionName memn)
tag = $"<a href=\"tg://user?id={memn.user_id}\">";
else if (nextEntity is InputMessageEntityMentionName imemn)
tag = $"<a href=\"tg://user?id={imemn.user_id.UserId ?? client.UserId}\">";
}
else if (nextEntity is MessageEntityCustomEmoji mecu)
if (premium) tag = $"<tg-emoji emoji-id=\"{mecu.document_id}\">";
else continue;
else if (nextEntity is MessageEntityPre mep && !string.IsNullOrEmpty(mep.language))
{
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));
closings.Insert(index, closing);
sb.Insert(i, tag); i += tag.Length;
}
}
switch (sb[i])
{
case '&': sb.Insert(i + 1, "amp;"); i += 4; break;
case '<': sb.Insert(i, "&lt"); sb[i += 3] = ';'; break;
case '>': sb.Insert(i, "&gt"); sb[i += 3] = ';'; break;
}
}
return sb.ToString();
}
static readonly Dictionary<Type, string> EntityToTag = new()
{
[typeof(MessageEntityBold)] = "b",
[typeof(MessageEntityItalic)] = "i",
[typeof(MessageEntityCode)] = "code",
[typeof(MessageEntityPre)] = "pre",
[typeof(MessageEntityTextUrl)] = "a",
[typeof(MessageEntityMentionName)] = "a",
[typeof(InputMessageEntityMentionName)] = "a",
[typeof(MessageEntityUnderline)] = "u",
[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;");
}
}

View file

@ -1,93 +1,210 @@
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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
internal sealed partial class Session : IDisposable
{
public long AuthKeyID;
public byte[] AuthKey;
public long Salt;
public long Id;
public int Seqno;
public long ServerTicksOffset;
public long LastSentMsgId;
public TL.DcOption DataCenter;
public byte[] User; // serialization of TL.User
public int ApiId;
public long UserId;
public int MainDC;
public Dictionary<int, DCSession> DCSessions = [];
public TL.DcOption[] DcOptions;
public sealed class DCSession
{
public byte[] AuthKey; // 2048-bit = 256 bytes
public long UserId;
public long OldSalt; // still accepted for a further 1800 seconds
public long Salt;
public SortedList<DateTime, long> Salts;
public TL.DcOption DataCenter;
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 == 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(); } }
const int MsgIdsN = 512;
private long[] _msgIds;
private int _msgIdsHead;
internal bool CheckNewMsgId(long msg_id)
{
if (_msgIds == null)
{
_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;
return true;
}
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;
while (min <= max) // binary search (rotated at newHead)
{
int mid = (min + max) / 2;
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;
}
return true;
}
}
public DateTime SessionStart => _sessionStart;
private readonly DateTime _sessionStart = DateTime.UtcNow;
private string _pathname;
private byte[] _apiHash; // used as AES key for encryption of session file
private readonly SHA256 _sha256 = SHA256.Create();
private Stream _store;
private byte[] _reuseKey; // used only if AES Encryptor.CanReuseTransform = false (Mono)
private byte[] _encrypted = new byte[16];
private ICryptoTransform _encryptor;
private Utf8JsonWriter _jsonWriter;
private readonly MemoryStream _jsonStream = new(4096);
internal static Session LoadOrCreate(string pathname, byte[] apiHash)
public void Dispose()
{
if (File.Exists(pathname))
_sha256.Dispose();
_store.Dispose();
_encryptor.Dispose();
_jsonWriter.Dispose();
_jsonStream.Dispose();
}
internal static Session LoadOrCreate(Stream store, byte[] rgbKey)
{
using var aes = Aes.Create();
Session session = null;
try
{
var length = (int)store.Length;
if (length > 0)
{
var input = new byte[length];
if (store.Read(input, 0, length) != length)
throw new WTException($"Can't read session block ({store.Position}, {length})");
using var sha256 = SHA256.Create();
using var decryptor = aes.CreateDecryptor(rgbKey, input[0..16]);
var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16);
if (!sha256.ComputeHash(utf8Json, 32, utf8Json.Length - 32).SequenceEqual(utf8Json[0..32]))
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;
Encryption.RNG.GetBytes(session._encrypted, 0, 16);
session._encryptor = aes.CreateEncryptor(rgbKey, session._encrypted);
if (!session._encryptor.CanReuseTransform) session._reuseKey = rgbKey;
session._jsonWriter = new Utf8JsonWriter(session._jsonStream, default);
return session;
}
catch (Exception ex)
{
store.Dispose();
throw new WTException($"Exception while reading session file: {ex.Message}\nUse the correct api_hash/id/key, or delete the file to start a new session", ex);
}
}
internal void Save() // must be called with lock(session)
{
JsonSerializer.Serialize(_jsonWriter, this, Helpers.JsonOptions);
var utf8Json = _jsonStream.GetBuffer();
var utf8JsonLen = (int)_jsonStream.Position;
int encryptedLen = 64 + (utf8JsonLen & ~15);
lock (_store) // while updating _encrypted buffer and writing to store
{
if (encryptedLen > _encrypted.Length)
Array.Copy(_encrypted, _encrypted = new byte[encryptedLen + 256], 16);
_encryptor.TransformBlock(_sha256.ComputeHash(utf8Json, 0, utf8JsonLen), 0, 32, _encrypted, 16);
_encryptor.TransformBlock(utf8Json, 0, encryptedLen - 64, _encrypted, 48);
_encryptor.TransformFinalBlock(utf8Json, encryptedLen - 64, utf8JsonLen & 15).CopyTo(_encrypted, encryptedLen - 16);
if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable
using (var aes = Aes.Create())
_encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]);
try
{
var session = Load(pathname, apiHash);
session._pathname = pathname;
session._apiHash = apiHash;
Helpers.Log(2, "Loaded previous session");
return session;
_store.Position = 0;
_store.Write(_encrypted, 0, encryptedLen);
_store.SetLength(encryptedLen);
}
catch (Exception ex)
{
throw new ApplicationException($"Exception while reading session file: {ex.Message}\nDelete the file to start a new session", ex);
Helpers.Log(4, $"{_store} raised {ex}");
}
}
return new Session { _pathname = pathname, _apiHash = apiHash, Id = Helpers.RandomLong() };
}
internal static Session Load(string pathname, byte[] apiHash)
{
var input = File.ReadAllBytes(pathname);
using var aes = Aes.Create();
using var decryptor = aes.CreateDecryptor(apiHash, input[0..16]);
var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16);
if (!SHA256.HashData(utf8Json.AsSpan(32)).SequenceEqual(utf8Json[0..32]))
throw new ApplicationException("Integrity check failed in session loading");
return JsonSerializer.Deserialize<Session>(utf8Json.AsSpan(32), Helpers.JsonOptions);
}
internal void Save()
{
var utf8Json = JsonSerializer.SerializeToUtf8Bytes(this, Helpers.JsonOptions);
var finalBlock = new byte[16];
var output = new byte[(16 + 32 + utf8Json.Length + 15) & ~15];
Encryption.RNG.GetBytes(output, 0, 16);
using var aes = Aes.Create();
using var encryptor = aes.CreateEncryptor(_apiHash, output[0..16]);
encryptor.TransformBlock(SHA256.HashData(utf8Json), 0, 32, output, 16);
encryptor.TransformBlock(utf8Json, 0, utf8Json.Length & ~15, output, 48);
utf8Json.AsSpan(utf8Json.Length & ~15).CopyTo(finalBlock);
encryptor.TransformFinalBlock(finalBlock, 0, utf8Json.Length & 15).CopyTo(output.AsMemory(48 + utf8Json.Length & ~15));
File.WriteAllBytes(_pathname, output);
}
internal (long msgId, int seqno) NewMsg(bool isContent)
{
long msgId = DateTime.UtcNow.Ticks + ServerTicksOffset - 621355968000000000L;
msgId = msgId * 428 + (msgId >> 24) * 25110956; // approximately unixtime*2^32 and divisible by 4
if (msgId <= LastSentMsgId) msgId = LastSentMsgId += 4; else LastSentMsgId = msgId;
int seqno = isContent ? Seqno++ * 2 + 1 : Seqno * 2;
Save();
return (msgId, seqno);
}
internal DateTime MsgIdToStamp(long serverMsgId)
=> new((serverMsgId >> 32) * 10000000 - ServerTicksOffset + 621355968000000000L, DateTimeKind.Utc);
internal void Reset(TL.DcOption newDC = null)
{
DataCenter = newDC;
AuthKeyID = Salt = Seqno = 0;
AuthKey = User = null;
_jsonStream.Position = 0;
_jsonWriter.Reset();
}
}
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 { } }
public override void SetLength(long value) { }
private readonly byte[] _header = new byte[8];
private int _nextPosition = 8;
public SessionStore(string pathname)
: base(pathname, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 1) // no in-app buffering
{
if (base.Read(_header, 0, 8) == 8)
{
var position = BinaryPrimitives.ReadInt32LittleEndian(_header);
var length = BinaryPrimitives.ReadInt32LittleEndian(_header.AsSpan(4));
base.Position = position;
Length = length;
_nextPosition = position + length;
}
}
public override void Write(byte[] buffer, int offset, int count)
{
if (_nextPosition > count * 3) _nextPosition = 8;
base.Position = _nextPosition;
base.Write(buffer, offset, count);
BinaryPrimitives.WriteInt32LittleEndian(_header, _nextPosition);
BinaryPrimitives.WriteInt32LittleEndian(_header.AsSpan(4), count);
_nextPosition += count;
base.Position = 0;
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

@ -1,10 +1,13 @@
// This file is (mainly) generated automatically using the Generator class
using System;
using System;
using System.Threading.Tasks;
using TL.Methods;
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 : ITLObject
public sealed partial class ResPQ : IObject
{
public Int128 nonce;
public Int128 server_nonce;
@ -12,8 +15,8 @@ namespace TL
public long[] server_public_key_fingerprints;
}
[TLDef(0x83C95AEC)] //p_q_inner_data# pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data
public class PQInnerData : ITLObject
[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 partial class PQInnerData : IObject
{
public byte[] pq;
public byte[] p;
@ -22,30 +25,51 @@ namespace TL
public Int128 server_nonce;
public Int256 new_nonce;
}
[TLDef(0xA9F55F95)] //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
[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 sealed partial class PQInnerDataDc : PQInnerData
{
public int dc;
}
[TLDef(0x56FDDF88)] //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
[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 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 sealed partial class PQInnerDataTempDc : PQInnerData
{
public int dc;
public int expires_in; // seconds
public int expires_in;
}
public abstract class ServerDHParams : ITLObject
[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 sealed partial class BindAuthKeyInner : IObject
{
public long nonce;
public long temp_auth_key_id;
public long perm_auth_key_id;
public long temp_session_id;
public DateTime expires_at;
}
public abstract partial class ServerDHParams : IObject
{
public Int128 nonce;
public Int128 server_nonce;
}
[TLDef(0x79CB045D)] //server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params
public class ServerDHParamsFail : ServerDHParams { public Int128 new_nonce_hash; }
[TLDef(0xD0E8075C)] //server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:bytes = Server_DH_Params
public class ServerDHParamsOk : ServerDHParams { public byte[] encrypted_answer; }
[TLDef(0x79CB045D, inheritBefore = true)] //server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params
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 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 : ITLObject
public sealed partial class ServerDHInnerData : IObject
{
public Int128 nonce;
public Int128 server_nonce;
@ -56,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 : ITLObject
public sealed partial class ClientDHInnerData : IObject
{
public Int128 nonce;
public Int128 server_nonce;
@ -64,50 +88,124 @@ namespace TL
public byte[] g_b;
}
public abstract class SetClientDHParamsAnswer : ITLObject
public abstract partial class SetClientDHParamsAnswer : IObject
{
public Int128 nonce;
public Int128 server_nonce;
public Int128 new_nonce_hashN; // 16 low order bytes from SHA1(new_nonce + (01=ok, 02=retry, 03=fail) + 8 high order bytes from SHA1(auth_key))
}
[TLDef(0x3BCBF734)] //dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer
public class DHGenOk : SetClientDHParamsAnswer { }
[TLDef(0x46DC1FB9)] //dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer
public class DHGenRetry : SetClientDHParamsAnswer { }
[TLDef(0xA69DAE02)] //dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer
public class DHGenFail : SetClientDHParamsAnswer { }
[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 : ITLObject
[TLDef(0x3BCBF734, inheritBefore = true)] //dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer
public sealed partial class DhGenOk : SetClientDHParamsAnswer
{
public long nonce;
public long temp_auth_key_id;
public long perm_auth_key_id;
public long temp_session_id;
public DateTime expires_at;
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 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 sealed partial class DhGenFail : SetClientDHParamsAnswer
{
public Int128 new_nonce_hash3;
}
[TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult
public class RpcResult : ITLObject
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 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 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 sealed partial class BadServerSalt : BadMsgNotification
{
public long new_server_salt;
}
[TLDef(0xDA69FB52)] //msgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq
public sealed partial class MsgsStateReq : IObject
{
public long[] msg_ids;
}
[TLDef(0x04DEB57D)] //msgs_state_info#04deb57d req_msg_id:long info:bytes = MsgsStateInfo
public sealed partial class MsgsStateInfo : IObject
{
public long req_msg_id;
public object result;
public byte[] info;
}
[TLDef(0x8CC0D131)] //msgs_all_info#8cc0d131 msg_ids:Vector<long> info:bytes = MsgsAllInfo
public sealed partial class MsgsAllInfo : IObject
{
public long[] msg_ids;
public byte[] info;
}
public abstract partial class MsgDetailedInfoBase : IObject
{
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 sealed partial class MsgDetailedInfo : MsgDetailedInfoBase
{
public long msg_id;
public long answer_msg_id;
public int bytes;
public int status;
public override long AnswerMsgId => answer_msg_id;
public override int Bytes => bytes;
public override int Status => status;
}
[TLDef(0x809DB6DF)] //msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo
public sealed partial class MsgNewDetailedInfo : MsgDetailedInfoBase
{
public long answer_msg_id;
public int bytes;
public int status;
public override long AnswerMsgId => answer_msg_id;
public override int Bytes => bytes;
public override int Status => status;
}
[TLDef(0x7D861A08)] //msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq
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 : ITLObject
public sealed partial class RpcError : IObject
{
public int error_code;
public string error_message;
}
public abstract class RpcDropAnswer : ITLObject { }
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;
@ -115,7 +213,7 @@ namespace TL
}
[TLDef(0x0949D9DC)] //future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt
public class FutureSalt : ITLObject
public sealed partial class FutureSalt : IObject
{
public DateTime valid_since;
public DateTime valid_until;
@ -123,7 +221,7 @@ namespace TL
}
[TLDef(0xAE500895)] //future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts
public class FutureSalts : ITLObject
public sealed partial class FutureSalts : IObject
{
public long req_msg_id;
public DateTime now;
@ -131,168 +229,204 @@ namespace TL
}
[TLDef(0x347773C5)] //pong#347773c5 msg_id:long ping_id:long = Pong
public class Pong : ITLObject
public sealed partial class Pong : IObject
{
public long msg_id;
public long ping_id;
}
public abstract class DestroySessionRes : ITLObject { public long session_id; }
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 : ITLObject { }
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;
public long server_salt;
}
public abstract class MessageContainer : ITLObject { }
[TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer
public class MsgContainer : MessageContainer { public _Message[] messages; }
#pragma warning disable IDE1006 // Naming Styles
//[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message
public class _Message
[TLDef(0x9299359F)] //http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait
public sealed partial class HttpWait : IObject
{
public long msg_id;
public int seqno;
public int bytes;
public ITLObject body;
}
#pragma warning restore IDE1006 // Naming Styles
public abstract class MessageCopy : ITLObject { }
[TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy
public class MsgCopy : MessageCopy { public _Message orig_message; }
[TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object
public class GzipPacked : ITLObject { public byte[] packed_data; }
[TLDef(0x62D6B459)] //msgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck
public class MsgsAck : ITLObject { 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 : ITLObject
{
public long bad_msg_id;
public int bad_msg_seqno;
public int error_code;
}
[TLDef(0xEDAB447B)] //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 long new_server_salt; }
[TLDef(0x7D861A08)] //msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq
public class MsgResendReq : ITLObject { public long[] msg_ids; }
[TLDef(0xDA69FB52)] //msgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq
public class MsgsStateReq : ITLObject { public long[] msg_ids; }
[TLDef(0x04DEB57D)] //msgs_state_info#04deb57d req_msg_id:long info:bytes = MsgsStateInfo
public class MsgsStateInfo : ITLObject
{
public long req_msg_id;
public byte[] info;
public int max_delay;
public int wait_after;
public int max_wait;
}
[TLDef(0x8CC0D131)] //msgs_all_info#8cc0d131 msg_ids:Vector<long> info:bytes = MsgsAllInfo
public class MsgsAllInfo : ITLObject
[TLDef(0xD433AD73)] //ipPort#d433ad73 ipv4:int port:int = IpPort
public partial class IpPort : IObject
{
public long[] msg_ids;
public byte[] info;
public int ipv4;
public int port;
}
[TLDef(0x37982646, inheritBefore = true)] //ipPortSecret#37982646 ipv4:int port:int secret:bytes = IpPort
public sealed partial class IpPortSecret : IpPort
{
public byte[] secret;
}
public abstract class MsgDetailedInfoBase : ITLObject { }
[TLDef(0x276D3EC6)] //msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo
public class MsgDetailedInfo : MsgDetailedInfoBase
[TLDef(0x4679B65F)] //accessPointRule#4679b65f phone_prefix_rules:bytes dc_id:int ips:vector<IpPort> = AccessPointRule
public sealed partial class AccessPointRule : IObject
{
public long msg_id;
public long answer_msg_id;
public int bytes;
public int status;
}
[TLDef(0x809DB6DF)] //msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo
public class MsgNewDetailedInfo : MsgDetailedInfoBase
{
public long answer_msg_id;
public int bytes;
public int status;
public byte[] phone_prefix_rules;
public int dc_id;
public IpPort[] ips;
}
[TLDef(0x7A19CB76)] //rsa_public_key n:string e:string = RSAPublicKey
public class RSAPublicKey : ITLObject
[TLDef(0x5A592A6C)] //help.configSimple#5a592a6c date:int expires:int rules:vector<AccessPointRule> = help.ConfigSimple
public sealed partial class Help_ConfigSimple : IObject
{
public byte[] n;
public byte[] e;
public DateTime date;
public DateTime expires;
public AccessPointRule[] rules;
}
public abstract class DestroyAuthKeyRes : ITLObject { }
[TLDef(0xF660E1D4)] //destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes
public class DestroyAuthKeyOk : DestroyAuthKeyRes { }
[TLDef(0x0A9F2259)] //destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes
public class DestroyAuthKeyNone : DestroyAuthKeyRes { }
[TLDef(0xEA109B13)] //destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes
public class DestroyAuthKeyFail : DestroyAuthKeyRes { }
// ---functions---
public static partial class Fn // ---functions---
public static class MTProtoExtensions
{
[TLDef(0x60469778)] //req_pq#60469778 nonce:int128 = ResPQ
public class ReqPQ : ITLFunction<ResPQ> { public Int128 nonce; }
[TLDef(0xBE7E8EF1)] //req_pq_multi#be7e8ef1 nonce:int128 = ResPQ
public class ReqPQmulti : ResPQ { }
public static Task<ResPQ> ReqPq(this Client client, Int128 nonce)
=> client.InvokeBare(new ReqPq
{
nonce = 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 : ITLFunction<ServerDHParams>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] p;
public byte[] q;
public long public_key_fingerprint;
public byte[] encrypted_data;
}
public static Task<ResPQ> ReqPqMulti(this Client client, Int128 nonce)
=> client.InvokeBare(new ReqPqMulti
{
nonce = nonce,
});
[TLDef(0xF5045F1F)] //set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer
public class SetClientDHParams : ITLFunction<SetClientDHParamsAnswer>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] encrypted_data;
}
public static Task<ServerDHParams> ReqDHParams(this Client client, Int128 nonce, Int128 server_nonce, byte[] p, byte[] q, long public_key_fingerprint, byte[] encrypted_data)
=> client.InvokeBare(new ReqDHParams
{
nonce = nonce,
server_nonce = server_nonce,
p = p,
q = q,
public_key_fingerprint = public_key_fingerprint,
encrypted_data = encrypted_data,
});
[TLDef(0x58E4A740)] //rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer
public class ReqRpcDropAnswer : ITLFunction<RpcDropAnswer> { public long req_msg_id; }
public static Task<SetClientDHParamsAnswer> SetClientDHParams(this Client client, Int128 nonce, Int128 server_nonce, byte[] encrypted_data)
=> client.InvokeBare(new SetClientDHParams
{
nonce = nonce,
server_nonce = server_nonce,
encrypted_data = encrypted_data,
});
[TLDef(0xB921BD04)] //get_future_salts#b921bd04 num:int = FutureSalts
public class GetFutureSalts : ITLFunction<FutureSalts> { public int num; }
public static Task<DestroyAuthKeyRes> DestroyAuthKey(this Client client)
=> client.Invoke(new DestroyAuthKey
{
});
[TLDef(0x7ABE77EC)] //ping#7abe77ec ping_id:long = Pong
public class Ping : ITLFunction<Pong> { public long ping_id; }
public static Task<RpcDropAnswer> RpcDropAnswer(this Client client, long req_msg_id)
=> client.Invoke(new Methods.RpcDropAnswer
{
req_msg_id = req_msg_id,
});
[TLDef(0xF3427B8C)] //ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong
public class PingDelayDisconnect : ITLFunction<Pong>
{
public long ping_id;
public int disconnect_delay; // seconds
}
public static Task<FutureSalts> GetFutureSalts(this Client client, int num)
=> client.Invoke(new GetFutureSalts
{
num = num,
});
[TLDef(0xE7512126)] //destroy_session#e7512126 session_id:long = DestroySessionRes
public class DestroySession : ITLFunction<DestroySessionRes> { public long session_id; }
public static Task<Pong> Ping(this Client client, long ping_id)
=> client.Invoke(new Ping
{
ping_id = ping_id,
});
[TLDef(0x9299359F)] //http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait
public class HttpWait : ITLObject
{
public int max_delay; // ms
public int wait_after; // ms
public int max_wait; // ms
}
public static Task<Pong> PingDelayDisconnect(this Client client, long ping_id, int disconnect_delay)
=> client.Invoke(new PingDelayDisconnect
{
ping_id = ping_id,
disconnect_delay = disconnect_delay,
});
[TLDef(0xD1435160)] //destroy_auth_key#d1435160 = DestroyAuthKeyRes
public class DestroyAuthKey : ITLFunction<DestroyAuthKeyRes> { }
public static Task<DestroySessionRes> DestroySession(this Client client, long session_id)
=> client.Invoke(new DestroySession
{
session_id = session_id,
});
}
}
namespace TL.Methods
{
#pragma warning disable IDE1006
[TLDef(0x60469778)] //req_pq#60469778 nonce:int128 = ResPQ
public sealed partial class ReqPq : IMethod<ResPQ>
{
public Int128 nonce;
}
[TLDef(0xBE7E8EF1)] //req_pq_multi#be7e8ef1 nonce:int128 = 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 sealed partial class ReqDHParams : IMethod<ServerDHParams>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] p;
public byte[] q;
public long public_key_fingerprint;
public byte[] encrypted_data;
}
[TLDef(0xF5045F1F)] //set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer
public sealed partial class SetClientDHParams : IMethod<SetClientDHParamsAnswer>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] encrypted_data;
}
[TLDef(0xD1435160)] //destroy_auth_key#d1435160 = DestroyAuthKeyRes
public sealed partial class DestroyAuthKey : IMethod<DestroyAuthKeyRes> { }
[TLDef(0x58E4A740)] //rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer
public sealed partial class RpcDropAnswer : IMethod<TL.RpcDropAnswer>
{
public long req_msg_id;
}
[TLDef(0xB921BD04)] //get_future_salts#b921bd04 num:int = FutureSalts
public sealed partial class GetFutureSalts : IMethod<FutureSalts>
{
public int num;
}
[TLDef(0x7ABE77EC)] //ping#7abe77ec ping_id:long = 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 sealed partial class PingDelayDisconnect : IMethod<Pong>
{
public long ping_id;
public int disconnect_delay;
}
[TLDef(0xE7512126)] //destroy_session#e7512126 session_id:long = DestroySessionRes
public sealed partial class DestroySession : IMethod<DestroySessionRes>
{
public long session_id;
}
}

File diff suppressed because one or more lines are too long

14854
src/TL.SchemaFuncs.cs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

776
src/TL.Xtended.cs Normal file
View file

@ -0,0 +1,776 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
namespace TL
{
public interface IPeerInfo
{
long ID { get; }
bool IsActive { get; }
string MainUsername { get; }
InputPeer ToInputPeer();
}
partial class InputPeer
{
public static readonly InputPeerSelf Self = new();
public abstract long ID { get; }
}
partial class InputPeerSelf
{
public override long ID => 0;
}
partial class InputPeerChat
{
/// <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() { }
public override long ID => chat_id;
}
partial class InputPeerUser
{
/// <param name="user_id">User identifier</param>
/// <param name="access_hash">⚠ <b>REQUIRED FIELD</b>. See FAQ for how to obtain it<br/><strong>access_hash</strong> value from the <see cref="User"/> structure</param>
public InputPeerUser(long user_id, long access_hash) { this.user_id = user_id; this.access_hash = access_hash; }
internal InputPeerUser() { }
public static implicit operator InputUser(InputPeerUser user) => new(user.user_id, user.access_hash);
public override long ID => user_id;
}
partial class InputPeerChannel
{
/// <param name="channel_id">Channel identifier</param>
/// <param name="access_hash">⚠ <b>REQUIRED FIELD</b>. See FAQ for how to obtain it<br/><strong>access_hash</strong> value from the <see cref="Channel"/> structure</param>
public InputPeerChannel(long channel_id, long access_hash) { this.channel_id = channel_id; this.access_hash = access_hash; }
internal InputPeerChannel() { }
public static implicit operator InputChannel(InputPeerChannel channel) => new(channel.channel_id, channel.access_hash);
public override long ID => channel_id;
}
partial class InputPeerUserFromMessage
{
public override long ID => user_id;
}
partial class InputPeerChannelFromMessage
{
public override long ID => channel_id;
}
partial class InputUserBase { public abstract long? UserId { get; } }
partial class InputUserSelf { public override long? UserId => null; }
partial class InputUserFromMessage { public override long? UserId => user_id; }
partial class InputUser
{
public override long? UserId => user_id;
public static InputUserSelf Self => new();
/// <param name="user_id">User identifier</param>
/// <param name="access_hash">⚠ <b>REQUIRED FIELD</b>. See FAQ for how to obtain it<br/><strong>access_hash</strong> value from the <see cref="User"/> structure</param>
public InputUser(long user_id, long access_hash) { this.user_id = user_id; this.access_hash = access_hash; }
internal InputUser() { }
public static implicit operator InputPeerUser(InputUser user) => new(user.user_id, user.access_hash);
}
partial class InputFileBase
{
public abstract InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint);
public abstract InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret);
/// <param name="isSquareVideo10s"><see langword="false"/> for a profile photo. <see langword="null"/> for auto-detection<br/><see langword="true"/> for a profile video. The video <u>MUST</u> be square, 10 seconds max, larger than 160x160</param>
public InputChatUploadedPhoto ToInputChatPhoto(bool? isSquareVideo10s = null)
{
if (isSquareVideo10s ?? Path.GetExtension(Name)?.ToLowerInvariant() is ".mp4")
return new InputChatUploadedPhoto { video = this, flags = InputChatUploadedPhoto.Flags.has_video };
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
{
public InputMediaUploadedDocument() { }
public InputMediaUploadedDocument(InputFileBase inputFile, string mimeType)
{
file = inputFile;
mime_type = mimeType;
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;
}
}
partial class InputPhoto
{
public static implicit operator InputMediaPhoto(InputPhoto photo) => new() { id = photo };
}
/// <remarks>Use the <c>UserOrChat(peer)</c> method from the root class you received, in order to convert this to a more useful <see cref="User"/> or <see cref="ChatBase"/></remarks>
partial class Peer
{
public abstract long ID { get; }
protected internal abstract IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats);
}
partial class PeerUser
{
public override string ToString() => "user " + user_id;
public override long ID => user_id;
protected internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats) => users.TryGetValue(user_id, out var user) ? user : null;
}
partial class PeerChat
{
public override string ToString() => "chat " + chat_id;
public override long ID => chat_id;
protected internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats) => chats.TryGetValue(chat_id, out var chat) ? chat : null;
}
partial class PeerChannel
{
public override string ToString() => "channel " + channel_id;
public override long ID => channel_id;
protected internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats) => chats.TryGetValue(channel_id, out var chat) ? chat : null;
}
partial class UserBase : IPeerInfo
{
public abstract long ID { get; }
public abstract bool IsActive { get; }
public abstract string MainUsername { get; }
public abstract InputPeer ToInputPeer();
protected abstract InputUser ToInputUser();
public static implicit operator InputPeer(UserBase user) => user?.ToInputPeer();
public static implicit operator InputUser(UserBase user) => user?.ToInputUser();
}
partial class UserEmpty
{
public override long ID => id;
public override bool IsActive => false;
public override string MainUsername => null;
public override string ToString() => null;
public override InputPeer ToInputPeer() => null;
protected override InputUser ToInputUser() => null;
}
partial class User
{
public override long ID => id;
public override bool IsActive => (flags & Flags.deleted) == 0;
public override string MainUsername => username ?? usernames?.FirstOrDefault(u => u.flags.HasFlag(Username.Flags.active))?.username;
public override string ToString() => MainUsername is string uname ? '@' + uname : last_name == null ? first_name : $"{first_name} {last_name}";
public override InputPeer ToInputPeer() => new InputPeerUser(id, access_hash);
protected override InputUser ToInputUser() => new(id, access_hash);
/// <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 - 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>
partial class UserStatusLastWeek { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(5); }
/// <remarks>between 6-7 days and a month</remarks>
partial class UserStatusLastMonth { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(20); }
partial class ChatBase : IPeerInfo
{
/// <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>
public abstract bool IsBanned(ChatBannedRights.Flags flags = 0);
public abstract InputPeer ToInputPeer();
public static implicit operator InputPeer(ChatBase chat) => chat?.ToInputPeer();
}
partial class ChatEmpty
{
public override bool IsActive => false;
public override ChatPhoto Photo => null;
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => true;
public override InputPeer ToInputPeer() => null;
public override string ToString() => $"ChatEmpty {id}";
}
partial class Chat
{
public override bool IsActive => (flags & (Flags.left | Flags.deactivated)) == 0;
public override ChatPhoto Photo => photo;
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => ((default_banned_rights?.flags ?? 0) & flags) != 0;
public override InputPeer ToInputPeer() => new InputPeerChat(id);
public override string ToString() => $"Chat \"{title}\"" + (flags.HasFlag(Flags.deactivated) ? " [deactivated]" : null);
}
partial class ChatForbidden
{
public override bool IsActive => false;
public override ChatPhoto Photo => null;
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => true;
public override InputPeer ToInputPeer() => new InputPeerChat(id);
public override string ToString() => $"ChatForbidden {id} \"{title}\"";
}
partial class Channel
{
public override bool IsActive => (flags & Flags.left) == 0;
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 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);
public override string ToString() => $"ChannelForbidden {id} \"{title}\"";
}
partial class ChatFullBase { public abstract int ParticipantsCount { get; } }
partial class ChatFull { public override int ParticipantsCount => participants.Participants.Length; }
partial class ChannelFull { public override int ParticipantsCount => participants_count; }
partial class ChatParticipantBase { public abstract bool IsAdmin { get; } }
partial class ChatParticipant { public override bool IsAdmin => false; }
partial class ChatParticipantCreator { public override bool IsAdmin => true; }
partial class ChatParticipantAdmin { public override bool IsAdmin => true; }
partial class ChatParticipantsBase { public abstract ChatParticipantBase[] Participants { get; }}
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..]}]"; }
partial class MessageMedia { ///<summary>Use this helper method to send a copy of the media without downloading it</summary>
///<remarks>Quiz poll may need to be voted before obtaining the correct answers. Dice will not replicate same value. TTL ignored<br/>May return <see langword="null"/> for Invoice and other unsupported media types</remarks>
public virtual InputMedia ToInputMedia() => null; }
partial class MessageMediaPhoto { public override InputMedia ToInputMedia() => new InputMediaPhoto { id = photo }; }
partial class MessageMediaGeo { public override InputMedia ToInputMedia() => new InputMediaGeoPoint { geo_point = geo }; }
partial class MessageMediaContact { public override InputMedia ToInputMedia() => new InputMediaContact { phone_number = phone_number, first_name = first_name, last_name = last_name, vcard = vcard }; }
partial class MessageMediaDocument { public override InputMedia ToInputMedia() => new InputMediaDocument { id = document }; }
partial class MessageMediaVenue { public override InputMedia ToInputMedia() => new InputMediaVenue { geo_point = geo, title = title, address = address, provider = provider, venue_id = venue_id, venue_type = venue_type }; }
partial class MessageMediaGame { public override InputMedia ToInputMedia() => new InputMediaGame { id = game }; }
partial class MessageMediaGeoLive { public override InputMedia ToInputMedia() => new InputMediaGeoLive { geo_point = geo, heading = heading, period = period, proximity_notification_radius = proximity_notification_radius,
flags = (period != 0 ? InputMediaGeoLive.Flags.has_period : 0) | (flags.HasFlag(Flags.has_heading) ? InputMediaGeoLive.Flags.has_heading : 0) | (flags.HasFlag(Flags.has_proximity_notification_radius) ? InputMediaGeoLive.Flags.has_proximity_notification_radius : 0) }; }
partial class MessageMediaPoll { public override InputMedia ToInputMedia() => new InputMediaPoll { poll = poll, solution = results.solution, solution_entities = results.solution_entities,
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
{
public abstract long ID { get; }
protected abstract InputPhoto ToInputPhoto();
public static implicit operator InputPhoto(PhotoBase photo) => photo.ToInputPhoto();
public static implicit operator InputMediaPhoto(PhotoBase photo) => photo.ToInputPhoto();
}
partial class PhotoEmpty
{
public override long ID => id;
protected override InputPhoto ToInputPhoto() => null;
}
partial class Photo
{
public override long ID => id;
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
{
public abstract int Width { get; }
public abstract int Height { get; }
public abstract int FileSize { get; }
}
partial class PhotoSizeEmpty
{
public override int Width => 0;
public override int Height => 0;
public override int FileSize => 0;
}
partial class PhotoSize
{
public override int Width => w;
public override int Height => h;
public override int FileSize => size;
}
partial class PhotoCachedSize
{
public override int Width => w;
public override int Height => h;
public override int FileSize => bytes.Length;
}
partial class PhotoStrippedSize
{
public override int Width => bytes[2];
public override int Height => bytes[1];
public override int FileSize => bytes.Length;
}
partial class PhotoSizeProgressive
{
public override int Width => w;
public override int Height => h;
public override int FileSize => sizes.Last();
}
partial class PhotoPathSize
{
public override int Width => -1;
public override int Height => -1;
public override int FileSize => bytes.Length;
}
namespace Layer23
{
partial class PhotoSize
{
public override int Width => w;
public override int Height => h;
public override int FileSize => size;
}
partial class PhotoCachedSize
{
public override int Width => w;
public override int Height => h;
public override int FileSize => bytes.Length;
}
}
partial class GeoPoint
{
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 }; }
partial class WallPaperNoFile { protected override InputWallPaperBase ToInputWallPaper() => new InputWallPaperNoFile { id = id }; }
partial class Contacts_Blocked { public IPeerInfo UserOrChat(PeerBlocked peer) => peer.peer_id?.UserOrChat(users, chats); }
partial class Messages_DialogsBase { public IPeerInfo UserOrChat(DialogBase dialog) => UserOrChat(dialog.Peer);
public abstract int TotalCount { get; } }
partial class Messages_Dialogs { public override int TotalCount => dialogs.Length; }
partial class Messages_DialogsSlice { public override int TotalCount => count; }
partial class Messages_DialogsNotModified { public override int TotalCount => count; }
partial class Messages_MessagesBase { public abstract int Count { get; } public abstract int Offset { get; } }
partial class Messages_Messages { public override int Count => messages.Length; public override int Offset => 0; }
partial class Messages_MessagesSlice { public override int Count => count; public override int Offset => offset_id_offset; }
partial class Messages_ChannelMessages { public override int Count => count; public override int Offset => offset_id_offset; }
partial class Messages_MessagesNotModified { public override int Count => count; public override int Offset => 0; }
partial class Updates_DifferenceBase { public abstract Updates_State State { get; } }
partial class Updates_DifferenceEmpty { public override Updates_State State => null; }
partial class Updates_Difference { public override Updates_State State => state; }
partial class Updates_DifferenceSlice { public override Updates_State State => intermediate_state; }
partial class Updates_DifferenceTooLong { public override Updates_State State => null; }
partial class UpdatesBase
{
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 = [];
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 => []; }
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
{
flags = (Message.Flags)flags | (flags.HasFlag(Flags.out_) ? 0 : Message.Flags.has_from_id), id = id, date = date,
message = message, entities = entities, reply_to = reply_to,
from_id = flags.HasFlag(Flags.out_) ? null : new PeerUser { user_id = user_id },
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 UpdateNewMessage
{
message = new Message
{
flags = (Message.Flags)flags | Message.Flags.has_from_id, id = id, date = date,
message = message, entities = entities, reply_to = reply_to,
from_id = new PeerUser { user_id = from_id },
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 }; }
partial class EncryptedFile
{
public static implicit operator InputEncryptedFile(EncryptedFile file) => file == null ? null : new InputEncryptedFile { id = file.id, access_hash = file.access_hash };
public static implicit operator InputEncryptedFileLocation(EncryptedFile file) => file == null ? null : new InputEncryptedFileLocation { id = file.id, access_hash = file.access_hash };
public InputEncryptedFileLocation ToFileLocation() => new() { id = id, access_hash = access_hash };
}
partial class InputDocument
{
public static implicit operator InputMediaDocument(InputDocument document) => new() { id = document };
}
partial class DocumentBase
{
public abstract long ID { get; }
protected abstract InputDocument ToInputDocument();
public static implicit operator InputDocument(DocumentBase document) => document.ToInputDocument();
public static implicit operator InputMediaDocument(DocumentBase document) => document.ToInputDocument();
}
partial class DocumentEmpty
{
public override long ID => id;
protected override InputDocument ToInputDocument() => null;
}
partial class Document
{
public override long ID => id;
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();
}
partial class SendMessageAction
{
public override string ToString()
{
var type = GetType().Name[11..^6];
for (int i = 1; i < type.Length; i++)
if (char.IsUpper(type[i]))
return type.ToLowerInvariant().Insert(i, "ing ").Remove(i - 1, type[i - 1] == 'e' ? 1 : 0);
return type.ToLowerInvariant();
}
}
partial class SpeakingInGroupCallAction { public override string ToString() => "speaking in group call"; }
partial class SendMessageTypingAction { public override string ToString() => "typing"; }
partial class SendMessageCancelAction { public override string ToString() => "stopping"; }
partial class SendMessageGeoLocationAction { public override string ToString() => "selecting a location"; }
partial class SendMessageGamePlayAction { public override string ToString() => "playing a game"; }
partial class SendMessageHistoryImportAction { public override string ToString() => "importing history"; }
partial class SendMessageEmojiInteraction { public override string ToString() => "clicking on emoji"; }
partial class SendMessageEmojiInteractionSeen { public override string ToString() => "watching emoji reaction"; }
partial class InputStickerSet
{
public static implicit operator InputStickerSet(string shortName) => new InputStickerSetShortName { short_name = shortName };
}
partial class StickerSet
{
public static implicit operator InputStickerSetID(StickerSet stickerSet) => new() { id = stickerSet.id, access_hash = stickerSet.access_hash };
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0060")]
public InputStickerSetThumb ToFileLocation(PhotoSizeBase thumbSize) => new() { stickerset = this, thumb_version = thumb_version };
public PhotoSizeBase LargestThumbSize => thumbs?.Aggregate((agg, next) => (long)next.Width * next.Height > (long)agg.Width * agg.Height ? next : agg);
}
partial class MessageEntity
{
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; }
}
partial class InputChannel
{
/// <param name="channel_id">Channel identifier</param>
/// <param name="access_hash">⚠ <b>REQUIRED FIELD</b>. See FAQ for how to obtain it<br/><strong>access_hash</strong> value from the <see cref="Channel"/> structure</param>
public InputChannel(long channel_id, long access_hash) { this.channel_id = channel_id; this.access_hash = access_hash; }
internal InputChannel() { }
public static implicit operator InputPeerChannel(InputChannel channel) => new(channel.channel_id, channel.access_hash);
}
partial class Contacts_ResolvedPeer
{
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="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
{
public abstract MessageBase[] NewMessages { get; }
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 => [];
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
{
public override MessageBase[] NewMessages => new_messages;
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
{
public override MessageBase[] NewMessages => messages;
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
{
public virtual bool IsAdmin => false;
public abstract long UserId { get; }
}
partial class ChannelParticipantCreator
{
public override bool IsAdmin => true;
public override long UserId => user_id;
}
partial class ChannelParticipantAdmin
{
public override bool IsAdmin => true;
public override long UserId => user_id;
}
partial class ChannelParticipant { public override long UserId => user_id; }
partial class ChannelParticipantSelf { public override long UserId => user_id; }
partial class ChannelParticipantBanned { public override long UserId => peer is PeerUser pu ? pu.user_id : 0; }
partial class ChannelParticipantLeft { public override long UserId => peer is PeerUser pu ? pu.user_id : 0; }
partial class Messages_PeerDialogs { public IPeerInfo UserOrChat(DialogBase dialog) => dialog.Peer?.UserOrChat(users, chats); }
partial class Game { public static implicit operator InputGameID(Game game) => new() { id = game.id, access_hash = game.access_hash }; }
partial class WebDocumentBase { public T GetAttribute<T>() where T : DocumentAttribute => Attributes.OfType<T>().FirstOrDefault(); }
partial class WebDocument { public static implicit operator InputWebFileLocation(WebDocument doc) => new() { url = doc.url, access_hash = doc.access_hash }; }
partial class PhoneCallBase { public static implicit operator InputPhoneCall(PhoneCallBase call) => new() { id = call.ID, access_hash = call.AccessHash }; }
partial class ChannelAdminLogEventsFilter
{
public static implicit operator ChannelAdminLogEventsFilter(Flags flags) => flags == 0 ? null : new() { flags = flags };
}
partial class InputMessage
{
public static implicit operator InputMessage(int id) => new InputMessageID { id = id };
}
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
{
public static implicit operator InputSecureFile(SecureFile file) => new() { id = file.id, access_hash = file.access_hash };
public InputSecureFileLocation ToFileLocation() => new() { id = id, access_hash = access_hash };
}
partial class JsonObjectValue { public override string ToString() => $"{HttpUtility.JavaScriptStringEncode(key, true)}:{value}"; }
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); }
partial class JsonString { public override object ToNative() => value; public override string ToString() => HttpUtility.JavaScriptStringEncode(value, true); }
partial class JsonArray
{
public override string ToString()
{
var sb = new StringBuilder().Append('[');
for (int i = 0; i < value.Length; i++)
sb.Append(i == 0 ? "" : ",").Append(value[i]);
return sb.Append(']').ToString();
}
public object[] ToNativeArray() => [.. value.Select(v => v.ToNative())];
public override object ToNative()
{
if (value.Length == 0) return Array.Empty<object>();
var first = value[0].ToNative();
var T = first.GetType();
var array = Array.CreateInstance(T, value.Length); // create an array T[] of the native type
array.SetValue(first, 0);
for (int i = 1; i < value.Length; i++)
{
var elem = value[i].ToNative();
if (elem.GetType() != T) return ToNativeArray(); // incompatible => return an object[] instead
array.SetValue(elem, i);
}
return array;
}
}
partial class JsonObject
{
/// <summary>Returns a JSON serialization string for this object</summary>
public override string ToString()
{
var sb = new StringBuilder().Append('{');
for (int i = 0; i < value.Length; i++)
sb.Append(i == 0 ? "" : ",").Append(value[i]);
return sb.Append('}').ToString();
}
/// <summary>Returns the given entry in native form (<see langword="bool"/>, <see langword="double"/>, <see langword="string"/>, <see cref="Dictionary{TKey, TValue}">Dictionary</see> or <see cref="Array"/>), or <see langword="null"/> if the key is not found</summary>
public object this[string key] => value.FirstOrDefault(v => v.key == key)?.value.ToNative();
/// <summary>Converts the entries to a Dictionary with keys and values in native form (<see langword="bool"/>, <see langword="double"/>, <see langword="string"/>, <see cref="Dictionary{TKey, TValue}">Dictionary</see> or <see cref="Array"/>)</summary>
public Dictionary<string, object> ToDictionary() => value.ToDictionary(v => v.key, v => v.value.ToNative());
public override object ToNative()
{
if (value.Length == 0) return new Dictionary<string, object>();
var first = value[0].value.ToNative();
var T = first.GetType(); // create a Dictionary<string, T> of the native type T:
var dic = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeof(string), T)) as System.Collections.IDictionary;
dic.Add(value[0].key, first);
for (int i = 1; i < value.Length; i++)
{
var elem = value[i].value.ToNative();
if (elem.GetType() != T) return ToDictionary(); // incompatible => return a Dictionary<string, object> instead
dic.Add(value[i].key, elem);
}
return dic;
}
}
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; }
}

487
src/TL.cs
View file

@ -1,113 +1,165 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using WTelegram;
#pragma warning disable IDE1006 // Naming Styles
namespace TL
{
public interface ITLObject { }
public interface ITLFunction<R> : ITLObject { }
#if MTPG
public interface IObject { void WriteTL(BinaryWriter writer); }
#else
public interface IObject { }
#endif
public interface IMethod<out ReturnType> : IObject { }
public interface IPeerResolver { IPeerInfo UserOrChat(Peer peer); }
public static partial class Schema
[AttributeUsage(AttributeTargets.Class)]
public sealed class TLDefAttribute(uint ctorNb) : Attribute
{
internal static byte[] Serialize(ITLObject msg)
public readonly uint CtorNb = ctorNb;
public bool inheritBefore;
}
[AttributeUsage(AttributeTargets.Field)]
public sealed class IfFlagAttribute(int bit) : Attribute
{
public readonly int Bit = bit;
}
public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message)
{
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 = x;
public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); }
}
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 memStream = new MemoryStream(1024);
using (var writer = new BinaryWriter(memStream))
Serialize(writer, msg);
return memStream.ToArray();
using var ms = new MemoryStream(384);
using var writer = new BinaryWriter(ms);
writer.WriteTLObject(obj);
return ms.ToArray();
}
internal static T Deserialize<T>(byte[] bytes) where T : ITLObject
public static void WriteTLObject<T>(this BinaryWriter writer, T obj) where T : IObject
{
using var memStream = new MemoryStream(bytes);
using var reader = new BinaryReader(memStream);
return Deserialize<T>(reader);
}
internal static void Serialize(BinaryWriter writer, ITLObject msg)
{
var type = msg.GetType();
var ctorNb = type.GetCustomAttribute<TLDefAttribute>().CtorNb;
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;
writer.Write(ctorNb);
SerializeObject(writer, msg);
}
internal static T Deserialize<T>(BinaryReader reader) where T : ITLObject
{
var ctorNb = reader.ReadUInt32();
if (!Table.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
return (T)DeserializeObject(reader, realType);
}
internal static void SerializeObject(BinaryWriter writer, object obj)
{
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0;
IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
ulong flags = 0;
IfFlagAttribute ifFlag;
foreach (var field in fields)
{
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1 << ifFlag.Bit)) == 0) continue;
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1UL << ifFlag.Bit)) == 0) continue;
object value = field.GetValue(obj);
if (value == null)
SerializeNull(writer, field.FieldType);
else
SerializeValue(writer, value);
if (field.Name.Equals("Flags", StringComparison.OrdinalIgnoreCase)) flags = (int)value;
writer.WriteTLValue(value, field.FieldType);
if (field.FieldType.IsEnum)
if (field.Name == "flags") flags = (uint)value;
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
}
#endif
}
internal static ITLObject DeserializeObject(BinaryReader reader, Type type)
public static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0)
{
var obj = Activator.CreateInstance(type);
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0;
if (ctorNb == 0) ctorNb = reader.ReadUInt32();
#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)
var tlDef = type.GetCustomAttribute<TLDefAttribute>();
var obj = Activator.CreateInstance(type, true);
IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
ulong flags = 0;
IfFlagAttribute ifFlag;
foreach (var field in fields)
{
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1 << ifFlag.Bit)) == 0) continue;
object value = DeserializeValue(reader, field.FieldType);
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1UL << ifFlag.Bit)) == 0) continue;
object value = reader.ReadTLValue(field.FieldType);
field.SetValue(obj, value);
if (field.Name.Equals("Flags", StringComparison.OrdinalIgnoreCase)) flags = (int)value;
if (field.FieldType.IsEnum)
if (field.Name == "flags") flags = (uint)value;
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
}
return type == typeof(GzipPacked) ? UnzipPacket((GzipPacked)obj) : (ITLObject)obj;
return (IObject)obj;
#endif
}
internal static void SerializeValue(BinaryWriter writer, object value)
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)
{
if (value == null)
{
writer.WriteTLNull(valueType);
return;
}
var type = value.GetType();
switch (Type.GetTypeCode(type))
{
case TypeCode.Int32: writer.Write((int)value); break;
case TypeCode.UInt32: writer.Write((uint)value); break;
case TypeCode.Int64: writer.Write((long)value); break;
case TypeCode.UInt32: writer.Write((uint)value); break;
case TypeCode.UInt64: writer.Write((ulong)value); break;
case TypeCode.Double: writer.Write((double)value); break;
case TypeCode.String: SerializeBytes(writer, Encoding.UTF8.GetBytes((string)value)); break;
case TypeCode.DateTime: writer.Write((uint)(((DateTime)value).ToUniversalTime().Ticks / 10000000 - 62135596800L)); break;
case TypeCode.Boolean: writer.Write((bool)value ? 0x997275b5 : 0xbc799737); break;
case TypeCode.String: writer.WriteTLString((string)value); break;
case TypeCode.Boolean: writer.Write((bool)value ? 0x997275B5 : 0xBC799737); break;
case TypeCode.DateTime: writer.WriteTLStamp((DateTime)value); break;
case TypeCode.Object:
if (type.IsArray)
{
if (value is byte[] bytes)
SerializeBytes(writer, bytes);
writer.WriteTLBytes(bytes);
else
SerializeVector(writer, (Array)value);
}
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 (type.IsValueType)
SerializeObject(writer, value);
else if (value is ITLObject tlObject)
Serialize(writer, tlObject);
else if (type.IsEnum) // needed for Mono (enums in generic types are seen as TypeCode.Object)
writer.Write((uint)value);
else
ShouldntBeHere();
break;
@ -117,71 +169,185 @@ namespace TL
}
}
internal static object DeserializeValue(BinaryReader reader, Type type)
internal static object ReadTLValue(this BinaryReader reader, Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.Int32: return reader.ReadInt32();
case TypeCode.UInt32: return reader.ReadUInt32();
case TypeCode.Int64: return reader.ReadInt64();
case TypeCode.UInt32: return reader.ReadUInt32();
case TypeCode.UInt64: return reader.ReadUInt64();
case TypeCode.Double: return reader.ReadDouble();
case TypeCode.String: return Encoding.UTF8.GetString(DeserializeBytes(reader));
case TypeCode.DateTime: return new DateTime((reader.ReadUInt32() + 62135596800L) * 10000000, DateTimeKind.Utc);
case TypeCode.String: return reader.ReadTLString();
case TypeCode.DateTime: return reader.ReadTLStamp();
case TypeCode.Boolean:
return reader.ReadUInt32() switch
{
0x997275b5 => true,
0xbc799737 => false,
var value => throw new ApplicationException($"Invalid boolean value #{value:x}")
Layer.RpcErrorCtor => reader.ReadTLObject(Layer.RpcErrorCtor),
var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}")
};
case TypeCode.Object:
if (type.IsArray)
{
if (type == typeof(byte[]))
return DeserializeBytes(reader);
else if (type == typeof(_Message[]))
return DeserializeMessages(reader);
return reader.ReadTLBytes();
else
return DeserializeVector(reader, type);
return reader.ReadTLVector(type);
}
else if (type == typeof(Int128))
return new Int128(reader);
else if (type == typeof(Int256))
return new Int256(reader);
else if (type.IsValueType)
return DeserializeObject(reader, type);
else if (type == typeof(Dictionary<long, User>))
return reader.ReadTLDictionary<User>();
else if (type == typeof(Dictionary<long, ChatBase>))
return reader.ReadTLDictionary<ChatBase>();
else
return Deserialize<ITLObject>(reader);
return reader.ReadTLObject();
default:
ShouldntBeHere();
return null;
}
}
private static void SerializeVector(BinaryWriter writer, Array array)
internal static void WriteTLMessages(this BinaryWriter writer, List<_Message> messages)
{
writer.Write(VectorCtor);
int count = array.Length;
writer.Write(count);
for (int i = 0; i < count; i++)
SerializeValue(writer, array.GetValue(i));
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);
}
}
private static object DeserializeVector(BinaryReader reader, Type type)
internal static void WriteTLVector(this BinaryWriter writer, Array array)
{
var ctorNb = reader.ReadInt32();
if (ctorNb != VectorCtor) throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
var elementType = type.GetElementType();
int count = reader.ReadInt32();
Array array = (Array)Activator.CreateInstance(type, count);
writer.Write(Layer.VectorCtor);
if (array == null) { writer.Write(0); return; }
int count = array.Length;
writer.Write(count);
var elementType = array.GetType().GetElementType();
for (int i = 0; i < count; i++)
array.SetValue(DeserializeValue(reader, elementType), i);
writer.WriteTLValue(array.GetValue(i), elementType);
}
internal static void WriteTLRawVector(this BinaryWriter writer, Array array, int elementSize)
{
var startPos = writer.BaseStream.Position;
int count = array.Length;
var elementType = array.GetType().GetElementType();
for (int i = count - 1; i >= 0; i--)
{
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;
}
private static void SerializeBytes(BinaryWriter writer, byte[] bytes)
internal static Array ReadTLVector(this BinaryReader reader, Type type)
{
var elementType = type.GetElementType();
uint ctorNb = reader.ReadUInt32();
if (ctorNb == Layer.VectorCtor)
{
int count = reader.ReadInt32();
Array array = (Array)Activator.CreateInstance(type, count);
if (elementType.IsEnum)
for (int i = 0; i < count; i++)
array.SetValue(Enum.ToObject(elementType, reader.ReadTLValue(elementType)), i);
else
for (int i = 0; i < count; i++)
array.SetValue(reader.ReadTLValue(elementType), i);
return array;
}
else if (ctorNb < 1024 && !elementType.IsAbstract && elementType.GetCustomAttribute<TLDefAttribute>() is TLDefAttribute attr)
{
int count = (int)ctorNb;
Array array = (Array)Activator.CreateInstance(type, count);
for (int i = 0; i < count; i++)
array.SetValue(reader.ReadTLObject(attr.CtorNb), i);
return array;
}
else
throw new WTelegram.WTException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
}
internal static Dictionary<long, T> ReadTLDictionary<T>(this BinaryReader reader) where T : class, IPeerInfo
{
uint ctorNb = reader.ReadUInt32();
if (ctorNb != Layer.VectorCtor)
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 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((int)Math.Min(Math.Max(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L, 0), int.MaxValue));
internal static DateTime ReadTLStamp(this BinaryReader reader) => reader.ReadInt32() switch
{
<= 0 => default,
int.MaxValue => DateTime.MaxValue,
int unixstamp => new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc)
};
internal static void WriteTLString(this BinaryWriter writer, string str)
{
if (str == null)
writer.Write(0);
else
writer.WriteTLBytes(Encoding.UTF8.GetBytes(str));
}
internal static string ReadTLString(this BinaryReader reader)
=> Encoding.UTF8.GetString(reader.ReadTLBytes());
internal static void WriteTLBytes(this BinaryWriter writer, byte[] bytes)
{
if (bytes == null) { writer.Write(0); return; }
int length = bytes.Length;
if (length < 254)
writer.Write((byte)length);
@ -194,7 +360,7 @@ namespace TL
while (++length % 4 != 0) writer.Write((byte)0);
}
private static byte[] DeserializeBytes(BinaryReader reader)
internal static byte[] ReadTLBytes(this BinaryReader reader)
{
byte[] bytes;
int length = reader.ReadByte();
@ -202,7 +368,7 @@ namespace TL
bytes = reader.ReadBytes(length);
else
{
length = reader.ReadInt16() + (reader.ReadByte() << 16);
length = reader.ReadUInt16() + (reader.ReadByte() << 16);
bytes = reader.ReadBytes(length);
length += 3;
}
@ -210,49 +376,32 @@ namespace TL
return bytes;
}
internal static void SerializeNull(BinaryWriter writer, Type type)
internal static void WriteTLNull(this BinaryWriter writer, Type type)
{
if (!type.IsArray)
writer.Write(NullCtor);
else if (type != typeof(byte[]))
writer.Write(VectorCtor);
writer.Write(0); // null arrays are serialized as empty
}
private static _Message[] DeserializeMessages(BinaryReader reader)
{
int count = reader.ReadInt32();
var array = new _Message[count];
for (int i = 0; i < count; i++)
if (type == typeof(string)) { }
else if (!type.IsArray)
{
array[i] = new _Message
{
msg_id = reader.ReadInt64(),
seqno = reader.ReadInt32(),
bytes = reader.ReadInt32(),
};
var pos = reader.BaseStream.Position;
try
{
array[i].body = (ITLObject)DeserializeValue(reader, typeof(ITLObject));
}
catch (Exception ex)
{
Helpers.Log(4, ex.ToString());
}
reader.BaseStream.Position = pos + array[i].bytes;
writer.Write(Layer.Nullables.TryGetValue(type, out uint nullCtor) ? nullCtor : Layer.NullCtor);
return;
}
return array;
else if (type != typeof(byte[]))
writer.Write(Layer.VectorCtor); // not raw bytes but a vector => needs a VectorCtor
writer.Write(0); // null arrays/strings are serialized as empty
}
private static ITLObject UnzipPacket(GzipPacked obj)
internal static object ReadTLGzipped(this BinaryReader reader, Type type)
{
using var reader = new BinaryReader(new GZipStream(new MemoryStream(obj.packed_data), CompressionMode.Decompress));
var result = Deserialize<ITLObject>(reader);
Helpers.Log(1, $" → {result.GetType().Name}");
return result;
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
@ -260,36 +409,17 @@ namespace TL
#endif
}
[AttributeUsage(AttributeTargets.Class)]
public class TLDefAttribute : Attribute
{
public readonly uint CtorNb;
public TLDefAttribute(uint ctorNb) => CtorNb = ctorNb;
/*public TLDefAttribute(string def)
{
var hash = def.IndexOfAny(new[] { '#', ' ' });
CtorNb = def[hash] == ' ' ? Force.Crc32.Crc32Algorithm.Compute(System.Text.Encoding.UTF8.GetBytes(def))
: uint.Parse(def[(hash + 1)..def.IndexOf(' ', hash)], System.Globalization.NumberStyles.HexNumber);
}*/
}
[AttributeUsage(AttributeTargets.Field)]
public class IfFlagAttribute : Attribute
{
public readonly int Bit;
public IfFlagAttribute(int bit) => Bit = bit;
}
public struct Int128
{
public byte[] raw;
public Int128(BinaryReader reader) => raw = reader.ReadBytes(16);
public Int128(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[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() => HashCode.Combine(raw[0], raw[1]);
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;
}
@ -298,11 +428,68 @@ namespace TL
public byte[] raw;
public Int256(BinaryReader reader) => raw = reader.ReadBytes(32);
public Int256(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[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() => HashCode.Combine(raw[0], raw[1]);
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 sealed partial class RSAPublicKey : IObject
{
public byte[] n;
public byte[] e;
}
[TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult
public sealed partial class RpcResult : IObject
{
public long req_msg_id;
public object result;
}
[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message
public sealed partial class _Message(long msgId, int seqno, IObject obj) : IObject
{
public long msg_id = msgId;
public int seqno = seqno;
public int bytes;
public IObject body = obj;
}
[TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer
public sealed partial class MsgContainer : IObject { public List<_Message> messages; }
[TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy
public sealed partial class MsgCopy : IObject { public _Message orig_message; }
[TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object
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);
}
}

201
src/TlsStream.cs Normal file
View file

@ -0,0 +1,201 @@
using System;
using System.Buffers.Binary;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
// necessary for .NET Standard 2.0 compilation:
#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync'
namespace WTelegram
{
internal sealed class TlsStream(Stream innerStream) : Helpers.IndirectStream(innerStream)
{
private int _tlsFrameleft;
private readonly byte[] _tlsSendHeader = [0x17, 0x03, 0x03, 0, 0];
private readonly byte[] _tlsReadHeader = new byte[5];
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)
{
if (_tlsFrameleft == 0)
{
if (await _innerStream.FullReadAsync(_tlsReadHeader, 5, ct) != 5)
return 0;
if (_tlsReadHeader[0] != 0x17 || _tlsReadHeader[1] != 0x03 || _tlsReadHeader[2] != 0x03)
throw new WTException("Could not read frame data : Invalid TLS header");
_tlsFrameleft = (_tlsReadHeader[3] << 8) + _tlsReadHeader[4];
}
var read = await _innerStream.ReadAsync(buffer, offset, Math.Min(count, _tlsFrameleft), ct);
_tlsFrameleft -= read;
return read;
}
public override async Task WriteAsync(byte[] buffer, int start, int count, CancellationToken ct)
{
for (int offset = 0; offset < count;)
{
int len = Math.Min(count - offset, 2878);
_tlsSendHeader[3] = (byte)(len >> 8);
_tlsSendHeader[4] = (byte)len;
await _innerStream.WriteAsync(_tlsSendHeader, 0, _tlsSendHeader.Length, ct);
await _innerStream.WriteAsync(buffer, start + offset, len, ct);
offset += len;
}
}
public static async Task<TlsStream> HandshakeAsync(Stream stream, byte[] key, byte[] domain, CancellationToken ct)
{
var clientHello = TlsClientHello(key, domain);
await stream.WriteAsync(clientHello, 0, clientHello.Length, ct);
var part1 = new byte[5];
if (await stream.FullReadAsync(part1, 5, ct) == 5)
if (part1[0] == 0x16 && part1[1] == 0x03 && part1[2] == 0x03)
{
var part2size = BinaryPrimitives.ReadUInt16BigEndian(part1.AsSpan(3));
var part23 = new byte[part2size + TlsServerHello3.Length + 2];
if (await stream.FullReadAsync(part23, part23.Length, ct) == part23.Length)
if (TlsServerHello3.SequenceEqual(part23.Skip(part2size).Take(TlsServerHello3.Length)))
{
var part4size = BinaryPrimitives.ReadUInt16BigEndian(part23.AsSpan(part23.Length - 2));
var part4 = new byte[part4size];
if (await stream.FullReadAsync(part4, part4size, ct) == part4size)
{
var serverDigest = part23[6..38];
Array.Clear(part23, 6, 32); // clear server digest from received parts
var hmc = new HMACSHA256(key); // hash the client digest + all received parts
hmc.TransformBlock(clientHello, 11, 32, null, 0);
hmc.TransformBlock(part1, 0, part1.Length, null, 0);
hmc.TransformBlock(part23, 0, part23.Length, null, 0);
hmc.TransformFinalBlock(part4, 0, part4.Length);
if (serverDigest.SequenceEqual(hmc.Hash))
{
Helpers.Log(2, "TLS Handshake succeeded");
await stream.WriteAsync(TlsClientPrefix, 0, TlsClientPrefix.Length, ct);
return new TlsStream(stream);
}
}
}
}
throw new WTException("TLS Handshake failed");
}
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 = [
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
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)
{
var greases = new byte[7];
Encryption.RNG.GetBytes(greases);
for (int i = 0; i < 7; i++) greases[i] = (byte)((greases[i] & 0xF0) + 0x0A);
if (greases[3] == greases[2]) greases[3] ^= 0x10;
var buffer = new byte[517];
TlsClientHello1.CopyTo(buffer, 0);
Encryption.RNG.GetBytes(buffer, 44, 32);
buffer[43] = buffer[77] = 0x20;
buffer[78] = buffer[79] = greases[0];
TlsClientHello2.CopyTo(buffer, 80);
buffer[114] = buffer[115] = greases[2];
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);
var digest = hmac.ComputeHash(buffer);
var stamp = BinaryPrimitives.ReadInt32LittleEndian(digest.AsSpan(28));
stamp ^= (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
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,36 +2,63 @@
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net5.0;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<RootNamespace>WTelegram</RootNamespace>
<Deterministic>true</Deterministic>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageId>WTelegramClient</PackageId>
<Description>Telegram client library written 100% in C# and .NET Core</Description>
<Authors>Wizou</Authors>
<Copyright>Copyright © Olivier Marcoux 2021</Copyright>
<PackageTags>Telegram;Client;Api;UserBot;MTProto</PackageTags>
<PackageProjectUrl>https://github.com/wiz0u/WTelegramClient</PackageProjectUrl>
<VersionPrefix>0.0.0</VersionPrefix>
<VersionSuffix>layer.216</VersionSuffix>
<Description>Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 216
Release Notes:
$(ReleaseNotes)</Description>
<Copyright>Copyright © Olivier Marcoux 2021-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<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>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;1573;1591;0419</NoWarn>
<PackageTags>Telegram;MTProto;Client;Api;UserBot</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<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 Remove=".gitattributes" />
<None Remove=".gitignore" />
<None Remove="azure-pipelines.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="\" />
<None Include="..\logo.png" Link="Data\logo.png" Pack="true" PackagePath="\" />
</ItemGroup>
<!-- Disabled because SourceLink navigation prevents a clear display of the API
<ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<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.3" />
<PackageReference Include="System.Memory" Version="4.5.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>