Compare commits

..

No commits in common. "master" and "2.3.1" have entirely different histories.

43 changed files with 5811 additions and 26438 deletions

3
.github/FUNDING.yml vendored
View file

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

View file

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

77
.github/dev.yml vendored
View file

@ -1,63 +1,38 @@
pr: none pr: none
trigger: trigger:
branches: - master
include: [ master ]
paths:
exclude: [ '.github', '*.md', 'Examples' ]
name: 4.3.2-dev.$(Rev:r) name: 2.3.1-dev.$(Rev:r)
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest
variables: variables:
buildConfiguration: 'Release' buildConfiguration: 'Release'
Release_Notes: $[replace(variables['Build.SourceVersionMessage'], '"', '''''')]
stages: steps:
- stage: publish - task: UseDotNet@2
jobs: displayName: 'Use .NET Core sdk'
- job: publish inputs:
steps: packageType: 'sdk'
- task: UseDotNet@2 version: '6.0.x'
displayName: 'Use .NET Core sdk' includePreviewVersions: true
inputs:
packageType: 'sdk'
version: '9.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2 - task: DotNetCoreCLI@2
inputs: inputs:
command: 'pack' command: 'pack'
packagesToPack: 'src/WTelegramClient.csproj' packagesToPack: '**/*.csproj'
includesymbols: true includesymbols: true
versioningScheme: 'byEnvVar' versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber' versionEnvVar: 'Build.BuildNumber'
buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)" buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true'
# buildProperties: 'NoWarn="0419;1573;1591";AllowedOutputExtensionsInPackageBuildOutputFolder=".dll;.xml;.pdb"'
- task: NuGetCommand@2 - task: NuGetCommand@2
inputs: inputs:
command: 'push' command: 'push'
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.*upkg'
publishPackageMetadata: true publishPackageMetadata: true
nuGetFeedType: 'external' nuGetFeedType: 'internal'
publishFeedCredentials: 'nuget.org' publishVstsFeed: 'WTelegramClient/WTelegramClient'
- 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'

15
.github/release.yml vendored
View file

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

View file

@ -1,24 +0,0 @@
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'

View file

@ -1,68 +0,0 @@
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 }}

View file

@ -1,82 +0,0 @@
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 }}

View file

@ -1,29 +0,0 @@
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,8 +3,6 @@
## ##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
launchSettings.json
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo

View file

@ -1,4 +1,4 @@
# Example programs using WTelegramClient ## Example programs using WTelegramClient
For these examples to work as a fully-functional Program.cs, be sure to start with these lines: For these examples to work as a fully-functional Program.cs, be sure to start with these lines:
```csharp ```csharp
@ -7,312 +7,62 @@ using System.Linq;
using TL; using TL;
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
await client.LoginUserIfNeeded(); var myself = await client.LoginUserIfNeeded();
``` ```
In this case, environment variables are used for configuration so make sure to In this case, environment variables are used for configuration so make sure to
go to your **Project Properties > Debug > Launch Profiles > Environment variables** go to your **Project Properties > Debug > Environment variables**
and add at least these variables with adequate values: **api_id, api_hash, phone_number** and add at least these variables with adequate value: **api_id, api_hash, phone_number**
Remember that these are just simple example codes that you should adjust to your needs. 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, 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)!
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 answers to [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient).
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> <a name="msg-by-name"></a>
## Send a message to someone by @username ### Send a message to someone by @username
```csharp ```csharp
var resolved = await client.Contacts_ResolveUsername("JsonDumpBot"); // username without the @ var resolved = await client.Contacts_ResolveUsername("MyEch0_Bot"); // username without the @
await client.SendMessageAsync(resolved, "/start"); 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. *Note: This also works if the @username points to a channel/group, but you must already have joined that channel before posting there.
If the username is invalid/unused, the API call raises an RpcException.* If the username is invalid/unused, the API call raises an exception.*
<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: To prevent spam, Telegram may restrict your ability to add new phone numbers.*
<a name="markdown"></a> <a name="markdown"></a>
<a name="html"></a> <a name="html"></a>
## Convert message to/from HTML or Markdown, and send it to ourself (Saved Messages) ### Send an HTML/Markdown formatted message to ourself (Saved Messages)
```csharp ```csharp
// HTML-formatted text: // HTML-formatted text:
var text = $"Hello <u>dear <b>{HtmlText.Escape(client.User.first_name)}</b></u>\n" + var text = $"Hello <u>dear <b>{HtmlText.Escape(myself.first_name)}</b></u>\n" +
"Enjoy this <code>userbot</code> written with <a href=\"https://github.com/wiz0u/WTelegramClient\">WTelegramClient</a>"; "Enjoy this <code>userbot</code> written with <a href=\"https://github.com/wiz0u/WTelegramClient\">WTelegramClient</a>";
var entities = client.HtmlToEntities(ref text); var entities = client.HtmlToEntities(ref text);
var sent = await client.SendMessageAsync(InputPeer.Self, text, entities: entities); var sent = await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
// if you need to convert a sent/received Message to HTML: (easier to store) // if you need to convert a Message to HTML: (for easier storage)
text = client.EntitiesToHtml(sent.message, sent.entities); text = client.EntitiesToHtml(sent.message, sent.entities);
```
```csharp
// Markdown-style text: // Markdown-style text:
var text2 = $"Hello __dear *{Markdown.Escape(client.User.first_name)}*__\n" + var text2 = $"Hello __dear *{Markdown.Escape(myself.first_name)}*__\n" +
"Enjoy this `userbot` written with [WTelegramClient](https://github.com/wiz0u/WTelegramClient)"; "Enjoy this `userbot` written with [WTelegramClient](https://github.com/wiz0u/WTelegramClient)";
var entities2 = client.MarkdownToEntities(ref text2); var entities2 = client.MarkdownToEntities(ref text2);
var sent2 = await client.SendMessageAsync(InputPeer.Self, text2, entities: entities2); var sent2 = await client.SendMessageAsync(InputPeer.Self, text2, entities: entities2);
// if you need to convert a sent/received Message to Markdown: (easier to store) // if you need to convert a Message to Markdown: (for easier storage)
text2 = client.EntitiesToMarkdown(sent2.message, sent2.entities); 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. See [MarkdownV2 formatting style](https://core.telegram.org/bots/api/#markdownv2-style) and [HTML formatting style](https://core.telegram.org/bots/api/#html-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))* *Note: For the `tg://user?id=` notation to work, that user's access hash must have been collected first ([see below](#collect-access-hash))*
<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> <a name="fun"></a>
## Fun with stickers, GIFs, dice, and animated emojies ### Fun with stickers, GIFs, dice, and animated emojies
```csharp ```csharp
// • List all stickerSets the user has added to his account // • List all stickerSets the user has added to his account
var allStickers = await client.Messages_GetAllStickers(); var allStickers = await client.Messages_GetAllStickers();
@ -323,13 +73,13 @@ foreach (var stickerSet in allStickers.sets)
// • Send a random sticker from the user's favorites stickers // • Send a random sticker from the user's favorites stickers
var favedStickers = await client.Messages_GetFavedStickers(); var favedStickers = await client.Messages_GetFavedStickers();
var stickerDoc = favedStickers.stickers[new Random().Next(favedStickers.stickers.Length)]; var stickerDoc = favedStickers.stickers[new Random().Next(favedStickers.stickers.Length)];
await client.SendMessageAsync(InputPeer.Self, null, stickerDoc); await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDocument { id = stickerDoc });
// • Send a specific sticker given the stickerset shortname and emoticon // • Send a specific sticker given the stickerset shortname and emoticon
var friendlyPanda = await client.Messages_GetStickerSet("Friendly_Panda"); var friendlyPanda = await client.Messages_GetStickerSet(new InputStickerSetShortName { short_name = "Friendly_Panda" });
var laughId = friendlyPanda.packs.First(p => p.emoticon == "😂").documents[0]; var laughId = friendlyPanda.packs.First(p => p.emoticon == "😂").documents[0];
var laughDoc = friendlyPanda.documents.First(d => d.ID == laughId); var laughDoc = friendlyPanda.documents.First(d => d.ID == laughId);
await client.SendMessageAsync(InputPeer.Self, null, laughDoc); await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDocument { id = laughDoc });
// • Send a GIF from an internet URL // • Send a GIF from an internet URL
await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDocumentExternal await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDocumentExternal
@ -341,7 +91,7 @@ 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 // • 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 appConfig = await client.Help_GetAppConfig();
var emojies_send_dice = appConfig.config["emojies_send_dice"] as string[]; var emojies_send_dice = appConfig["emojies_send_dice"] as string[];
var dice_emoji = emojies_send_dice[new Random().Next(emojies_send_dice.Length)]; 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 }); var diceMsg = await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDice { emoticon = dice_emoji });
Console.WriteLine("Dice result:" + ((MessageMediaDice)diceMsg.media).value); Console.WriteLine("Dice result:" + ((MessageMediaDice)diceMsg.media).value);
@ -360,75 +110,132 @@ var typing = await client.Messages_SetTyping(InputPeer.Self, new SendMessageEmoj
await Task.Delay(5000); await Task.Delay(5000);
``` ```
<a name="reaction"></a><a name="pinned"></a><a name="custom_emoji"></a> <a name="list-chats"></a>
## Fun with custom emojies and reactions on pinned messages ### List all chats (groups/channels) the user is in and send a message to one
```csharp ```csharp
// • Sending a message with custom emojies in Markdown to ourself: var chats = await client.Messages_GetAllChats();
var text = "Vicksy says Hi! ![👋](tg://emoji?id=5190875290439525089)"; foreach (var (id, chat) in chats.chats)
var entities = client.MarkdownToEntities(ref text, premium: true); if (chat.IsActive)
await client.SendMessageAsync(InputPeer.Self, text, entities: entities); Console.WriteLine($"{id} : {chat}");
// also available in HTML: <tg-emoji emoji-id="5190875290439525089">👋</tg-emoji> 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 small private 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](Examples/Program_GetAllChats.cs)
// • Fetch all available standard emoji reactions <a name="schedule-msg"></a>
var all_emoji = await client.Messages_GetAvailableReactions(); ### 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);
```
<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(); var chats = await client.Messages_GetAllChats();
var chat = chats.chats[1234567890]; // the chat we want InputPeer peer = chats.chats[ChatId];
var inputFile = await client.UploadFileAsync(Filepath);
// • Check reactions available in this chat await client.SendMediaAsync(peer, "Here is the photo", inputFile);
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="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 InputMedia[]
{
photoFromTelegram, // PhotoBase has implicit conversion to InputMediaPhoto
new InputMediaUploadedPhoto { file = uploadedFile },
new InputMediaPhotoExternal() { url = photoUrl },
};
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="list-dialogs"></a>
### List all dialogs (chats/groups/channels/user chat) the user is in
```csharp
var dialogs = await client.Messages_GetAllDialogs();
foreach (var 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;
}
```
*Note: 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](Examples/Program_ListenUpdates.cs).
<a name="list-members"></a>
### Get all members from a chat
For a simple 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) 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 this 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);
```
<a name="join-channel"></a> <a name="join-channel"></a>
## Join a channel/group by their public name or invite link ### Join a channel/group by @channelname
* 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 ```csharp
var resolved = await client.Contacts_ResolveUsername("channelname"); // without the @ var resolved = await client.Contacts_ResolveUsername("channelname"); // without the @
if (resolved.Chat is Channel channel) if (resolved.Chat is Channel channel)
await client.Channels_JoinChannel(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> <a name="add-members"></a>
## Add/Invite/Remove someone in a chat ### Add/Invite/Remove someone in a chat
```csharp ```csharp
var chats = await client.Messages_GetAllChats(); var chats = await client.Messages_GetAllChats();
var chat = chats.chats[1234567890]; // the target chat 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: After the above code, once you [have obtained](https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#access-hash) an `InputUser` or `User`, you can:
```csharp ```csharp
// • Directly add the user to a Chat/Channel/group: // • Directly add the user to a Chat/Channel/group:
var miu = await client.AddChatUser(chat, user); 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 // You may get exception USER_PRIVACY_RESTRICTED if the user has denied the right to be added to a chat
// or a result with miu.missing_invitees listing users that denied the right to be added to a chat // or exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
// • Obtain the main invite link for the chat, and send it to the user: // • Obtain the main invite link for the chat, and send it to the user:
var mcf = await client.GetFullChat(chat); var mcf = await client.GetFullChat(chat);
@ -446,17 +253,24 @@ await client.Messages_DeleteExportedChatInvite(chat, invite.link);
await client.DeleteChatUser(chat, user); await client.DeleteChatUser(chat, user);
``` ```
<a name="msg-by-phone"></a> <a name="history"></a>
## Send a message to someone by phone number ### Get all messages (history) from a chat
```csharp ```csharp
var contacts = await client.Contacts_ImportContacts(new[] { new InputPhoneContact { phone = "+PHONENUMBER" } }); var chats = await client.Messages_GetAllChats();
if (contacts.imported.Length > 0) InputPeer peer = chats.chats[1234567890]; // the chat we want
await client.SendMessageAsync(contacts.users[contacts.imported[0].user_id], "Hello!"); 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)
if (msgBase is Message msg)
Console.WriteLine(msg.message);
offset_id = messages.Messages[^1].ID;
}
``` ```
*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> <a name="contacts"></a>
## Retrieve the current user's contacts list ### Retrieve the current user's contacts list
There are two different methods. Here is the simpler one: There are two different methods. Here is the simpler one:
```csharp ```csharp
var contacts = await client.Contacts_GetContacts(); var contacts = await client.Contacts_GetContacts();
@ -484,126 +298,120 @@ finally
} }
``` ```
<a name="updates"></a>
### Monitor all Telegram events happening for the user
This is done through the `client.Update` callback event.
See [Examples/Program_ListenUpdates.cs](Examples/Program_ListenUpdates.cs).
<a name="monitor-msg"></a>
### Monitor new messages being posted in chats
You have to handle `client.Update` events containing an `UpdateNewMessage`.
See the `DisplayMessage` method in [Examples/Program_ListenUpdates.cs](Examples/Program_ListenUpdates.cs).
You can filter specific chats the message are posted in, by looking at the `Message.peer_id` field.
<a name="download"></a>
### Downloading photos, medias, files
This is done using the helper method `client.DownloadFileAsync(file, outputStream)`
that simplify 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](Examples/Program_DownloadSavedMedia.cs) that download all media files you forward to yourself (Saved Messages)
<a name="collect-access-hash"></a> <a name="collect-access-hash"></a>
<a name="collect-users-chats"></a> ### Collect Access Hash and save them for later use
<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. You can automate the collection of `access_hash` for the various resources obtained in response to API calls or Update events,
This is also the case for updates passed to `client.OnUpdates`. so that you don't have to remember them by yourself or ask the API about them each time.
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);
}
```
This is done by activating the experimental `client.CollectAccessHash` system.
See [Examples/Program_CollectAccessHash.cs](Examples/Program_CollectAccessHash.cs) for how to enable it, and save/restore them for later use.
<a name="proxy"></a> <a name="proxy"></a>
## Use a proxy or MTProxy to connect to Telegram ### Use a proxy 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/): SOCKS/HTTP proxies can be used through the `client.TcpHandler` delegate and a proxy library like [StarkSoftProxy](https://www.nuget.org/packages/StarkSoftProxy/):
```csharp ```csharp
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
client.TcpHandler = async (address, port) => client.TcpHandler = async (address, port) =>
{ {
var proxy = new Socks5ProxyClient(ProxyHost, ProxyPort, ProxyUsername, ProxyPassword); var proxy = new Socks5ProxyClient(ProxyHost, ProxyPort, ProxyUsername, ProxyPassword);
//var proxy = xNet.Socks5ProxyClient.Parse("host:port:username:password");
return proxy.CreateConnection(address, port); return proxy.CreateConnection(address, port);
}; };
await client.LoginUserIfNeeded(); var myself = await client.LoginUserIfNeeded();
```
or with [xNetStandard](https://www.nuget.org/packages/xNetStandard/):
```csharp
client.TcpHandler = async (address, port) =>
{
var proxy = xNet.Socks5ProxyClient.Parse("host:port:username:password");
return proxy.CreateConnection(address, port);
};
``` ```
<a name="mtproxy"></a> <a name="mtproxy"></a>
MTProxy (MTProto proxy) can be used to prevent ISP blocking Telegram servers, through the `client.MTProxyUrl` property: MTProxy (MTProto proxy) can be used to prevent ISP blocks, through the `client.MTProxyUrl` property:
```csharp ```csharp
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable); using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
client.MTProxyUrl = "https://t.me/proxy?server=...&port=...&secret=..."; client.MTProxyUrl = "http://t.me/proxy?server=...&port=...&secret=...";
await client.LoginUserIfNeeded(); var myself = 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* *Note: WTelegramClient always uses transport obfuscation when connecting to Telegram servers, even without MTProxy*
<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 VS Output debugging pane in addition (+=) to default Console screen logging:
WTelegram.Helpers.Log += (lvl, str) => System.Diagnostics.Debug.WriteLine(str);
// • 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}");
// • In an ASP.NET service, you will typically send logs to an ILogger:
WTelegram.Helpers.Log = (lvl, str) => _logger.Log((LogLevel)lvl, str);
```
<a name="2FA"></a> <a name="2FA"></a>
## Change 2FA password ### Change 2FA password
```csharp ```csharp
const string old_password = "password"; // current password if any (unused otherwise) const string old_password = "password"; // current password if any (unused otherwise)
const string new_password = "new_password"; // or null to disable 2FA const string new_password = "new_password"; // or null to disable 2FA
var accountPwd = await client.Account_GetPassword(); var accountPassword = await client.Account_GetPassword();
var password = accountPwd.current_algo == null ? null : await WTelegram.Client.InputCheckPassword(accountPwd, old_password); var password = accountPassword.current_algo == null ? null : await WTelegram.Client.InputCheckPassword(accountPassword, old_password);
accountPwd.current_algo = null; // makes InputCheckPassword generate a new password accountPassword.current_algo = null; // makes InputCheckPassword generate a new password
var new_password_hash = new_password == null ? null : await WTelegram.Client.InputCheckPassword(accountPwd, new_password); var new_password_hash = new_password == null ? null : await WTelegram.Client.InputCheckPassword(accountPassword, new_password);
await client.Account_UpdatePasswordSettings(password, new Account_PasswordInputSettings await client.Account_UpdatePasswordSettings(password, new Account_PasswordInputSettings
{ {
flags = Account_PasswordInputSettings.Flags.has_new_algo, flags = Account_PasswordInputSettings.Flags.has_new_algo,
new_password_hash = new_password_hash?.A, new_password_hash = new_password_hash?.A,
new_algo = accountPwd.new_algo, new_algo = accountPassword.new_algo,
hint = "new password hint", hint = "new password hint",
}); });
``` ```
<a name="reaction"></a>
<a name="pinned"></a>
### Send a message reaction on pinned messages
This code fetches the available reactions in a given chat, and sends the first reaction emoji (usually 👍) on the last 2 pinned messages:
```csharp
var chats = await client.Messages_GetAllChats();
var chat = chats.chats[1234567890]; // the chat we want
var full = await client.GetFullChat(chat);
var reaction = full.full_chat.AvailableReactions[0]; // choose the first available reaction emoji
var messages = await client.Messages_Search<InputMessagesFilterPinned>(chat, limit: 2);
foreach (var msg in messages.Messages)
await client.Messages_SendReaction(chat, msg.ID, reaction);
```
<a name="database"></a><a name="sessionStore"></a><a name="customStore"></a> <a name="database"></a><a name="sessionStore"></a><a name="customStore"></a>
## Store session data to database or elsewhere, instead of files ### 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, 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. you can choose to store the session data somewhere else, like in a database.
@ -611,14 +419,4 @@ 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. 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. 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) 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#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).

Binary file not shown.

View file

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using TL;
namespace WTelegramClientTest
{
static class Program_CollectAccessHash
{
private const string StateFilename = "SavedState.json";
private const long DurovID = 1006503122; // known ID for Durov's Channel
private static SavedState savedState = new();
// go to Project Properties > Debug > 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 load/save/use collected access hash.");
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
client.CollectAccessHash = true;
if (File.Exists(StateFilename))
{
Console.WriteLine("Loading previously saved access hashes from disk...");
using (var stateStream = File.OpenRead(StateFilename))
savedState = await JsonSerializer.DeserializeAsync<SavedState>(stateStream);
foreach (var id_hash in savedState.Channels) client.SetAccessHashFor<Channel>(id_hash.Key, id_hash.Value);
foreach (var id_hash in savedState.Users) client.SetAccessHashFor<User>(id_hash.Key, id_hash.Value);
}
Console.WriteLine("Connecting to Telegram...");
await client.LoginUserIfNeeded();
var durovAccessHash = client.GetAccessHashFor<Channel>(DurovID);
if (durovAccessHash != 0)
{
// we already know the access hash for Durov's Channel, so we can directly use it
Console.WriteLine($"Channel @durov has ID {DurovID} and access hash was already collected: {durovAccessHash:X}");
}
else
{
// Zero means the access hash for Durov's Channel was not collected yet.
// So we need to obtain it through Client API calls whose results contains the access_hash field, such as:
// - Messages_GetAllChats (see Program_GetAllChats.cs for an example on how to use it)
// - Messages_GetAllDialogs (see Program_ListenUpdates.cs for an example on how to use it)
// - Contacts_ResolveUsername (see below for an example on how to use it)
// and many more API methods...
// The access_hash fields can be found inside instance of User, Channel, Photo, Document, etc..
// usually listed through their base class UserBase, ChatBase, PhotoBase, DocumentBase, etc...
Console.WriteLine("Resolving channel @durov to get its ID, access hash and other infos...");
var durovResolved = await client.Contacts_ResolveUsername("durov"); // @durov = Durov's Channel
if (durovResolved.peer.ID != DurovID)
throw new Exception("@durov has changed channel ID ?!");
durovAccessHash = client.GetAccessHashFor<Channel>(DurovID); // should have been collected from the previous API result
if (durovAccessHash == 0)
throw new Exception("No access hash was automatically collected !? (shouldn't happen)");
Console.WriteLine($"Channel @durov has ID {DurovID} and access hash was automatically collected: {durovAccessHash:X}");
}
Console.WriteLine("With the access hash, we can now join the channel for example.");
await client.Channels_JoinChannel(new InputChannel(DurovID, durovAccessHash));
Console.WriteLine("Channel joined. Press any key to save and exit");
Console.ReadKey(true);
Console.WriteLine("Saving all collected access hashes to disk for next run...");
savedState.Channels = client.AllAccessHashesFor<Channel>().ToList();
savedState.Users = client.AllAccessHashesFor<User>().ToList();
using (var stateStream = File.Create(StateFilename))
await JsonSerializer.SerializeAsync(stateStream, savedState);
}
class SavedState
{
public List<KeyValuePair<long, long>> Channels { get; set; } = new();
public List<KeyValuePair<long, long>> Users { get; set; } = new();
}
}
}

View file

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

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using TL; using TL;
@ -9,7 +10,7 @@ namespace WTelegramClientTest
// This code is similar to what you should have obtained if you followed the README introduction // 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 // 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 // go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
static string Config(string what) static string Config(string what)
{ {
if (what == "api_id") return Environment.GetEnvironmentVariable("api_id"); if (what == "api_id") return Environment.GetEnvironmentVariable("api_id");
@ -24,7 +25,7 @@ namespace WTelegramClientTest
static async Task Main(string[] _) static async Task Main(string[] _)
{ {
await using var client = new WTelegram.Client(Config); using var client = new WTelegram.Client(Config);
var user = await client.LoginUserIfNeeded(); var user = await client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})"); Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})");
@ -33,10 +34,10 @@ namespace WTelegramClientTest
foreach (var (id, chat) in chats.chats) foreach (var (id, chat) in chats.chats)
switch (chat) switch (chat)
{ {
case Chat smallgroup when smallgroup.IsActive: case Chat smallgroup when (smallgroup.flags & Chat.Flags.deactivated) == 0:
Console.WriteLine($"{id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members"); Console.WriteLine($"{id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members");
break; break;
case Channel channel when channel.IsChannel: case Channel channel when (channel.flags & Channel.Flags.broadcast) != 0:
Console.WriteLine($"{id}: Channel {channel.username}: {channel.title}"); Console.WriteLine($"{id}: Channel {channel.username}: {channel.title}");
//Console.WriteLine($" → access_hash = {channel.access_hash:X}"); //Console.WriteLine($" → access_hash = {channel.access_hash:X}");
break; break;

View file

@ -6,9 +6,9 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TL; using TL;
// This is an example userbot designed to run on a Heroku account with a PostgreSQL database for session storage // This is an example userbot designed to run on a free Heroku account with a free PostgreSQL database for session storage
// This userbot simply answer "Pong" when someone sends him a "Ping" private message (or in Saved Messages) // 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 // 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) // When run locally, close the window or type ALT-F4 to exit cleanly and save session (similar to Heroku SIGTERM)
namespace WTelegramClientTest namespace WTelegramClientTest
@ -17,20 +17,20 @@ namespace WTelegramClientTest
{ {
static WTelegram.Client Client; static WTelegram.Client Client;
static User My; static User My;
static readonly Dictionary<long, User> Users = []; static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = []; static readonly Dictionary<long, ChatBase> Chats = new();
// See steps at the end of this file to setup required Environment variables // See steps at the end of this file to setup required Environment variables
static async Task Main(string[] _) static async Task Main(string[] _)
{ {
var exit = new SemaphoreSlim(0); var exit = new SemaphoreSlim(0);
AppDomain.CurrentDomain.ProcessExit += (s, e) => exit.Release(); // detect SIGTERM to exit gracefully AppDomain.CurrentDomain.ProcessExit += (s, e) => exit.Release(); // detect SIGTERM to exit gracefully
var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"), Environment.GetEnvironmentVariable("SESSION_NAME")); var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"));
// if DB does not contain a session yet, client will be run in interactive mode // 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); Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store);
await using (Client) using (Client)
{ {
Client.OnUpdates += Client_OnUpdates; Client.Update += Client_Update;
My = await Client.LoginUserIfNeeded(); My = await Client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})"); Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
var dialogs = await Client.Messages_GetAllDialogs(); var dialogs = await Client.Messages_GetAllDialogs();
@ -39,20 +39,20 @@ namespace WTelegramClientTest
} }
} }
private static async Task Client_OnUpdates(UpdatesBase updates) private static async void Client_Update(IObject arg)
{ {
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats); updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList) foreach (var update in updates.UpdateList)
{ {
Console.WriteLine(update.GetType().Name); Console.WriteLine(update.GetType().Name);
if (update is UpdateNewMessage { message: Message { peer_id: PeerUser { user_id: var user_id } } msg }) // private message 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))
if (Users.TryGetValue(user_id, out var user)) {
{ Console.WriteLine($"New message from {user}: {msg.message}");
Console.WriteLine($"New message from {user}: {msg.message}"); if (msg.message.Equals("Ping", StringComparison.OrdinalIgnoreCase))
if (msg.message.Equals("Ping", StringComparison.OrdinalIgnoreCase)) await Client.SendMessageAsync(user, "Pong");
await Client.SendMessageAsync(user, "Pong"); }
}
} }
} }
} }
@ -61,21 +61,19 @@ namespace WTelegramClientTest
class PostgreStore : Stream class PostgreStore : Stream
{ {
private readonly NpgsqlConnection _sql; private readonly NpgsqlConnection _sql;
private readonly string _sessionName; private byte[] _data;
private readonly byte[] _data; private int _dataLen;
private readonly int _dataLen; private DateTime _lastWrite;
private Task _delayedWrite;
/// <param name="databaseUrl">Heroku DB URL of the form "postgres://user:password@host:port/database"</param> public PostgreStore(string databaseUrl) // Heroku DB URL of the form "postgres://user:password@host:port/database"
/// <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(':', '/', '@'); 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 = 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(); _sql.Open();
using (var create = new NpgsqlCommand("CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql)) using (var create = new NpgsqlCommand($"CREATE TABLE IF NOT EXISTS WTelegram (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
create.ExecuteNonQuery(); create.ExecuteNonQuery();
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql); using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram WHERE name = 'session'", _sql);
using var rdr = cmd.ExecuteReader(); using var rdr = cmd.ExecuteReader();
if (rdr.Read()) if (rdr.Read())
_dataLen = (_data = rdr[0] as byte[]).Length; _dataLen = (_data = rdr[0] as byte[]).Length;
@ -83,6 +81,7 @@ namespace WTelegramClientTest
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
_delayedWrite?.Wait();
_sql.Dispose(); _sql.Dispose();
} }
@ -94,9 +93,18 @@ namespace WTelegramClientTest
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock() 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); _data = buffer; _dataLen = count;
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]); if (_delayedWrite != null) return;
cmd.ExecuteNonQuery(); var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds;
if (left < 0)
{
using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram (name, data) VALUES ('session', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
cmd.ExecuteNonQuery();
_lastWrite = DateTime.UtcNow;
}
else // delay writings for a full second
_delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } });
} }
public override long Length => _dataLen; public override long Length => _dataLen;
@ -113,8 +121,8 @@ namespace WTelegramClientTest
/****************************************************************************************************************************** /******************************************************************************************************************************
HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT: HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- From your Heroku.com account dashboard, create a new app - From your free Heroku.com account dashboard, create a new app (Free)
- Navigate to the app Resources and add the add-on "Heroku Postgres" - Navigate to the app Resources and add the add-on "Heroku Postgres" (Hobby Dev - Free)
- Navigate to the app Settings, click Reveal Config Vars and save the Heroku git URL and the value of DATABASE_URL - 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 "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 - Add a new var named "phone_number" with the phone_number of the user this userbot will manage
@ -122,7 +130,7 @@ HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files - In 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 - In this repository folder, create a new .NET Console project with this Program.cs file
- Add these Nuget packages: WTelegramClient and Npgsql - 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 - In Project properties > Debug > 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 - 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 - 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 test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong"
@ -133,7 +141,8 @@ HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- Paste inside the line you copied, and replace the initial "web" with "worker:" (don't forget the colon) - 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. - 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) - 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. - Now your userbot should be running 24/7. Note however that a full month of usage is 31*24 = 744 dyno hours.
- To prevent AUTH_KEY_DUPLICATED issues, set a SESSION_NAME env variable in your local VS project with a value like "PC" By default a free account gets 550 free dyno hours per month after which your app is stopped. If you register
a credit card with your account, 450 additional free dyno hours are offered at no charge, which should be enough for 24/7
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 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

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using TL; using TL;
@ -7,54 +8,53 @@ namespace WTelegramClientTest
static class Program_ListenUpdates static class Program_ListenUpdates
{ {
static WTelegram.Client Client; static WTelegram.Client Client;
static WTelegram.UpdateManager Manager;
static User My; static User My;
static readonly Dictionary<long, User> Users = new();
static readonly Dictionary<long, ChatBase> Chats = new();
// go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number // go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main(string[] _) static async Task Main(string[] _)
{ {
Console.WriteLine("The program will display updates received for the logged-in user. Press any key to terminate"); 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); WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
Client = new WTelegram.Client(Environment.GetEnvironmentVariable); Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
await using (Client) using (Client)
{ {
Manager = Client.WithUpdateManager(Client_OnUpdate/*, "Updates.state"*/); Client.Update += Client_Update;
My = await Client.LoginUserIfNeeded(); My = await Client.LoginUserIfNeeded();
// Note: on login, Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged Users[My.id] = My;
// Note that 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})"); 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 // 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 var dialogs = await Client.Messages_GetAllDialogs(); // dialogs = groups/channels/users
dialogs.CollectUsersChats(Manager.Users, Manager.Chats); dialogs.CollectUsersChats(Users, Chats);
Console.ReadKey(); 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 void Client_Update(IObject arg)
private static Task HandleMessage(MessageBase messageBase, bool edit = false) {
if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
switch (update)
{
case UpdateNewMessage unm: DisplayMessage(unm.message); break;
case UpdateEditMessage uem: DisplayMessage(uem.message, true); break;
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.username} {uun.first_name} {uun.last_name}"); break;
case UpdateUserPhoto uup: Console.WriteLine($"{User(uup.user_id)} has changed profile photo"); break;
default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above cases
}
}
private static void DisplayMessage(MessageBase messageBase, bool edit = false)
{ {
if (edit) Console.Write("(Edit): "); if (edit) Console.Write("(Edit): ");
switch (messageBase) switch (messageBase)
@ -62,11 +62,11 @@ namespace WTelegramClientTest
case Message m: Console.WriteLine($"{Peer(m.from_id) ?? m.post_author} in {Peer(m.peer_id)}> {m.message}"); break; 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; 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 User(long id) => 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 Chat(long id) => Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
private static string Peer(Peer peer) => Manager.UserOrChat(peer)?.ToString() ?? $"Peer {peer?.ID}"; private static string Peer(Peer peer) => peer is null ? null : peer is PeerUser user ? User(user.user_id)
: peer is PeerChat or PeerChannel ? Chat(peer.ID) : $"Peer {peer.ID}";
} }
} }

View file

@ -1,70 +0,0 @@
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

@ -1,117 +0,0 @@
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);
}
}
}

Binary file not shown.

237
FAQ.md
View file

@ -1,13 +1,11 @@
# FAQ ## FAQ
Before asking questions, make sure to **[read through the ReadMe first](README.md)**, 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), 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. 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> <a name="remove-logs"></a>
## 1. How to remove the Console logs? #### 1. How to remove the Console logs?
Writing the library logs to the Console is the default behavior of the `WTelegram.Helpers.Log` delegate. 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, ...). 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, ...).
@ -16,14 +14,13 @@ In any case, it is not recommended to totally ignore those logs because you woul
Read the [example about logging settings](EXAMPLES.md#logging) for how to write logs to a file. Read the [example about logging settings](EXAMPLES.md#logging) for how to write logs to a file.
<a name="multiple-users"></a> <a name="multiple-users"></a>
## 2. How to handle multiple user accounts #### 2. How to handle multiple user accounts
The WTelegram.session file contains the authentication keys negociated for the current user. 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. 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. 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 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. 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, If you need to manage these user accounts in parallel, you can create multiple instances of WTelegram.Client,
@ -35,7 +32,7 @@ Your api_id/api_hash represents your application, and shouldn't change with each
<a name="GUI"></a> <a name="GUI"></a>
<a name="ASPNET"></a> <a name="ASPNET"></a>
## 3. How to use the library in a WinForms, WPF or ASP.NET application #### 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 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. 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.
@ -45,57 +42,51 @@ This might require adding a reference *(and `using`)* to the Microsoft.VisualBas
A more complex solution requires the use of a `ManualResetEventSlim` that you will wait for in Config callback, 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. 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.
You can download such full example apps [for WinForms](https://github.com/wiz0u/WTelegramClient/raw/master/Examples/WinForms_app.zip) and [for ASP.NET](https://github.com/wiz0u/WTelegramClient/raw/master/Examples/ASPnet_webapp.zip)
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> <a name="access-hash"></a>
## 4. How to use IDs and access_hash? Why the error `CHANNEL_INVALID` or `USER_ID_INVALID`? #### 4. Where to get the 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. An `access_hash` is required by Telegram when dealing with a channel, user, photo, document, etc...
This serves as a proof that the logged-in user is entitled to access it (otherwise, anybody with the ID could access it)
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. > A small private `Chat` don't need an access_hash and can be queried using their `chat_id` only.
This serves as a proof that the logged-in user is entitled to access that channel/user/photo/document/... However most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
(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. 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...). 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 **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...
*(if you have a `Peer` object, you can convert it to a `User`/`Channel`/`Chat` via the `UserOrChat` helper from the root class that contained the peer)*
You obtain the `access_hash` through TL **description structures** like `Channel`, `User`, `Photo`, `Document` that you receive through updates Once you obtained the description structure, there are 3 methods for building your `Input...` structure:
or when you query them through API methods like `Messages_GetAllChats`, `Messages_GetAllDialogs`, `Contacts_ResolveUsername`, etc... * **Recommended:** If you take a look at the **description structure** class or base class `ChatBase/UserBase`,
you will see that they have conversion implicit operators or methods that can create the `Input...` structure for you automatically.
So you can just pass that structure you already have, in place of the `Input...` argument, it will work!
* Alternatively, you can manually create the `Input...` structure yourself by extracting the `access_hash` from the **description structure**
* If you have enabled the [CollectAccessHash system](EXAMPLES.md#collect-access-hash) at the start of your session, it will have collected the `access_hash` automatically when you obtained the description structure.
You can then retrieve it with `client.GetAccessHashFor<User/Channel/Photo/Document>(id)`
You can use the [`UserOrChat` and `CollectUsersChats` methods](EXAMPLES.md#collect-users-chats) to help you in obtaining/collecting ⚠️ *An `access_hash` obtained from a User/Channel structure with flag `min` may not be used for most requests. See [Min constructors](https://core.telegram.org/api/min).*
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> <a name="dev-versions"></a>
## 5. I need to test a feature that has been recently developed but seems not available in my program #### 5. I need to test a feature that has been developed but not yet released in WTelegramClient nuget
The developmental versions of the library are now available as **pre-release** on Nuget (with `-dev` in the version number) The developmental versions of the library are available through Azure DevOps as part of the Continuous Integration builds after each Github commit.
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. You can access these versions for testing in your program by going to our [private nuget feed](https://dev.azure.com/wiz0u/WTelegramClient/_packaging?_a=package&feed=WTelegramClient&view=overview&package=WTelegramClient&protocolType=NuGet),
then click on "Connect to feed" and follow the steps to setup your dev environment.
After that, you should be able to see/install the pre-release versions in your Nuget package manager and install them in your application. *(make sure you enable the **pre-release** checkbox)*
<a name="wrong-server"></a> <a name="wrong-server"></a>
## 6. Telegram asks me to signup (firstname, lastname) even for an existing account #### 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. 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. 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...` 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. 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. 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. 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 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 ...") > System.TypeInitializationException (FileNotFoundException for "System.Text.Json Version=5.0.0.0 ...")
@ -103,14 +94,12 @@ If you use the Github source project in an old .NET Framework 4.x or .NET Core x
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. 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> <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. #### 7. I get error FLOOD_WAIT_8xxxx or PEER_FLOOD, PHONE_NUMBER_BANNED. 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 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. 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 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. 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.
@ -119,7 +108,7 @@ If you think your phone number was banned from Telegram for a wrong reason, you
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. 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> <a name="prevent-ban"></a>
## 8. How to NOT get banned from Telegram? #### 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. **Do not share publicly your app's ID and hash!** They cannot be regenerated and are bound to your Telegram account.
@ -142,26 +131,16 @@ Here are some advices from [another similar library](https://github.com/gotd/td/
* Do not abuse, spam or use it for other suspicious activities. * Do not abuse, spam or use it for other suspicious activities.
* Implement a rate limiting system. * Implement a rate limiting system.
Some additional advices from me: If your client displays Telegram channels to the user, you have to support and display [official sponsored messages](https://core.telegram.org/api/sponsored-messages).
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> <a name="chat-id"></a>
## 9. Why the error `CHAT_ID_INVALID`? #### 9. Why the error `CHAT_ID_INVALID`?
Most chat groups you see are likely of type `Channel`, not `Chat`. 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). 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`. 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. All API methods taking a `long api_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_` ... 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_` ...
@ -169,28 +148,26 @@ However, note that those Channel-compatible methods will require an `InputChanne
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). 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="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 id.
## 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: There can be several reasons why `chats.chats[id]` raise an error:
- You're searching for a user instead of a chat ID. - The user account you're currently logged-in as has not joined this particular chat.
Private messages with a user are not called "chats". See [Terminology in ReadMe](README.md#terminology). 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.
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) - You're trying to use a Telegram.Bot (or TDLib) numerical ID, like -1001234567890
- 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). Telegram Client API don't use these kind of IDs for chats. Remove the -100 prefix and try again with the rest (1234567890).
- You're trying to use a user ID 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 `chats.chats` dictionary is empty. - 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) 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 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) ([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) 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. 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> <a name="shutdown"></a>
## 11. I get "Connection shut down" errors in my logs #### 10. 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: There are various reasons why you may get this error. Here are the explanation and how to solve it:
@ -204,18 +181,15 @@ If Telegram servers decide to shutdown this secondary connection, it's not an is
This should be transparent and pending API calls should automatically be resent upon reconnection. 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. 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, 3) If you reach `MaxAutoReconnects` disconnections, then the **Update** event handler will receive a `ReactorError` object to notify you of the problem.
and pending API calls throw the network IOException. In this case, the recommended action would be to dispose the client and recreate one
In this case, the recommended action would be to dispose the client and recreate one (see example [Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs))
4) In case of slow Internet connection or if you break in the debugger for some time, 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. 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)*. 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> <a name="TLSharp"></a>
## 12. How to migrate from TLSharp? How to sign-in/sign-up/register account properly? #### 11. How to migrate from TLSharp? How to sign-in/sign-up/register account?
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. 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.
@ -226,137 +200,48 @@ See the [full method list](https://core.telegram.org/methods) (just replace the
A session file is created or resumed automatically on startup, and maintained up-to-date automatically throughout the session. 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. 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. You don't have to call methods Auth_SignIn/SignUp/.. manually anymore 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)). Your Config callback just need to provide the various login answers if they are needed.
In particular, it will detect and handle automatically and properly the various login cases/particularity like: In particular, it will detect and handle automatically the various login cases/particularity like:
* Login not necessary (when a session is resumed with an already logged-in user) * 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") * 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") * 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) * Request to resend the verification code through alternate ways like SMS (if your Config answer an empty "verification_code" initially)
* Transient failures, slowness to respond, wrong code/password, checks for encryption key safety, etc.. * Transient failures, slowness to respond, check 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... Contrary to TLSharp, WTelegram supports MTProto v2.0, protocol security checks, transport obfuscation, MTProto Proxy, real-time updates, multiple DC connections, API documentation in Intellisense...
<a name="heroku"></a><a name="vps"></a><a name="host"></a> <a name="heroku"></a><a name="vps"></a><a name="host"></a>
## 13. How to host my userbot online? #### 12. 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, 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). 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. 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: There are many cheap VPS Hosting offers available, and some even have free tier, like 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. See [Examples/Program_Heroku.cs](Examples/Program_Heroku.cs) 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> <a name="troubleshoot"></a>
# Troubleshooting guide ## Troubleshooting guide
Here is a list of common issues and how to fix them so that your program work correctly: 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? 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. 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)). When built in DEBUG mode, the source code connects to Telegram test servers (see also [FAQ #6](#wrong-server)).
So you can either: So you can either:
- **Recommended:** Use the [official Nuget package](https://www.nuget.org/packages/WTelegramClient) - **Recommended:** Use the [official Nuget package](https://www.nuget.org/packages/WTelegramClient) or the [private nuget feed of development builds](https://dev.azure.com/wiz0u/WTelegramClient/_packaging?_a=package&feed=WTelegramClient&package=WTelegramClient&protocolType=NuGet)
- Build your code in RELEASE mode - 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) - 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? 2) Did you call `LoginUserIfNeeded()`?
If you don't complete authentication as a user (or bot), you have access to a very limited subset of Telegram APIs. If you don't authenticate 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? 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. 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? 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. 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. For a console program, this is typical done by waiting for a key or some close event.
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`) 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). 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()`
It is therefore necessary to go through the authentification again. This can be done by deleting the WTelegram.session file, or at runtime by calling `client.Reset()`
<a name="manager"></a>
# About the UpdateManager
The UpdateManager does the following:
- ensure the correct sequential order of receiving updates (Telegram may send them in wrong order)
- fetch the missing updates if there was a gap (missing update) in the flow of incoming updates
- resume the flow of updates where you left off if you stopped your program (with saved state)
- collect the users & chats from updates automatically for you _(by default)_
- simplifies the handling of the various containers of update (UpdatesBase)
To use the UpdateManager, instead of setting `client.OnUpdates`, you call:
```csharp
// if you don't care about missed updates while your program was down:
var manager = client.WithUpdateManager(OnUpdate);
// if you want to recover missed updates using the state saved on the last run of your program
var manager = client.WithUpdateManager(OnUpdate, "Updates.state");
// to save the state later, preferably after disposing the client:
manager.SaveState("Updates.state")
```
Your `OnUpdate` method will directly take a single `Update` as parameter, instead of a container of updates.
The `manager.Users` and `manager.Chats` dictionaries will collect the users/chats data from updates.
You can also feed them manually from result of your API calls by calling `result.CollectUsersChats(manager.Users, manager.Chats);` and resolve Peer fields via `manager.UserOrChat(peer)`
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) for an example of implementation.
Notes:
- set `manager.Log` if you want different logger settings than the client
- `WithUpdateManager()` has other parameters for advanced use

View file

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

172
README.md
View file

@ -1,56 +1,49 @@
[![API Layer](https://img.shields.io/badge/API_Layer-220-blueviolet)](https://corefork.telegram.org/methods) [![NuGet version](https://img.shields.io/nuget/v/WTelegramClient)](https://www.nuget.org/packages/WTelegramClient/)
[![NuGet version](https://img.shields.io/nuget/v/WTelegramClient?color=00508F)](https://www.nuget.org/packages/WTelegramClient/) [![Build Status](https://img.shields.io/azure-devops/build/wiz0u/WTelegramClient/7)](https://dev.azure.com/wiz0u/WTelegramClient/_build?definitionId=7)
[![NuGet prerelease](https://img.shields.io/nuget/vpre/WTelegramClient?color=C09030&label=dev+nuget)](https://www.nuget.org/packages/WTelegramClient/absoluteLatest) [![API Layer](https://img.shields.io/badge/API_Layer-140-blueviolet)](https://corefork.telegram.org/methods)
[![Donate](https://img.shields.io/badge/Help_this_project:-Donate-ff4444)](https://buymeacoffee.com/wizou) [![dev nuget](https://img.shields.io/badge/dynamic/json?color=ffc040&label=dev%20nuget&query=%24.versions%5B0%5D&url=https%3A%2F%2Fpkgs.dev.azure.com%2Fwiz0u%2F81bd92b7-0bb9-4701-b426-09090b27e037%2F_packaging%2F46ce0497-7803-4bd4-8c6c-030583e7c371%2Fnuget%2Fv3%2Fflat2%2Fwtelegramclient%2Findex.json)](https://dev.azure.com/wiz0u/WTelegramClient/_packaging?_a=package&feed=WTelegramClient&package=WTelegramClient&protocolType=NuGet)
[![Support Chat](https://img.shields.io/badge/Chat_with_us-on_Telegram-0088cc)](https://t.me/WTelegramClient)
[![Donate](https://img.shields.io/badge/Help_this_project:-Donate-ff4444)](http://wizou.fr/donate.html)
## *_Telegram Client API library written 100% in C# and .NET_* ## _Telegram Client API library written 100% in C# and .NET Standard_
This library allows you to connect to Telegram and control a user programmatically (or a bot, but [WTelegramBot](https://www.nuget.org/packages/WTelegramBot) is much easier for that).
All the Telegram Client APIs (MTProto) are supported so you can do everything the user could do with a full Telegram GUI client.
Library was developed solely by one unemployed guy. [Donations](https://buymeacoffee.com/wizou) or [Patreon memberships are welcome](https://patreon.com/wizou).
This ReadMe is a **quick but important tutorial** to learn the fundamentals about this library. Please read it all. This ReadMe is a **quick but important tutorial** to learn the fundamentals about this library. Please read it all.
This library allows you to connect to Telegram and control a user programmatically (or a bot, but [Telegram.Bot](https://github.com/TelegramBots/Telegram.Bot) is much easier for that).
All the Telegram Client APIs are supported so you can do everything the user could do with a full Telegram GUI client.
> ⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**... >⚠️ This library relies on asynchronous C# programming (`async/await`) so make sure you are familiar with this advanced topic before proceeding.
> If you are a beginner in C#, starting a project based on this library might not be a great idea. >If you are a beginner in C#, starting a project based on this library might not be a great idea.
# How to use # How to use
After installing WTelegramClient through [Nuget](https://www.nuget.org/packages/WTelegramClient/), your first Console program will be as simple as: After installing WTelegramClient through Nuget, your first Console program will be as simple as:
```csharp ```csharp
static async Task Main(string[] _) static async Task Main(string[] _)
{ {
using var client = new WTelegram.Client(); using var client = new WTelegram.Client();
var myself = await client.LoginUserIfNeeded(); var my = await client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {myself} (id {myself.id})"); Console.WriteLine($"We are logged-in as {my.username ?? my.first_name + " " + my.last_name} (id {my.id})");
} }
``` ```
When run, this will prompt you interactively for your App **api_hash** and **api_id** (that you obtain through Telegram's 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.
[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. Those api hash/id represent your application and one can be used for handling many user accounts.
Then it will attempt to sign-in *(login)* as a user for which you must enter the **phone_number** and the **verification_code** 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 or another Telegram client app the user is connected to).
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 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**. *(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. 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`. All these login scenarios are handled automatically within the call to `LoginUserIfNeeded`.
After login, you now have access to the **[full range of Telegram Client APIs](https://corefork.telegram.org/methods)**. And that's it, 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(...)` All those API methods are available *(with an underscore in the method name, instead of a dot)*, like this: `await client.Method_Name(...)`
# Saved session # Saved session
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. 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 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.
and the authentication keys that were negotiated with Telegram so that you needn't sign-in again every time.
That file path is configurable (**session_pathname**), and under various circumstances *(changing user or server address, write permissions)* That file path is configurable (session_pathname), 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.
you may want to change it or simply delete the existing session file in order to restart the authentification process.
# Non-interactive configuration # Non-interactive configuration
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. 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.
@ -76,65 +69,42 @@ 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. 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. Those shown above are the only ones that have no default values and should be provided by your method.
Returning `null` for verification_code or password will show a prompt for console apps, or an error otherwise.
Returning `""` for verification_code requests resending the code through another method (SMS or Call).
Returning `null` for verification_code or password will show a prompt for console apps, or an error otherwise Another simple approach is to pass `Environment.GetEnvironmentVariable` as the config callback and define the configuration items as environment variables.
*(see [FAQ #3](https://wiz0u.github.io/WTelegramClient/FAQ#GUI) for WinForms)* Undefined variables get the default `null` behavior.
Returning `""` for verification_code requests the resending of the code through another system (SMS or Call).
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. 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 [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 # Example of API call
> The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes > The Telegram API makes extensive usage of base and derived classes, so be ready to use the various syntaxes C# offer to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
All the Telegram API classes/methods are fully documented through Intellisense: Place your mouse over a class/method name, 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.
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://corefork.telegram.org/schema) are defined in the `TL` namespace, 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`.
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://corefork.telegram.org/method/messages.getAllChats) API function, 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:
enumerating the various groups/channels the user is in, and then using `client.SendMessageAsync` helper function to easily send a message:
```csharp ```csharp
using TL; using TL;
... ...
var chats = await client.Messages_GetAllChats(); var chats = await client.Messages_GetAllChats();
Console.WriteLine("This user has joined the following:"); Console.WriteLine("This user has joined the following:");
foreach (var (id, chat) in chats.chats) foreach (var (id, chat) in chats.chats)
if (chat.IsActive) switch (chat) // example of downcasting to their real classes:
Console.WriteLine($"{id,10}: {chat}"); {
case Chat smallgroup when smallgroup.IsActive:
Console.WriteLine($"{id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members");
break;
case Channel group when group.IsGroup:
Console.WriteLine($"{id}: Group {group.username}: {group.title}");
break;
case Channel channel:
Console.WriteLine($"{id}: Channel {channel.username}: {channel.title}");
break;
}
Console.Write("Type a chat ID to send a message: "); Console.Write("Type a chat ID to send a message: ");
long chatId = long.Parse(Console.ReadLine()); long chatId = long.Parse(Console.ReadLine());
var target = chats.chats[chatId]; var target = chats.chats[chatId];
@ -142,68 +112,50 @@ Console.WriteLine($"Sending a message in chat {chatId}: {target.Title}");
await client.SendMessageAsync(target, "Hello, World"); await client.SendMessageAsync(target, "Hello, World");
``` ```
➡️ You can find lots of useful code snippets in [EXAMPLES](https://wiz0u.github.io/WTelegramClient/EXAMPLES) ➡️ You can find more useful code snippets in [EXAMPLES.md](https://github.com/wiz0u/WTelegramClient/blob/master/EXAMPLES.md) and in the [Examples subdirectory](https://github.com/wiz0u/WTelegramClient/tree/master/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> <a name="terminology"></a>
# Terminology in Telegram Client API # 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: 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))*, - `Channel` : A (large or public) chat group *(sometimes called supergroup)* or a broadcast channel (the `broadcast` flag differenciate those)
or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (the `broadcast` flag differentiate those) - `Chat` : A private simple chat group 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 be `deactivated`)
- `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`!** **⚠️ 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)* - chats : In plural or general meaning, it means either `Chat` or `Channel`
- `Peer`: Either a `Chat`, a `Channel` or a `User` - `Peer` : Either a `Chat`, `Channel` or a private chat with a `User`
- **Dialog**: Status of chat with a `Peer` *(draft, last message, unread count, pinned...)*. It represents each line from your Telegram chat list. - Dialog : The current status of a 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. - DC (DataCenter) : There are a few datacenters depending on where in the world the user (or an uploaded media file) is from.
See [FAQ #4](https://wiz0u.github.io/WTelegramClient/FAQ#access-hash) to learn more about it. - 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://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#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 # Other things to know
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. The Client class also offers an `Update` event that is triggered when Telegram servers sends Updates (like new messages or status), independently of your API requests. See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs)
You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates.
See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) and [Examples/Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30)
An invalid API request can result in a `RpcException` being raised, reflecting the [error code and status text](https://revgram.github.io/errors.html) of the problem. 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.
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: The other configuration items that you can override include: **session_pathname, session_key, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, user_id**
`case "server_address": return "2>149.154.167.40:443"; // test DC`
The other configuration items that you can provide include: **session_pathname, email, email_verification_code, session_key, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, firebase, user_id, bot_token** Optional API parameters have a default value of `null` when unset. Passing `null` for a required string/array is the same as *empty* (0-length). Required API parameters/fields can sometimes be set to 0 or `null` when unused (check API documentation or experiment).
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).
I've added several useful converters, implicit cast or helper properties to various API objects so that they are more easy to manipulate. 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, 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** 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 # Library uses and limitations
This library can be used for any Telegram scenario including: This library can be used for any Telegram scenarios including:
- Sequential or parallel automated steps based on API requests/responses - Sequential or parallel automated steps based on API requests/responses
- Real-time [monitoring](https://wiz0u.github.io/WTelegramClient/EXAMPLES#updates) of incoming Updates/Messages - Real-time [monitoring](https://github.com/wiz0u/WTelegramClient/blob/master/EXAMPLES.md#updates) of incoming Updates/Messages
- [Download](https://wiz0u.github.io/WTelegramClient/EXAMPLES#download)/[upload](https://wiz0u.github.io/WTelegramClient/EXAMPLES#upload) of files/media - Download/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 - 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), It has been tested in a Console app, [in a WinForms app](https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#gui),
[in ASP.NET webservice](https://wiz0u.github.io/WTelegramClient/Examples/ASPnet_webapp.zip), and in Xamarin/Android. [in ASP.NET webservice](https://github.com/wiz0u/WTelegramClient/blob/master/EXAMPLES.md#logging), and in Xamarin/Android.
Don't use this library for Spam or Scam. Respect Telegram [Terms of Service](https://telegram.org/tos) Please 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.
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), Developers feedback is welcome in the Telegram support group [@WTelegramClient](https://t.me/WTelegramClient)
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) You can also check our [📖 Frequently Asked Questions](https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md) for more help and troubleshooting guide.
If you like this library, you can [buy me a coffee](https://buymeacoffee.com/wizou) ❤ This will help the project keep going. If you like this library, please [consider a donation](http://wizou.fr/donate.html).❤ This will help the project keep going.
© 2021-2025 Olivier Marcoux

View file

@ -1,266 +0,0 @@
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

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

View file

@ -2,6 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using TL; using TL;
@ -13,62 +15,90 @@ namespace WTelegram
{ {
partial class Client partial class Client
{ {
/// <summary>Used to indicate progression of file download/upload</summary> #region Collect Access Hash system
/// <param name="transmitted">transmitted bytes</param> /// <summary>Enable the collection of id/access_hash pairs (experimental)</summary>
/// <param name="totalSize">total size of file in bytes, or 0 if unknown</param> public bool CollectAccessHash { get; set; }
public delegate void ProgressCallback(long transmitted, long totalSize); readonly Dictionary<Type, Dictionary<long, long>> _accessHashes = new();
public IEnumerable<KeyValuePair<long, long>> AllAccessHashesFor<T>() where T : IObject
=> _accessHashes.GetValueOrDefault(typeof(T));
/// <summary>Retrieve the access_hash associated with this id (for a TL class) if it was collected</summary>
/// <remarks>This requires <see cref="CollectAccessHash"/> to be set to <see langword="true"/> first.
/// <br/>See <see href="https://github.com/wiz0u/WTelegramClient/tree/master/Examples/Program_CollectAccessHash.cs">Examples/Program_CollectAccessHash.cs</see> for how to use this</remarks>
/// <typeparam name="T">a TL object class. For example User, Channel or Photo</typeparam>
public long GetAccessHashFor<T>(long id) where T : IObject
{
lock (_accessHashes)
return _accessHashes.GetOrCreate(typeof(T)).TryGetValue(id, out var access_hash) ? access_hash : 0;
}
public void SetAccessHashFor<T>(long id, long access_hash) where T : IObject
{
lock (_accessHashes)
_accessHashes.GetOrCreate(typeof(T))[id] = access_hash;
}
static readonly FieldInfo userFlagsField = typeof(User).GetField("flags");
static readonly FieldInfo channelFlagsField = typeof(Channel).GetField("flags");
internal void CollectField(FieldInfo fieldInfo, object obj, object access_hash)
{
if (fieldInfo.Name != "access_hash") return;
if (access_hash is not long accessHash) return;
var type = fieldInfo.ReflectedType;
if ((type == typeof(User) && ((User.Flags)userFlagsField.GetValue(obj)).HasFlag(User.Flags.min)) ||
(type == typeof(Channel) && ((Channel.Flags)channelFlagsField.GetValue(obj)).HasFlag(Channel.Flags.min)))
return; // access_hash from Min constructors are mostly useless. see https://core.telegram.org/api/min
if (type.GetField("id") is not FieldInfo idField) return;
if (idField.GetValue(obj) is not long id)
if (idField.GetValue(obj) is not int idInt) return;
else id = idInt;
lock (_accessHashes)
_accessHashes.GetOrCreate(type)[id] = accessHash;
}
#endregion
/// <summary>Helper method to upload a file to Telegram</summary> #region Client TL Helpers
/// <summary>Helper function to upload a file to Telegram</summary>
/// <param name="pathname">Path to the file to upload</param> /// <param name="pathname">Path to the file to upload</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</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> /// <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) public Task<InputFileBase> UploadFileAsync(string pathname, ProgressCallback progress = null)
=> UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname), progress); => UploadFileAsync(File.OpenRead(pathname), Path.GetFileName(pathname), progress);
/// <summary>Helper method to upload a file to Telegram</summary> /// <summary>Helper function 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="stream">Content of the file to upload. This method close/dispose the stream</param>
/// <param name="filename">Name of the file</param> /// <param name="filename">Name of the file</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</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> /// <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) public async Task<InputFileBase> UploadFileAsync(Stream stream, string filename, ProgressCallback progress = null)
{ {
var client = await GetClientForDC(-_dcSession.DcID, true); using var md5 = MD5.Create();
using (stream) using (stream)
{ {
const long SMALL_FILE_MAX_SIZE = 10 << 20; long transmitted = 0, length = stream.Length;
bool hasLength = stream.CanSeek; var isBig = length >= 10 * 1024 * 1024;
long transmitted = 0, length = hasLength ? stream.Length : -1; int file_total_parts = (int)((length - 1) / FilePartSize) + 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(); long file_id = Helpers.RandomLong();
int file_part = 0, read; int file_part = 0, read;
var tasks = new Dictionary<int, Task>(); var tasks = new Dictionary<int, Task>();
bool abort = false; bool abort = false;
for (long bytesLeft = hasLength ? length : long.MaxValue; !abort && bytesLeft != 0; file_part++) for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
{ {
var bytes = new byte[Math.Min(FilePartSize, bytesLeft)]; var bytes = new byte[Math.Min(FilePartSize, bytesLeft)];
read = await stream.FullReadAsync(bytes, bytes.Length, default); read = await stream.FullReadAsync(bytes, bytes.Length, default);
await _parallelTransfers.WaitAsync(); await _parallelTransfers.WaitAsync();
bytesLeft -= read; 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); var task = SavePart(file_part, bytes);
lock (tasks) tasks[file_part] = task; lock (tasks) tasks[file_part] = task;
if (read < FilePartSize && bytesLeft != 0) throw new WTException($"Failed to fully read stream ({read},{bytesLeft})"); if (!isBig)
md5.TransformBlock(bytes, 0, read, null, 0);
if (read < FilePartSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})");
async Task SavePart(int file_part, byte[] bytes) async Task SavePart(int file_part, byte[] bytes)
{ {
try try
{ {
if (isBig) if (isBig)
await client.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes); await this.Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
else else
await client.Upload_SaveFilePart(file_id, file_part, bytes); await this.Upload_SaveFilePart(file_id, file_part, bytes);
lock (tasks) { transmitted += bytes.Length; tasks.Remove(file_part); } lock (tasks) { transmitted += bytes.Length; tasks.Remove(file_part); }
progress?.Invoke(transmitted, length); progress?.Invoke(transmitted, length);
} }
@ -84,89 +114,69 @@ namespace WTelegram
} }
} }
Task[] remainingTasks; Task[] remainingTasks;
lock (tasks) remainingTasks = [.. tasks.Values]; lock (tasks) remainingTasks = tasks.Values.ToArray();
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
if (!isBig) md5.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename } return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
: new InputFile { id = file_id, parts = file_total_parts, name = filename }; : new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash };
} }
} }
/// <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> /// <summary>Search messages 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> /// <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="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="text">Text search request</param>
/// <param name="offset_id">Only return messages starting from the specified message ID</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> /// <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() public Task<Messages_MessagesBase> Messages_Search<T>(InputPeer peer, string text = null, int offset_id = 0, int limit = int.MaxValue) where T : MessagesFilter, new()
=> this.Messages_Search(peer, q, new T(), offset_id: offset_id, limit: limit); => this.Messages_Search(peer, text, 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> /// <summary>Helper function to send a media message more easily</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="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="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="mediaFile">Media file already uploaded to TG <i>(see <see cref="UploadFileAsync">UploadFileAsync</see>)</i></param>
/// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, <c>"video"</c> for a streamable MP4 video, or a MIME type to send as a document</param> /// <param name="mimeType"><see langword="null"/> for automatic detection, <c>"photo"</c> for an inline photo, or a MIME type to send as a document</param>
/// <param name="reply_to_msg_id">Your message is a reply to an existing message with this ID, in the same chat</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="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="schedule_date">UTC timestamp when the message should be sent</param>
/// <returns>The transmitted message confirmed by Telegram</returns> /// <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) public Task<Message> SendMediaAsync(InputPeer peer, string caption, InputFileBase mediaFile, string mimeType = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
{ {
mimeType ??= Path.GetExtension(uploadedFile.Name)?.ToLowerInvariant() switch mimeType ??= Path.GetExtension(mediaFile.Name)?.ToLowerInvariant() switch
{ {
".jpg" or ".jpeg" or ".png" or ".bmp" => "photo", ".jpg" or ".jpeg" or ".png" or ".bmp" => "photo",
".mp4" => "video",
".gif" => "image/gif", ".gif" => "image/gif",
".webp" => "image/webp", ".webp" => "image/webp",
".mp4" => "video/mp4",
".mp3" => "audio/mpeg", ".mp3" => "audio/mpeg",
".wav" => "audio/x-wav", ".wav" => "audio/x-wav",
_ => "", // send as generic document with undefined MIME type _ => "", // send as generic document with undefined MIME type
}; };
if (mimeType == "photo") if (mimeType == "photo")
return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = uploadedFile }, reply_to_msg_id, entities, schedule_date); return SendMessageAsync(peer, caption, new InputMediaUploadedPhoto { file = mediaFile }, reply_to_msg_id, entities, schedule_date);
else if (mimeType == "video") return SendMessageAsync(peer, caption, new InputMediaUploadedDocument(mediaFile, mimeType), reply_to_msg_id, entities, schedule_date);
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 function to send a text or media message easily</summary>
/// <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="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="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="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="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="entities">Text formatting entities. 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="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> /// <param name="disable_preview">Should website/media preview be shown or not, for URLs in your message</param>
/// <returns>The transmitted message as confirmed by Telegram</returns> /// <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) public async Task<Message> SendMessageAsync(InputPeer peer, string text, InputMedia media = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default, bool disable_preview = false)
{ {
UpdatesBase updates; UpdatesBase updates;
long random_id = Helpers.RandomLong(); long random_id = Helpers.RandomLong();
if (media == null) if (media == null)
updates = await this.Messages_SendMessage(peer, text, random_id, entities: entities, updates = await this.Messages_SendMessage(peer, text, random_id, no_webpage: disable_preview, entities: entities,
no_webpage: preview == LinkPreview.Disabled, invert_media: preview == LinkPreview.AboveText, reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
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 else
updates = await this.Messages_SendMedia(peer, media, text, random_id, entities: entities, 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); reply_to_msg_id: reply_to_msg_id == 0 ? null : reply_to_msg_id, schedule_date: schedule_date == default ? null : schedule_date);
if (updates is UpdateShortSentMessage sent) OnUpdate(updates);
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; int msgId = -1;
foreach (var update in updates.UpdateList) foreach (var update in updates.UpdateList)
{ {
@ -177,34 +187,41 @@ namespace WTelegram
case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg; case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg;
} }
} }
if (updates is UpdateShortSentMessage sent)
{
return new Message
{
flags = (Message.Flags)sent.flags | (reply_to_msg_id == 0 ? 0 : Message.Flags.has_reply_to) | (peer is InputPeerSelf ? 0 : Message.Flags.has_from_id),
id = sent.id, date = sent.date, message = text, entities = sent.entities, media = sent.media, ttl_period = sent.ttl_period,
reply_to = reply_to_msg_id == 0 ? null : new MessageReplyHeader { reply_to_msg_id = reply_to_msg_id },
from_id = peer is InputPeerSelf ? null : new PeerUser { user_id = _session.UserId },
peer_id = InputToPeer(peer)
};
}
return null; return null;
} }
/// <summary>Helper method to send an album (media group) of photos or documents more easily</summary> /// <summary>Helper function 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="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="medias">An array 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="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="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="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="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 last of the media group messages, confirmed by Telegram</returns>
/// <returns>The media group messages, as received by Telegram</returns>
/// <remarks> /// <remarks>
/// * The caption/entities are set on the first media<br/> /// * The caption/entities are set on the last 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. /// * <see cref="InputMediaDocumentExternal"/> and <see cref="InputMediaPhotoExternal"/> are supported by downloading the file from the web via HttpClient and sending it to Telegram.
/// WTelegramClient proxy settings don't apply to HttpClient<br/> /// 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 /// * You may run into errors if you mix, in the same album, photos and file documents having no thumbnails/video attributes
/// </remarks> /// </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) public async Task<Message> SendAlbumAsync(InputPeer peer, InputMedia[] medias, string caption = null, int reply_to_msg_id = 0, MessageEntity[] entities = null, DateTime schedule_date = default)
{ {
System.Net.Http.HttpClient httpClient = null; System.Net.Http.HttpClient httpClient = null;
int i = 0, length = medias.Count; var multiMedia = new InputSingleMedia[medias.Length];
var multiMedia = new InputSingleMedia[length]; for (int i = 0; i < medias.Length; i++)
var random_id = Helpers.RandomLong();
foreach (var media in medias)
{ {
var ism = multiMedia[i] = new InputSingleMedia { random_id = random_id + i, media = media }; var ism = multiMedia[i] = new InputSingleMedia { random_id = Helpers.RandomLong(), media = medias[i] };
i++;
retry: retry:
switch (ism.media) switch (ism.media)
{ {
@ -216,96 +233,53 @@ namespace WTelegram
var mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imud); var mmd = (MessageMediaDocument)await this.Messages_UploadMedia(peer, imud);
ism.media = mmd.document; ism.media = mmd.document;
break; 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: 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; string mimeType = null;
inputFile = await UploadFromUrl(imde.url); var inputFile = await UploadFromUrl(imde.url);
if (videoUrlAsFile || mimeType?.StartsWith("video/") != true) ism.media = new InputMediaUploadedDocument(inputFile, mimeType);
ism.media = new InputMediaUploadedDocument(inputFile, mimeType); goto retry;
else case InputMediaPhotoExternal impe:
ism.media = new InputMediaUploadedDocument(inputFile, mimeType, new DocumentAttributeVideo { flags = DocumentAttributeVideo.Flags.supports_streaming }); inputFile = await UploadFromUrl(impe.url);
ism.media = new InputMediaUploadedPhoto { file = inputFile };
goto retry; goto retry;
async Task<InputFileBase> UploadFromUrl(string url) async Task<InputFileBase> UploadFromUrl(string url)
{ {
var filename = Path.GetFileName(new Uri(url).LocalPath); var filename = Path.GetFileName(new Uri(url).LocalPath);
httpClient ??= new(); httpClient ??= new();
using var response = await httpClient.GetAsync(url, System.Net.Http.HttpCompletionOption.ResponseHeadersRead); var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync(); using var stream = await response.Content.ReadAsStreamAsync();
mimeType = response.Content.Headers.ContentType?.MediaType; mimeType = response.Content.Headers.ContentType?.MediaType;
if (response.Content.Headers.ContentLength is long length) if (response.Content.Headers.ContentLength is long length)
return await UploadFileAsync(new Helpers.IndirectStream(stream) { ContentLength = length }, filename); return await UploadFileAsync(new Helpers.IndirectStream(stream) { ContentLength = length }, filename);
else else
return await UploadFileAsync(stream, filename); {
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
return await UploadFileAsync(ms, filename);
}
} }
} }
} }
var firstMedia = multiMedia[0]; var lastMedia = multiMedia[^1];
firstMedia.message = caption; lastMedia.message = caption;
firstMedia.entities = entities; lastMedia.entities = entities;
if (entities != null) firstMedia.flags = InputSingleMedia.Flags.has_entities; if (entities != null) lastMedia.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 updates = await this.Messages_SendMultiMedia(peer, multiMedia, reply_to_msg_id: reply_to_msg_id, schedule_date: schedule_date);
var msgIds = new int[length]; OnUpdate(updates);
var result = new Message[length]; int msgId = -1;
foreach (var update in updates.UpdateList) foreach (var update in updates.UpdateList)
{ {
switch (update) switch (update)
{ {
case UpdateMessageID updMsgId: msgIds[updMsgId.random_id - random_id] = updMsgId.id; break; case UpdateMessageID updMsgId when updMsgId.random_id == lastMedia.random_id: msgId = updMsgId.id; break;
case UpdateNewMessage { message: Message message }: result[Array.IndexOf(msgIds, message.id)] = message; break; case UpdateNewMessage { message: Message message } when message.id == msgId: return message;
case UpdateNewScheduledMessage { message: Message schedMsg }: result[Array.IndexOf(msgIds, schedMsg.id)] = schedMsg; break; case UpdateNewScheduledMessage { message: Message schedMsg } when schedMsg.id == msgId: return schedMsg;
} }
} }
return result; return null;
}
/// <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 private Peer InputToPeer(InputPeer peer) => peer switch
@ -334,18 +308,6 @@ namespace WTelegram
return await DownloadFileAsync(fileLocation, outputStream, photo.dc_id, photoSize.FileSize, progress); 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> /// <summary>Download a document from Telegram into the outputStream</summary>
/// <param name="document">The document to download</param> /// <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="outputStream">Stream to write the file content to. This method does not close/dispose the stream</param>
@ -361,18 +323,6 @@ namespace WTelegram
return thumbSize == null ? document.mime_type : "image/" + fileType; 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> /// <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="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="outputStream">Stream to write file content to. This method does not close/dispose the stream</param>
@ -380,16 +330,15 @@ namespace WTelegram
/// <param name="fileSize">(optional) Expected file size</param> /// <param name="fileSize">(optional) Expected file size</param>
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param> /// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
/// <returns>The file type</returns> /// <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) public async Task<Storage_FileType> DownloadFileAsync(InputFileLocationBase fileLocation, Stream outputStream, int dc_id = 0, int fileSize = 0, ProgressCallback progress = null)
{ {
Storage_FileType fileType = Storage_FileType.unknown; Storage_FileType fileType = Storage_FileType.unknown;
var client = dc_id == 0 ? this : await GetClientForDC(-dc_id, true); var client = dc_id == 0 ? this : await GetClientForDC(dc_id, true);
using var writeSem = new SemaphoreSlim(1); using var writeSem = new SemaphoreSlim(1);
bool canSeek = outputStream.CanSeek; long streamStartPos = outputStream.Position;
long streamStartPos = canSeek ? outputStream.Position : 0; int fileOffset = 0, maxOffsetSeen = 0;
long fileOffset = 0, maxOffsetSeen = 0;
long transmitted = 0; long transmitted = 0;
var tasks = new Dictionary<long, Task>(); var tasks = new Dictionary<int, Task>();
progress?.Invoke(0, fileSize); progress?.Invoke(0, fileSize);
bool abort = false; bool abort = false;
while (!abort) while (!abort)
@ -398,16 +347,15 @@ namespace WTelegram
var task = LoadPart(fileOffset); var task = LoadPart(fileOffset);
lock (tasks) tasks[fileOffset] = task; lock (tasks) tasks[fileOffset] = task;
if (dc_id == 0) { await task; dc_id = client._dcSession.DcID; } if (dc_id == 0) { await task; dc_id = client._dcSession.DcID; }
if (!canSeek) await task;
fileOffset += FilePartSize; fileOffset += FilePartSize;
if (fileSize != 0 && fileOffset >= fileSize) if (fileSize != 0 && fileOffset >= fileSize)
{ {
if (await task != ((fileSize - 1) % FilePartSize) + 1) if (await task != ((fileSize - 1) % FilePartSize) + 1)
throw new WTException("Downloaded file size does not match expected file size"); throw new ApplicationException("Downloaded file size does not match expected file size");
break; break;
} }
async Task<int> LoadPart(long offset) async Task<int> LoadPart(int offset)
{ {
Upload_FileBase fileBase; Upload_FileBase fileBase;
try try
@ -416,7 +364,7 @@ namespace WTelegram
} }
catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X") catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X")
{ {
client = await GetClientForDC(-ex.X, true); client = await GetClientForDC(ex.X, true);
fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize); fileBase = await client.Upload_GetFile(fileLocation, offset, FilePartSize);
} }
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID") catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID")
@ -434,7 +382,7 @@ namespace WTelegram
_parallelTransfers.Release(); _parallelTransfers.Release();
} }
if (fileBase is not Upload_File fileData) if (fileBase is not Upload_File fileData)
throw new WTException("Upload_GetFile returned unsupported " + fileBase?.GetType().Name); throw new ApplicationException("Upload_GetFile returned unsupported " + fileBase?.GetType().Name);
if (fileData.bytes.Length != FilePartSize) abort = true; if (fileData.bytes.Length != FilePartSize) abort = true;
if (fileData.bytes.Length != 0) if (fileData.bytes.Length != 0)
{ {
@ -442,7 +390,7 @@ namespace WTelegram
await writeSem.WaitAsync(); await writeSem.WaitAsync();
try try
{ {
if (canSeek && streamStartPos + offset != outputStream.Position) // if we're about to write out of order if (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 await outputStream.FlushAsync(); // async flush, otherwise Seek would do a sync flush
outputStream.Seek(streamStartPos + offset, SeekOrigin.Begin); outputStream.Seek(streamStartPos + offset, SeekOrigin.Begin);
@ -450,7 +398,6 @@ namespace WTelegram
await outputStream.WriteAsync(fileData.bytes, 0, fileData.bytes.Length); await outputStream.WriteAsync(fileData.bytes, 0, fileData.bytes.Length);
maxOffsetSeen = Math.Max(maxOffsetSeen, offset + fileData.bytes.Length); maxOffsetSeen = Math.Max(maxOffsetSeen, offset + fileData.bytes.Length);
transmitted += fileData.bytes.Length; transmitted += fileData.bytes.Length;
progress?.Invoke(transmitted, fileSize);
} }
catch (Exception) catch (Exception)
{ {
@ -460,6 +407,7 @@ namespace WTelegram
finally finally
{ {
writeSem.Release(); writeSem.Release();
progress?.Invoke(transmitted, fileSize);
} }
} }
lock (tasks) tasks.Remove(offset); lock (tasks) tasks.Remove(offset);
@ -467,10 +415,10 @@ namespace WTelegram
} }
} }
Task[] remainingTasks; Task[] remainingTasks;
lock (tasks) remainingTasks = [.. tasks.Values]; lock (tasks) remainingTasks = tasks.Values.ToArray();
await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception await Task.WhenAll(remainingTasks); // wait completion and eventually propagate any task exception
await outputStream.FlushAsync(); await outputStream.FlushAsync();
if (canSeek) outputStream.Seek(streamStartPos + maxOffsetSeen, SeekOrigin.Begin); outputStream.Seek(streamStartPos + maxOffsetSeen, SeekOrigin.Begin);
return fileType; return fileType;
} }
@ -525,17 +473,6 @@ namespace WTelegram
return true; 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> /// <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> /// <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> /// <returns>See <a href="https://corefork.telegram.org/constructor/messages.dialogs"/></returns>
@ -547,30 +484,23 @@ namespace WTelegram
case Messages_DialogsSlice mds: case Messages_DialogsSlice mds:
var dialogList = new List<DialogBase>(); var dialogList = new List<DialogBase>();
var messageList = new List<MessageBase>(); var messageList = new List<MessageBase>();
int skip = 0; while (dialogs.Dialogs.Length != 0)
while (dialogs.Dialogs.Length > skip)
{ {
dialogList.AddRange(skip == 0 ? dialogs.Dialogs : dialogs.Dialogs[skip..]); dialogList.AddRange(dialogs.Dialogs);
messageList.AddRange(dialogs.Messages); messageList.AddRange(dialogs.Messages);
skip = 0; var lastDialog = dialogs.Dialogs[^1];
int last = dialogs.Dialogs.Length - 1; var lastMsg = dialogs.Messages.LastOrDefault(m => m.Peer.ID == lastDialog.Peer.ID && m.ID == lastDialog.TopMessage);
var lastDialog = dialogs.Dialogs[last]; var offsetPeer = dialogs.UserOrChat(lastDialog).ToInputPeer();
retryDate: dialogs = await this.Messages_GetDialogs(lastMsg?.Date ?? default, lastDialog.TopMessage, offsetPeer, folder_id: folder_id);
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; 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.chats) mds.chats[key] = value;
foreach (var (key, value) in md.users) mds.users[key] = value; foreach (var (key, value) in md.users) mds.users[key] = value;
} }
mds.dialogs = [.. dialogList]; mds.dialogs = dialogList.ToArray();
mds.messages = [.. messageList]; mds.messages = messageList.ToArray();
return mds; return mds;
case Messages_Dialogs md: return md; case Messages_Dialogs md: return md;
default: throw new WTException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name); default: throw new ApplicationException("Messages_GetDialogs returned unexpected " + dialogs?.GetType().Name);
} }
} }
@ -580,12 +510,12 @@ namespace WTelegram
/// <param name="alphabet1">first letters used to search for in participants names<br/>(default values crafted with ♥ to find most latin and cyrillic names)</param> /// <param name="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="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> /// <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> /// <returns>Field count indicates the total count of members. Field participants 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> /// <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) 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; alphabet2 ??= alphabet1;
var result = new Channels_ChannelParticipants { chats = [], users = [] }; var result = new Channels_ChannelParticipants { chats = new(), users = new() };
var user_ids = new HashSet<long>(); var user_ids = new HashSet<long>();
var participants = new List<ChannelParticipantBase>(); var participants = new List<ChannelParticipantBase>();
@ -601,7 +531,7 @@ namespace WTelegram
await GetWithFilter(new ChannelParticipantsKicked { q = "" }, (f, c) => new ChannelParticipantsKicked { q = f.q + c }, alphabet1); 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); await GetWithFilter(new ChannelParticipantsBanned { q = "" }, (f, c) => new ChannelParticipantsBanned { q = f.q + c }, alphabet1);
} }
result.participants = [.. participants]; result.participants = participants.ToArray();
return result; return result;
async Task GetWithFilter<T>(T filter, Func<T, char, T> recurse = null, string alphabet = null) where T : ChannelParticipantsFilter async Task GetWithFilter<T>(T filter, Func<T, char, T> recurse = null, string alphabet = null) where T : ChannelParticipantsFilter
@ -617,7 +547,7 @@ namespace WTelegram
foreach (var kvp in ccp.users) result.users[kvp.Key] = kvp.Value; foreach (var kvp in ccp.users) result.users[kvp.Key] = kvp.Value;
lock (participants) lock (participants)
foreach (var participant in ccp.participants) foreach (var participant in ccp.participants)
if (user_ids.Add(participant.UserId)) if (user_ids.Add(participant.UserID))
participants.Add(participant); participants.Add(participant);
offset += ccp.participants.Length; offset += ccp.participants.Length;
if (offset >= ccp.count || ccp.participants.Length == 0) break; if (offset >= ccp.count || ccp.participants.Length == 0) break;
@ -629,322 +559,78 @@ namespace WTelegram
} }
} }
/// <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> public Task<UpdatesBase> AddChatUser(InputPeer peer, InputUserBase user, int fwd_limit = int.MaxValue) => peer switch
/// <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 }; InputPeerChat chat => this.Messages_AddChatUser(chat.chat_id, user, fwd_limit),
var result = await this.Channels_GetAdminLog(channel, q, events_filter: events_filter, admins: admins); InputPeerChannel channel => this.Channels_InviteToChannel(channel, new[] { user }),
var resultFull = result; _ => throw new ArgumentException("This method works on Chat & Channel only"),
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> public Task<UpdatesBase> DeleteChatUser(InputPeer peer, InputUser user, bool revoke_history = true) => peer switch
/// <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), InputPeerChat chat => this.Messages_DeleteChatUser(chat.chat_id, user, revoke_history),
InputPeerChannel channel => this.Channels_EditBanned(channel, user, new ChatBannedRights { flags = ChatBannedRights.Flags.view_messages }), InputPeerChannel channel => this.Channels_EditBanned(channel, user, new ChatBannedRights { flags = ChatBannedRights.Flags.view_messages }),
_ => throw new ArgumentException(OnlyChatChannel), _ => throw new ArgumentException("This method works on Chat & Channel only"),
}; };
/// <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> public Task<UpdatesBase> LeaveChat(InputPeer peer, bool revoke_history = true) => peer switch
/// <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), InputPeerChat chat => this.Messages_DeleteChatUser(chat.chat_id, InputUser.Self, revoke_history),
InputPeerChannel channel => this.Channels_LeaveChannel(channel), InputPeerChannel channel => this.Channels_LeaveChannel(channel),
_ => throw new ArgumentException(OnlyChatChannel), _ => throw new ArgumentException("This method works on Chat & Channel only"),
}; };
/// <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) public async Task<UpdatesBase> EditChatAdmin(InputPeer peer, InputUserBase user, bool is_admin)
{ {
switch (peer) switch (peer)
{ {
case InputPeerChat chat: case InputPeerChat chat:
await this.Messages_EditChatAdmin(chat.chat_id, user, is_admin); await this.Messages_EditChatAdmin(chat.chat_id, user, is_admin);
return new Updates { date = DateTime.UtcNow, users = [], updates = [], return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
chats = (await this.Messages_GetChats(chat.chat_id)).chats }; chats = (await this.Messages_GetChats(new[] { chat.chat_id })).chats };
case InputPeerChannel channel: case InputPeerChannel channel:
return await this.Channels_EditAdmin(channel, user, return await this.Channels_EditAdmin(channel, user,
new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x1E8BF : 0 }, null); new ChatAdminRights { flags = is_admin ? (ChatAdminRights.Flags)0x8BF : 0 }, null);
default: default:
throw new ArgumentException(OnlyChatChannel); throw new ArgumentException("This method works on Chat & Channel only");
} }
} }
/// <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 public Task<UpdatesBase> EditChatPhoto(InputPeer peer, InputChatPhotoBase photo) => peer switch
{ {
InputPeerChat chat => this.Messages_EditChatPhoto(chat.chat_id, photo), InputPeerChat chat => this.Messages_EditChatPhoto(chat.chat_id, photo),
InputPeerChannel channel => this.Channels_EditPhoto(channel, photo), InputPeerChannel channel => this.Channels_EditPhoto(channel, photo),
_ => throw new ArgumentException(OnlyChatChannel), _ => throw new ArgumentException("This method works on Chat & Channel only"),
}; };
/// <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 public Task<UpdatesBase> EditChatTitle(InputPeer peer, string title) => peer switch
{ {
InputPeerChat chat => this.Messages_EditChatTitle(chat.chat_id, title), InputPeerChat chat => this.Messages_EditChatTitle(chat.chat_id, title),
InputPeerChannel channel => this.Channels_EditTitle(channel, title), InputPeerChannel channel => this.Channels_EditTitle(channel, title),
_ => throw new ArgumentException(OnlyChatChannel), _ => throw new ArgumentException("This method works on Chat & Channel only"),
}; };
/// <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 public Task<Messages_ChatFull> GetFullChat(InputPeer peer) => peer switch
{ {
InputPeerChat chat => this.Messages_GetFullChat(chat.chat_id), InputPeerChat chat => this.Messages_GetFullChat(chat.chat_id),
InputPeerChannel channel => this.Channels_GetFullChannel(channel), InputPeerChannel channel => this.Channels_GetFullChannel(channel),
_ => throw new ArgumentException(OnlyChatChannel), _ => throw new ArgumentException("This method works on Chat & Channel only"),
}; };
/// <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) public async Task<UpdatesBase> DeleteChat(InputPeer peer)
{ {
switch (peer) switch (peer)
{ {
case InputPeerChat chat: case InputPeerChat chat:
await this.Messages_DeleteChat(chat.chat_id); await this.Messages_DeleteChat(chat.chat_id);
return new Updates { date = DateTime.UtcNow, users = [], updates = [], return new Updates { date = DateTime.UtcNow, users = new(), updates = Array.Empty<Update>(),
chats = (await this.Messages_GetChats(chat.chat_id)).chats }; chats = (await this.Messages_GetChats(new[] { chat.chat_id })).chats };
case InputPeerChannel channel: case InputPeerChannel channel:
return await this.Channels_DeleteChannel(channel); return await this.Channels_DeleteChannel(channel);
default: default:
throw new ArgumentException(OnlyChatChannel); throw new ArgumentException("This method works on Chat & Channel only");
}
}
/// <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}");
} }
} }
#endregion
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -6,21 +6,20 @@ using System.Linq;
using System.Net; using System.Net;
using System.Numerics; using System.Numerics;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks;
#if NETCOREAPP2_1_OR_GREATER #if NETCOREAPP2_1_OR_GREATER
namespace WTelegram namespace WTelegram
{ {
static partial class Compat static class Compat
{ {
internal static BigInteger BigEndianInteger(byte[] value) => new(value, true, true); internal static BigInteger BigEndianInteger(byte[] value) => new(value, true, true);
internal static IPEndPoint IPEndPoint_Parse(string addr) => IPEndPoint.Parse(addr); internal static IPEndPoint IPEndPoint_Parse(string addr) => IPEndPoint.Parse(addr);
} }
} }
#else // Compatibility shims for methods missing in netstandard2.0: #else
namespace WTelegram namespace WTelegram
{ {
static partial class Compat static class Compat
{ {
internal static BigInteger BigEndianInteger(byte[] value) internal static BigInteger BigEndianInteger(byte[] value)
{ {
@ -48,7 +47,7 @@ namespace WTelegram
return length; return length;
} }
public static V GetValueOrDefault<K, V>(this IReadOnlyDictionary<K, V> dictionary, K key, V defaultValue = default) public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dictionary, K key, V defaultValue = default)
=> dictionary.TryGetValue(key, out V value) ? value : defaultValue; => 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; } public static void Deconstruct<K, V>(this KeyValuePair<K, V> kvp, out K key, out V value) { key = kvp.Key; value = kvp.Value; }
@ -77,13 +76,6 @@ static class Convert
internal static string ToHexString(byte[] data) => BitConverter.ToString(data).Replace("-", ""); 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(); 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 #endif
#if NETSTANDARD2_0 #if NETSTANDARD2_0
@ -95,7 +87,7 @@ namespace System.Runtime.CompilerServices
{ {
if (array == null) throw new ArgumentNullException(); if (array == null) throw new ArgumentNullException();
var (offset, length) = range.GetOffsetAndLength(array.Length); var (offset, length) = range.GetOffsetAndLength(array.Length);
if (length == 0) return []; if (length == 0) return Array.Empty<T>();
var dest = typeof(T).IsValueType || typeof(T[]) == array.GetType() ? new T[length] var dest = typeof(T).IsValueType || typeof(T[]) == array.GetType() ? new T[length]
: (T[])Array.CreateInstance(array.GetType().GetElementType()!, length); : (T[])Array.CreateInstance(array.GetType().GetElementType()!, length);
Array.Copy(array, offset, dest, 0, length); Array.Copy(array, offset, dest, 0, length);
@ -106,16 +98,3 @@ namespace System.Runtime.CompilerServices
internal class IsExternalInit { } internal class IsExternalInit { }
} }
#endif #endif
namespace WTelegram
{
static partial class Compat
{
internal static Task WaitAsync(this Task source, int timeout)
#if NET8_0_OR_GREATER
=> source?.WaitAsync(TimeSpan.FromMilliseconds(timeout)) ?? Task.CompletedTask;
#else
=> source == null ? Task.CompletedTask : Task.WhenAny(source, Task.Delay(timeout));
#endif
}
}

View file

@ -4,7 +4,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -13,17 +12,17 @@ using static WTelegram.Compat;
namespace WTelegram namespace WTelegram
{ {
public static class Encryption internal static class Encryption
{ {
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = []; internal static readonly RNGCryptoServiceProvider RNG = new();
internal static readonly RandomNumberGenerator RNG = RandomNumberGenerator.Create(); private static readonly Dictionary<long, RSAPublicKey> PublicKeys = new();
internal static readonly Aes AesECB = Aes.Create(); private static readonly Aes AesECB = Aes.Create();
static Encryption() static Encryption()
{ {
AesECB.Mode = CipherMode.ECB; AesECB.Mode = CipherMode.ECB;
AesECB.Padding = PaddingMode.Zeros; AesECB.Padding = PaddingMode.Zeros;
if (AesECB.BlockSize != 128) throw new WTException("AES Blocksize is not 16 bytes"); if (AesECB.BlockSize != 128) throw new ApplicationException("AES Blocksize is not 16 bytes");
} }
internal static async Task CreateAuthorizationKey(Client client, Session.DCSession session) internal static async Task CreateAuthorizationKey(Client client, Session.DCSession session)
@ -33,12 +32,12 @@ namespace WTelegram
var sha256 = SHA256.Create(); var sha256 = SHA256.Create();
//1) //1)
var nonce = new TL.Int128(RNG); var nonce = new Int128(RNG);
var resPQ = await client.ReqPqMulti(nonce); var resPQ = await client.ReqPqMulti(nonce);
//2) //2)
if (resPQ.nonce != nonce) throw new WTException("Nonce mismatch"); if (resPQ.nonce != nonce) throw new ApplicationException("Nonce mismatch");
var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey); var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey);
if (fingerprint == 0) throw new WTException("Couldn't match any server_public_key_fingerprints"); if (fingerprint == 0) throw new ApplicationException("Couldn't match any server_public_key_fingerprints");
var publicKey = PublicKeys[fingerprint]; var publicKey = PublicKeys[fingerprint];
Helpers.Log(2, $"Selected public key with fingerprint {fingerprint:X}"); Helpers.Log(2, $"Selected public key with fingerprint {fingerprint:X}");
//3) //3)
@ -57,23 +56,21 @@ namespace WTelegram
new_nonce = new Int256(RNG), new_nonce = new Int256(RNG),
dc = session.DataCenter?.id ?? 0 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; byte[] encrypted_data = null;
{ {
//4.1) RSA_PAD(data, server_public_key) //4.1) RSA_PAD(data, server_public_key)
using var clearStream = new MemoryStream(256); using var clearStream = new MemoryStream(256);
using var writer = new BinaryWriter(clearStream); using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
byte[] aes_key = new byte[32], zero_iv = new byte[32]; byte[] aes_key = new byte[32], zero_iv = new byte[32];
var n = BigEndianInteger(publicKey.n); var n = BigEndianInteger(publicKey.n);
do while (encrypted_data == null)
{ {
RNG.GetBytes(aes_key); RNG.GetBytes(aes_key);
clearStream.Position = 0; clearStream.Position = 0;
clearStream.Write(aes_key, 0, 32); // write aes_key as prefix for initial Sha256 computation clearStream.Write(aes_key, 0, 32); // write aes_key as prefix for initial Sha256 computation
writer.WriteTLObject(pqInnerData); writer.WriteTLObject(pqInnerData);
int clearLength = (int)clearStream.Position - 32; // length before padding int clearLength = (int)clearStream.Position - 32; // length before padding
if (clearLength > 144) throw new WTException("PQInnerData too big"); if (clearLength > 144) throw new ApplicationException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer(); byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, 32 + clearLength, 192 - clearLength); RNG.GetBytes(clearBuffer, 32 + clearLength, 192 - clearLength);
sha256.ComputeHash(clearBuffer, 0, 32 + 192).CopyTo(clearBuffer, 224); // append Sha256 sha256.ComputeHash(clearBuffer, 0, 32 + 192).CopyTo(clearBuffer, 224); // append Sha256
@ -86,40 +83,39 @@ namespace WTelegram
var x = BigEndianInteger(clearBuffer); var x = BigEndianInteger(clearBuffer);
if (x < n) // if good result, encrypt with RSA key: if (x < n) // if good result, encrypt with RSA key:
encrypted_data = BigInteger.ModPow(x, BigEndianInteger(publicKey.e), n).To256Bytes(); encrypted_data = BigInteger.ModPow(x, BigEndianInteger(publicKey.e), n).To256Bytes();
} while (encrypted_data == null); // otherwise, repeat the steps } // otherwise, repeat the steps
} }
var serverDHparams = await client.ReqDHParams(pqInnerData.nonce, pqInnerData.server_nonce, pqInnerData.p, pqInnerData.q, fingerprint, encrypted_data); var serverDHparams = await client.ReqDHParams(pqInnerData.nonce, pqInnerData.server_nonce, pqInnerData.p, pqInnerData.q, fingerprint, encrypted_data);
//5) //5)
var localTime = DateTimeOffset.UtcNow; var localTime = DateTimeOffset.UtcNow;
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new WTException("not server_DH_params_ok"); if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new ApplicationException("not server_DH_params_ok");
if (serverDHparamsOk.nonce != nonce) throw new WTException("Nonce mismatch"); if (serverDHparamsOk.nonce != nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch"); if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(sha1, resPQ.server_nonce, pqInnerData.new_nonce); var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, pqInnerData.new_nonce);
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false); var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
using var answerReader = new BinaryReader(new MemoryStream(answer)); using var encryptedReader = new TL.BinaryReader(new MemoryStream(answer), client);
var answerHash = answerReader.ReadBytes(20); var answerHash = encryptedReader.ReadBytes(20);
var answerObj = answerReader.ReadTLObject(); var answerObj = encryptedReader.ReadTLObject();
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new WTException("not server_DH_inner_data"); if (answerObj is not ServerDHInnerData serverDHinnerData) throw new ApplicationException("not server_DH_inner_data");
long padding = answerReader.BaseStream.Length - answerReader.BaseStream.Position; long padding = encryptedReader.BaseStream.Length - encryptedReader.BaseStream.Position;
if (padding >= 16) throw new WTException("Too much pad"); if (padding >= 16) throw new ApplicationException("Too much pad");
if (!Enumerable.SequenceEqual(sha1.ComputeHash(answer, 20, answer.Length - (int)padding - 20), answerHash)) if (!Enumerable.SequenceEqual(sha1.ComputeHash(answer, 20, answer.Length - (int)padding - 20), answerHash))
throw new WTException("Answer SHA1 mismatch"); throw new ApplicationException("Answer SHA1 mismatch");
if (serverDHinnerData.nonce != nonce) throw new WTException("Nonce mismatch"); if (serverDHinnerData.nonce != nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch"); if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var g_a = BigEndianInteger(serverDHinnerData.g_a); var g_a = BigEndianInteger(serverDHinnerData.g_a);
var dh_prime = BigEndianInteger(serverDHinnerData.dh_prime); var dh_prime = BigEndianInteger(serverDHinnerData.dh_prime);
CheckGoodPrime(dh_prime, serverDHinnerData.g); ValidityChecks(dh_prime, serverDHinnerData.g);
session.lastSentMsgId = 0; session.LastSentMsgId = 0;
session.serverTicksOffset = (serverDHinnerData.server_time - localTime).Ticks; session.ServerTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
Helpers.Log(1, $"Time offset: {session.serverTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC"); Helpers.Log(1, $"Time offset: {session.ServerTicksOffset} | Server: {serverDHinnerData.server_time.TimeOfDay} UTC | Local: {localTime.TimeOfDay} UTC");
//6) //6)
var salt = new byte[256]; var bData = new byte[256];
RNG.GetBytes(salt); RNG.GetBytes(bData);
var b = BigEndianInteger(salt); var b = BigEndianInteger(bData);
var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime); var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime);
CheckGoodGaAndGb(g_a, dh_prime); ValidityChecksDH(g_a, g_b, dh_prime);
CheckGoodGaAndGb(g_b, dh_prime);
var clientDHinnerData = new ClientDHInnerData var clientDHinnerData = new ClientDHInnerData
{ {
nonce = nonce, nonce = nonce,
@ -130,7 +126,7 @@ namespace WTelegram
{ {
using var clearStream = new MemoryStream(384); using var clearStream = new MemoryStream(384);
clearStream.Position = 20; // skip SHA1 area (to be patched) clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream); using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
writer.WriteTLObject(clientDHinnerData); writer.WriteTLObject(clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length) int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int paddingToAdd = (0x7FFFFFF0 - clearLength) % 16; int paddingToAdd = (0x7FFFFFF0 - clearLength) % 16;
@ -149,96 +145,74 @@ namespace WTelegram
var authKeyHash = sha1.ComputeHash(authKey); var authKeyHash = sha1.ComputeHash(authKey);
retry_id = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash); // (auth_key_aux_hash) retry_id = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash); // (auth_key_aux_hash)
//9) //9)
if (setClientDHparamsAnswer is not DhGenOk dhGenOk) throw new WTException("not dh_gen_ok"); if (setClientDHparamsAnswer is not DhGenOk dhGenOk) throw new ApplicationException("not dh_gen_ok");
if (dhGenOk.nonce != nonce) throw new WTException("Nonce mismatch"); if (dhGenOk.nonce != nonce) throw new ApplicationException("Nonce mismatch");
if (dhGenOk.server_nonce != resPQ.server_nonce) throw new WTException("Server Nonce mismatch"); if (dhGenOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var expected_new_nonceN = new byte[32 + 1 + 8]; var expected_new_nonceN = new byte[32 + 1 + 8];
pqInnerData.new_nonce.raw.CopyTo(expected_new_nonceN, 0); pqInnerData.new_nonce.raw.CopyTo(expected_new_nonceN, 0);
expected_new_nonceN[32] = 1; expected_new_nonceN[32] = 1;
Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash) Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash)
if (!Enumerable.SequenceEqual(dhGenOk.new_nonce_hash1.raw, sha1.ComputeHash(expected_new_nonceN).Skip(4))) if (!Enumerable.SequenceEqual(dhGenOk.new_nonce_hash1.raw, sha1.ComputeHash(expected_new_nonceN).Skip(4)))
throw new WTException("setClientDHparamsAnswer.new_nonce_hashN mismatch"); throw new ApplicationException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
session.authKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12)); session.AuthKeyID = BinaryPrimitives.ReadInt64LittleEndian(authKeyHash.AsSpan(12));
session.AuthKey = authKey; session.AuthKey = authKey;
session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw); session.Salt = BinaryPrimitives.ReadInt64LittleEndian(pqInnerData.new_nonce.raw) ^ BinaryPrimitives.ReadInt64LittleEndian(resPQ.server_nonce.raw);
session.OldSalt = session.Salt;
(byte[] key, byte[] iv) ConstructTmpAESKeyIV(Int128 server_nonce, Int256 new_nonce)
{
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(server_nonce, 0, 16);
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
sha1.Initialize();
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
sha1.Initialize();
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
sha1.Initialize();
return (tmp_aes_key, tmp_aes_iv);
}
} }
public static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(SHA1 sha1, TL.Int128 server_nonce, Int256 new_nonce) private static void ValidityChecks(BigInteger p, int g)
{
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)"); Helpers.Log(2, "Verifying encryption key safety... (this should happen only once per DC)");
// check that 2^2047 <= p < 2^2048 // check that 2^2047 <= p < 2^2048
if (p.GetBitLength() != 2048) throw new WTException("p is not 2048-bit number"); if (p.GetBitLength() != 2048) throw new ApplicationException("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. // 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 if (g switch
{ {
2 => p % 8 != 7, 2 => p % 8 != 7,
3 => p % 3 != 2, 3 => p % 3 != 2,
4 => false, 4 => false,
5 => (int)(p % 5) is not 1 and not 4, 5 => (mod_r = p % 5) != 1 && mod_r != 4,
6 => (int)(p % 24) is not 19 and not 23, 6 => (mod_r = p % 24) != 19 && mod_r != 23,
7 => (int)(p % 7) is not 3 and not 5 and not 6, 7 => (mod_r = p % 7) != 3 && mod_r != 5 && mod_r != 6,
_ => true, _ => true,
}) })
throw new WTException("Bad prime mod 4g"); throw new ApplicationException("Bad prime mod 4g");
// check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime) // 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 ApplicationException("p is not a prime number");
if (!p.IsProbablePrime()) throw new WTException("p is not a prime number"); if (!((p - 1) / 2).IsProbablePrime()) throw new ApplicationException("(p - 1) / 2 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... private static void ValidityChecksDH(BigInteger g_a, BigInteger g_b, BigInteger dh_prime)
[
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. // 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. // 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) var l = BigInteger.One << (2048 - 64);
throw new WTException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}"); var r = dh_prime - l;
if (g_a < l || g_a > r || g_b < l || g_b > r)
throw new ApplicationException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}");
} }
/// <summary>Load a specific Telegram server public key</summary>
/// <param name="pem">A string starting with <c>-----BEGIN RSA PUBLIC KEY-----</c></param>
public static void LoadPublicKey(string pem) public static void LoadPublicKey(string pem)
{ {
using var rsa = RSA.Create(); using var rsa = RSA.Create();
@ -247,7 +221,10 @@ namespace WTelegram
var rsaParam = rsa.ExportParameters(false); var rsaParam = rsa.ExportParameters(false);
if (rsaParam.Modulus[0] == 0) rsaParam.Modulus = rsaParam.Modulus[1..]; if (rsaParam.Modulus[0] == 0) rsaParam.Modulus = rsaParam.Modulus[1..];
var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent }; var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent };
var bareData = publicKey.ToBytes(); using var memStream = new MemoryStream(280);
using (var writer = new BinaryWriter(memStream))
writer.WriteTLObject(publicKey);
var bareData = memStream.ToArray();
var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(bareData, 4, bareData.Length - 4).AsSpan(12)); // 64 lower-order bits of SHA1 var fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(bareData, 4, bareData.Length - 4).AsSpan(12)); // 64 lower-order bits of SHA1
PublicKeys[fingerprint] = publicKey; PublicKeys[fingerprint] = publicKey;
Helpers.Log(1, $"Loaded a public key with fingerprint {fingerprint:X}"); Helpers.Log(1, $"Loaded a public key with fingerprint {fingerprint:X}");
@ -275,10 +252,11 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
-----END RSA PUBLIC KEY-----"); -----END RSA PUBLIC KEY-----");
} }
public static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, int x, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256) internal static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, byte[] authKey, byte[] msgKey, int msgKeyOffset, SHA256 sha256)
{ {
// first, construct AES key & IV // first, construct AES key & IV
byte[] aes_key = new byte[32], aes_iv = new byte[32]; byte[] aes_key = new byte[32], aes_iv = new byte[32];
int x = encrypt ? 0 : 8;
sha256.TransformBlock(msgKey, msgKeyOffset, 16, null, 0); // msgKey sha256.TransformBlock(msgKey, msgKeyOffset, 16, null, 0); // msgKey
sha256.TransformFinalBlock(authKey, x, 36); // authKey[x:36] sha256.TransformFinalBlock(authKey, x, 36); // authKey[x:36]
var sha256_a = sha256.Hash; var sha256_a = sha256.Hash;
@ -296,48 +274,57 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt); return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt);
} }
public static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt) private static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
{ {
if (input.Length % 16 != 0) throw new WTException("AES_IGE input size not divisible by 16"); if (input.Length % 16 != 0) throw new ApplicationException("intput size not divisible by 16");
using var aesCrypto = encrypt ? AesECB.CreateEncryptor(aes_key, null) : AesECB.CreateDecryptor(aes_key, null); // code adapted from PHP implementation found at https://mgp25.com/AESIGE/
var output = new byte[input.Length]; var output = new byte[input.Length];
var prevBytes = (byte[])aes_iv.Clone(); var xPrev = aes_iv.AsSpan(encrypt ? 16 : 0, 16);
var span = MemoryMarshal.Cast<byte, long>(input); var yPrev = aes_iv.AsSpan(encrypt ? 0 : 16, 16);
var sout = MemoryMarshal.Cast<byte, long>(output.AsSpan()); using var aesCrypto = encrypt ? AesECB.CreateEncryptor(aes_key, null) : AesECB.CreateDecryptor(aes_key, null);
var prev = MemoryMarshal.Cast<byte, long>(prevBytes.AsSpan()); byte[] yXOR = new byte[16];
if (!encrypt) { (prev[2], prev[0]) = (prev[0], prev[2]); (prev[3], prev[1]) = (prev[1], prev[3]); } for (int i = 0; i < input.Length; i += 16)
for (int i = 0, count = input.Length / 8; i < count;)
{ {
sout[i] = span[i] ^ prev[0]; sout[i + 1] = span[i + 1] ^ prev[1]; for (int j = 0; j < 16; j++)
aesCrypto.TransformBlock(output, i * 8, 16, output, i * 8); yXOR[j] = (byte)(input[i + j] ^ yPrev[j]);
prev[0] = sout[i] ^= prev[2]; prev[1] = sout[i + 1] ^= prev[3]; aesCrypto.TransformBlock(yXOR, 0, 16, output, i);
prev[2] = span[i++]; prev[3] = span[i++]; for (int j = 0; j < 16; j++)
output[i + j] ^= xPrev[j];
xPrev = input.Slice(i, 16);
yPrev = output.AsSpan(i, 16);
} }
return output; return output;
} }
#if OBFUSCATION #if OBFUSCATION
public sealed class AesCtr(byte[] key, byte[] ivec) : IDisposable internal class AesCtr : IDisposable
{ {
readonly ICryptoTransform _encryptor = AesECB.CreateEncryptor(key, null); readonly ICryptoTransform encryptor;
readonly byte[] _ecount = new byte[16]; readonly byte[] ivec;
int _num; readonly byte[] ecount = new byte[16];
int num;
public void Dispose() => _encryptor.Dispose(); public AesCtr(byte[] key, byte[] iv)
public void EncryptDecrypt(Span<byte> buffer)
{ {
for (int i = 0; i < buffer.Length; i++) encryptor = AesECB.CreateEncryptor(key, null);
ivec = iv;
}
public void Dispose() => encryptor.Dispose();
public void EncryptDecrypt(byte[] buffer, int length)
{
for (int i = 0; i < length; i++)
{ {
if (_num == 0) if (num == 0)
{ {
_encryptor.TransformBlock(ivec, 0, 16, _ecount, 0); encryptor.TransformBlock(ivec, 0, 16, ecount, 0);
for (int n = 15; n >= 0; n--) // increment big-endian counter for (int n = 15; n >= 0; n--) // increment big-endian counter
if (++ivec[n] != 0) break; if (++ivec[n] != 0) break;
} }
buffer[i] ^= _ecount[_num]; buffer[i] ^= ecount[num];
_num = (_num + 1) % 16; num = (num + 1) % 16;
} }
} }
} }
@ -372,7 +359,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
var sendCtr = new AesCtr(sendKey, sendIV); var sendCtr = new AesCtr(sendKey, sendIV);
var recvCtr = new AesCtr(recvKey, recvIV); var recvCtr = new AesCtr(recvKey, recvIV);
var encrypted = (byte[])preamble.Clone(); var encrypted = (byte[])preamble.Clone();
sendCtr.EncryptDecrypt(encrypted); sendCtr.EncryptDecrypt(encrypted, 64);
for (int i = 56; i < 64; i++) for (int i = 56; i < 64; i++)
preamble[i] = encrypted[i]; preamble[i] = encrypted[i];
return (sendCtr, recvCtr, preamble); return (sendCtr, recvCtr, preamble);
@ -381,19 +368,21 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
internal static async Task<InputCheckPasswordSRP> Check2FA(Account_Password accountPassword, Func<Task<string>> getPassword) internal static async Task<InputCheckPasswordSRP> Check2FA(Account_Password accountPassword, Func<Task<string>> getPassword)
{ {
bool newPassword = false;
if (accountPassword.current_algo is not PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow algo) if (accountPassword.current_algo is not PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow algo)
if (accountPassword.current_algo == null && (algo = accountPassword.new_algo as PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow) != null) if (accountPassword.current_algo == null && (algo = accountPassword.new_algo as PasswordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow) != null)
{ {
int salt1len = algo.salt1.Length; int salt1len = algo.salt1.Length;
Array.Resize(ref algo.salt1, salt1len + 32); Array.Resize(ref algo.salt1, salt1len + 32);
RNG.GetBytes(algo.salt1, salt1len, 32); RNG.GetBytes(algo.salt1, salt1len, 32);
newPassword = true;
} }
else else
throw new WTException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name); throw new ApplicationException("2FA authentication uses an unsupported algo: " + accountPassword.current_algo?.GetType().Name);
var g = new BigInteger(algo.g); var g = new BigInteger(algo.g);
var p = BigEndianInteger(algo.p); var p = BigEndianInteger(algo.p);
var validTask = Task.Run(() => CheckGoodPrime(p, algo.g)); var validTask = Task.Run(() => ValidityChecks(p, algo.g));
System.Threading.Thread.Sleep(100); System.Threading.Thread.Sleep(100);
Helpers.Log(3, $"This account has enabled 2FA. A password is needed. {accountPassword.hint}"); Helpers.Log(3, $"This account has enabled 2FA. A password is needed. {accountPassword.hint}");
@ -422,7 +411,7 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
var x = BigEndianInteger(sha256.Hash); var x = BigEndianInteger(sha256.Hash);
var v = BigInteger.ModPow(g, x, p); var v = BigInteger.ModPow(g, x, p);
if (accountPassword.current_algo == null) // we're computing a new password if (newPassword)
{ {
await validTask; await validTask;
return new InputCheckPasswordSRP { A = v.To256Bytes() }; return new InputCheckPasswordSRP { A = v.To256Bytes() };
@ -516,55 +505,4 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB
} }
#endif #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

@ -4,18 +4,9 @@ using System.IO;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; 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 namespace WTelegram
{ {
public static class Helpers public static class Helpers
@ -25,93 +16,10 @@ namespace WTelegram
/// <summary>For serializing indented Json with fields included</summary> /// <summary>For serializing indented Json with fields included</summary>
public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true, public static readonly JsonSerializerOptions JsonOptions = new() { IncludeFields = true, WriteIndented = true,
#if NET8_0_OR_GREATER IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull };
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault ? null : WTelegramContext.Default,
Converters = { new TLJsonConverter(), new JsonStringEnumConverter() },
#endif
IgnoreReadOnlyProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault };
#if NET8_0_OR_GREATER private static readonly ConsoleColor[] LogLevelToColor = new[] { ConsoleColor.DarkGray, ConsoleColor.DarkCyan, ConsoleColor.Cyan,
public sealed class TLJsonConverter : JsonConverter<object> ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue };
{
public override bool CanConvert(Type typeToConvert)
=> typeToConvert.IsAbstract || typeToConvert == typeof(Dictionary<long, TL.User>) || typeToConvert == typeof(Dictionary<long, TL.ChatBase>);
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (typeToConvert == typeof(Dictionary<long, TL.User>))
{
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("Expected array for users dictionary");
var users = new Dictionary<long, TL.User>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
var user = JsonSerializer.Deserialize<TL.User>(ref reader, options);
if (user != null) users[user.id] = user;
}
return users;
}
else if (typeToConvert == typeof(Dictionary<long, TL.ChatBase>))
{
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("Expected array for chats dictionary");
var chats = new Dictionary<long, TL.ChatBase>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
var chat = (TL.ChatBase)Read(ref reader, typeof(TL.ChatBase), options);
if (chat != null) chats[chat.ID] = chat;
}
return chats;
}
else if (reader.TokenType == JsonTokenType.Null)
return null;
else if (reader.TokenType == JsonTokenType.StartObject)
{
var typeReader = reader;
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.PropertyName || typeReader.GetString() != "$")
throw new JsonException("Expected $ type property");
if (!typeReader.Read() || typeReader.TokenType != JsonTokenType.String)
throw new JsonException("Invalid $ type property");
var type = typeReader.GetString();
var actualType = typeToConvert.Assembly.GetType("TL." + type);
if (!typeToConvert.IsAssignableFrom(actualType))
throw new JsonException($"Incompatible $ type: {type} -> {typeToConvert}");
return JsonSerializer.Deserialize(ref reader, actualType, options);
}
throw new JsonException($"Unexpected token type: {reader.TokenType}");
}
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
{
if (value is Dictionary<long, TL.User> users)
{
writer.WriteStartArray();
foreach (var element in users.Values)
JsonSerializer.Serialize(writer, element, options);
writer.WriteEndArray();
}
else if (value is Dictionary<long, TL.ChatBase> chats)
{
writer.WriteStartArray();
foreach (var element in chats.Values)
Write(writer, element, options);
writer.WriteEndArray();
}
else if (value is null)
writer.WriteNullValue();
else
{
var actualType = value.GetType();
var jsonObject = JsonSerializer.SerializeToElement(value, actualType, options);
writer.WriteStartObject();
writer.WriteString("$", actualType.Name);
foreach (var property in jsonObject.EnumerateObject())
if (char.IsLower(property.Name[0]))
property.WriteTo(writer);
writer.WriteEndObject();
}
}
}
#endif
private static readonly ConsoleColor[] LogLevelToColor = [ ConsoleColor.DarkGray, ConsoleColor.DarkCyan,
ConsoleColor.Cyan, ConsoleColor.Yellow, ConsoleColor.Red, ConsoleColor.Magenta, ConsoleColor.DarkBlue ];
private static void DefaultLogger(int level, string message) private static void DefaultLogger(int level, string message)
{ {
Console.ForegroundColor = LogLevelToColor[level]; Console.ForegroundColor = LogLevelToColor[level];
@ -126,10 +34,9 @@ namespace WTelegram
public static long RandomLong() public static long RandomLong()
{ {
#if NETCOREAPP2_1_OR_GREATER #if NETCOREAPP2_1_OR_GREATER
long value = 0; Span<long> span = stackalloc long[1];
System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes( System.Security.Cryptography.RandomNumberGenerator.Fill(System.Runtime.InteropServices.MemoryMarshal.AsBytes(span));
System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref value, 1))); return span[0];
return value;
#else #else
var span = new byte[8]; var span = new byte[8];
Encryption.RNG.GetBytes(span); Encryption.RNG.GetBytes(span);
@ -178,7 +85,7 @@ namespace WTelegram
return result; return result;
} }
internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L103 internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L90
{ {
if (pq < 2) return 1; if (pq < 2) return 1;
var random = new Random(); var random = new Random();
@ -192,16 +99,26 @@ namespace WTelegram
for (int j = 1; j < lim; j++) for (int j = 1; j < lim; j++)
{ {
iter++; iter++;
// x = (q + x * x) % pq ulong a = x;
ulong res = q, a = x; ulong b = x;
while (x != 0) ulong c = q;
// c += a * b
while (b != 0)
{ {
if ((x & 1) != 0) if ((b & 1) != 0)
res = (res + a) % pq; {
a = (a + a) % pq; c += a;
x >>= 1; if (c >= pq)
c -= pq;
}
a += a;
if (a >= pq)
a -= pq;
b >>= 1;
} }
x = res;
x = c;
ulong z = x < y ? pq + x - y : x - y; ulong z = x < y ? pq + x - y : x - y;
g = gcd(z, pq); g = gcd(z, pq);
if (g != 1) if (g != 1)
@ -221,25 +138,44 @@ namespace WTelegram
} }
return g; return g;
static ulong gcd(ulong left, ulong right) static ulong gcd(ulong a, ulong b)
{ {
while (right != 0) if (a == 0) return b;
if (b == 0) return a;
int shift = 0;
while ((a & 1) == 0 && (b & 1) == 0)
{ {
ulong num = left % right; a >>= 1;
left = right; b >>= 1;
right = num; 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;
} }
return left;
} }
} }
public static int MillerRabinIterations { get; set; } = 64; // 64 is OpenSSL default for 2048-bits numbers public static int MillerRabinIterations { get; set; } = 64; // 64 is OpenSSL default for 2048-bits numbers
private static readonly HashSet<BigInteger> GoodPrimes = new();
/// <summary>MillerRabin primality test</summary> /// <summary>MillerRabin primality test</summary>
/// <param name="n">The number to check for primality</param> /// <param name="n">The number to check for primality</param>
public static bool IsProbablePrime(this BigInteger n) public static bool IsProbablePrime(this BigInteger n)
{ {
var n_minus_one = n - BigInteger.One; var n_minus_one = n - BigInteger.One;
if (n_minus_one.Sign <= 0) return false; if (n_minus_one.Sign <= 0) return false;
if (GoodPrimes.Contains(n)) return true;
int s; int s;
var d = n_minus_one; var d = n_minus_one;
@ -274,11 +210,12 @@ namespace WTelegram
} }
if (r == 0) return false; if (r == 0) return false;
} }
GoodPrimes.Add(n);
return true; return true;
} }
internal static readonly byte[] StrippedThumbJPG = // see https://core.telegram.org/api/files#stripped-thumbnails internal static readonly byte[] StrippedThumbJPG = // see https://core.telegram.org/api/files#stripped-thumbnails
[ {
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 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, 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, 0x1e, 0x23, 0x1e, 0x19, 0x28, 0x23, 0x21, 0x23, 0x2d, 0x2b, 0x28, 0x30, 0x3c, 0x64, 0x41, 0x3c, 0x37, 0x37,
@ -315,7 +252,7 @@ namespace WTelegram
0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 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, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03, 0x01, 0x00, 0x02, 0x11, 0x03, 0x11, 0x00,
0x3f, 0x00 0x3f, 0x00
]; };
internal static string GetSystemVersion() internal static string GetSystemVersion()
{ {
@ -327,27 +264,22 @@ namespace WTelegram
internal static string GetAppVersion() internal static string GetAppVersion()
=> (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0"; => (Assembly.GetEntryAssembly() ?? Array.Find(AppDomain.CurrentDomain.GetAssemblies(), a => a.EntryPoint != null))?.GetName().Version.ToString() ?? "0.0";
public class IndirectStream(Stream innerStream) : Stream public class IndirectStream : Stream
{ {
public IndirectStream(Stream innerStream) => _innerStream = innerStream;
public long? ContentLength; public long? ContentLength;
protected readonly Stream _innerStream = innerStream; protected readonly Stream _innerStream;
public override bool CanRead => _innerStream.CanRead; public override bool CanRead => _innerStream.CanRead;
public override bool CanSeek => ContentLength.HasValue || _innerStream.CanSeek; public override bool CanSeek => _innerStream.CanSeek;
public override bool CanWrite => _innerStream.CanWrite; public override bool CanWrite => _innerStream.CanWrite;
public override long Length => ContentLength ?? _innerStream.Length; public override long Length => ContentLength ?? _innerStream.Length;
public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; } public override long Position { get => _innerStream.Position; set => _innerStream.Position = value; }
public override void Flush() => _innerStream.Flush(); public override void Flush() => _innerStream.Flush();
public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin);
public override void SetLength(long value) => _innerStream.SetLength(value); 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 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(); 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) { }
}
} }

View file

@ -1,626 +0,0 @@
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;
}
}
}

View file

@ -1,570 +0,0 @@
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.deleted | User.Flags.bot | User.Flags.bot_chat_history |
User.Flags.bot_nochats | User.Flags.verified | User.Flags.restricted | User.Flags.has_bot_inline_placeholder |
User.Flags.bot_inline_geo | User.Flags.support | User.Flags.scam | User.Flags.fake | User.Flags.bot_attach_menu |
User.Flags.premium | User.Flags.has_emoji_status;
const User.Flags2 updated_flags2 = User.Flags2.has_usernames | User.Flags2.stories_unavailable |
User.Flags2.has_color | User.Flags2.has_profile_color | User.Flags2.contact_require_premium |
User.Flags2.bot_business | User.Flags2.bot_has_main_app | User.Flags2.bot_forum_view;
// tdlib updated flags: deleted | bot | bot_chat_history | bot_nochats | verified | bot_inline_geo
// | support | scam | fake | bot_attach_menu | premium
// tdesktop non-updated flags: bot | bot_chat_history | bot_nochats | bot_attach_menu
// updated flags2: stories_unavailable | main_app | bot_business | bot_forum_view (tdlib) | contact_require_premium (tdesktop)
prevUser.flags = (prevUser.flags & ~updated_flags) | (user.flags & updated_flags);
prevUser.flags2 = (prevUser.flags2 & ~updated_flags2) | (user.flags2 & updated_flags2);
prevUser.first_name ??= user.first_name; // tdlib: not updated ; tdesktop: updated only if unknown
prevUser.last_name ??= user.last_name; // tdlib: not updated ; tdesktop: updated only if unknown
//prevUser.username ??= user.username; // tdlib/tdesktop: not updated
prevUser.phone ??= user.phone; // tdlib: updated only if unknown ; tdesktop: not updated
if (prevUser.flags.HasFlag(User.Flags.apply_min_photo) && user.photo != null)
{
prevUser.photo = user.photo; // tdlib/tdesktop: updated on apply_min_photo
prevUser.flags |= User.Flags.has_photo;
}
prevUser.bot_info_version = user.bot_info_version; // tdlib: updated ; tdesktop: not updated
prevUser.restriction_reason = user.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevUser.bot_inline_placeholder = user.bot_inline_placeholder;// tdlib: updated ; tdesktop: ignored
if (user.lang_code != null)
prevUser.lang_code = user.lang_code; // tdlib: updated if present ; tdesktop: ignored
prevUser.emoji_status = user.emoji_status; // tdlib/tdesktop: updated
//prevUser.usernames = user.usernames; // tdlib/tdesktop: not updated
if (user.stories_max_id != null)
prevUser.stories_max_id = user.stories_max_id; // tdlib: updated if > 0 ; tdesktop: not updated
prevUser.color = user.color; // tdlib/tdesktop: updated
prevUser.profile_color = user.profile_color; // tdlib/tdesktop: unimplemented yet
_users[user.id] = prevUser;
}
}
public void Collect(IEnumerable<ChatBase> chats)
{
lock (_chats)
foreach (var chat in chats)
if (chat is not Channel channel)
_chats[chat.ID] = chat;
else if (!_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel)
_chats[channel.id] = channel;
else if (!channel.flags.HasFlag(Channel.Flags.min) || prevChannel.flags.HasFlag(Channel.Flags.min))
{
if (channel.participants_count == 0) channel.participants_count = prevChannel.participants_count; // non-min channel can lack this info
_chats[channel.id] = channel;
}
else
{ // update previously full channel from min channel:
const Channel.Flags updated_flags = (Channel.Flags)0x7FDC0BE0;
const Channel.Flags2 updated_flags2 = (Channel.Flags2)0x781;
// tdesktop updated flags: broadcast | verified | megagroup | signatures | scam | has_link | slowmode_enabled
// | call_active | call_not_empty | fake | gigagroup | noforwards | join_to_send | join_request | forum
// tdlib nonupdated flags: broadcast | signatures | call_active | call_not_empty | noforwards
prevChannel.flags = (prevChannel.flags & ~updated_flags) | (channel.flags & updated_flags);
prevChannel.flags2 = (prevChannel.flags2 & ~updated_flags2) | (channel.flags2 & updated_flags2);
prevChannel.title = channel.title; // tdlib/tdesktop: updated
prevChannel.username = channel.username; // tdlib/tdesktop: updated
prevChannel.photo = channel.photo; // tdlib: updated if not banned ; tdesktop: updated
prevChannel.restriction_reason = channel.restriction_reason; // tdlib: updated ; tdesktop: not updated
prevChannel.default_banned_rights = channel.default_banned_rights; // tdlib/tdesktop: updated
if (channel.participants_count > 0)
prevChannel.participants_count = channel.participants_count; // tdlib/tdesktop: updated if present
prevChannel.usernames = channel.usernames; // tdlib/tdesktop: updated
prevChannel.color = channel.color; // tdlib: not updated ; tdesktop: updated
prevChannel.profile_color = channel.profile_color; // tdlib/tdesktop: ignored
prevChannel.emoji_status = channel.emoji_status; // tdlib: not updated ; tdesktop: updated
prevChannel.level = channel.level; // tdlib: not updated ; tdesktop: updated
_chats[channel.id] = prevChannel;
}
}
public bool HasUser(long id) { lock (_users) return _users.ContainsKey(id); }
public bool HasChat(long id) { lock (_chats) return _chats.ContainsKey(id); }
}
/// <summary>Accumulate users/chats found in this structure in your dictionaries, ignoring <see href="https://core.telegram.org/api/min">Min constructors</see> when the full object is already stored</summary>
/// <param name="structure">The structure having a <c>users</c></param>
public static void CollectUsersChats(this IPeerResolver structure, IDictionary<long, User> users, IDictionary<long, ChatBase> chats)
=> structure.UserOrChat(new CollectorPeer(users, 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

@ -7,76 +7,32 @@ using System.Net;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.Json; 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 namespace WTelegram
{ {
internal sealed partial class Session : IDisposable internal class Session : IDisposable
{ {
public int ApiId; public int ApiId;
public long UserId; public long UserId;
public int MainDC; public int MainDC;
public Dictionary<int, DCSession> DCSessions = []; public Dictionary<int, DCSession> DCSessions = new();
public TL.DcOption[] DcOptions; public TL.DcOption[] DcOptions;
public sealed class DCSession public class DCSession
{ {
public long Id;
public long AuthKeyID;
public byte[] AuthKey; // 2048-bit = 256 bytes public byte[] AuthKey; // 2048-bit = 256 bytes
public long UserId; public long UserId;
public long OldSalt; // still accepted for a further 1800 seconds
public long Salt; public long Salt;
public SortedList<DateTime, long> Salts; public int Seqno;
public long ServerTicksOffset;
public long LastSentMsgId;
public TL.DcOption DataCenter; 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 Client Client;
internal int DcID => DataCenter == null ? 0 : DataCenter.flags.HasFlag(TL.DcOption.Flags.media_only) ? -DataCenter.id : DataCenter.id; internal int DcID => DataCenter?.id ?? 0;
internal IPEndPoint EndPoint => DataCenter == null ? null : new(IPAddress.Parse(DataCenter.ip_address), DataCenter.port); 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; } 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; public DateTime SessionStart => _sessionStart;
@ -109,31 +65,28 @@ namespace WTelegram
{ {
var input = new byte[length]; var input = new byte[length];
if (store.Read(input, 0, length) != length) if (store.Read(input, 0, length) != length)
throw new WTException($"Can't read session block ({store.Position}, {length})"); throw new ApplicationException($"Can't read session block ({store.Position}, {length})");
using var sha256 = SHA256.Create(); using var sha256 = SHA256.Create();
using var decryptor = aes.CreateDecryptor(rgbKey, input[0..16]); using var decryptor = aes.CreateDecryptor(rgbKey, input[0..16]);
var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16); var utf8Json = decryptor.TransformFinalBlock(input, 16, input.Length - 16);
if (!sha256.ComputeHash(utf8Json, 32, utf8Json.Length - 32).SequenceEqual(utf8Json[0..32])) if (!sha256.ComputeHash(utf8Json, 32, utf8Json.Length - 32).SequenceEqual(utf8Json[0..32]))
throw new WTException("Integrity check failed in session loading"); throw new ApplicationException("Integrity check failed in session loading");
session = JsonSerializer.Deserialize<Session>(utf8Json.AsSpan(32), Helpers.JsonOptions); session = JsonSerializer.Deserialize<Session>(utf8Json.AsSpan(32), Helpers.JsonOptions);
Helpers.Log(2, "Loaded previous session"); 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) catch (Exception ex)
{ {
store.Dispose(); 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); throw new ApplicationException($"Exception while reading session file: {ex.Message}\nDelete the file to start a new session", ex);
} }
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;
} }
internal void Save() // must be called with lock(session) internal void Save() // must be called with lock(session)
@ -152,23 +105,16 @@ namespace WTelegram
if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable if (!_encryptor.CanReuseTransform) // under Mono, AES encryptor is not reusable
using (var aes = Aes.Create()) using (var aes = Aes.Create())
_encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]); _encryptor = aes.CreateEncryptor(_reuseKey, _encrypted[0..16]);
try _store.Position = 0;
{ _store.Write(_encrypted, 0, encryptedLen);
_store.Position = 0; _store.SetLength(encryptedLen);
_store.Write(_encrypted, 0, encryptedLen);
_store.SetLength(encryptedLen);
}
catch (Exception ex)
{
Helpers.Log(4, $"{_store} raised {ex}");
}
} }
_jsonStream.Position = 0; _jsonStream.Position = 0;
_jsonWriter.Reset(); _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) internal class SessionStore : FileStream
{ {
public override long Length { get; } public override long Length { get; }
public override long Position { get => base.Position; set { } } public override long Position { get => base.Position; set { } }
@ -183,6 +129,7 @@ namespace WTelegram
{ {
var position = BinaryPrimitives.ReadInt32LittleEndian(_header); var position = BinaryPrimitives.ReadInt32LittleEndian(_header);
var length = BinaryPrimitives.ReadInt32LittleEndian(_header.AsSpan(4)); var length = BinaryPrimitives.ReadInt32LittleEndian(_header.AsSpan(4));
if (position < 0 || length < 0 || position >= 65536 || length >= 32768) { position = 0; length = (int)base.Length; }
base.Position = position; base.Position = position;
Length = length; Length = length;
_nextPosition = position + length; _nextPosition = position + length;
@ -201,10 +148,4 @@ namespace WTelegram
base.Write(_header, 0, 8); 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) { }
}
} }

393
src/TL.Extensions.cs Normal file
View file

@ -0,0 +1,393 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
namespace TL
{
public static class Extensions
{
private class CollectorPeer : Peer
{
public override long ID => 0;
internal Dictionary<long, User> _users;
internal Dictionary<long, ChatBase> _chats;
internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
{
lock (_users)
foreach (var user in users.Values)
if (user != null)
if (!user.flags.HasFlag(User.Flags.min) || !_users.TryGetValue(user.id, out var prevUser) || prevUser.flags.HasFlag(User.Flags.min))
_users[user.id] = user;
lock (_chats)
foreach (var kvp in chats)
if (kvp.Value is not Channel channel)
_chats[kvp.Key] = kvp.Value;
else if (!channel.flags.HasFlag(Channel.Flags.min) || !_chats.TryGetValue(channel.id, out var prevChat) || prevChat is not Channel prevChannel || prevChannel.flags.HasFlag(Channel.Flags.min))
_chats[kvp.Key] = channel;
return null;
}
}
/// <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, Dictionary<long, User> users, Dictionary<long, ChatBase> chats)
=> structure.UserOrChat(new CollectorPeer { _users = users, _chats = chats });
}
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="client">Client, used for getting access_hash for <c>tg://user?id=</c> URLs</param>
/// <param name="text">[in] The Markdown text<br/>[out] The same (plain) text, stripped of all Markdown notation</param>
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="WTelegram.Client.SendMessageAsync">SendMessageAsync</see> or <see cref="WTelegram.Client.SendMediaAsync">SendMediaAsync</see></returns>
public static MessageEntity[] MarkdownToEntities(this WTelegram.Client client, ref string text)
{
var entities = new List<MessageEntity>();
var sb = new StringBuilder(text);
for (int offset = 0; offset < sb.Length;)
{
switch (sb[offset])
{
case '\r': sb.Remove(offset, 1); break;
case '\\': sb.Remove(offset++, 1); break;
case '*': ProcessEntity<MessageEntityBold>(); break;
case '~': ProcessEntity<MessageEntityStrike>(); break;
case '_':
if (offset + 1 < sb.Length && sb[offset + 1] == '_')
{
sb.Remove(offset, 1);
ProcessEntity<MessageEntityUnderline>();
}
else
ProcessEntity<MessageEntityItalic>();
break;
case '|':
if (offset + 1 < sb.Length && sb[offset + 1] == '|')
{
sb.Remove(offset, 1);
ProcessEntity<MessageEntitySpoiler>();
}
else
offset++;
break;
case '`':
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>();
break;
case '[':
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1 });
sb.Remove(offset, 1);
break;
case ']':
if (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 user_id) && client.GetAccessHashFor<User>(user_id) is long hash)
entities[lastIndex] = new InputMessageEntityMentionName { offset = textUrl.offset, length = textUrl.length, user_id = new InputUser(user_id, hash) };
sb.Remove(offset, offset2 - offset);
break;
}
}
offset++;
break;
default: offset++; break;
}
void ProcessEntity<T>() where T : MessageEntity, new()
{
if (entities.LastOrDefault(e => e.length == -1) is T prevEntity)
prevEntity.length = offset - prevEntity.offset;
else
entities.Add(new T { offset = offset, length = -1 });
sb.Remove(offset, 1);
}
}
text = sb.ToString();
return entities.Count == 0 ? null : entities.ToArray();
}
/// <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="TL.Message.message"/></param>
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="TL.Message.entities"/></param>
/// <returns>The message text with MarkdownV2 formattings</returns>
public static string EntitiesToMarkdown(this WTelegram.Client client, string message, MessageEntity[] entities)
{
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];
for (int offset = 0, i = 0; ; offset++, i++)
{
while (closings.Count != 0 && offset == closings[0].offset)
{
var md = closings[0].md;
if (i > 0 && md[0] == '_' && sb[i - 1] == '_') md = '\r' + md;
sb.Insert(i, md); i += md.Length;
closings.RemoveAt(0);
}
if (i == sb.Length) break;
while (offset == nextEntity?.offset)
{
if (entityToMD.TryGetValue(nextEntity.GetType(), out var md))
{
var closing = (nextEntity.offset + nextEntity.length, md);
if (md[0] == '[')
{
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 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;
}
nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null;
}
switch (sb[i])
{
case '_': case '*': case '~': case '`': case '#': case '+': case '-': case '=': case '.': case '!':
case '[': case ']': case '(': case ')': case '{': case '}': case '>': case '|': case '\\':
sb.Insert(i, '\\'); 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)] = "||",
};
/// <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)
{
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="client">Client, used for getting access_hash for <c>tg://user?id=</c> URLs</param>
/// <param name="text">[in] The HTML-formatted text<br/>[out] The same (plain) text, stripped of all HTML tags</param>
/// <returns>The array of formatting entities that you can pass (along with the plain text) to <see cref="WTelegram.Client.SendMessageAsync">SendMessageAsync</see> or <see cref="WTelegram.Client.SendMediaAsync">SendMediaAsync</see></returns>
public static MessageEntity[] HtmlToEntities(this WTelegram.Client client, ref string text)
{
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 == '&')
{
for (end = offset + 1; end < sb.Length; end++)
if (sb[end] == ';') break;
if (end >= sb.Length) break;
var html = HttpUtility.HtmlDecode(sb.ToString(offset, end - offset + 1));
if (html.Length == 1)
{
sb[offset] = html[0];
sb.Remove(++offset, end - offset + 1);
}
else
offset = end + 1;
}
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" when closing:
case "tg-spoiler": ProcessEntity<MessageEntitySpoiler>(); break;
case "code": ProcessEntity<MessageEntityCode>(); break;
case "pre": ProcessEntity<MessageEntityPre>(); 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.StartsWith("a href=\"") && tag.EndsWith("\""))
{
tag = tag[8..^1];
if (tag.StartsWith("tg://user?id=") && long.TryParse(tag[13..], out var user_id) && client.GetAccessHashFor<User>(user_id) is long hash)
entities.Add(new InputMessageEntityMentionName { offset = offset, length = -1, user_id = new InputUser(user_id, hash) });
else
entities.Add(new MessageEntityTextUrl { offset = offset, length = -1, url = tag });
}
else if (tag.StartsWith("code class=\"language-") && tag.EndsWith("\""))
{
if (entities.LastOrDefault(e => e.length == -1) is MessageEntityPre prevEntity)
prevEntity.language = tag[21..^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++;
}
text = sb.ToString();
return entities.Count == 0 ? null : entities.ToArray();
}
/// <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="TL.Message.message"/></param>
/// <param name="entities">The array of formatting entities, typically obtained from <see cref="TL.Message.entities"/></param>
/// <returns>The message text with HTML formatting tags</returns>
public static string EntitiesToHtml(this WTelegram.Client client, string message, MessageEntity[] entities)
{
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;
while (offset == nextEntity?.offset)
{
if (entityToTag.TryGetValue(nextEntity.GetType(), out var tag))
{
var closing = (nextEntity.offset + nextEntity.length, $"</{tag}>");
if (tag[0] == 'a')
{
if (nextEntity is MessageEntityTextUrl metu)
tag = $"<a href=\"{metu.url}\">";
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 MessageEntityPre mep && !string.IsNullOrEmpty(mep.language))
{
closing.Item2 = "</code></pre>";
tag = $"<pre><code class=\"language-{mep.language}\">";
}
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;
}
nextEntity = ++entityIndex < entities.Length ? entities[entityIndex] : null;
}
switch (sb[i])
{
case '&': sb.Insert(i + 1, "amp;"); i += 4; break;
case '<': sb.Remove(i, 1).Insert(i, "&lt;"); i += 3; break;
case '>': sb.Remove(i, 1).Insert(i, "&gt;"); 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",
};
/// <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,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Web; using System.Web;
@ -12,52 +11,32 @@ namespace TL
{ {
long ID { get; } long ID { get; }
bool IsActive { get; } bool IsActive { get; }
string MainUsername { get; }
InputPeer ToInputPeer(); InputPeer ToInputPeer();
} }
partial class InputPeer partial class InputPeer { public static InputPeerSelf Self => new(); }
{
public static readonly InputPeerSelf Self = new();
public abstract long ID { get; }
}
partial class InputPeerSelf
{
public override long ID => 0;
}
partial class InputPeerChat 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> /// <summary>⚠ Only for small private Chat. Chat groups of type Channel must use InputPeerChannel. See <see href="https://github.com/wiz0u/WTelegramClient/blob/master/README.md#terminology">Terminology</see> in README</summary>
/// <param name="chat_id">Chat identifier</param> /// <param name="chat_id">Chat identifier</param>
public InputPeerChat(long chat_id) => this.chat_id = chat_id; public InputPeerChat(long chat_id) => this.chat_id = chat_id;
internal InputPeerChat() { } internal InputPeerChat() { }
public override long ID => chat_id;
} }
partial class InputPeerUser partial class InputPeerUser
{ {
/// <param name="user_id">User identifier</param> /// <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> /// <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"/> constructor</param>
public InputPeerUser(long user_id, long access_hash) { this.user_id = user_id; this.access_hash = access_hash; } public InputPeerUser(long user_id, long access_hash) { this.user_id = user_id; this.access_hash = access_hash; }
internal InputPeerUser() { } internal InputPeerUser() { }
public static implicit operator InputUser(InputPeerUser user) => new(user.user_id, user.access_hash); public static implicit operator InputUser(InputPeerUser user) => new(user.user_id, user.access_hash);
public override long ID => user_id;
} }
partial class InputPeerChannel partial class InputPeerChannel
{ {
/// <param name="channel_id">Channel identifier</param> /// <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> /// <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"/> constructor</param>
public InputPeerChannel(long channel_id, long access_hash) { this.channel_id = channel_id; this.access_hash = access_hash; } public InputPeerChannel(long channel_id, long access_hash) { this.channel_id = channel_id; this.access_hash = access_hash; }
internal InputPeerChannel() { } internal InputPeerChannel() { }
public static implicit operator InputChannel(InputPeerChannel channel) => new(channel.channel_id, channel.access_hash); 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 InputUserBase { public abstract long? UserId { get; } }
@ -68,7 +47,7 @@ namespace TL
public override long? UserId => user_id; public override long? UserId => user_id;
public static InputUserSelf Self => new(); public static InputUserSelf Self => new();
/// <param name="user_id">User identifier</param> /// <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> /// <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"/> constructor</param>
public InputUser(long user_id, long access_hash) { this.user_id = user_id; this.access_hash = access_hash; } public InputUser(long user_id, long access_hash) { this.user_id = user_id; this.access_hash = access_hash; }
internal InputUser() { } internal InputUser() { }
public static implicit operator InputPeerUser(InputUser user) => new(user.user_id, user.access_hash); public static implicit operator InputPeerUser(InputUser user) => new(user.user_id, user.access_hash);
@ -78,64 +57,16 @@ namespace TL
{ {
public abstract InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint); public abstract InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint);
public abstract InputSecureFileBase ToInputSecureFile(byte[] file_hash, byte[] secret); 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 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 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 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 partial class InputFileBig
{ {
public override InputEncryptedFileBase ToInputEncryptedFile(int key_fingerprint) => new InputEncryptedFileBigUploaded { id = id, parts = parts, key_fingerprint = key_fingerprint }; 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 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 partial class InputPhoto
@ -143,36 +74,34 @@ namespace TL
public static implicit operator InputMediaPhoto(InputPhoto photo) => new() { id = photo }; 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 partial class Peer
{ {
public abstract long ID { get; } public abstract long ID { get; }
protected internal abstract IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats); abstract internal IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats);
} }
partial class PeerUser partial class PeerUser
{ {
public override string ToString() => "user " + user_id; public override string ToString() => "user " + user_id;
public override long ID => 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; internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats) => users.TryGetValue(user_id, out var user) ? user : null;
} }
partial class PeerChat partial class PeerChat
{ {
public override string ToString() => "chat " + chat_id; public override string ToString() => "chat " + chat_id;
public override long ID => 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; internal override IPeerInfo UserOrChat(Dictionary<long, User> users, Dictionary<long, ChatBase> chats) => chats.TryGetValue(chat_id, out var chat) ? chat : null;
} }
partial class PeerChannel partial class PeerChannel
{ {
public override string ToString() => "channel " + channel_id; public override string ToString() => "channel " + channel_id;
public override long ID => 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; 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 partial class UserBase : IPeerInfo
{ {
public abstract long ID { get; } public abstract long ID { get; }
public abstract bool IsActive { get; } public abstract bool IsActive { get; }
public abstract string MainUsername { get; }
public abstract InputPeer ToInputPeer(); public abstract InputPeer ToInputPeer();
protected abstract InputUser ToInputUser(); protected abstract InputUser ToInputUser();
public static implicit operator InputPeer(UserBase user) => user?.ToInputPeer(); public static implicit operator InputPeer(UserBase user) => user?.ToInputPeer();
@ -182,7 +111,6 @@ namespace TL
{ {
public override long ID => id; public override long ID => id;
public override bool IsActive => false; public override bool IsActive => false;
public override string MainUsername => null;
public override string ToString() => null; public override string ToString() => null;
public override InputPeer ToInputPeer() => null; public override InputPeer ToInputPeer() => null;
protected override InputUser ToInputUser() => null; protected override InputUser ToInputUser() => null;
@ -191,51 +119,35 @@ namespace TL
{ {
public override long ID => id; public override long ID => id;
public override bool IsActive => (flags & Flags.deleted) == 0; 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() => username != null ? '@' + username : last_name == null ? first_name : $"{first_name} {last_name}";
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); public override InputPeer ToInputPeer() => new InputPeerUser(id, access_hash);
protected override InputUser ToInputUser() => new(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> /// <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 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; } } /// <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 (this is also always shown to blocked users)</remarks>
partial class UserStatusOnline { internal override TimeSpan LastSeenAgo => TimeSpan.Zero; } partial class UserStatus { /// <summary>An estimation of the number of days ago the user was last seen (online=0, recently=1, lastWeek=5, lastMonth=20)<br/><see cref="UserStatus"/> = <c>null</c> means a long time ago, more than a month (this is also always shown to blocked users)</summary>
partial class UserStatusOffline { internal override TimeSpan LastSeenAgo => DateTime.UtcNow - was_online; } public abstract TimeSpan LastSeenAgo { get; } }
partial class UserStatusOnline { public override TimeSpan LastSeenAgo => TimeSpan.Zero; }
partial class UserStatusOffline { public override TimeSpan LastSeenAgo => DateTime.UtcNow - new DateTime((was_online + 62135596800L) * 10000000, DateTimeKind.Utc); }
/// <remarks>covers anything between 1 second and 2-3 days</remarks> /// <remarks>covers anything between 1 second and 2-3 days</remarks>
partial class UserStatusRecently { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(1); } partial class UserStatusRecently { public override TimeSpan LastSeenAgo => TimeSpan.FromDays(1); }
/// <remarks>between 2-3 and seven days</remarks> /// <remarks>between 2-3 and seven days</remarks>
partial class UserStatusLastWeek { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(5); } partial class UserStatusLastWeek { public override TimeSpan LastSeenAgo => TimeSpan.FromDays(5); }
/// <remarks>between 6-7 days and a month</remarks> /// <remarks>between 6-7 days and a month</remarks>
partial class UserStatusLastMonth { internal override TimeSpan LastSeenAgo => TimeSpan.FromDays(20); } partial class UserStatusLastMonth { public override TimeSpan LastSeenAgo => TimeSpan.FromDays(20); }
partial class ChatBase : IPeerInfo partial class ChatBase : IPeerInfo
{ {
/// <summary>Is this chat among current user active chats?</summary> /// <summary>Is this chat among current user active chats?</summary>
public abstract bool IsActive { get; } 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; } public abstract ChatPhoto Photo { get; }
/// <summary>returns true if you're banned of any of these rights</summary> /// <summary>returns true if you're banned of any of these rights</summary>
public abstract bool IsBanned(ChatBannedRights.Flags flags = 0); public abstract bool IsBanned(ChatBannedRights.Flags flags = 0);
public abstract InputPeer ToInputPeer(); public abstract InputPeer ToInputPeer();
public static implicit operator InputPeer(ChatBase chat) => chat?.ToInputPeer(); public static implicit operator InputPeer(ChatBase chat) => chat.ToInputPeer();
} }
partial class ChatEmpty partial class ChatEmpty
{ {
@ -264,30 +176,18 @@ namespace TL
partial class Channel partial class Channel
{ {
public override bool IsActive => (flags & Flags.left) == 0; 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 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 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 override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash);
public static implicit operator InputChannel(Channel channel) => new(channel.id, channel.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 override string ToString() =>
public IEnumerable<string> ActiveUsernames (flags.HasFlag(Flags.broadcast) ? "Channel " : "Group ") + (username != null ? '@' + username : $"\"{title}\"");
{ public bool IsChannel => (flags & Flags.broadcast) != 0;
get public bool IsGroup => (flags & Flags.broadcast) == 0;
{
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 partial class ChannelForbidden
{ {
public override bool IsActive => false; public override bool IsActive => false;
public override bool IsChannel => (flags & Flags.broadcast) != 0;
public override ChatPhoto Photo => null; public override ChatPhoto Photo => null;
public override bool IsBanned(ChatBannedRights.Flags flags = 0) => true; public override bool IsBanned(ChatBannedRights.Flags flags = 0) => true;
public override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash); public override InputPeer ToInputPeer() => new InputPeerChannel(id, access_hash);
@ -304,31 +204,9 @@ namespace TL
partial class ChatParticipantAdmin { public override bool IsAdmin => true; } partial class ChatParticipantAdmin { public override bool IsAdmin => true; }
partial class ChatParticipantsBase { public abstract ChatParticipantBase[] Participants { get; }} partial class ChatParticipantsBase { public abstract ChatParticipantBase[] Participants { get; }}
partial class ChatParticipantsForbidden { public override ChatParticipantBase[] Participants => []; } partial class ChatParticipantsForbidden { public override ChatParticipantBase[] Participants => Array.Empty<ChatParticipantBase>(); }
partial class ChatParticipants { public override ChatParticipantBase[] Participants => 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 partial class PhotoBase
{ {
public abstract long ID { get; } public abstract long ID { get; }
@ -347,9 +225,7 @@ namespace TL
protected override InputPhoto ToInputPhoto() => new() { id = id, access_hash = access_hash, file_reference = file_reference }; protected override InputPhoto ToInputPhoto() => new() { id = id, access_hash = access_hash, file_reference = file_reference };
public InputPhotoFileLocation ToFileLocation() => ToFileLocation(LargestPhotoSize); 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(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 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 partial class PhotoSizeBase
@ -410,24 +286,18 @@ namespace TL
} }
} }
partial class GeoPoint public partial class InputMediaUploadedDocument
{ {
public static implicit operator InputGeoPoint(GeoPoint geo) => new() { lat = geo.lat, lon = geo.lon, accuracy_radius = geo.accuracy_radius, flags = (InputGeoPoint.Flags)geo.flags }; public InputMediaUploadedDocument() { }
public InputMediaUploadedDocument(InputFileBase inputFile, string mimeType)
{
file = inputFile;
mime_type = mimeType;
if (inputFile.Name is string filename) attributes = new[] { new DocumentAttributeFilename { file_name = filename } };
}
} }
partial class InputNotifyPeerBase partial class Contacts_Blocked { public IPeerInfo UserOrChat(PeerBlocked peer) => peer.peer_id.UserOrChat(users, chats); }
{
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); partial class Messages_DialogsBase { public IPeerInfo UserOrChat(DialogBase dialog) => UserOrChat(dialog.Peer);
public abstract int TotalCount { get; } } public abstract int TotalCount { get; } }
partial class Messages_Dialogs { public override int TotalCount => dialogs.Length; } partial class Messages_Dialogs { public override int TotalCount => dialogs.Length; }
@ -451,39 +321,36 @@ namespace TL
public abstract Update[] UpdateList { get; } public abstract Update[] UpdateList { get; }
public virtual Dictionary<long, User> Users => NoUsers; public virtual Dictionary<long, User> Users => NoUsers;
public virtual Dictionary<long, ChatBase> Chats => NoChats; public virtual Dictionary<long, ChatBase> Chats => NoChats;
private static readonly Dictionary<long, User> NoUsers = []; private static readonly Dictionary<long, User> NoUsers = new();
private static readonly Dictionary<long, ChatBase> NoChats = []; private static readonly Dictionary<long, ChatBase> NoChats = new();
public virtual (long mbox_id, int pts, int pts_count) GetMBox() => default;
} }
partial class UpdatesCombined partial class UpdatesCombined
{ {
public override Update[] UpdateList => updates; public override Update[] UpdateList => updates;
public override Dictionary<long, User> Users => users; public override Dictionary<long, User> Users => users;
public override Dictionary<long, ChatBase> Chats => chats; 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 partial class Updates
{ {
public override Update[] UpdateList => updates; public override Update[] UpdateList => updates;
public override Dictionary<long, User> Users => users; public override Dictionary<long, User> Users => users;
public override Dictionary<long, ChatBase> Chats => chats; 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 UpdatesTooLong { public override Update[] UpdateList => Array.Empty<Update>(); }
partial class UpdateShort { public override Update[] UpdateList => [update]; } partial class UpdateShort { public override Update[] UpdateList => new[] { update }; }
partial class UpdateShortSentMessage { public override Update[] UpdateList => []; } partial class UpdateShortSentMessage { public override Update[] UpdateList => Array.Empty<Update>(); }
partial class UpdateShortMessage { public override Update[] UpdateList => [ new UpdateNewMessage partial class UpdateShortMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage
{ {
message = new Message message = new Message
{ {
flags = (Message.Flags)flags | (flags.HasFlag(Flags.out_) ? 0 : Message.Flags.has_from_id), id = id, date = date, flags = (Message.Flags)flags | Message.Flags.has_from_id, id = id, date = date,
message = message, entities = entities, reply_to = reply_to, message = message, entities = entities, reply_to = reply_to,
from_id = flags.HasFlag(Flags.out_) ? null : new PeerUser { user_id = user_id }, from_id = new PeerUser { user_id = user_id },
peer_id = 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 fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period
}, pts = pts, pts_count = pts_count }, pts = pts, pts_count = pts_count
} ]; } } }; }
partial class UpdateShortChatMessage { public override Update[] UpdateList => [ new UpdateNewMessage partial class UpdateShortChatMessage { public override Update[] UpdateList => new[] { new UpdateNewMessage
{ {
message = new Message message = new Message
{ {
@ -493,15 +360,11 @@ namespace TL
peer_id = new PeerChat { chat_id = chat_id }, peer_id = new PeerChat { chat_id = chat_id },
fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period fwd_from = fwd_from, via_bot_id = via_bot_id, ttl_period = ttl_period
}, pts = pts, pts_count = pts_count }, 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 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 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 }; public InputEncryptedFileLocation ToFileLocation() => new() { id = id, access_hash = access_hash };
} }
@ -525,13 +388,11 @@ namespace TL
partial class Document partial class Document
{ {
public override long ID => id; public override long ID => id;
public override string ToString() => $"{Filename ?? $"Document {mime_type}"} {size:N0} bytes"; public override string ToString() => Filename is string filename ? base.ToString() + ": " + filename : base.ToString();
public string Filename => GetAttribute<DocumentAttributeFilename>()?.file_name; public string Filename => attributes.OfType<DocumentAttributeFilename>().FirstOrDefault()?.file_name;
protected override InputDocument ToInputDocument() => new() { id = id, access_hash = access_hash, file_reference = file_reference }; 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(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 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 partial class SendMessageAction
@ -554,33 +415,15 @@ namespace TL
partial class SendMessageEmojiInteraction { public override string ToString() => "clicking on emoji"; } partial class SendMessageEmojiInteraction { public override string ToString() => "clicking on emoji"; }
partial class SendMessageEmojiInteractionSeen { public override string ToString() => "watching emoji reaction"; } 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 partial class StickerSet
{ {
public static implicit operator InputStickerSetID(StickerSet stickerSet) => new() { id = stickerSet.id, access_hash = stickerSet.access_hash }; 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 partial class InputChannel
{ {
/// <param name="channel_id">Channel identifier</param> /// <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> /// <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"/> constructor</param>
public InputChannel(long channel_id, long access_hash) { this.channel_id = channel_id; this.access_hash = access_hash; } public InputChannel(long channel_id, long access_hash) { this.channel_id = channel_id; this.access_hash = access_hash; }
internal InputChannel() { } internal InputChannel() { }
public static implicit operator InputPeerChannel(InputChannel channel) => new(channel.channel_id, channel.access_hash); public static implicit operator InputPeerChannel(InputChannel channel) => new(channel.channel_id, channel.access_hash);
@ -588,13 +431,11 @@ namespace TL
partial class Contacts_ResolvedPeer partial class Contacts_ResolvedPeer
{ {
public static implicit operator InputPeer(Contacts_ResolvedPeer resolved) => resolved?.UserOrChat.ToInputPeer(); 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> /// <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; 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> /// <returns>A <see cref="Channel"/> or <see cref="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; 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 partial class Updates_ChannelDifferenceBase
@ -603,15 +444,13 @@ namespace TL
public abstract Update[] OtherUpdates { get; } public abstract Update[] OtherUpdates { get; }
public abstract bool Final { get; } public abstract bool Final { get; }
public abstract int Timeout { get; } public abstract int Timeout { get; }
public abstract int Pts { get; }
} }
partial class Updates_ChannelDifferenceEmpty partial class Updates_ChannelDifferenceEmpty
{ {
public override MessageBase[] NewMessages => []; public override MessageBase[] NewMessages => Array.Empty<MessageBase>();
public override Update[] OtherUpdates => []; public override Update[] OtherUpdates => Array.Empty<Update>();
public override bool Final => flags.HasFlag(Flags.final); public override bool Final => flags.HasFlag(Flags.final);
public override int Timeout => timeout; public override int Timeout => timeout;
public override int Pts => pts;
} }
partial class Updates_ChannelDifference partial class Updates_ChannelDifference
{ {
@ -619,7 +458,6 @@ namespace TL
public override Update[] OtherUpdates => other_updates; public override Update[] OtherUpdates => other_updates;
public override bool Final => flags.HasFlag(Flags.final); public override bool Final => flags.HasFlag(Flags.final);
public override int Timeout => timeout; public override int Timeout => timeout;
public override int Pts => pts;
} }
partial class Updates_ChannelDifferenceTooLong partial class Updates_ChannelDifferenceTooLong
{ {
@ -627,54 +465,31 @@ namespace TL
public override Update[] OtherUpdates => null; public override Update[] OtherUpdates => null;
public override bool Final => flags.HasFlag(Flags.final); public override bool Final => flags.HasFlag(Flags.final);
public override int Timeout => timeout; public override int Timeout => timeout;
public override int Pts => dialog is Dialog d ? d.pts : 0;
} }
partial class ChannelParticipantBase partial class ChannelParticipantBase
{ {
public virtual bool IsAdmin => false; public virtual bool IsAdmin => false;
public abstract long UserId { get; } public abstract long UserID { get; }
} }
partial class ChannelParticipantCreator partial class ChannelParticipantCreator
{ {
public override bool IsAdmin => true; public override bool IsAdmin => true;
public override long UserId => user_id; public override long UserID => user_id;
} }
partial class ChannelParticipantAdmin partial class ChannelParticipantAdmin
{ {
public override bool IsAdmin => true; public override bool IsAdmin => true;
public override long UserId => user_id; public override long UserID => user_id;
} }
partial class ChannelParticipant { 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 ChannelParticipantSelf { public override long UserID => user_id; }
partial class ChannelParticipantBanned { public override long UserId => peer is PeerUser pu ? pu.user_id : 0; } 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 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 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 WebDocument { public static implicit operator InputWebFileLocation(WebDocument doc) => new() { url = doc.url, access_hash = doc.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 partial class SecureFile
{ {
@ -683,23 +498,11 @@ namespace TL
} }
partial class JsonObjectValue { public override string ToString() => $"{HttpUtility.JavaScriptStringEncode(key, true)}:{value}"; } partial class JsonObjectValue { public override string ToString() => $"{HttpUtility.JavaScriptStringEncode(key, true)}:{value}"; }
partial class JSONValue { public abstract object ToNative(); partial class JSONValue { public abstract object ToNative(); }
private static JsonObjectValue FromJsonProperty(System.Text.Json.JsonProperty p) => new() { key = p.Name, value = FromJsonElement(p.Value) }; partial class JsonNull { public override object ToNative() => null; public override string ToString() => "null"; }
public static JSONValue FromJsonElement(System.Text.Json.JsonElement elem) => elem.ValueKind switch 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); }
System.Text.Json.JsonValueKind.True or partial class JsonString { public override object ToNative() => value; public override string ToString() => HttpUtility.JavaScriptStringEncode(value, true); }
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 partial class JsonArray
{ {
public override string ToString() public override string ToString()
@ -709,7 +512,7 @@ namespace TL
sb.Append(i == 0 ? "" : ",").Append(value[i]); sb.Append(i == 0 ? "" : ",").Append(value[i]);
return sb.Append(']').ToString(); return sb.Append(']').ToString();
} }
public object[] ToNativeArray() => [.. value.Select(v => v.ToNative())]; public object[] ToNativeArray() => value.Select(v => v.ToNative()).ToArray();
public override object ToNative() public override object ToNative()
{ {
if (value.Length == 0) return Array.Empty<object>(); if (value.Length == 0) return Array.Empty<object>();
@ -756,21 +559,4 @@ namespace TL
return dic; 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; }
} }

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -2,64 +2,39 @@
namespace TL namespace TL
{ {
#pragma warning disable IDE1006, CS1574 /// <summary>Object describes the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessage"/></para></summary>
/// <summary>Object describes the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessage"/></para> <para>Derived classes: <see cref="DecryptedMessage"/>, <see cref="DecryptedMessageService"/></para></summary> public abstract class DecryptedMessageBase : IObject
public abstract partial class DecryptedMessageBase : IObject
{ {
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
public virtual uint FFlags => default;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public virtual long RandomId => default; public abstract long RandomId { get; }
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary>
public virtual int Ttl => default;
/// <summary>Message text</summary>
public virtual string Message => default;
/// <summary>Media content</summary>
public virtual DecryptedMessageMedia Media => default;
/// <summary>Message <a href="https://corefork.telegram.org/api/entities">entities</a> for styled text (parameter added in layer 45)</summary>
public virtual MessageEntity[] Entities => default;
/// <summary>Specifies the ID of the inline bot that generated the message (parameter added in layer 45)</summary>
public virtual string ViaBotName => default;
/// <summary>Random message ID of the message this message replies to (parameter added in layer 45)</summary>
public virtual long ReplyToRandom => default;
/// <summary>Random group ID, assigned by the author of message.<br/>Multiple encrypted messages with a photo attached and with the same group ID indicate an <a href="https://corefork.telegram.org/api/files#albums-grouped-media">album or grouped media</a> (parameter added in layer 45)</summary>
public virtual long Grouped => default;
/// <summary>Random bytes, removed in layer 17.</summary>
public virtual byte[] RandomBytes => default;
public virtual DecryptedMessageAction Action => default;
} }
/// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para> <para>Derived classes: <see cref="DecryptedMessageMediaPhoto"/>, <see cref="DecryptedMessageMediaVideo"/>, <see cref="DecryptedMessageMediaGeoPoint"/>, <see cref="DecryptedMessageMediaContact"/>, <see cref="DecryptedMessageMediaDocument"/>, <see cref="DecryptedMessageMediaAudio"/>, <see cref="DecryptedMessageMediaExternalDocument"/>, <see cref="DecryptedMessageMediaVenue"/>, <see cref="DecryptedMessageMediaWebPage"/></para></summary> /// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para></summary>
/// <remarks>a <see langword="null"/> value means <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaEmpty">decryptedMessageMediaEmpty</a></remarks> /// <remarks>a <c>null</c> value means <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaEmpty">decryptedMessageMediaEmpty</a></remarks>
public abstract partial class DecryptedMessageMedia : IObject public abstract class DecryptedMessageMedia : IObject { }
{
public virtual string MimeType => default;
internal virtual (long size, byte[] key, byte[] iv) SizeKeyIV { get => default; set => throw new WTelegram.WTException("Incompatible DecryptedMessageMedia"); }
}
/// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para> <para>Derived classes: <see cref="DecryptedMessageActionSetMessageTTL"/>, <see cref="DecryptedMessageActionReadMessages"/>, <see cref="DecryptedMessageActionDeleteMessages"/>, <see cref="DecryptedMessageActionScreenshotMessages"/>, <see cref="DecryptedMessageActionFlushHistory"/>, <see cref="DecryptedMessageActionResend"/>, <see cref="DecryptedMessageActionNotifyLayer"/>, <see cref="DecryptedMessageActionTyping"/>, <see cref="DecryptedMessageActionRequestKey"/>, <see cref="DecryptedMessageActionAcceptKey"/>, <see cref="DecryptedMessageActionAbortKey"/>, <see cref="DecryptedMessageActionCommitKey"/>, <see cref="DecryptedMessageActionNoop"/></para></summary> /// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para></summary>
public abstract partial class DecryptedMessageAction : IObject { } public abstract class DecryptedMessageAction : IObject { }
/// <summary>Indicates the location of a photo, will be deprecated soon <para>See <a href="https://corefork.telegram.org/type/FileLocation"/></para> <para>Derived classes: <see cref="FileLocationUnavailable"/>, <see cref="FileLocation"/></para></summary> /// <summary>Indicates the location of a photo, will be deprecated soon <para>See <a href="https://corefork.telegram.org/type/FileLocation"/></para></summary>
public abstract partial class FileLocationBase : IObject public abstract class FileLocationBase : IObject
{ {
/// <summary>Volume ID</summary> /// <summary>Server volume</summary>
public virtual long VolumeId => default; public abstract long VolumeId { get; }
/// <summary>Local ID</summary> /// <summary>File ID</summary>
public virtual int LocalId => default; public abstract int LocalId { get; }
/// <summary>Secret</summary> /// <summary>Checksum to access the file</summary>
public virtual long Secret => default; public abstract long Secret { get; }
} }
namespace Layer8 namespace Layer8
{ {
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary> /// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x1F814F1F)] [TLDef(0x1F814F1F)]
public sealed partial class DecryptedMessage : DecryptedMessageBase public class DecryptedMessage : DecryptedMessageBase
{ {
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id; public long random_id;
/// <summary>Random bytes, removed in layer 17.</summary>
public byte[] random_bytes; public byte[] random_bytes;
/// <summary>Message text</summary> /// <summary>Message text</summary>
public string message; public string message;
@ -68,35 +43,24 @@ namespace TL
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public override long RandomId => random_id; public override long RandomId => random_id;
/// <summary>Message text</summary>
public override string Message => message;
/// <summary>Media content</summary>
public override DecryptedMessageMedia Media => media;
/// <summary>Random bytes, removed in layer 17.</summary>
public override byte[] RandomBytes => random_bytes;
} }
/// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary> /// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary>
[TLDef(0xAA48327D)] [TLDef(0xAA48327D)]
public sealed partial class DecryptedMessageService : DecryptedMessageBase public class DecryptedMessageService : DecryptedMessageBase
{ {
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary> /// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public long random_id; public long random_id;
/// <summary>Random bytes, removed in Layer 17.</summary>
public byte[] random_bytes; public byte[] random_bytes;
/// <summary>Action relevant to the service message</summary> /// <summary>Action relevant to the service message</summary>
public DecryptedMessageAction action; public DecryptedMessageAction action;
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary> /// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public override long RandomId => random_id; public override long RandomId => random_id;
/// <summary>Random bytes, removed in Layer 17.</summary>
public override byte[] RandomBytes => random_bytes;
/// <summary>Action relevant to the service message</summary>
public override DecryptedMessageAction Action => action;
} }
/// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary> /// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary>
[TLDef(0x32798A8C)] [TLDef(0x32798A8C)]
public sealed partial class DecryptedMessageMediaPhoto : DecryptedMessageMedia public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
{ {
/// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary> /// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary>
public byte[] thumb; public byte[] thumb;
@ -114,13 +78,10 @@ namespace TL
public byte[] key; public byte[] key;
/// <summary>Initialization vector</summary> /// <summary>Initialization vector</summary>
public byte[] iv; public byte[] iv;
public override string MimeType => "image/jpeg";
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary> /// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x4CEE6EF3)] [TLDef(0x4CEE6EF3)]
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia public class DecryptedMessageMediaVideo : DecryptedMessageMedia
{ {
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary> /// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
public byte[] thumb; public byte[] thumb;
@ -140,12 +101,10 @@ namespace TL
public byte[] key; public byte[] key;
/// <summary>Initialization vector</summary> /// <summary>Initialization vector</summary>
public byte[] iv; public byte[] iv;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>GeoPoint attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaGeoPoint"/></para></summary> /// <summary>GeoPoint attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaGeoPoint"/></para></summary>
[TLDef(0x35480A59)] [TLDef(0x35480A59)]
public sealed partial class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia public class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia
{ {
/// <summary>Latitude of point</summary> /// <summary>Latitude of point</summary>
public double lat; public double lat;
@ -154,7 +113,7 @@ namespace TL
} }
/// <summary>Contact attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaContact"/></para></summary> /// <summary>Contact attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaContact"/></para></summary>
[TLDef(0x588A0A97)] [TLDef(0x588A0A97)]
public sealed partial class DecryptedMessageMediaContact : DecryptedMessageMedia public class DecryptedMessageMediaContact : DecryptedMessageMedia
{ {
/// <summary>Phone number</summary> /// <summary>Phone number</summary>
public string phone_number; public string phone_number;
@ -167,7 +126,7 @@ namespace TL
} }
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary> /// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0xB095434B)] [TLDef(0xB095434B)]
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia public class DecryptedMessageMediaDocument : DecryptedMessageMedia
{ {
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary> /// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
public byte[] thumb; public byte[] thumb;
@ -175,25 +134,19 @@ namespace TL
public int thumb_w; public int thumb_w;
/// <summary>Thumbnail height</summary> /// <summary>Thumbnail height</summary>
public int thumb_h; public int thumb_h;
/// <summary>File name, moved to <c>attributes</c> in Layer 45.</summary>
public string file_name; public string file_name;
/// <summary>File MIME-type</summary> /// <summary>File MIME-type</summary>
public string mime_type; public string mime_type;
/// <summary>Document size (<see cref="int"/> on layer &lt;143, <see cref="long"/> on layer &gt;=143)</summary> /// <summary>Document size</summary>
public int size; public int size;
/// <summary>Key to decrypt the attached document file</summary> /// <summary>Key to decrypt the attached document file</summary>
public byte[] key; public byte[] key;
/// <summary>Initialization</summary> /// <summary>Initialization</summary>
public byte[] iv; public byte[] iv;
/// <summary>File MIME-type</summary>
public override string MimeType => mime_type;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary> /// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
[TLDef(0x6080758F)] [TLDef(0x6080758F)]
public sealed partial class DecryptedMessageMediaAudio : DecryptedMessageMedia public class DecryptedMessageMediaAudio : DecryptedMessageMedia
{ {
/// <summary>Audio duration in seconds</summary> /// <summary>Audio duration in seconds</summary>
public int duration; public int duration;
@ -203,122 +156,63 @@ namespace TL
public byte[] key; public byte[] key;
/// <summary>Initialization vector</summary> /// <summary>Initialization vector</summary>
public byte[] iv; public byte[] iv;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Setting of a message lifetime after reading. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionSetMessageTTL"/></para></summary> /// <summary>Setting of a message lifetime after reading. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionSetMessageTTL"/></para></summary>
[TLDef(0xA1733AEC)] [TLDef(0xA1733AEC)]
public sealed partial class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction public class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction
{ {
/// <summary>Lifetime in seconds</summary> /// <summary>Lifetime in seconds</summary>
public int ttl_seconds; public int ttl_seconds;
} }
/// <summary>Messages marked as read. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionReadMessages"/></para></summary> /// <summary>Messages marked as read. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionReadMessages"/></para></summary>
[TLDef(0x0C4F40BE)] [TLDef(0x0C4F40BE)]
public sealed partial class DecryptedMessageActionReadMessages : DecryptedMessageAction public class DecryptedMessageActionReadMessages : DecryptedMessageAction
{ {
/// <summary>List of message IDs</summary> /// <summary>List of message IDs</summary>
public long[] random_ids; public long[] random_ids;
} }
/// <summary>Deleted messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionDeleteMessages"/></para></summary> /// <summary>Deleted messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionDeleteMessages"/></para></summary>
[TLDef(0x65614304)] [TLDef(0x65614304)]
public sealed partial class DecryptedMessageActionDeleteMessages : DecryptedMessageAction public class DecryptedMessageActionDeleteMessages : DecryptedMessageAction
{ {
/// <summary>List of deleted message IDs</summary> /// <summary>List of deleted message IDs</summary>
public long[] random_ids; public long[] random_ids;
} }
/// <summary>A screenshot was taken. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionScreenshotMessages"/></para></summary> /// <summary>A screenshot was taken. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionScreenshotMessages"/></para></summary>
[TLDef(0x8AC1F475)] [TLDef(0x8AC1F475)]
public sealed partial class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction public class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction
{ {
/// <summary>List of affected message ids (that appeared on the screenshot)</summary> /// <summary>List of affected message ids (that appeared on the screenshot)</summary>
public long[] random_ids; public long[] random_ids;
} }
/// <summary>The entire message history has been deleted. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionFlushHistory"/></para></summary> /// <summary>The entire message history has been deleted. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionFlushHistory"/></para></summary>
[TLDef(0x6719E45C)] [TLDef(0x6719E45C)]
public sealed partial class DecryptedMessageActionFlushHistory : DecryptedMessageAction { } public class DecryptedMessageActionFlushHistory : DecryptedMessageAction { }
} }
namespace Layer23 namespace Layer17
{ {
/// <summary>Image description. <para>See <a href="https://corefork.telegram.org/constructor/photoSize"/></para></summary>
[TLDef(0x77BFB61B)]
public sealed partial class PhotoSize : PhotoSizeBase
{
/// <summary><a href="https://corefork.telegram.org/api/files#image-thumbnail-types">Thumbnail type »</a></summary>
public string type;
public FileLocationBase location;
/// <summary>Image width</summary>
public int w;
/// <summary>Image height</summary>
public int h;
/// <summary>File size</summary>
public int size;
/// <summary><a href="https://corefork.telegram.org/api/files#image-thumbnail-types">Thumbnail type »</a></summary>
public override string Type => type;
}
/// <summary>Description of an image and its content. <para>See <a href="https://corefork.telegram.org/constructor/photoCachedSize"/></para></summary>
[TLDef(0xE9A734FA)]
public sealed partial class PhotoCachedSize : PhotoSizeBase
{
/// <summary>Thumbnail type</summary>
public string type;
public FileLocationBase location;
/// <summary>Image width</summary>
public int w;
/// <summary>Image height</summary>
public int h;
/// <summary>Binary data, file content</summary>
public byte[] bytes;
/// <summary>Thumbnail type</summary>
public override string Type => type;
}
/// <summary>User is uploading a video. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadVideoAction"/></para></summary> /// <summary>User is uploading a video. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadVideoAction"/></para></summary>
[TLDef(0x92042FF7)] [TLDef(0x92042FF7)]
public sealed partial class SendMessageUploadVideoAction : SendMessageAction { } public class SendMessageUploadVideoAction : SendMessageAction { }
/// <summary>User is uploading a voice message. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadAudioAction"/></para></summary> /// <summary>User is uploading a voice message. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadAudioAction"/></para></summary>
[TLDef(0xE6AC8A6F)] [TLDef(0xE6AC8A6F)]
public sealed partial class SendMessageUploadAudioAction : SendMessageAction { } public class SendMessageUploadAudioAction : SendMessageAction { }
/// <summary>User is uploading a photo. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadPhotoAction"/></para></summary> /// <summary>User is uploading a photo. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadPhotoAction"/></para></summary>
[TLDef(0x990A3C1A)] [TLDef(0x990A3C1A)]
public sealed partial class SendMessageUploadPhotoAction : SendMessageAction { } public class SendMessageUploadPhotoAction : SendMessageAction { }
/// <summary>User is uploading a file. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadDocumentAction"/></para></summary> /// <summary>User is uploading a file. <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadDocumentAction"/></para></summary>
[TLDef(0x8FAEE98E)] [TLDef(0x8FAEE98E)]
public sealed partial class SendMessageUploadDocumentAction : SendMessageAction { } public class SendMessageUploadDocumentAction : SendMessageAction { }
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
[TLDef(0xFB0A5727)]
public sealed partial class DocumentAttributeSticker : DocumentAttribute { }
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
[TLDef(0x5910CCCB)]
public sealed partial class DocumentAttributeVideo : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
/// <summary>Video width</summary>
public int w;
/// <summary>Video height</summary>
public int h;
}
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
[TLDef(0x051448E5)]
public sealed partial class DocumentAttributeAudio : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
}
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary> /// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x204D3878)] [TLDef(0x204D3878)]
public sealed partial class DecryptedMessage : DecryptedMessageBase public class DecryptedMessage : DecryptedMessageBase
{ {
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id; public long random_id;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary> /// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public int ttl; public int ttl;
/// <summary>Message text</summary> /// <summary>Message text</summary>
public string message; public string message;
@ -327,16 +221,10 @@ namespace TL
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public override long RandomId => random_id; public override long RandomId => random_id;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary>
public override int Ttl => ttl;
/// <summary>Message text</summary>
public override string Message => message;
/// <summary>Media content</summary>
public override DecryptedMessageMedia Media => media;
} }
/// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary> /// <summary>Contents of an encrypted service message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageService"/></para></summary>
[TLDef(0x73164160)] [TLDef(0x73164160)]
public sealed partial class DecryptedMessageService : DecryptedMessageBase public class DecryptedMessageService : DecryptedMessageBase
{ {
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary> /// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public long random_id; public long random_id;
@ -345,13 +233,11 @@ namespace TL
/// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary> /// <summary>Random message ID, assigned by the message author.<br/>Must be equal to the ID passed to the sending method.</summary>
public override long RandomId => random_id; public override long RandomId => random_id;
/// <summary>Action relevant to the service message</summary>
public override DecryptedMessageAction Action => action;
} }
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary> /// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x524A415D)] [TLDef(0x524A415D)]
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia public class DecryptedMessageMediaVideo : DecryptedMessageMedia
{ {
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary> /// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
public byte[] thumb; public byte[] thumb;
@ -361,7 +247,7 @@ namespace TL
public int thumb_h; public int thumb_h;
/// <summary>Duration of video in seconds</summary> /// <summary>Duration of video in seconds</summary>
public int duration; public int duration;
/// <summary>MIME-type of the video file<br/>Parameter added in Layer 17.</summary> /// <summary>MIME-type of the video file<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public string mime_type; public string mime_type;
/// <summary>Image width</summary> /// <summary>Image width</summary>
public int w; public int w;
@ -373,19 +259,14 @@ namespace TL
public byte[] key; public byte[] key;
/// <summary>Initialization vector</summary> /// <summary>Initialization vector</summary>
public byte[] iv; public byte[] iv;
/// <summary>MIME-type of the video file<br/>Parameter added in Layer 17.</summary>
public override string MimeType => mime_type;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary> /// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
[TLDef(0x57E0A9CB)] [TLDef(0x57E0A9CB)]
public sealed partial class DecryptedMessageMediaAudio : DecryptedMessageMedia public class DecryptedMessageMediaAudio : DecryptedMessageMedia
{ {
/// <summary>Audio duration in seconds</summary> /// <summary>Audio duration in seconds</summary>
public int duration; public int duration;
/// <summary>MIME-type of the audio file<br/>Parameter added in Layer 13.</summary> /// <summary>MIME-type of the audio file<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-13">Layer 13</a>.</summary>
public string mime_type; public string mime_type;
/// <summary>File size</summary> /// <summary>File size</summary>
public int size; public int size;
@ -393,40 +274,11 @@ namespace TL
public byte[] key; public byte[] key;
/// <summary>Initialization vector</summary> /// <summary>Initialization vector</summary>
public byte[] iv; public byte[] iv;
/// <summary>MIME-type of the audio file<br/>Parameter added in Layer 13.</summary>
public override string MimeType => mime_type;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
}
/// <summary>Non-e2e documented forwarded from non-secret chat <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaExternalDocument"/></para></summary>
[TLDef(0xFA95B0DD)]
public sealed partial class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
{
/// <summary>Document ID</summary>
public long id;
/// <summary>access hash</summary>
public long access_hash;
/// <summary>Date</summary>
public DateTime date;
/// <summary>Mime type</summary>
public string mime_type;
/// <summary>Size</summary>
public int size;
/// <summary>Thumbnail</summary>
public PhotoSizeBase thumb;
/// <summary>DC ID</summary>
public int dc_id;
/// <summary>Attributes for media types</summary>
public DocumentAttribute[] attributes;
/// <summary>Mime type</summary>
public override string MimeType => mime_type;
} }
/// <summary>Request for the other party in a Secret Chat to automatically resend a contiguous range of previously sent messages, as explained in <a href="https://corefork.telegram.org/api/end-to-end/seq_no">Sequence number is Secret Chats</a>. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionResend"/></para></summary> /// <summary>Request for the other party in a Secret Chat to automatically resend a contiguous range of previously sent messages, as explained in <a href="https://corefork.telegram.org/api/end-to-end/seq_no">Sequence number is Secret Chats</a>. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionResend"/></para></summary>
[TLDef(0x511110B0)] [TLDef(0x511110B0)]
public sealed partial class DecryptedMessageActionResend : DecryptedMessageAction public class DecryptedMessageActionResend : DecryptedMessageAction
{ {
/// <summary><c>out_seq_no</c> of the first message to be resent, with correct parity</summary> /// <summary><c>out_seq_no</c> of the first message to be resent, with correct parity</summary>
public int start_seq_no; public int start_seq_no;
@ -435,119 +287,50 @@ namespace TL
} }
/// <summary>A notification stating the API layer that is used by the client. You should use your current layer and take notice of the layer used on the other side of a conversation when sending messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNotifyLayer"/></para></summary> /// <summary>A notification stating the API layer that is used by the client. You should use your current layer and take notice of the layer used on the other side of a conversation when sending messages. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNotifyLayer"/></para></summary>
[TLDef(0xF3048883)] [TLDef(0xF3048883)]
public sealed partial class DecryptedMessageActionNotifyLayer : DecryptedMessageAction public class DecryptedMessageActionNotifyLayer : DecryptedMessageAction
{ {
/// <summary>Layer number, must be <strong>17</strong> or higher (this constructor was introduced in Layer 17.</summary> /// <summary>Layer number, must be <strong>17</strong> or higher (this constructor was introduced in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>).</summary>
public int layer; public int layer;
} }
/// <summary>User is preparing a message: typing, recording, uploading, etc. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionTyping"/></para></summary> /// <summary>User is preparing a message: typing, recording, uploading, etc. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionTyping"/></para></summary>
[TLDef(0xCCB27641)] [TLDef(0xCCB27641)]
public sealed partial class DecryptedMessageActionTyping : DecryptedMessageAction public class DecryptedMessageActionTyping : DecryptedMessageAction
{ {
/// <summary>Type of action</summary> /// <summary>Type of action</summary>
public SendMessageAction action; public SendMessageAction action;
} }
/// <summary>Request rekeying, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionRequestKey"/></para></summary>
[TLDef(0xF3C9611B)]
public sealed partial class DecryptedMessageActionRequestKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
/// <summary>g_a, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public byte[] g_a;
}
/// <summary>Accept new key <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAcceptKey"/></para></summary>
[TLDef(0x6FE1735B)]
public sealed partial class DecryptedMessageActionAcceptKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
/// <summary>B parameter, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public byte[] g_b;
/// <summary>Key fingerprint, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long key_fingerprint;
}
/// <summary>Abort rekeying <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAbortKey"/></para></summary>
[TLDef(0xDD05EC6B)]
public sealed partial class DecryptedMessageActionAbortKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
}
/// <summary>Commit new key, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionCommitKey"/></para></summary>
[TLDef(0xEC2E0B9B)]
public sealed partial class DecryptedMessageActionCommitKey : DecryptedMessageAction
{
/// <summary>Exchange ID, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long exchange_id;
/// <summary>Key fingerprint, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long key_fingerprint;
}
/// <summary>NOOP action <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNoop"/></para></summary>
[TLDef(0xA82FDD63)]
public sealed partial class DecryptedMessageActionNoop : DecryptedMessageAction { }
/// <summary>Sets the layer number for the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageLayer"/></para></summary> /// <summary>Sets the layer number for the contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageLayer"/></para></summary>
[TLDef(0x1BE31789)] [TLDef(0x1BE31789)]
public sealed partial class DecryptedMessageLayer : IObject public class DecryptedMessageLayer : IObject
{ {
/// <summary>Set of random bytes to prevent content recognition in short encrypted messages.<br/>Clients are required to check that there are at least 15 random bytes included in each message. Messages with less than 15 random bytes must be ignored.<br/>Parameter moved here from <see cref="DecryptedMessage"/> in Layer 17.</summary> /// <summary>Set of random bytes to prevent content recognition in short encrypted messages.<br/>Clients are required to check that there are at least 15 random bytes included in each message. Messages with less than 15 random bytes must be ignored.<br/>Parameter moved here from <see cref="DecryptedMessage"/> in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public byte[] random_bytes; public byte[] random_bytes;
/// <summary>Layer number. Mimimal value - <strong>17</strong> (the layer in which the constructor was added).</summary> /// <summary>Layer number. Mimimal value - <strong>17</strong> (the layer in which the constructor was added).</summary>
public int layer; public int layer;
/// <summary>2x the number of messages in the sender's inbox (including deleted and service messages), incremented by 1 if current user was not the chat creator<br/>Parameter added in Layer 17.</summary> /// <summary>2x the number of messages in the sender's inbox (including deleted and service messages), incremented by 1 if current user was not the chat creator<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public int in_seq_no; public int in_seq_no;
/// <summary>2x the number of messages in the recipient's inbox (including deleted and service messages), incremented by 1 if current user was the chat creator<br/>Parameter added in Layer 17.</summary> /// <summary>2x the number of messages in the recipient's inbox (including deleted and service messages), incremented by 1 if current user was the chat creator<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public int out_seq_no; public int out_seq_no;
/// <summary>The content of message itself</summary> /// <summary>The content of message itself</summary>
public DecryptedMessageBase message; public DecryptedMessageBase message;
} }
/// <summary>File is currently unavailable. <para>See <a href="https://corefork.telegram.org/constructor/fileLocationUnavailable"/></para></summary>
[TLDef(0x7C596B46)]
public sealed partial class FileLocationUnavailable : FileLocationBase
{
/// <summary>Volume ID</summary>
public long volume_id;
/// <summary>Local ID</summary>
public int local_id;
/// <summary>Secret</summary>
public long secret;
/// <summary>Volume ID</summary>
public override long VolumeId => volume_id;
/// <summary>Local ID</summary>
public override int LocalId => local_id;
/// <summary>Secret</summary>
public override long Secret => secret;
}
/// <summary>File location. <para>See <a href="https://corefork.telegram.org/constructor/fileLocation"/></para></summary>
[TLDef(0x53D69076)]
public sealed partial class FileLocation : FileLocationBase
{
/// <summary>DC ID</summary>
public int dc_id;
/// <summary>Volume ID</summary>
public long volume_id;
/// <summary>Local ID</summary>
public int local_id;
/// <summary>Secret</summary>
public long secret;
/// <summary>Volume ID</summary>
public override long VolumeId => volume_id;
/// <summary>Local ID</summary>
public override int LocalId => local_id;
/// <summary>Secret</summary>
public override long Secret => secret;
}
} }
namespace Layer45 namespace Layer45
{ {
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
[TLDef(0x3A556302)]
public class DocumentAttributeSticker : DocumentAttribute
{
/// <summary>Alternative emoji representation of sticker</summary>
public string alt;
/// <summary>Associated stickerset</summary>
public InputStickerSet stickerset;
}
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary> /// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
[TLDef(0xDED218E0)] [TLDef(0xDED218E0)]
public sealed partial class DocumentAttributeAudio : DocumentAttribute public class DocumentAttributeAudio : DocumentAttribute
{ {
/// <summary>Duration in seconds</summary> /// <summary>Duration in seconds</summary>
public int duration; public int duration;
@ -556,37 +339,16 @@ namespace TL
/// <summary>Performer</summary> /// <summary>Performer</summary>
public string performer; public string performer;
} }
}
namespace Layer46
{
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
[TLDef(0x3A556302)]
public sealed partial class DocumentAttributeSticker : DocumentAttribute
{
/// <summary>Alternative emoji representation of sticker</summary>
public string alt;
/// <summary>Associated stickerset</summary>
public InputStickerSet stickerset;
}
/// <summary>Message entity representing a <a href="https://corefork.telegram.org/api/mentions">user mention</a>: for <em>creating</em> a mention use <see cref="InputMessageEntityMentionName"/>. <para>See <a href="https://corefork.telegram.org/constructor/messageEntityMentionName"/></para></summary>
[TLDef(0x352DCA58, inheritBefore = true)]
public sealed partial class MessageEntityMentionName : MessageEntity
{
/// <summary>Identifier of the user that was mentioned</summary>
public int user_id;
}
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary> /// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x36B091DE)] [TLDef(0x36B091DE)]
public sealed partial class DecryptedMessage : DecryptedMessageBase public class DecryptedMessage : DecryptedMessageBase
{ {
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary> /// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
public Flags flags; public Flags flags;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id; public long random_id;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary> /// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public int ttl; public int ttl;
/// <summary>Message text</summary> /// <summary>Message text</summary>
public string message; public string message;
@ -611,27 +373,13 @@ namespace TL
has_via_bot_name = 0x800, has_via_bot_name = 0x800,
} }
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
public override uint FFlags => (uint)flags;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public override long RandomId => random_id; public override long RandomId => random_id;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary>
public override int Ttl => ttl;
/// <summary>Message text</summary>
public override string Message => message;
/// <summary>Media content</summary>
public override DecryptedMessageMedia Media => media;
/// <summary>Message <a href="https://corefork.telegram.org/api/entities">entities</a> for styled text (parameter added in layer 45)</summary>
public override MessageEntity[] Entities => entities;
/// <summary>Specifies the ID of the inline bot that generated the message (parameter added in layer 45)</summary>
public override string ViaBotName => via_bot_name;
/// <summary>Random message ID of the message this message replies to (parameter added in layer 45)</summary>
public override long ReplyToRandom => reply_to_random_id;
} }
/// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary> /// <summary>Photo attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaPhoto"/></para></summary>
[TLDef(0xF1FA8D78)] [TLDef(0xF1FA8D78)]
public sealed partial class DecryptedMessageMediaPhoto : DecryptedMessageMedia public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
{ {
/// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary> /// <summary>Content of thumbnail file (JPEGfile, quality 55, set in a square 90x90)</summary>
public byte[] thumb; public byte[] thumb;
@ -651,13 +399,10 @@ namespace TL
public byte[] iv; public byte[] iv;
/// <summary>Caption</summary> /// <summary>Caption</summary>
public string caption; public string caption;
public override string MimeType => "image/jpeg";
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary> /// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x970C8C0E)] [TLDef(0x970C8C0E)]
public sealed partial class DecryptedMessageMediaVideo : DecryptedMessageMedia public class DecryptedMessageMediaVideo : DecryptedMessageMedia
{ {
/// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary> /// <summary>Content of thumbnail file (JPEG file, quality 55, set in a square 90x90)</summary>
public byte[] thumb; public byte[] thumb;
@ -667,7 +412,7 @@ namespace TL
public int thumb_h; public int thumb_h;
/// <summary>Duration of video in seconds</summary> /// <summary>Duration of video in seconds</summary>
public int duration; public int duration;
/// <summary>MIME-type of the video file<br/>Parameter added in Layer 17.</summary> /// <summary>MIME-type of the video file<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public string mime_type; public string mime_type;
/// <summary>Image width</summary> /// <summary>Image width</summary>
public int w; public int w;
@ -681,15 +426,10 @@ namespace TL
public byte[] iv; public byte[] iv;
/// <summary>Caption</summary> /// <summary>Caption</summary>
public string caption; public string caption;
/// <summary>MIME-type of the video file<br/>Parameter added in Layer 17.</summary>
public override string MimeType => mime_type;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary> /// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0x7AFE8AE2)] [TLDef(0x7AFE8AE2)]
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia public class DecryptedMessageMediaDocument : DecryptedMessageMedia
{ {
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary> /// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary>
public byte[] thumb; public byte[] thumb;
@ -699,7 +439,7 @@ namespace TL
public int thumb_h; public int thumb_h;
/// <summary>File MIME-type</summary> /// <summary>File MIME-type</summary>
public string mime_type; public string mime_type;
/// <summary>Document size (<see cref="int"/> on layer &lt;143, <see cref="long"/> on layer &gt;=143)</summary> /// <summary>Document size</summary>
public int size; public int size;
/// <summary>Key to decrypt the attached document file</summary> /// <summary>Key to decrypt the attached document file</summary>
public byte[] key; public byte[] key;
@ -709,15 +449,10 @@ namespace TL
public DocumentAttribute[] attributes; public DocumentAttribute[] attributes;
/// <summary>Caption</summary> /// <summary>Caption</summary>
public string caption; public string caption;
/// <summary>File MIME-type</summary>
public override string MimeType => mime_type;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = (checked((int)value.size), value.key, value.iv); }
} }
/// <summary>Venue <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVenue"/></para></summary> /// <summary>Venue <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVenue"/></para></summary>
[TLDef(0x8A0DF56F)] [TLDef(0x8A0DF56F)]
public sealed partial class DecryptedMessageMediaVenue : DecryptedMessageMedia public class DecryptedMessageMediaVenue : DecryptedMessageMedia
{ {
/// <summary>Latitude of venue</summary> /// <summary>Latitude of venue</summary>
public double lat; public double lat;
@ -727,58 +462,31 @@ namespace TL
public string title; public string title;
/// <summary>Address</summary> /// <summary>Address</summary>
public string address; public string address;
/// <summary>Venue provider: currently only "foursquare" and "gplaces" (Google Places) need to be supported</summary> /// <summary>Venue provider: currently only "foursquare" needs to be supported</summary>
public string provider; public string provider;
/// <summary>Venue ID in the provider's database</summary> /// <summary>Venue ID in the provider's database</summary>
public string venue_id; public string venue_id;
} }
/// <summary>Webpage preview <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaWebPage"/></para></summary> /// <summary>Webpage preview <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaWebPage"/></para></summary>
[TLDef(0xE50511D8)] [TLDef(0xE50511D8)]
public sealed partial class DecryptedMessageMediaWebPage : DecryptedMessageMedia public class DecryptedMessageMediaWebPage : DecryptedMessageMedia
{ {
/// <summary>URL of webpage</summary> /// <summary>URL of webpage</summary>
public string url; public string url;
} }
} }
namespace Layer66
{
/// <summary>User is uploading a round video <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadRoundAction"/></para></summary>
[TLDef(0xBB718624)]
public sealed partial class SendMessageUploadRoundAction : SendMessageAction { }
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
[TLDef(0x0EF02CE6)]
public sealed partial class DocumentAttributeVideo : DocumentAttribute
{
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary>
public Flags flags;
/// <summary>Duration in seconds</summary>
public int duration;
/// <summary>Video width</summary>
public int w;
/// <summary>Video height</summary>
public int h;
[Flags] public enum Flags : uint
{
/// <summary>Whether this is a round video</summary>
round_message = 0x1,
}
}
}
namespace Layer73 namespace Layer73
{ {
/// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary> /// <summary>Contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessage"/></para></summary>
[TLDef(0x91CC4674)] [TLDef(0x91CC4674)]
public sealed partial class DecryptedMessage : DecryptedMessageBase public class DecryptedMessage : DecryptedMessageBase
{ {
/// <summary>Extra bits of information, use <c>flags.HasFlag(...)</c> to test for those</summary> /// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
public Flags flags; public Flags flags;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public long random_id; public long random_id;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary> /// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in <a href="https://corefork.telegram.org/api/layers#layer-17">Layer 17</a>.</summary>
public int ttl; public int ttl;
/// <summary>Message text</summary> /// <summary>Message text</summary>
public string message; public string message;
@ -797,7 +505,6 @@ namespace TL
{ {
/// <summary>Field <see cref="reply_to_random_id"/> has a value</summary> /// <summary>Field <see cref="reply_to_random_id"/> has a value</summary>
has_reply_to_random_id = 0x8, has_reply_to_random_id = 0x8,
silent = 0x20,
/// <summary>Field <see cref="entities"/> has a value</summary> /// <summary>Field <see cref="entities"/> has a value</summary>
has_entities = 0x80, has_entities = 0x80,
/// <summary>Field <see cref="media"/> has a value</summary> /// <summary>Field <see cref="media"/> has a value</summary>
@ -808,66 +515,182 @@ namespace TL
has_grouped_id = 0x20000, has_grouped_id = 0x20000,
} }
/// <summary>Flags, see <a href="https://corefork.telegram.org/mtproto/TL-combinators#conditional-fields">TL conditional fields</a> (added in layer 45)</summary>
public override uint FFlags => (uint)flags;
/// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary> /// <summary>Random message ID, assigned by the author of message.<br/>Must be equal to the ID passed to sending method.</summary>
public override long RandomId => random_id; public override long RandomId => random_id;
/// <summary>Message lifetime. Has higher priority than <see cref="Layer8.DecryptedMessageActionSetMessageTTL"/>.<br/>Parameter added in Layer 17.</summary>
public override int Ttl => ttl;
/// <summary>Message text</summary>
public override string Message => message;
/// <summary>Media content</summary>
public override DecryptedMessageMedia Media => media;
/// <summary>Message <a href="https://corefork.telegram.org/api/entities">entities</a> for styled text (parameter added in layer 45)</summary>
public override MessageEntity[] Entities => entities;
/// <summary>Specifies the ID of the inline bot that generated the message (parameter added in layer 45)</summary>
public override string ViaBotName => via_bot_name;
/// <summary>Random message ID of the message this message replies to (parameter added in layer 45)</summary>
public override long ReplyToRandom => reply_to_random_id;
/// <summary>Random group ID, assigned by the author of message.<br/>Multiple encrypted messages with a photo attached and with the same group ID indicate an <a href="https://corefork.telegram.org/api/files#albums-grouped-media">album or grouped media</a> (parameter added in layer 45)</summary>
public override long Grouped => grouped_id;
} }
} }
namespace Layer101 namespace Layer20
{ {
/// <summary>Message entity representing a block quote. <para>See <a href="https://corefork.telegram.org/constructor/messageEntityBlockquote"/></para></summary> /// <summary>Request rekeying, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionRequestKey"/></para></summary>
[TLDef(0x020DF5D0)] [TLDef(0xF3C9611B)]
public sealed partial class MessageEntityBlockquote : MessageEntity { } public class DecryptedMessageActionRequestKey : DecryptedMessageAction
}
namespace Layer143
{
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0x6ABD9782)]
public sealed partial class DecryptedMessageMediaDocument : DecryptedMessageMedia
{ {
/// <summary>Thumbnail-file contents (JPEG-file, quality 55, set in a 90x90 square)</summary> /// <summary>Exchange ID</summary>
public byte[] thumb; public long exchange_id;
/// <summary>Thumbnail width</summary> /// <summary>g_a, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public int thumb_w; public byte[] g_a;
/// <summary>Thumbnail height</summary> }
public int thumb_h; /// <summary>Accept new key <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAcceptKey"/></para></summary>
/// <summary>File MIME-type</summary> [TLDef(0x6FE1735B)]
public class DecryptedMessageActionAcceptKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
/// <summary>B parameter, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public byte[] g_b;
/// <summary>Key fingerprint, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long key_fingerprint;
}
/// <summary>Abort rekeying <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionAbortKey"/></para></summary>
[TLDef(0xDD05EC6B)]
public class DecryptedMessageActionAbortKey : DecryptedMessageAction
{
/// <summary>Exchange ID</summary>
public long exchange_id;
}
/// <summary>Commit new key, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a> <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionCommitKey"/></para></summary>
[TLDef(0xEC2E0B9B)]
public class DecryptedMessageActionCommitKey : DecryptedMessageAction
{
/// <summary>Exchange ID, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long exchange_id;
/// <summary>Key fingerprint, see <a href="https://corefork.telegram.org/api/end-to-end/pfs">rekeying process</a></summary>
public long key_fingerprint;
}
/// <summary>NOOP action <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionNoop"/></para></summary>
[TLDef(0xA82FDD63)]
public class DecryptedMessageActionNoop : DecryptedMessageAction { }
}
namespace Layer23
{
/// <summary>Image description. <para>See <a href="https://corefork.telegram.org/constructor/photoSize"/></para></summary>
[TLDef(0x77BFB61B)]
public partial class PhotoSize : PhotoSizeBase
{
/// <summary>Thumbnail type</summary>
public string type;
public FileLocationBase location;
/// <summary>Image width</summary>
public int w;
/// <summary>Image height</summary>
public int h;
/// <summary>File size</summary>
public int size;
/// <summary>Thumbnail type</summary>
public override string Type => type;
}
/// <summary>Description of an image and its content. <para>See <a href="https://corefork.telegram.org/constructor/photoCachedSize"/></para></summary>
[TLDef(0xE9A734FA)]
public partial class PhotoCachedSize : PhotoSizeBase
{
/// <summary>Thumbnail type</summary>
public string type;
public FileLocationBase location;
/// <summary>Image width</summary>
public int w;
/// <summary>Image height</summary>
public int h;
/// <summary>Binary data, file content</summary>
public byte[] bytes;
/// <summary>Thumbnail type</summary>
public override string Type => type;
}
/// <summary>Defines a sticker <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeSticker"/></para></summary>
[TLDef(0xFB0A5727)]
public class DocumentAttributeSticker : DocumentAttribute { }
/// <summary>Defines a video <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeVideo"/></para></summary>
[TLDef(0x5910CCCB)]
public class DocumentAttributeVideo : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
/// <summary>Video width</summary>
public int w;
/// <summary>Video height</summary>
public int h;
}
/// <summary>Represents an audio file <para>See <a href="https://corefork.telegram.org/constructor/documentAttributeAudio"/></para></summary>
[TLDef(0x051448E5)]
public class DocumentAttributeAudio : DocumentAttribute
{
/// <summary>Duration in seconds</summary>
public int duration;
}
/// <summary>Non-e2e documented forwarded from non-secret chat <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaExternalDocument"/></para></summary>
[TLDef(0xFA95B0DD)]
public class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
{
/// <summary>Document ID</summary>
public long id;
/// <summary>access hash</summary>
public long access_hash;
/// <summary>Date</summary>
public DateTime date;
/// <summary>Mime type</summary>
public string mime_type; public string mime_type;
/// <summary>Document size (<see cref="int"/> on layer &lt;143, <see cref="long"/> on layer &gt;=143)</summary> /// <summary>Size</summary>
public long size; public int size;
/// <summary>Key to decrypt the attached document file</summary> /// <summary>Thumbnail</summary>
public byte[] key; public PhotoSizeBase thumb;
/// <summary>Initialization</summary> /// <summary>DC ID</summary>
public byte[] iv; public int dc_id;
/// <summary>Document attributes for media types</summary> /// <summary>Attributes for media types</summary>
public DocumentAttribute[] attributes; public DocumentAttribute[] attributes;
/// <summary>Caption</summary> }
public string caption;
/// <summary>File MIME-type</summary> /// <summary>File is currently unavailable. <para>See <a href="https://corefork.telegram.org/constructor/fileLocationUnavailable"/></para></summary>
public override string MimeType => mime_type; [TLDef(0x7C596B46)]
public class FileLocationUnavailable : FileLocationBase
{
/// <summary>Server volume</summary>
public long volume_id;
/// <summary>File ID</summary>
public int local_id;
/// <summary>Checksum to access the file</summary>
public long secret;
internal override (long size, byte[] key, byte[] iv) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } /// <summary>Server volume</summary>
public override long VolumeId => volume_id;
/// <summary>File ID</summary>
public override int LocalId => local_id;
/// <summary>Checksum to access the file</summary>
public override long Secret => secret;
}
/// <summary>File location. <para>See <a href="https://corefork.telegram.org/constructor/fileLocation"/></para></summary>
[TLDef(0x53D69076)]
public class FileLocation : FileLocationBase
{
/// <summary>Number of the data center holding the file</summary>
public int dc_id;
/// <summary>Server volume</summary>
public long volume_id;
/// <summary>File ID</summary>
public int local_id;
/// <summary>Checksum to access the file</summary>
public long secret;
/// <summary>Server volume</summary>
public override long VolumeId => volume_id;
/// <summary>File ID</summary>
public override int LocalId => local_id;
/// <summary>Checksum to access the file</summary>
public override long Secret => secret;
} }
} }
namespace Layer144 namespace Layer66
{
/// <summary>User is uploading a round video <para>See <a href="https://corefork.telegram.org/constructor/sendMessageUploadRoundAction"/></para></summary>
[TLDef(0xBB718624)]
public class SendMessageUploadRoundAction : SendMessageAction { }
}
namespace Layer46
{ } { }
} }

File diff suppressed because it is too large Load diff

276
src/TL.cs
View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
@ -8,122 +7,93 @@ using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
#pragma warning disable IDE1006 // Naming Styles
namespace TL namespace TL
{ {
#if MTPG
public interface IObject { void WriteTL(BinaryWriter writer); }
#else
public interface IObject { } public interface IObject { }
#endif public interface IMethod<ReturnType> : IObject { }
public interface IMethod<out ReturnType> : IObject { }
public interface IPeerResolver { IPeerInfo UserOrChat(Peer peer); } public interface IPeerResolver { IPeerInfo UserOrChat(Peer peer); }
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public sealed class TLDefAttribute(uint ctorNb) : Attribute public class TLDefAttribute : Attribute
{ {
public readonly uint CtorNb = ctorNb; public readonly uint CtorNb;
public TLDefAttribute(uint ctorNb) => CtorNb = ctorNb;
public bool inheritBefore; public bool inheritBefore;
} }
[AttributeUsage(AttributeTargets.Field)] [AttributeUsage(AttributeTargets.Field)]
public sealed class IfFlagAttribute(int bit) : Attribute public class IfFlagAttribute : Attribute
{ {
public readonly int Bit = bit; public readonly int Bit;
public IfFlagAttribute(int bit) => Bit = bit;
} }
public sealed class RpcException(int code, string message, int x = -1) : WTelegram.WTException(message) public class RpcException : Exception
{ {
public readonly int Code = code; public readonly int Code;
/// <summary>The value of X in the message, -1 if no variable X was found</summary> /// <summary>The value of X in the message, -1 if no variable X was found</summary>
public readonly int X = x; public readonly int X;
public RpcException(int code, string message, int x = -1) : base(message) { Code = code; X = x; }
public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); } public override string ToString() { var str = base.ToString(); return str.Insert(str.IndexOf(':') + 1, " " + Code); }
} }
public sealed partial class ReactorError : IObject public class ReactorError : IObject
{ {
public Exception Exception; public Exception Exception;
public void WriteTL(BinaryWriter writer) => throw new NotSupportedException();
} }
public static class Serialization internal class BinaryReader : System.IO.BinaryReader
{ {
[EditorBrowsable(EditorBrowsableState.Never)] public readonly WTelegram.Client Client;
public static byte[] ToBytes<T>(this T obj) where T : IObject public BinaryReader(Stream stream, WTelegram.Client client) : base(stream) => Client = client;
{ }
using var ms = new MemoryStream(384);
using var writer = new BinaryWriter(ms);
writer.WriteTLObject(obj);
return ms.ToArray();
}
public static void WriteTLObject<T>(this BinaryWriter writer, T obj) where T : IObject internal static class Serialization
{
internal static void WriteTLObject<T>(this BinaryWriter writer, T obj) where T : IObject
{ {
if (obj == null) { writer.WriteTLNull(typeof(T)); return; } if (obj == null) { writer.WriteTLNull(typeof(T)); return; }
#if MTPG
obj.WriteTL(writer);
#else
var type = obj.GetType(); var type = obj.GetType();
var tlDef = type.GetCustomAttribute<TLDefAttribute>(); var tlDef = type.GetCustomAttribute<TLDefAttribute>();
var ctorNb = tlDef.CtorNb; var ctorNb = tlDef.CtorNb;
writer.Write(ctorNb); writer.Write(ctorNb);
IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
ulong flags = 0; uint flags = 0;
IfFlagAttribute ifFlag; IfFlagAttribute ifFlag;
foreach (var field in fields) foreach (var field in fields)
{ {
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1UL << ifFlag.Bit)) == 0) continue; if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1U << ifFlag.Bit)) == 0) continue;
object value = field.GetValue(obj); object value = field.GetValue(obj);
writer.WriteTLValue(value, field.FieldType); writer.WriteTLValue(value, field.FieldType);
if (field.FieldType.IsEnum) if (field.FieldType.IsEnum && field.Name == "flags") flags = (uint)value;
if (field.Name == "flags") flags = (uint)value;
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
} }
#endif
} }
public static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0) internal static IObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0)
{ {
if (ctorNb == 0) ctorNb = reader.ReadUInt32(); if (ctorNb == 0) ctorNb = reader.ReadUInt32();
#if MTPG if (ctorNb == Layer.GZipedCtor)
if (!Layer.Table.TryGetValue(ctorNb, out var ctor)) using (var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress), reader.Client))
throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}"); return ReadTLObject(gzipReader);
return ctor?.Invoke(reader);
#else
if (ctorNb == Layer.GZipedCtor) return (IObject)reader.ReadTLGzipped(typeof(IObject));
if (!Layer.Table.TryGetValue(ctorNb, out var type)) if (!Layer.Table.TryGetValue(ctorNb, out var type))
throw new WTelegram.WTException($"Cannot find type for ctor #{ctorNb:x}"); throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
if (type == null) return null; // nullable ctor (class meaning is associated with null) if (type == null) return null; // nullable ctor (class meaning is associated with null)
var tlDef = type.GetCustomAttribute<TLDefAttribute>(); var tlDef = type.GetCustomAttribute<TLDefAttribute>();
var obj = Activator.CreateInstance(type, true); var obj = Activator.CreateInstance(type, true);
IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public); IEnumerable<FieldInfo> fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); if (tlDef.inheritBefore) fields = fields.GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
ulong flags = 0; uint flags = 0;
IfFlagAttribute ifFlag; IfFlagAttribute ifFlag;
foreach (var field in fields) foreach (var field in fields)
{ {
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1UL << ifFlag.Bit)) == 0) continue; if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1U << ifFlag.Bit)) == 0) continue;
object value = reader.ReadTLValue(field.FieldType); object value = reader.ReadTLValue(field.FieldType);
field.SetValue(obj, value); field.SetValue(obj, value);
if (field.FieldType.IsEnum) if (field.FieldType.IsEnum && field.Name == "flags") flags = (uint)value;
if (field.Name == "flags") flags = (uint)value; if (reader.Client.CollectAccessHash) reader.Client.CollectField(field, obj, value);
else if (field.Name == "flags2") flags |= (ulong)(uint)value << 32;
} }
return (IObject)obj; return (IObject)obj;
#endif
}
public static IMethod<X> ReadTLMethod<X>(this BinaryReader reader)
{
uint ctorNb = reader.ReadUInt32();
if (!Layer.Methods.TryGetValue(ctorNb, out var ctor))
throw new WTelegram.WTException($"Cannot find method for ctor #{ctorNb:x}");
var method = ctor?.Invoke(reader);
if (method is IMethod<bool> && typeof(X) == typeof(object))
method = new BoolMethod { query = method };
return (IMethod<X>)method;
} }
internal static void WriteTLValue(this BinaryWriter writer, object value, Type valueType) internal static void WriteTLValue(this BinaryWriter writer, object value, Type valueType)
@ -148,16 +118,16 @@ namespace TL
if (type.IsArray) if (type.IsArray)
if (value is byte[] bytes) if (value is byte[] bytes)
writer.WriteTLBytes(bytes); writer.WriteTLBytes(bytes);
else if (value is _Message[] messages)
writer.WriteTLMessages(messages);
else else
writer.WriteTLVector((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) else if (value is Int128 int128)
writer.Write(int128); writer.Write(int128);
else if (value is Int256 int256) else if (value is Int256 int256)
writer.Write(int256); writer.Write(int256);
else if (value is IObject tlObject)
WriteTLObject(writer, tlObject);
else if (type.IsEnum) // needed for Mono (enums in generic types are seen as TypeCode.Object) else if (type.IsEnum) // needed for Mono (enums in generic types are seen as TypeCode.Object)
writer.Write((uint)value); writer.Write((uint)value);
else else
@ -186,7 +156,7 @@ namespace TL
0x997275b5 => true, 0x997275b5 => true,
0xbc799737 => false, 0xbc799737 => false,
Layer.RpcErrorCtor => reader.ReadTLObject(Layer.RpcErrorCtor), Layer.RpcErrorCtor => reader.ReadTLObject(Layer.RpcErrorCtor),
var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}") var value => throw new ApplicationException($"Invalid boolean value #{value:x}")
}; };
case TypeCode.Object: case TypeCode.Object:
if (type.IsArray) if (type.IsArray)
@ -201,9 +171,9 @@ namespace TL
else if (type == typeof(Int256)) else if (type == typeof(Int256))
return new Int256(reader); return new Int256(reader);
else if (type == typeof(Dictionary<long, User>)) else if (type == typeof(Dictionary<long, User>))
return reader.ReadTLDictionary<User>(); return reader.ReadTLDictionary<User>(u => u.ID);
else if (type == typeof(Dictionary<long, ChatBase>)) else if (type == typeof(Dictionary<long, ChatBase>))
return reader.ReadTLDictionary<ChatBase>(); return reader.ReadTLDictionary<ChatBase>(c => c.ID);
else else
return reader.ReadTLObject(); return reader.ReadTLObject();
default: default:
@ -212,26 +182,6 @@ namespace TL
} }
} }
internal static void WriteTLMessages(this BinaryWriter writer, List<_Message> messages)
{
writer.Write(messages.Count);
foreach (var msg in messages)
{
writer.Write(msg.msg_id);
writer.Write(msg.seqno);
var patchPos = writer.BaseStream.Position;
writer.Write(0); // patched below
if ((msg.seqno & 1) != 0)
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}");
else
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}");
writer.WriteTLObject(msg.body);
writer.BaseStream.Position = patchPos;
writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field
writer.Seek(0, SeekOrigin.End);
}
}
internal static void WriteTLVector(this BinaryWriter writer, Array array) internal static void WriteTLVector(this BinaryWriter writer, Array array)
{ {
writer.Write(Layer.VectorCtor); writer.Write(Layer.VectorCtor);
@ -243,40 +193,24 @@ namespace TL
writer.WriteTLValue(array.GetValue(i), elementType); writer.WriteTLValue(array.GetValue(i), elementType);
} }
internal static void WriteTLRawVector(this BinaryWriter writer, Array array, int elementSize) internal static void WriteTLMessages(this BinaryWriter writer, _Message[] messages)
{ {
var startPos = writer.BaseStream.Position; writer.Write(messages.Length);
int count = array.Length; foreach (var msg in messages)
var elementType = array.GetType().GetElementType();
for (int i = count - 1; i >= 0; i--)
{ {
writer.BaseStream.Position = startPos + i * elementSize; writer.Write(msg.msg_id);
writer.WriteTLValue(array.GetValue(i), elementType); writer.Write(msg.seqno);
var patchPos = writer.BaseStream.Position;
writer.Write(0); // patched below
writer.WriteTLObject(msg.body);
if ((msg.seqno & 1) != 0)
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38} #{(short)msg.msg_id.GetHashCode():X4}");
else
WTelegram.Helpers.Log(1, $" → {msg.body.GetType().Name.TrimEnd('_'),-38}");
writer.BaseStream.Position = patchPos;
writer.Write((int)(writer.BaseStream.Length - patchPos - 4)); // patch bytes field
writer.Seek(0, SeekOrigin.End);
} }
writer.BaseStream.Position = startPos;
writer.Write(count);
writer.BaseStream.Position = startPos + count * elementSize + 4;
}
internal static List<T> ReadTLRawVector<T>(this BinaryReader reader, uint ctorNb)
{
int count = reader.ReadInt32();
var list = new List<T>(count);
for (int i = 0; i < count; i++)
list.Add((T)reader.ReadTLObject(ctorNb));
return list;
}
internal static T[] ReadTLVector<T>(this BinaryReader reader)
{
var elementType = typeof(T);
if (reader.ReadUInt32() is not Layer.VectorCtor and uint ctorNb)
throw new WTelegram.WTException($"Cannot deserialize {elementType.Name}[] with ctor #{ctorNb:x}");
int count = reader.ReadInt32();
var array = new T[count];
for (int i = 0; i < count; i++)
array[i] = (T)reader.ReadTLValue(elementType);
return array;
} }
internal static Array ReadTLVector(this BinaryReader reader, Type type) internal static Array ReadTLVector(this BinaryReader reader, Type type)
@ -304,35 +238,33 @@ namespace TL
return array; return array;
} }
else else
throw new WTelegram.WTException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}"); throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
} }
internal static Dictionary<long, T> ReadTLDictionary<T>(this BinaryReader reader) where T : class, IPeerInfo internal static Dictionary<long, T> ReadTLDictionary<T>(this BinaryReader reader, Func<T, long> getID) where T : class
{ {
uint ctorNb = reader.ReadUInt32(); uint ctorNb = reader.ReadUInt32();
var elementType = typeof(T);
if (ctorNb != Layer.VectorCtor) if (ctorNb != Layer.VectorCtor)
throw new WTelegram.WTException($"Cannot deserialize Vector<{typeof(T).Name}> with ctor #{ctorNb:x}"); throw new ApplicationException($"Cannot deserialize Vector<{elementType.Name}> with ctor #{ctorNb:x}");
int count = reader.ReadInt32(); int count = reader.ReadInt32();
var dict = new Dictionary<long, T>(count); var dict = new Dictionary<long, T>(count);
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
var obj = reader.ReadTLObject(); var value = (T)reader.ReadTLValue(elementType);
if (obj is T value) dict[value.ID] = value; dict[getID(value)] = value is UserEmpty ? null : 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; return dict;
} }
internal static void WriteTLStamp(this BinaryWriter writer, DateTime datetime) internal static void WriteTLStamp(this BinaryWriter writer, DateTime datetime)
=> writer.Write((int)Math.Min(Math.Max(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L, 0), int.MaxValue)); => writer.Write(datetime == DateTime.MaxValue ? int.MaxValue : (int)(datetime.ToUniversalTime().Ticks / 10000000 - 62135596800L));
internal static DateTime ReadTLStamp(this BinaryReader reader) => reader.ReadInt32() switch internal static DateTime ReadTLStamp(this BinaryReader reader)
{ {
<= 0 => default, int unixstamp = reader.ReadInt32();
int.MaxValue => DateTime.MaxValue, return unixstamp == int.MaxValue ? DateTime.MaxValue : new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc);
int unixstamp => new((unixstamp + 62135596800L) * 10000000, DateTimeKind.Utc) }
};
internal static void WriteTLString(this BinaryWriter writer, string str) internal static void WriteTLString(this BinaryWriter writer, string str)
{ {
@ -389,19 +321,6 @@ namespace TL
writer.Write(0); // null arrays/strings are serialized as empty writer.Write(0); // null arrays/strings are serialized as empty
} }
internal static object ReadTLGzipped(this BinaryReader reader, Type type)
{
using var gzipReader = new BinaryReader(new GZipStream(new MemoryStream(reader.ReadTLBytes()), CompressionMode.Decompress));
return gzipReader.ReadTLValue(type);
}
internal static bool ReadTLBool(this BinaryReader reader) => reader.ReadUInt32() switch
{
0x997275b5 => true,
0xbc799737 => false,
var value => throw new WTelegram.WTException($"Invalid boolean value #{value:x}")
};
#if DEBUG #if DEBUG
private static void ShouldntBeHere() => System.Diagnostics.Debugger.Break(); private static void ShouldntBeHere() => System.Diagnostics.Debugger.Break();
#else #else
@ -413,13 +332,13 @@ namespace TL
{ {
public byte[] raw; public byte[] raw;
public Int128(BinaryReader reader) => raw = reader.ReadBytes(16); public Int128(System.IO.BinaryReader reader) => raw = reader.ReadBytes(16);
public Int128(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[16]); public Int128(RNGCryptoServiceProvider 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 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 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 readonly bool Equals(object obj) => obj is Int128 other && this == other; public override bool Equals(object obj) => obj is Int128 other && this == other;
public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0); public override int GetHashCode() => BitConverter.ToInt32(raw, 0);
public override readonly string ToString() => Convert.ToHexString(raw); public override string ToString() => Convert.ToHexString(raw);
public static implicit operator byte[](Int128 int128) => int128.raw; public static implicit operator byte[](Int128 int128) => int128.raw;
} }
@ -427,69 +346,48 @@ namespace TL
{ {
public byte[] raw; public byte[] raw;
public Int256(BinaryReader reader) => raw = reader.ReadBytes(32); public Int256(System.IO.BinaryReader reader) => raw = reader.ReadBytes(32);
public Int256(RandomNumberGenerator rng) => rng.GetBytes(raw = new byte[32]); public Int256(RNGCryptoServiceProvider 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 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 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 readonly bool Equals(object obj) => obj is Int256 other && this == other; public override bool Equals(object obj) => obj is Int256 other && this == other;
public override readonly int GetHashCode() => BitConverter.ToInt32(raw, 0); public override int GetHashCode() => BitConverter.ToInt32(raw, 0);
public override readonly string ToString() => Convert.ToHexString(raw); public override string ToString() => Convert.ToHexString(raw);
public static implicit operator byte[](Int256 int256) => int256.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 // 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 [TLDef(0x7A19CB76)] //RSA_public_key#7a19cb76 n:bytes e:bytes = RSAPublicKey
public sealed partial class RSAPublicKey : IObject public class RSAPublicKey : IObject
{ {
public byte[] n; public byte[] n;
public byte[] e; public byte[] e;
} }
[TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult [TLDef(0xF35C6D01)] //rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult
public sealed partial class RpcResult : IObject public class RpcResult : IObject
{ {
public long req_msg_id; public long req_msg_id;
public object result; public object result;
} }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006")]
[TLDef(0x5BB8E511)] //message#5bb8e511 msg_id:long seqno:int bytes:int body:Object = Message [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 class _Message
{ {
public long msg_id = msgId; public _Message(long msgId, int seqNo, IObject obj) { msg_id = msgId; seqno = seqNo; body = obj; }
public int seqno = seqno; public long msg_id;
public int seqno;
public int bytes; public int bytes;
public IObject body = obj; public IObject body;
} }
[TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer [TLDef(0x73F1F8DC)] //msg_container#73f1f8dc messages:vector<%Message> = MessageContainer
public sealed partial class MsgContainer : IObject { public List<_Message> messages; } public class MsgContainer : IObject { public _Message[] messages; }
[TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy [TLDef(0xE06046B2)] //msg_copy#e06046b2 orig_message:Message = MessageCopy
public sealed partial class MsgCopy : IObject { public _Message orig_message; } public class MsgCopy : IObject { public _Message orig_message; }
[TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object [TLDef(0x3072CFA1)] //gzip_packed#3072cfa1 packed_data:bytes = Object
public sealed partial class GzipPacked : IObject { public byte[] packed_data; } public class GzipPacked : IObject { public byte[] packed_data; }
public sealed class Null<X> : IObject
{
public readonly static Null<X> Instance = new();
public void WriteTL(BinaryWriter writer) => writer.WriteTLNull(typeof(X));
}
public sealed class BoolMethod : IMethod<object>
{
public IObject query;
public void WriteTL(BinaryWriter writer) => query.WriteTL(writer);
}
} }

View file

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

View file

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

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFrameworks>netstandard2.0;net5.0;net8.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;net5.0</TargetFrameworks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<RootNamespace>WTelegram</RootNamespace> <RootNamespace>WTelegram</RootNamespace>
<Deterministic>true</Deterministic> <Deterministic>true</Deterministic>
@ -11,54 +11,44 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<PackageId>WTelegramClient</PackageId> <PackageId>WTelegramClient</PackageId>
<Version>0.0.0</Version>
<Authors>Wizou</Authors> <Authors>Wizou</Authors>
<VersionPrefix>0.0.0</VersionPrefix> <Description>Telegram Client API library written 100% in C# and .NET Standard | Latest MTProto &amp; API layer version</Description>
<VersionSuffix>layer.220</VersionSuffix> <Copyright>Copyright © Olivier Marcoux 2021-2022</Copyright>
<Description>Telegram Client API (MTProto) library written 100% in C# and .NET Standard | Latest API layer: 220
Release Notes:
$(ReleaseNotes)</Description>
<Copyright>Copyright © Olivier Marcoux 2021-2025</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://wiz0u.github.io/WTelegramClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/wiz0u/WTelegramClient</PackageProjectUrl>
<PackageIcon>logo.png</PackageIcon> <PackageIcon>logo.png</PackageIcon>
<PublishRepositoryUrl>true</PublishRepositoryUrl> <PublishRepositoryUrl>true</PublishRepositoryUrl>
<RepositoryUrl>https://github.com/wiz0u/WTelegramClient.git</RepositoryUrl> <RepositoryUrl>https://github.com/wiz0u/WTelegramClient.git</RepositoryUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<PackageTags>Telegram;MTProto;Client;Api;UserBot</PackageTags> <PackageTags>Telegram;Client;Api;UserBot;MTProto;TLSharp;OpenTl</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>$(ReleaseNotes)</PackageReleaseNotes> <NoWarn>IDE0079;0419;1573;1591</NoWarn>
<NoWarn>NETSDK1138;CS0419;CS1573;CS1591</NoWarn> <DefineConstants>TRACE;OBFUSCATION</DefineConstants>
<DefineConstants>TRACE;OBFUSCATION;MTPG</DefineConstants>
<!--<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>-->
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<None Include="..\.github\workflows\dev.yml" Link="Data\dev.yml" /> <None Remove=".gitattributes" />
<None Include="..\.github\workflows\release.yml" Link="Data\release.yml" /> <None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<None Include="..\.github\dev.yml" Link="Data\dev.yml" />
<None Include="..\.github\release.yml" Link="Data\release.yml" />
<None Include="..\EXAMPLES.md" Link="Data\EXAMPLES.md" /> <None Include="..\EXAMPLES.md" Link="Data\EXAMPLES.md" />
<None Include="..\FAQ.md" Link="Data\FAQ.md" /> <None Include="..\FAQ.md" Link="Data\FAQ.md" />
<None Include="..\README.md" Link="Data\README.md" Pack="true" PackagePath="\" /> <None Include="..\README.md" Link="Data\README.md" Pack="true" PackagePath="\" />
<None Include="..\logo.png" Link="Data\logo.png" Pack="true" PackagePath="\" /> <None Include="..\logo.png" Link="Data\logo.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<!-- Disabled because SourceLink navigation prevents a clear display of the API
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" 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>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'"> <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="IndexRange" Version="1.0.3" /> <PackageReference Include="IndexRange" Version="1.0.0" />
<PackageReference Include="System.Memory" Version="4.5.5" /> <PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="System.Text.Json" Version="6.0.10" /> <PackageReference Include="System.Text.Json" Version="5.0.2" />
<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> </ItemGroup>
</Project> </Project>