diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index f6efc6d..51bae9d 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
-custom: ["http://wizou.fr/donate.html"]
+github: wiz0u
+custom: ["https://www.buymeacoffee.com/wizou", "http://t.me/WTelegramClientBot?start=donate"]
diff --git a/.github/dev.yml b/.github/dev.yml
index 052762e..6206e8e 100644
--- a/.github/dev.yml
+++ b/.github/dev.yml
@@ -1,38 +1,63 @@
-pr: none
+pr: none
trigger:
-- master
+ branches:
+ include: [ master ]
+ paths:
+ exclude: [ '.github', '*.md', 'Examples' ]
-name: 2.6.3-dev.$(Rev:r)
+name: 4.3.2-dev.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
+ Release_Notes: $[replace(variables['Build.SourceVersionMessage'], '"', '''''')]
-steps:
-- task: UseDotNet@2
- displayName: 'Use .NET Core sdk'
- inputs:
- packageType: 'sdk'
- version: '6.0.x'
- includePreviewVersions: true
+stages:
+ - stage: publish
+ jobs:
+ - job: publish
+ steps:
+ - task: UseDotNet@2
+ displayName: 'Use .NET Core sdk'
+ inputs:
+ packageType: 'sdk'
+ version: '9.x'
+ includePreviewVersions: true
-- task: DotNetCoreCLI@2
- inputs:
- command: 'pack'
- packagesToPack: '**/*.csproj'
- includesymbols: true
- versioningScheme: 'byEnvVar'
- versionEnvVar: 'Build.BuildNumber'
- buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true'
-# buildProperties: 'NoWarn="0419;1573;1591";AllowedOutputExtensionsInPackageBuildOutputFolder=".dll;.xml;.pdb"'
+ - task: DotNetCoreCLI@2
+ inputs:
+ command: 'pack'
+ packagesToPack: 'src/WTelegramClient.csproj'
+ includesymbols: true
+ versioningScheme: 'byEnvVar'
+ versionEnvVar: 'Build.BuildNumber'
+ buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)"
-- task: NuGetCommand@2
- inputs:
- command: 'push'
- packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.*upkg'
- publishPackageMetadata: true
- nuGetFeedType: 'internal'
- publishVstsFeed: 'WTelegramClient/WTelegramClient'
+ - task: NuGetCommand@2
+ inputs:
+ command: 'push'
+ packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
+ publishPackageMetadata: true
+ nuGetFeedType: 'external'
+ publishFeedCredentials: 'nuget.org'
+ - stage: notify
+ jobs:
+ - job: notify
+ pool:
+ server
+ steps:
+ - task: InvokeRESTAPI@1
+ inputs:
+ connectionType: 'connectedServiceName'
+ serviceConnection: 'Telegram Deploy Notice'
+ method: 'POST'
+ body: |
+ {
+ "status": "success",
+ "complete": true,
+ "message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\", \"commitMessage\": \"$(Release_Notes)\" }"
+ }
+ waitForCompletion: 'false'
diff --git a/.github/release.yml b/.github/release.yml
index b6de330..e4ca17f 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -1,13 +1,14 @@
pr: none
trigger: none
-name: 2.6.$(Rev:r)
+name: 4.3.$(Rev:r)
pool:
vmImage: ubuntu-latest
variables:
buildConfiguration: 'Release'
+ Release_Notes: $[replace(variables['releaseNotes'], '"', '''''')]
stages:
- stage: publish
@@ -21,17 +22,17 @@ stages:
displayName: 'Use .NET Core sdk'
inputs:
packageType: 'sdk'
- version: '6.0.x'
+ version: '9.x'
includePreviewVersions: true
- task: DotNetCoreCLI@2
inputs:
command: 'pack'
- packagesToPack: '**/*.csproj'
+ packagesToPack: 'src/WTelegramClient.csproj'
includesymbols: true
versioningScheme: 'byEnvVar'
versionEnvVar: 'Build.BuildNumber'
- buildProperties: 'NoWarn="0419;1573;1591";Version=$(Build.BuildNumber);ContinuousIntegrationBuild=true'
+ buildProperties: NoWarn="0419;1573;1591";ContinuousIntegrationBuild=true;Version=$(Build.BuildNumber);"ReleaseNotes=$(Release_Notes)"
- task: NuGetCommand@2
inputs:
@@ -58,13 +59,9 @@ stages:
serviceConnection: 'Telegram Deploy Notice'
method: 'POST'
body: |
- {
+ {
"status": "success",
"complete": true,
- "message": "{
- \"commitId\": \"$(Build.SourceVersion)\",
- \"buildNumber\": \"$(Build.BuildNumber)\",
- \"teamProjectName\": \"$(system.TeamProject)\"
- }"
+ "message": "{ \"commitId\": \"$(Build.SourceVersion)\", \"buildNumber\": \"$(Build.BuildNumber)\", \"teamProjectName\": \"$(System.TeamProject)\"}"
}
waitForCompletion: 'false'
diff --git a/.github/workflows/autolock.yml b/.github/workflows/autolock.yml
new file mode 100644
index 0000000..9a676bc
--- /dev/null
+++ b/.github/workflows/autolock.yml
@@ -0,0 +1,24 @@
+name: 'Auto-Lock Issues'
+
+on:
+ schedule:
+ - cron: '17 2 * * 1'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
+ discussions: write
+
+concurrency:
+ group: lock-threads
+
+jobs:
+ action:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/lock-threads@v5
+ with:
+ issue-inactive-days: '60'
+ pr-inactive-days: '60'
+ discussion-inactive-days: '60'
diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
new file mode 100644
index 0000000..de5e145
--- /dev/null
+++ b/.github/workflows/dev.yml
@@ -0,0 +1,68 @@
+name: Dev build
+
+on:
+ push:
+ branches: [ master ]
+ paths-ignore: [ '.**', 'Examples/**', '**.md' ]
+
+env:
+ PROJECT_PATH: src/WTelegramClient.csproj
+ CONFIGURATION: Release
+ RELEASE_NOTES: ${{ github.event.head_commit.message }}
+
+jobs:
+ build:
+ permissions:
+ id-token: write # enable GitHub OIDC token issuance for this job
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 30
+ - name: Determine version
+ run: |
+ git fetch --depth=30 --tags
+ DESCR_TAG=$(git describe --tags)
+ COMMITS=${DESCR_TAG#*-}
+ COMMITS=${COMMITS%-*}
+ LAST_TAG=${DESCR_TAG%%-*}
+ NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))-dev.$COMMITS
+ RELEASE_VERSION=${{vars.RELEASE_VERSION}}-dev.$COMMITS
+ if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi
+ echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+
+ # - name: Setup .NET
+ # uses: actions/setup-dotnet@v4
+ # with:
+ # dotnet-version: 8.0.x
+ - name: Pack
+ run: |
+ RELEASE_NOTES=${RELEASE_NOTES//$'\n'/%0A}
+ RELEASE_NOTES=${RELEASE_NOTES//\"/%22}
+ RELEASE_NOTES=${RELEASE_NOTES//,/%2C}
+ RELEASE_NOTES=${RELEASE_NOTES//;/%3B}
+ dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION -p:ReleaseNotes="$RELEASE_NOTES" --output packages
+ # - name: Upload artifact
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: packages
+ # path: packages/*.nupkg
+
+ - name: NuGet login (OIDC → temp API key)
+ uses: NuGet/login@v1
+ id: login
+ with:
+ user: ${{ secrets.NUGET_USER }}
+ - name: Nuget push
+ run: dotnet nuget push packages/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
+
+ - name: Deployment Notification
+ env:
+ JSON: |
+ {
+ "status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
+ "message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
+ }
+ run: |
+ curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..7bdbcd1
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,82 @@
+name: Release build
+
+on:
+ workflow_dispatch:
+ inputs:
+ release_notes:
+ description: 'Release notes'
+ required: true
+ version:
+ description: "Release version (leave empty for automatic versioning)"
+
+run-name: '📌 Release build ${{ inputs.version }}'
+
+env:
+ PROJECT_PATH: src/WTelegramClient.csproj
+ CONFIGURATION: Release
+ RELEASE_NOTES: ${{ inputs.release_notes }}
+ VERSION: ${{ inputs.version }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # For git tag
+ id-token: write # enable GitHub OIDC token issuance for this job
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 100
+ - name: Determine version
+ if: ${{ env.VERSION == '' }}
+ run: |
+ git fetch --depth=100 --tags
+ DESCR_TAG=$(git describe --tags)
+ LAST_TAG=${DESCR_TAG%%-*}
+ NEXT_VERSION=${LAST_TAG%.*}.$((${LAST_TAG##*.} + 1))
+ RELEASE_VERSION=${{vars.RELEASE_VERSION}}
+ if [[ "$RELEASE_VERSION" > "$NEXT_VERSION" ]] then VERSION=$RELEASE_VERSION; else VERSION=$NEXT_VERSION; fi
+ echo Last tag: $LAST_TAG · Next version: $NEXT_VERSION · Release version: $RELEASE_VERSION · Build version: $VERSION
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+ - name: Pack
+ run: |
+ RELEASE_NOTES=${RELEASE_NOTES//|/%0A}
+ RELEASE_NOTES=${RELEASE_NOTES// - /%0A- }
+ RELEASE_NOTES=${RELEASE_NOTES// /%0A%0A}
+ RELEASE_NOTES=${RELEASE_NOTES//$'\n'/%0A}
+ RELEASE_NOTES=${RELEASE_NOTES//\"/%22}
+ RELEASE_NOTES=${RELEASE_NOTES//,/%2C}
+ RELEASE_NOTES=${RELEASE_NOTES//;/%3B}
+ dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION -p:ReleaseNotes="$RELEASE_NOTES" --output packages
+ # - name: Upload artifact
+ # uses: actions/upload-artifact@v4
+ # with:
+ # name: packages
+ # path: packages/*.nupkg
+
+ - name: NuGet login (OIDC → temp API key)
+ uses: NuGet/login@v1
+ id: login
+ with:
+ user: ${{ secrets.NUGET_USER }}
+ - name: Nuget push
+ run: dotnet nuget push packages/*.nupkg --api-key ${{steps.login.outputs.NUGET_API_KEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
+
+ - name: Git tag
+ run: |
+ git tag $VERSION
+ git push --tags
+ - name: Deployment Notification
+ env:
+ JSON: |
+ {
+ "status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
+ "message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
+ }
+ run: |
+ curl -X POST -H "Content-Type: application/json" -d "$JSON" ${{ secrets.DEPLOYED_WEBHOOK }}
diff --git a/.github/workflows/telegram-api.yml b/.github/workflows/telegram-api.yml
new file mode 100644
index 0000000..efd921a
--- /dev/null
+++ b/.github/workflows/telegram-api.yml
@@ -0,0 +1,29 @@
+name: 'Telegram API issues'
+
+on:
+ issues:
+ types: [labeled]
+
+permissions:
+ issues: write
+
+jobs:
+ action:
+ if: contains(github.event.issue.labels.*.name, 'telegram api')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/support-requests@v4
+ with:
+ support-label: 'telegram api'
+ issue-comment: >
+ Please note that **Github issues** should be used only for problems with the library code itself.
+
+
+ For questions about Telegram API usage, you can search the [API official documentation](https://core.telegram.org/api#getting-started) and the [full list of methods](https://core.telegram.org/methods).
+
+ WTelegramClient covers 100% of the API and let you do anything you can do in an official client.
+
+
+ If the above links didn't answer your problem, [click here to ask your question on **StackOverflow**](https://stackoverflow.com/questions/ask?tags=c%23+wtelegramclient+telegram-api) so the whole community can help and benefit.
+ close-issue: true
+ issue-close-reason: 'not planned'
diff --git a/.gitignore b/.gitignore
index 9491a2f..0c768ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,8 @@
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+launchSettings.json
+
# User-specific files
*.rsuser
*.suo
diff --git a/EXAMPLES.md b/EXAMPLES.md
index 1415cd4..d00c4b5 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -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:
```csharp
@@ -7,44 +7,60 @@ using System.Linq;
using TL;
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
-var myself = await client.LoginUserIfNeeded();
+await client.LoginUserIfNeeded();
```
In this case, environment variables are used for configuration so make sure to
-go to your **Project Properties > Debug > Environment variables**
-and add at least these variables with adequate value: **api_id, api_hash, phone_number**
+go to your **Project Properties > Debug > Launch Profiles > Environment variables**
+and add at least these variables with adequate values: **api_id, api_hash, phone_number**
Remember that these are just simple example codes that you should adjust to your needs.
-In real production code, you might want to properly test the success of each operation or handle exceptions.
+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.
-ℹ️ WTelegramClient covers 100% of Telegram Client API, much more than the examples below: check the [full API methods list](https://corefork.telegram.org/methods)!
-More examples can also be found in the [Examples folder](Examples) and in answers to [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient).
+➡️ Use Ctrl-F to search this page for the example matching your needs
+WTelegramClient covers 100% of Telegram Client API, much more than the examples below: check the [full API methods list](https://corefork.telegram.org/methods)!
+More examples can also be found in the [Examples folder](https://github.com/wiz0u/WTelegramClient/tree/master/Examples) and in answers to [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient).
+
+
+## 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)
-### Send a message to someone by @username
+## Send a message to someone by @username
```csharp
-var resolved = await client.Contacts_ResolveUsername("MyEch0_Bot"); // username without the @
+var resolved = await client.Contacts_ResolveUsername("JsonDumpBot"); // username without the @
await client.SendMessageAsync(resolved, "/start");
```
*Note: This also works if the @username points to a channel/group, but you must already have joined that channel before sending a message to it.
-If the username is invalid/unused, the API call raises an exception.*
-
-
-### 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.*
+If the username is invalid/unused, the API call raises an RpcException.*
-### Convert message to/from HTML or Markdown, and send it to ourself (Saved Messages)
+## Convert message to/from HTML or Markdown, and send it to ourself (Saved Messages)
```csharp
// HTML-formatted text:
-var text = $"Hello dear {HtmlText.Escape(myself.first_name)}\n" +
- "Enjoy this userbot written with WTelegramClient";
+var text = $"Hello dear {HtmlText.Escape(client.User.first_name)}\n" +
+ "Enjoy this userbot written with WTelegramClient";
var entities = client.HtmlToEntities(ref text);
var sent = await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
// if you need to convert a sent/received Message to HTML: (easier to store)
@@ -52,7 +68,7 @@ text = client.EntitiesToHtml(sent.message, sent.entities);
```
```csharp
// Markdown-style text:
-var text2 = $"Hello __dear *{Markdown.Escape(myself.first_name)}*__\n" +
+var text2 = $"Hello __dear *{Markdown.Escape(client.User.first_name)}*__\n" +
"Enjoy this `userbot` written with [WTelegramClient](https://github.com/wiz0u/WTelegramClient)";
var entities2 = client.MarkdownToEntities(ref text2);
var sent2 = await client.SendMessageAsync(InputPeer.Self, text2, entities: entities2);
@@ -60,10 +76,243 @@ var sent2 = await client.SendMessageAsync(InputPeer.Self, text2, entities: entit
text2 = client.EntitiesToMarkdown(sent2.message, sent2.entities);
```
See [HTML formatting style](https://core.telegram.org/bots/api/#html-style) and [MarkdownV2 formatting style](https://core.telegram.org/bots/api/#markdownv2-style) for details.
-*Note: For the `tg://user?id=` notation to work, that user's access hash must have been collected first ([see below](#collect-access-hash))*
+*Note: For the `tg://user?id=` notation to work, you need to pass the _users dictionary in arguments ([see below](#collect-users-chats))*
+
+
+## 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)`
+
+
+## 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)
+
+
+## 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*
+
+
+## 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);`
+
+
+## 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).
+
+
+## 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.
+
+
+## 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()`_
+
+
+## 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);
+```
+
+
+## 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.*
+
+
+## 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().Select(m => m.media).OfType().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
+{
+ 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*
+
+
+## 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);
+```
+
+
+## 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*
-### Fun with stickers, GIFs, dice, and animated emojies
+## Fun with stickers, GIFs, dice, and animated emojies
```csharp
// • List all stickerSets the user has added to his account
var allStickers = await client.Messages_GetAllStickers();
@@ -92,7 +341,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
var appConfig = await client.Help_GetAppConfig();
-var emojies_send_dice = appConfig["emojies_send_dice"] as string[];
+var emojies_send_dice = appConfig.config["emojies_send_dice"] as string[];
var dice_emoji = emojies_send_dice[new Random().Next(emojies_send_dice.Length)];
var diceMsg = await client.SendMessageAsync(InputPeer.Self, null, new InputMediaDice { emoticon = dice_emoji });
Console.WriteLine("Dice result:" + ((MessageMediaDice)diceMsg.media).value);
@@ -111,114 +360,44 @@ var typing = await client.Messages_SetTyping(InputPeer.Self, new SendMessageEmoj
await Task.Delay(5000);
```
-
-### List all chats (groups/channels NOT users) that we joined and send a message to one
+
+## Fun with custom emojies and reactions on pinned messages
```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](Examples/Program_GetAllChats.cs)
+// • Sending a message with custom emojies in Markdown to ourself:
+var text = "Vicksy says Hi! ";
+var entities = client.MarkdownToEntities(ref text);
+await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
+// also available in HTML: 👋
-
-### List all dialogs (chats/groups/channels/user chat) we are currently 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).
-
-
-### 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);
-```
-
-
-### 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";
+// • Fetch all available standard emoji reactions
+var all_emoji = await client.Messages_GetAvailableReactions();
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);
-```
+var chat = chats.chats[1234567890]; // the chat we want
-
-### 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().Select(m => m.media).OfType().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[]
+// • Check reactions available in this chat
+var full = await client.GetFullChat(chat);
+Reaction reaction = full.full_chat.AvailableReactions switch
{
- photoFromTelegram, // PhotoBase has implicit conversion to InputMediaPhoto
- new InputMediaUploadedPhoto { file = uploadedFile },
- new InputMediaPhotoExternal() { url = photoUrl },
+ 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
};
-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*
+if (reaction == null) return;
-
-### Get all 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);
+// • Send the selected reaction on the last 2 pinned messages
+var messages = await client.Messages_Search(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*
-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 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);
-```
-### Join a channel/group by their public name or invite link
+## Join a channel/group by their public name or invite link
* For a public channel/group `@channelname`
If you have a link of the form `https://t.me/channelname`, you need to extract the `channelname` from the URL.
You can resolve the channel/group username and join it like this:
@@ -233,21 +412,23 @@ To use them, you need to extract the `HASH` part from the URL and then you can u
```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 of public 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.
-### Add/Invite/Remove someone in a chat
+## Add/Invite/Remove someone in a chat
```csharp
var chats = await client.Messages_GetAllChats();
var chat = chats.chats[1234567890]; // the target chat
```
-After the above code, once you [have obtained](https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#access-hash) an `InputUser` or `User`, you can:
+After the above code, once you [have obtained](FAQ.md#access-hash) an `InputUser` or `User`, you can:
```csharp
// • Directly add the user to a Chat/Channel/group:
-await client.AddChatUser(chat, user);
-// You may get exception USER_PRIVACY_RESTRICTED if the user has denied the right to be added to a chat
-// or exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
+var miu = await client.AddChatUser(chat, user);
+// You may get exception USER_NOT_MUTUAL_CONTACT if the user left the chat previously and you want to add him again
+// or a result with miu.missing_invitees listing users that denied the right to be added to a chat
// • Obtain the main invite link for the chat, and send it to the user:
var mcf = await client.GetFullChat(chat);
@@ -265,24 +446,17 @@ await client.Messages_DeleteExportedChatInvite(chat, invite.link);
await client.DeleteChatUser(chat, user);
```
-
-### Get all messages (history) from a chat
+
+## Send a message to someone by phone number
```csharp
-var chats = await client.Messages_GetAllChats();
-InputPeer peer = chats.chats[1234567890]; // the chat 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)
- if (msgBase is Message msg)
- Console.WriteLine(msg.message);
- offset_id = messages.Messages[^1].ID;
-}
+var contacts = await client.Contacts_ImportContacts(new[] { new InputPhoneContact { phone = "+PHONENUMBER" } });
+if (contacts.imported.Length > 0)
+ await client.SendMessageAsync(contacts.users[contacts.imported[0].user_id], "Hello!");
```
+*Note: Don't use this method too much. To prevent spam, Telegram may restrict your ability to add new phone numbers or ban your account.*
-### Retrieve the current user's contacts list
+## Retrieve the current user's contacts list
There are two different methods. Here is the simpler one:
```csharp
var contacts = await client.Contacts_GetContacts();
@@ -310,127 +484,126 @@ finally
}
```
-
-### Monitor all Telegram events happening for the user
-
-This is done through the `client.OnUpdate` callback event.
-Your event handler implementation can either return `Task.CompletedTask` or be an `async Task` method.
-
-See [Examples/Program_ListenUpdates.cs](Examples/Program_ListenUpdates.cs).
-
-
-### Monitor new messages being posted in chats in real-time
-
-You have to handle `client.OnUpdate` 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.
-
-
-### 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)
-
-### Collect Access Hash and save them for later use
+
+
+## Collect Users/Chats description structures and access hash
-You can automate the collection of `access_hash` for the various resources obtained in response to API calls or Updates,
-so that you don't have to remember them by yourself or ask the API about them each time.
+Many API calls return a structure with a `users` and a `chats` field at the root of the structure.
+This is also the case for updates passed to `client.OnUpdates`.
+
+These two dictionaries give details *(including access hash)* about the various users/chats that will be typically referenced in subobjects deeper in the structure,
+typically in the form of a `Peer` object or a `user_id`/`chat_id` field.
+
+In such case, the root structure inherits the `IPeerResolver` interface, and you can use the `UserOrChat(peer)` method to resolve a `Peer`
+into either a `User` or `ChatBase` (`Chat`,`Channel`...) description structure *(depending on the kind of peer it was describing)*
+
+You can also use the `CollectUsersChats` helper method to collect these 2 fields into 2 aggregate dictionaries to remember details
+*(including access hashes)* about all the users/chats you've encountered so far.
+This method also helps dealing with [incomplete `min` structures](https://core.telegram.org/api/min).
+
+Example of usage:
+```csharp
+private Dictionary _users = new();
+private Dictionary _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*
+
+
+## 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.
-### 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/):
+## Use a proxy or MTProxy to connect to Telegram
+SOCKS/HTTPS proxies can be used through the `client.TcpHandler` delegate and a proxy library like [StarkSoftProxy](https://www.nuget.org/packages/StarkSoftProxy/) or [xNetStandard](https://www.nuget.org/packages/xNetStandard/):
```csharp
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
client.TcpHandler = async (address, port) =>
{
var proxy = new Socks5ProxyClient(ProxyHost, ProxyPort, ProxyUsername, ProxyPassword);
+ //var proxy = xNet.Socks5ProxyClient.Parse("host:port:username:password");
return proxy.CreateConnection(address, port);
};
-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);
-};
+await client.LoginUserIfNeeded();
```
MTProxy (MTProto proxy) can be used to prevent ISP blocking Telegram servers, through the `client.MTProxyUrl` property:
```csharp
using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
-client.MTProxyUrl = "http://t.me/proxy?server=...&port=...&secret=...";
-var myself = await client.LoginUserIfNeeded();
+client.MTProxyUrl = "https://t.me/proxy?server=...&port=...&secret=...";
+await client.LoginUserIfNeeded();
```
-You can find a list of working MTProxies in channels like [@ProxyMTProto](https://t.me/ProxyMTProto) or [@MTProxyT](https://t.me/MTProxyT) *(right-click the "Connect" buttons)*
+You can find a list of working MTProxies in channels like [@ProxyMTProto](https://t.me/s/ProxyMTProto) or [@MTProxyT](https://t.me/s/MTProxyT) *(right-click the "Connect" buttons)*
If your Telegram client is already connected to such MTPROTO proxy, you can also export its URL by clicking on the shield button  and then **⋮** > **Share**
*Note: WTelegramClient always uses transport obfuscation when connecting to Telegram servers, even without MTProxy*
-
-### 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 (THIS IS NOT RECOMMENDED as you won't be able to diagnose any upcoming problem):
-WTelegram.Helpers.Log = (lvl, str) => { };
-```
-
-### Change 2FA password
+## Change 2FA password
```csharp
const string old_password = "password"; // current password if any (unused otherwise)
const string new_password = "new_password"; // or null to disable 2FA
-var accountPassword = await client.Account_GetPassword();
-var password = accountPassword.current_algo == null ? null : await WTelegram.Client.InputCheckPassword(accountPassword, old_password);
-accountPassword.current_algo = null; // makes InputCheckPassword generate a new password
-var new_password_hash = new_password == null ? null : await WTelegram.Client.InputCheckPassword(accountPassword, new_password);
+var accountPwd = await client.Account_GetPassword();
+var password = accountPwd.current_algo == null ? null : await WTelegram.Client.InputCheckPassword(accountPwd, old_password);
+accountPwd.current_algo = null; // makes InputCheckPassword generate a new password
+var new_password_hash = new_password == null ? null : await WTelegram.Client.InputCheckPassword(accountPwd, new_password);
await client.Account_UpdatePasswordSettings(password, new Account_PasswordInputSettings
{
flags = Account_PasswordInputSettings.Flags.has_new_algo,
new_password_hash = new_password_hash?.A,
- new_algo = accountPassword.new_algo,
+ new_algo = accountPwd.new_algo,
hint = "new password hint",
});
```
-
-
-### 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(chat, limit: 2);
-foreach (var msg in messages.Messages)
- await client.Messages_SendReaction(chat, msg.ID, reaction);
-```
-
-### 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,
you can choose to store the session data somewhere else, like in a database.
@@ -438,4 +611,14 @@ you can choose to store the session data somewhere else, like in a database.
The WTelegram.Client constructor takes an optional `sessionStore` parameter to allow storing sessions in an alternate manner.
Use it to pass a custom Stream-derived class that will **read** (first initial call to Length & Read) and **store** (subsequent Writes) session data to database.
-You can find an example for such custom session store in [Examples/Program_Heroku.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_Heroku.cs#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?ts=4#L61)
+
+
+## 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).
diff --git a/Examples/ASPnet_webapp.zip b/Examples/ASPnet_webapp.zip
index 238bda9..f1183ce 100644
Binary files a/Examples/ASPnet_webapp.zip and b/Examples/ASPnet_webapp.zip differ
diff --git a/Examples/Program_CollectAccessHash.cs b/Examples/Program_CollectAccessHash.cs
deleted file mode 100644
index 9537987..0000000
--- a/Examples/Program_CollectAccessHash.cs
+++ /dev/null
@@ -1,82 +0,0 @@
-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(stateStream);
- foreach (var id_hash in savedState.Channels) client.SetAccessHashFor(id_hash.Key, id_hash.Value);
- foreach (var id_hash in savedState.Users) client.SetAccessHashFor(id_hash.Key, id_hash.Value);
- }
-
- Console.WriteLine("Connecting to Telegram...");
- await client.LoginUserIfNeeded();
-
- var durovAccessHash = client.GetAccessHashFor(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(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().ToList();
- savedState.Users = client.AllAccessHashesFor().ToList();
- using (var stateStream = File.Create(StateFilename))
- await JsonSerializer.SerializeAsync(stateStream, savedState);
- }
-
- class SavedState
- {
- public List> Channels { get; set; } = new();
- public List> Users { get; set; } = new();
- }
- }
-}
diff --git a/Examples/Program_DownloadSavedMedia.cs b/Examples/Program_DownloadSavedMedia.cs
index d8ca67d..00022aa 100644
--- a/Examples/Program_DownloadSavedMedia.cs
+++ b/Examples/Program_DownloadSavedMedia.cs
@@ -1,7 +1,6 @@
using System;
-using System.Collections.Generic;
using System.IO;
-using System.Linq;
+using System.Threading;
using System.Threading.Tasks;
using TL;
@@ -9,19 +8,20 @@ namespace WTelegramClientTest
{
static class Program_DownloadSavedMedia
{
- // go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
- static async Task Main(string[] args)
+ // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
+ static async Task Main(string[] _)
{
Console.WriteLine("The program will download photos/medias from messages you send/forward to yourself (Saved Messages)");
- using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
+ var cts = new CancellationTokenSource();
+ await using var client = new WTelegram.Client(Environment.GetEnvironmentVariable);
var user = await client.LoginUserIfNeeded();
- client.OnUpdate += Client_OnUpdate;
+ client.OnUpdates += Client_OnUpdates;
Console.ReadKey();
+ cts.Cancel();
- async Task Client_OnUpdate(IObject arg)
+ async Task Client_OnUpdates(UpdatesBase updates)
{
- if (arg is not Updates { updates: var updates } upd) return;
- foreach (var update in updates)
+ foreach (var update in updates.UpdateList)
{
if (update is not UpdateNewMessage { message: Message message })
continue; // if it's not about a new message, ignore the update
@@ -30,11 +30,11 @@ namespace WTelegramClientTest
if (message.media is MessageMediaDocument { document: Document document })
{
- int slash = document.mime_type.IndexOf('/'); // quick & dirty conversion from MIME type to file extension
- var filename = slash > 0 ? $"{document.id}.{document.mime_type[(slash + 1)..]}" : $"{document.id}.bin";
+ var filename = document.Filename; // use document original filename, or build a name from document ID & MIME type:
+ filename ??= $"{document.id}.{document.mime_type[(document.mime_type.IndexOf('/') + 1)..]}";
Console.WriteLine("Downloading " + filename);
using var fileStream = File.Create(filename);
- await client.DownloadFileAsync(document, fileStream);
+ await client.DownloadFileAsync(document, fileStream, progress: (p, t) => cts.Token.ThrowIfCancellationRequested());
Console.WriteLine("Download finished");
}
else if (message.media is MessageMediaPhoto { photo: Photo photo })
diff --git a/Examples/Program_GetAllChats.cs b/Examples/Program_GetAllChats.cs
index b39e31f..f9ce1ce 100644
--- a/Examples/Program_GetAllChats.cs
+++ b/Examples/Program_GetAllChats.cs
@@ -1,5 +1,4 @@
using System;
-using System.Linq;
using System.Threading.Tasks;
using TL;
@@ -10,7 +9,7 @@ namespace WTelegramClientTest
// This code is similar to what you should have obtained if you followed the README introduction
// I've just added a few comments to explain further what's going on
- // go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
+ // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static string Config(string what)
{
if (what == "api_id") return Environment.GetEnvironmentVariable("api_id");
@@ -25,7 +24,7 @@ namespace WTelegramClientTest
static async Task Main(string[] _)
{
- using var client = new WTelegram.Client(Config);
+ await using var client = new WTelegram.Client(Config);
var user = await client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {user.username ?? user.first_name + " " + user.last_name} (id {user.id})");
@@ -34,10 +33,10 @@ namespace WTelegramClientTest
foreach (var (id, chat) in chats.chats)
switch (chat)
{
- case Chat smallgroup when (smallgroup.flags & Chat.Flags.deactivated) == 0:
+ case Chat smallgroup when smallgroup.IsActive:
Console.WriteLine($"{id}: Small group: {smallgroup.title} with {smallgroup.participants_count} members");
break;
- case Channel channel when (channel.flags & Channel.Flags.broadcast) != 0:
+ case Channel channel when channel.IsChannel:
Console.WriteLine($"{id}: Channel {channel.username}: {channel.title}");
//Console.WriteLine($" → access_hash = {channel.access_hash:X}");
break;
diff --git a/Examples/Program_Heroku.cs b/Examples/Program_Heroku.cs
index cfdb551..4ad3740 100644
--- a/Examples/Program_Heroku.cs
+++ b/Examples/Program_Heroku.cs
@@ -8,7 +8,7 @@ using TL;
// This is an example userbot designed to run on a Heroku account with a PostgreSQL database for session storage
// This userbot simply answer "Pong" when someone sends him a "Ping" private message (or in Saved Messages)
-// To use/install/deploy this userbot, follow the steps at the end of this file
+// To use/install/deploy this userbot ➡️ follow the steps at the end of this file
// When run locally, close the window or type ALT-F4 to exit cleanly and save session (similar to Heroku SIGTERM)
namespace WTelegramClientTest
@@ -17,8 +17,8 @@ namespace WTelegramClientTest
{
static WTelegram.Client Client;
static User My;
- static readonly Dictionary Users = new();
- static readonly Dictionary Chats = new();
+ static readonly Dictionary Users = [];
+ static readonly Dictionary Chats = [];
// See steps at the end of this file to setup required Environment variables
static async Task Main(string[] _)
@@ -28,9 +28,9 @@ namespace WTelegramClientTest
var store = new PostgreStore(Environment.GetEnvironmentVariable("DATABASE_URL"), Environment.GetEnvironmentVariable("SESSION_NAME"));
// if DB does not contain a session yet, client will be run in interactive mode
Client = new WTelegram.Client(store.Length == 0 ? null : Environment.GetEnvironmentVariable, store);
- using (Client)
+ await using (Client)
{
- Client.OnUpdate += Client_OnUpdate;
+ Client.OnUpdates += Client_OnUpdates;
My = await Client.LoginUserIfNeeded();
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
var dialogs = await Client.Messages_GetAllDialogs();
@@ -39,9 +39,8 @@ namespace WTelegramClientTest
}
}
- private static async Task Client_OnUpdate(IObject arg)
+ private static async Task Client_OnUpdates(UpdatesBase updates)
{
- if (arg is not UpdatesBase updates) return;
updates.CollectUsersChats(Users, Chats);
foreach (var update in updates.UpdateList)
{
@@ -63,10 +62,8 @@ namespace WTelegramClientTest
{
private readonly NpgsqlConnection _sql;
private readonly string _sessionName;
- private byte[] _data;
- private int _dataLen;
- private DateTime _lastWrite;
- private Task _delayedWrite;
+ private readonly byte[] _data;
+ private readonly int _dataLen;
/// Heroku DB URL of the form "postgres://user:password@host:port/database"
/// Entry name for the session data in the WTelegram_sessions table (default: "Heroku")
@@ -76,7 +73,7 @@ namespace WTelegramClientTest
var parts = databaseUrl.Split(':', '/', '@');
_sql = new NpgsqlConnection($"User ID={parts[3]};Password={parts[4]};Host={parts[5]};Port={parts[6]};Database={parts[7]};Pooling=true;SSL Mode=Require;Trust Server Certificate=True;");
_sql.Open();
- using (var create = new NpgsqlCommand($"CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
+ using (var create = new NpgsqlCommand("CREATE TABLE IF NOT EXISTS WTelegram_sessions (name text NOT NULL PRIMARY KEY, data bytea)", _sql))
create.ExecuteNonQuery();
using var cmd = new NpgsqlCommand($"SELECT data FROM WTelegram_sessions WHERE name = '{_sessionName}'", _sql);
using var rdr = cmd.ExecuteReader();
@@ -86,7 +83,6 @@ namespace WTelegramClientTest
protected override void Dispose(bool disposing)
{
- _delayedWrite?.Wait();
_sql.Dispose();
}
@@ -98,18 +94,9 @@ namespace WTelegramClientTest
public override void Write(byte[] buffer, int offset, int count) // Write call and buffer modifications are done within a lock()
{
- _data = buffer; _dataLen = count;
- if (_delayedWrite != null) return;
- var left = 1000 - (int)(DateTime.UtcNow - _lastWrite).TotalMilliseconds;
- if (left < 0)
- {
- using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
- cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
- cmd.ExecuteNonQuery();
- _lastWrite = DateTime.UtcNow;
- }
- else // delay writings for a full second
- _delayedWrite = Task.Delay(left).ContinueWith(t => { lock (this) { _delayedWrite = null; Write(_data, 0, _dataLen); } });
+ using var cmd = new NpgsqlCommand($"INSERT INTO WTelegram_sessions (name, data) VALUES ('{_sessionName}', @data) ON CONFLICT (name) DO UPDATE SET data = EXCLUDED.data", _sql);
+ cmd.Parameters.AddWithValue("data", count == buffer.Length ? buffer : buffer[offset..(offset + count)]);
+ cmd.ExecuteNonQuery();
}
public override long Length => _dataLen;
@@ -135,7 +122,7 @@ HOW TO USE AND DEPLOY THIS EXAMPLE HEROKU USERBOT:
- In Visual Studio, Clone the Heroku git repository and add some standard .gitignore .gitattributes files
- In this repository folder, create a new .NET Console project with this Program.cs file
- Add these Nuget packages: WTelegramClient and Npgsql
-- In Project properties > Debug > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
+- In Project properties > Debug > Launch Profiles > Environment variables, configure the same values for DATABASE_URL, api_hash, phone_number
- Run the project in Visual Studio. The first time, it should ask you interactively for elements to complete the connection
- On the following runs, the PostgreSQL database contains the session data and it should connect automatically
- You can test the userbot by sending him "Ping" in private message (or saved messages). It should respond with "Pong"
diff --git a/Examples/Program_ListenUpdates.cs b/Examples/Program_ListenUpdates.cs
index fd25eb6..59abe49 100644
--- a/Examples/Program_ListenUpdates.cs
+++ b/Examples/Program_ListenUpdates.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Threading.Tasks;
using TL;
@@ -8,56 +7,54 @@ namespace WTelegramClientTest
static class Program_ListenUpdates
{
static WTelegram.Client Client;
+ static WTelegram.UpdateManager Manager;
static User My;
- static readonly Dictionary Users = new();
- static readonly Dictionary Chats = new();
- // go to Project Properties > Debug > Environment variables and add at least these: api_id, api_hash, phone_number
+ // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
static async Task Main(string[] _)
{
Console.WriteLine("The program will display updates received for the logged-in user. Press any key to terminate");
WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
- using (Client)
+ await using (Client)
{
- Client.OnUpdate += Client_OnUpdate;
+ Manager = Client.WithUpdateManager(Client_OnUpdate/*, "Updates.state"*/);
My = await Client.LoginUserIfNeeded();
- Users[My.id] = My;
// Note: on login, Telegram may sends a bunch of updates/messages that happened in the past and were not acknowledged
Console.WriteLine($"We are logged-in as {My.username ?? My.first_name + " " + My.last_name} (id {My.id})");
// We collect all infos about the users/chats so that updates can be printed with their names
var dialogs = await Client.Messages_GetAllDialogs(); // dialogs = groups/channels/users
- dialogs.CollectUsersChats(Users, Chats);
+ dialogs.CollectUsersChats(Manager.Users, Manager.Chats);
+
Console.ReadKey();
- }
+ } // WTelegram.Client gets disposed when exiting this scope
+
+ //Manager.SaveState("Updates.state"); // if you want to resume missed updates on the next run (see WithUpdateManager above)
}
// if not using async/await, we could just return Task.CompletedTask
- private static async Task Client_OnUpdate(IObject arg)
+ private static async Task Client_OnUpdate(Update update)
{
- if (arg is not UpdatesBase updates) return;
- updates.CollectUsersChats(Users, Chats);
- foreach (var update in updates.UpdateList)
- switch (update)
- {
- case UpdateNewMessage unm: await DisplayMessage(unm.message); break;
- case UpdateEditMessage uem: await DisplayMessage(uem.message, true); break;
- // Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
- case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break;
- case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break;
- case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break;
- case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break;
- case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break;
- case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break;
- case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break;
- case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: @{uun.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
- }
+ switch (update)
+ {
+ case UpdateNewMessage unm: await HandleMessage(unm.message); break;
+ case UpdateEditMessage uem: await HandleMessage(uem.message, true); break;
+ // Note: UpdateNewChannelMessage and UpdateEditChannelMessage are also handled by above cases
+ case UpdateDeleteChannelMessages udcm: Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {Chat(udcm.channel_id)}"); break;
+ case UpdateDeleteMessages udm: Console.WriteLine($"{udm.messages.Length} message(s) deleted"); break;
+ case UpdateUserTyping uut: Console.WriteLine($"{User(uut.user_id)} is {uut.action}"); break;
+ case UpdateChatUserTyping ucut: Console.WriteLine($"{Peer(ucut.from_id)} is {ucut.action} in {Chat(ucut.chat_id)}"); break;
+ case UpdateChannelUserTyping ucut2: Console.WriteLine($"{Peer(ucut2.from_id)} is {ucut2.action} in {Chat(ucut2.channel_id)}"); break;
+ case UpdateChatParticipants { participants: ChatParticipants cp }: Console.WriteLine($"{cp.participants.Length} participants in {Chat(cp.chat_id)}"); break;
+ case UpdateUserStatus uus: Console.WriteLine($"{User(uus.user_id)} is now {uus.status.GetType().Name[10..]}"); break;
+ case UpdateUserName uun: Console.WriteLine($"{User(uun.user_id)} has changed profile name: {uun.first_name} {uun.last_name}"); break;
+ case UpdateUser uu: Console.WriteLine($"{User(uu.user_id)} has changed infos/photo"); break;
+ default: Console.WriteLine(update.GetType().Name); break; // there are much more update types than the above example cases
+ }
}
// in this example method, we're not using async/await, so we just return Task.CompletedTask
- private static Task DisplayMessage(MessageBase messageBase, bool edit = false)
+ private static Task HandleMessage(MessageBase messageBase, bool edit = false)
{
if (edit) Console.Write("(Edit): ");
switch (messageBase)
@@ -68,9 +65,8 @@ namespace WTelegramClientTest
return Task.CompletedTask;
}
- private static string User(long id) => Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
- private static string Chat(long id) => Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
- private static string Peer(Peer peer) => peer is null ? null : peer is PeerUser user ? User(user.user_id)
- : peer is PeerChat or PeerChannel ? Chat(peer.ID) : $"Peer {peer.ID}";
+ private static string User(long id) => Manager.Users.TryGetValue(id, out var user) ? user.ToString() : $"User {id}";
+ private static string Chat(long id) => Manager.Chats.TryGetValue(id, out var chat) ? chat.ToString() : $"Chat {id}";
+ private static string Peer(Peer peer) => Manager.UserOrChat(peer)?.ToString() ?? $"Peer {peer?.ID}";
}
}
diff --git a/Examples/Program_ReactorError.cs b/Examples/Program_ReactorError.cs
new file mode 100644
index 0000000..d84b8a0
--- /dev/null
+++ b/Examples/Program_ReactorError.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Threading.Tasks;
+using TL;
+
+namespace WTelegramClientTest
+{
+ static class Program_ReactorError
+ {
+ static WTelegram.Client Client;
+
+ // go to Project Properties > Debug > Launch Profiles > Environment variables and add at least these: api_id, api_hash, phone_number
+ static async Task Main(string[] _)
+ {
+ Console.WriteLine("The program demonstrate how to handle ReactorError. Press any key to terminate");
+ WTelegram.Helpers.Log = (l, s) => System.Diagnostics.Debug.WriteLine(s);
+ try
+ {
+ await CreateAndConnect();
+ Console.ReadKey();
+ }
+ finally
+ {
+ if (Client != null) await Client.DisposeAsync();
+ }
+ }
+
+ private static async Task CreateAndConnect()
+ {
+ Client = new WTelegram.Client(Environment.GetEnvironmentVariable);
+ Client.OnUpdates += Client_OnUpdates;
+ Client.OnOther += Client_OnOther;
+ var my = await Client.LoginUserIfNeeded();
+ Console.WriteLine($"We are logged-in as " + my);
+ }
+
+ private static async Task Client_OnOther(IObject arg)
+ {
+ if (arg is ReactorError err)
+ {
+ // typically: network connection was totally lost
+ Console.WriteLine($"Fatal reactor error: {err.Exception.Message}");
+ while (true)
+ {
+ Console.WriteLine("Disposing the client and trying to reconnect in 5 seconds...");
+ if (Client != null) await Client.DisposeAsync();
+ Client = null;
+ await Task.Delay(5000);
+ try
+ {
+ await CreateAndConnect();
+ break;
+ }
+ catch (Exception ex) when (ex is not ObjectDisposedException)
+ {
+ Console.WriteLine("Connection still failing: " + ex.Message);
+ }
+ }
+ }
+ else
+ Console.WriteLine("Other: " + arg.GetType().Name);
+ }
+
+ private static Task Client_OnUpdates(UpdatesBase updates)
+ {
+ foreach (var update in updates.UpdateList)
+ Console.WriteLine(update.GetType().Name);
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/Examples/Program_SecretChats.cs b/Examples/Program_SecretChats.cs
new file mode 100644
index 0000000..cff9092
--- /dev/null
+++ b/Examples/Program_SecretChats.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using TL;
+using WTelegram;
+
+namespace WTelegramClientTest
+{
+ static class Program_SecretChats
+ {
+ static Client Client;
+ static SecretChats Secrets;
+ static ISecretChat ActiveChat; // the secret chat currently selected
+ static readonly Dictionary Users = [];
+ static readonly Dictionary 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 Initiate Secret Chat with user (see /users)
+/discard [delete] Terminate active secret chat [and delete history]
+/select 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);
+ }
+ }
+}
diff --git a/Examples/WinForms_app.zip b/Examples/WinForms_app.zip
index 35c9bc3..419b21b 100644
Binary files a/Examples/WinForms_app.zip and b/Examples/WinForms_app.zip differ
diff --git a/FAQ.md b/FAQ.md
index a0af502..76a889d 100644
--- a/FAQ.md
+++ b/FAQ.md
@@ -1,11 +1,13 @@
-## FAQ
+# FAQ
Before asking questions, make sure to **[read through the ReadMe first](README.md)**,
take a look at the [example programs](EXAMPLES.md) or [StackOverflow questions](https://stackoverflow.com/questions/tagged/wtelegramclient),
and refer to the [API method list](https://corefork.telegram.org/methods) for the full range of Telegram services available in this library.
+➡️ Use Ctrl-F to search this page for the information you seek
+
-#### 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.
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, ...).
@@ -14,13 +16,14 @@ 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.
-#### 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.
You could switch the current user via an `Auth_Logout` followed by a `LoginUserIfNeeded` but that would require the user to sign in with a verification_code each time.
-Instead, if you want to deal with multiple users from the same machine, the recommended solution is to have a different session file for each user. This can be done by having your Config callback reply with a different filename (or folder) for "**session_pathname**" for each user.
+Instead, if you want to deal with multiple users from the same machine, the recommended solution is to have a different session file for each user.
+This can be done by having your Config callback reply with a different filename (or folder) for "**session_pathname**" for each user.
This way, you can keep separate session files (each with their authentication keys) for each user.
If you need to manage these user accounts in parallel, you can create multiple instances of WTelegram.Client,
@@ -32,7 +35,7 @@ Your api_id/api_hash represents your application, and shouldn't change with each
-#### 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 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.
@@ -42,43 +45,48 @@ 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,
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)
-#### 4. Where to get the access_hash? Why the error `CHANNEL_INVALID` or `USER_ID_INVALID`?
+## 4. How to use IDs and access_hash? Why the error `CHANNEL_INVALID` or `USER_ID_INVALID`?
-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)
+⚠️ In Telegram Client API *(contrary to Bot API)*, you **cannot** interact with channels/users/etc. with only their IDs.
-> A small private `Chat` don't need an access_hash and can be queried using their `chat_id` only.
-However most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
+You also need to obtain their `access_hash` which is specific to the resource you want to access AND to the currently logged-in user.
+This serves as a proof that the logged-in user is entitled to access that channel/user/photo/document/...
+(otherwise, anybody with the ID could access it)
+
+> A small private group `Chat` don't need an access_hash and can be queried using their `chat_id` only.
+However most common chat groups are not `Chat` but a `Channel` supergroup (without the `broadcast` flag). See [Terminology in ReadMe](README.md#terminology).
Some TL methods only applies to private `Chat`, some only applies to `Channel` and some to both.
The `access_hash` must usually be provided within the `Input...` structure you pass in argument to an API method (`InputPeer`, `InputChannel`, `InputUser`, etc...).
-You obtain the `access_hash` through **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)*
-Once you obtained the description structure, there are 3 methods for building your `Input...` request structure:
-* **Recommended:** If you take a look at the **description structure** 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(id)`
+You obtain the `access_hash` through TL **description structures** like `Channel`, `User`, `Photo`, `Document` that you receive through updates
+or when you query them through API methods like `Messages_GetAllChats`, `Messages_GetAllDialogs`, `Contacts_ResolveUsername`, etc...
-⚠️ *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).*
+You can use the [`UserOrChat` and `CollectUsersChats` methods](EXAMPLES.md#collect-users-chats) to help you in obtaining/collecting
+the description structures you receive via API calls or updates.
+
+Once you obtained the description structure, there are 2 methods for building your `Input...` request structure:
+* **Recommended:** Just pass that description structure you already have, in place of the `Input...` argument, it will work!
+*The implicit conversion operators on base classes like `ChatBase/UserBase` will create the `Input...` structure for you automatically.*
+* Alternatively, you can manually create the `Input...` structure yourself by extracting the `access_hash` from the description structure
+
+*Note: An `access_hash` obtained from a User/Channel structure with flag `min` may not be usable for most requests. See [Min constructors](https://core.telegram.org/api/min).*
-#### 5. I need to test a feature that has been developed but not yet released in WTelegramClient nuget
+## 5. I need to test a feature that has been recently developed but seems not available in my program
-The developmental versions of the library are available through Azure DevOps as part of the Continuous Integration builds after each Github commit.
+The developmental versions of the library are now available as **pre-release** on Nuget (with `-dev` in the version number)
-You can access these versions for testing in your program by going to our [private nuget feeds](https://dev.azure.com/wiz0u/WTelegramClient/_artifacts/feed/WTelegramClient/NuGet/WTelegramClient),
-click on button "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)*
+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.
-#### 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.
On these separate test servers, all created accounts and chats are periodically deleted, so you shouldn't use them under normal circumstances.
@@ -95,11 +103,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.
-#### 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 errors FLOOD_WAIT_X or PEER_FLOOD, PHONE_NUMBER_BANNED, USER_DEACTIVATED_BAN. I can't import phone numbers.
You can get these kind of problems if you abuse Telegram [Terms of Service](https://telegram.org/tos), or the [API Terms of Service](https://core.telegram.org/api/terms), or make excessive requests.
You can try to wait more between the requests, wait for a day or two to see if the requests become possible again.
+
>ℹ️ For FLOOD_WAIT_X with X < 60 seconds (see `client.FloodRetryThreshold`), WTelegramClient will automatically wait the specified delay and retry the request for you.
For longer delays, you can catch the thrown `RpcException` and check the value of property X.
@@ -110,7 +119,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.
-#### 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.
@@ -135,23 +144,24 @@ Here are some advices from [another similar library](https://github.com/gotd/td/
Some additional advices from me:
-5. Avoid repetitive polling or repetitive sequence of actions/requests: Save the initial results of your queries, and update those results when you're informed of a change through `OnUpdate` events.
-6. 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 happens often with VoIP or other easy-to-buy-online numbers, so expect fast ban)
-7. 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.
-8. 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.
+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.
-9. 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.
+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.
-10. 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).
+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).
-#### 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`.
This difference is important to understand. Please [read about the Terminology in ReadMe](README.md#terminology).
You typically get the error `CHAT_ID_INVALID` when you try to call API methods designed specifically for a `Chat`, with the ID of a `Channel`.
-All API methods taking a `long api_id` as a direct method parameter are for Chats and cannot be used with Channels.
+All API methods taking a `long chat_id` as a direct method parameter are for Chats and cannot be used with Channels.
There is probably another method achieving the same result but specifically designed for Channels, and it will have a similar name, but beginning with `Channels_` ...
@@ -159,26 +169,28 @@ 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).
-#### 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[id]` raise an error:
-- The user account you're currently logged-in as 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.
+There can be several reasons why `chats.chats` doesn't contain the chat you expect:
+- You're searching for a user instead of a chat ID.
+Private messages with a user are not called "chats". See [Terminology in ReadMe](README.md#terminology).
+To obtain the list of users (as well as chats and channels) the logged-in user is currenly engaged in a discussion with, you should [use the API method `Messages_GetAllDialogs`](EXAMPLES.md#list-dialogs)
+- The currently logged-in user account has not joined this particular chat.
+API method [`Messages_GetAllChats`](https://corefork.telegram.org/method/messages.getAllChats) will only return those chat groups/channels the user is in, not all Telegram chat groups.
+If you're looking for other Telegram groups/channels/users, try API methods [`Contacts_ResolveUsername`](EXAMPLES.md#msg-by-name) or `Contacts_Search`
- You're trying to use a Bot API (or TDLib) numerical ID, like -1001234567890
Telegram Client API don't use these kind of IDs for chats. Remove the -100 prefix and try again with the rest (1234567890).
-- 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.
This is the case if you are logged-in as a brand new user account (that hasn't join any chat groups/channels)
or if you are connected to a Test DC (a Telegram datacenter server for tests) instead of Production DC
([read FAQ #6](#wrong-server) for more)
To help determine if `chats.chats` is empty or does not contain a certain chat, you should [dump the chat list to the screen](EXAMPLES.md#list-chats)
-or simply use a debugger: Place a breakpoint after the Messages_GetAllChats call, run the program up to there, and use a Watch pane to display the content of the chats.chats dictionary.
+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.
-#### 11. I get "Connection shut down" errors in my logs
+## 11. I get "Connection shut down" errors in my logs
There are various reasons why you may get this error. Here are the explanation and how to solve it:
@@ -192,16 +204,18 @@ If Telegram servers decide to shutdown this secondary connection, it's not an is
This should be transparent and pending API calls should automatically be resent upon reconnection.
You can choose to increase `MaxAutoReconnects` if it happens too often because your Internet connection is unstable.
-3) If you reach `MaxAutoReconnects` disconnections, then the **OnUpdate** event handler will receive a `ReactorError` object to notify you of the problem.
+3) If you reach `MaxAutoReconnects` disconnections or a reconnection fails, then the **OnOther** event handler will receive a `ReactorError` object to notify you of the problem,
and pending API calls throw the network IOException.
-In this case, the recommended action would be to dispose the client and recreate one
+In this case, the recommended action would be to dispose the client and recreate one (see example [Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs))
4) In case of slow Internet connection or if you break in the debugger for some time,
you might also get Connection shutdown because your client couldn't send Pings to Telegram in the allotted time.
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.
+
-#### 12. How to migrate from TLSharp? How to sign-in/sign-up/register account properly?
+## 12. How to migrate from TLSharp? How to sign-in/sign-up/register account properly?
First, make sure you read the [ReadMe documentation](README.md) completely, it contains essential information and a quick tutorial to easily understand how to correctly use the library.
@@ -220,42 +234,129 @@ In particular, it will detect and handle automatically and properly the various
* Login not necessary (when a session is resumed with an already logged-in user)
* Logout required (if you want to change the logged-in user)
* 2FA password required (your Config needs to provide "password")
+* Email registration procedure required (your Config needs to provide "email", "email_verification_code")
* Account registration/sign-up required (your Config needs to provide "first_name", "last_name")
-* Request to resend the verification code through alternate ways like SMS (if your Config answer an empty "verification_code" initially)
+* Request to resend the verification code through alternate ways (if your Config answer an empty "verification_code" initially)
* Transient failures, slowness to respond, wrong code/password, checks for encryption key safety, etc..
-Contrary to TLSharp, WTelegramClient supports MTProto v2.0 (more secured), transport obfuscation, protocol security checks, MTProto Proxy, real-time updates, multiple DC connections, API documentation in Intellisense...
+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...
-#### 13. How to host my userbot online?
+## 13. How to host my userbot online?
If you need your userbot to run 24/7, you would typically design your userbot as a Console program, compiled for Linux or Windows,
and hosted online on any [VPS Hosting](https://www.google.com/search?q=vps+hosting) (Virtual Private Server).
Pure WebApp hosts might not be adequate as they will recycle (stop) your app if there is no incoming HTTP requests.
There are many cheap VPS Hosting offers available, for example Heroku:
-See [Examples/Program_Heroku.cs](Examples/Program_Heroku.cs) for such an implementation and the steps to host/deploy it.
+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.
+
+
+## 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.
+
+
+## 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 ``, add the following line:
+ `latest`
+ - 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(this KeyValuePair 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.
-## Troubleshooting guide
+# Troubleshooting guide
Here is a list of common issues and how to fix them so that your program work correctly:
+
1) Are you using the Nuget package or the library source code?
It is not recommended to copy/compile the source code of the library for a normal usage.
When built in DEBUG mode, the source code connects to Telegram test servers (see also [FAQ #6](#wrong-server)).
So you can either:
- - **Recommended:** Use the [official Nuget package](https://www.nuget.org/packages/WTelegramClient) or the [private nuget feed of development builds](https://dev.azure.com/wiz0u/WTelegramClient/_artifacts/feed/WTelegramClient/NuGet/WTelegramClient)
+ - **Recommended:** Use the [official Nuget package](https://www.nuget.org/packages/WTelegramClient)
- Build your code in RELEASE mode
- Modify your config callback to reply to "server_address" with the IP address of Telegram production servers (as found on your API development tools)
-2) Did you call `LoginUserIfNeeded()`?
-If you don't authenticate as a user (or bot), you have access to a very limited subset of Telegram APIs
+2) Did you call `Login` or `LoginUserIfNeeded` succesfully?
+If you don't complete authentication as a user (or bot), you have access to a very limited subset of Telegram APIs.
+Make sure your calls succeed and don't throw an exception.
3) Did you use `await` with every Client methods?
-This library is completely Task-based. You should learn, understand and use the [asynchronous model of C# programming](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) before proceeding further.
+This library is completely Task-based. You should learn, understand and use the [asynchronous model of C# programming](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/) before proceeding further.
+Using `.Result` or `.Wait()` can lead to deadlocks.
4) Is your program ending immediately instead of waiting for Updates?
-Your program must be running/waiting continuously in order for the background Task to receive and process the Updates. So make sure your main program doesn't end immediately. For a console program, this is typical done by waiting for a key or some close event.
+Your program must be running/waiting continuously in order for the background Task to receive and process the Updates.
+So make sure your main program doesn't end immediately or dispose the client too soon (via `using`?).
+For a console program, this is typical done by waiting for a key or some close event.
5) Is every Telegram API call rejected? (typically with an exception message like `AUTH_RESTART`)
-The user authentification might have failed at some point (or the user revoked the authorization). It is therefore necessary to go through the authentification again. This can be done by deleting the WTelegram.session file, or at runtime by calling `client.Reset()`
+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()`
+
+
+# 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
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
index 08900bc..3265a97 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2021 Olivier Marcoux
+Copyright (c) 2021-2024 Olivier Marcoux
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 8f056d5..05aecf3 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,19 @@
-[](https://www.nuget.org/packages/WTelegramClient/)
-[](https://dev.azure.com/wiz0u/WTelegramClient/_build?definitionId=7)
-[](https://corefork.telegram.org/methods)
-[](https://dev.azure.com/wiz0u/WTelegramClient/_artifacts/feed/WTelegramClient/NuGet/WTelegramClient)
-[](https://t.me/WTelegramClient)
-[](http://t.me/WTelegramBot?start=donate)
+[](https://corefork.telegram.org/methods)
+[](https://www.nuget.org/packages/WTelegramClient/)
+[](https://www.nuget.org/packages/WTelegramClient/absoluteLatest)
+[](https://buymeacoffee.com/wizou)
-## _Telegram Client API library written 100% in C# and .NET Standard_
+## *Telegram Client API library written 100% in C# and .NET*
-This library allows you to connect to Telegram and control a user programmatically (or a bot, but [Telegram.Bot](https://github.com/TelegramBots/Telegram.Bot) is much easier for that).
+This library allows you to connect to Telegram and control a user programmatically (or a bot, but [WTelegramBot](https://www.nuget.org/packages/WTelegramBot) is much easier for that).
All the Telegram Client APIs (MTProto) are supported so you can do everything the user could do with a full Telegram GUI client.
+Library was developed solely by one unemployed guy. [Donations](https://buymeacoffee.com/wizou) or [Patreon memberships are welcome](https://patreon.com/wizou).
+
This ReadMe is a **quick but important tutorial** to learn the fundamentals about this library. Please read it all.
->⚠️ This library 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.
+> ⚠️ This library requires understanding advanced C# techniques such as **asynchronous programming** or **subclass pattern matching**...
+> If you are a beginner in C#, starting a project based on this library might not be a great idea.
# How to use
@@ -22,18 +22,22 @@ After installing WTelegramClient through [Nuget](https://www.nuget.org/packages/
static async Task Main(string[] _)
{
using var client = new WTelegram.Client();
- var my = await client.LoginUserIfNeeded();
- Console.WriteLine($"We are logged-in as {my.username ?? my.first_name + " " + my.last_name} (id {my.id})");
+ var myself = await client.LoginUserIfNeeded();
+ Console.WriteLine($"We are logged-in as {myself} (id {myself.id})");
}
```
-When run, this will prompt you interactively for your App **api_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.
+When run, this will prompt you interactively for your App **api_hash** and **api_id** (that you obtain through Telegram's
+[API development tools](https://my.telegram.org/apps) page) and try to connect to Telegram servers.
Those api hash/id represent your application and one can be used for handling many user accounts.
-Then it will attempt to sign-in *(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).
+Then it will attempt to sign-in *(login)* as a user for which you must enter the **phone_number** and the **verification_code**
+that will be sent to this user (for example through SMS, Email, or another Telegram client app the user is connected to).
If the verification succeeds but the phone number is unknown to Telegram, the user might be prompted to sign-up
*(register their account by accepting the Terms of Service)* and provide their **first_name** and **last_name**.
If the account already exists and has enabled two-step verification (2FA) a **password** might be required.
+In some case, Telegram may request that you associate an **email** with your account for receiving login verification codes,
+you may skip this step by leaving **email** empty, otherwise the email address will first receive an **email_verification_code**.
All these login scenarios are handled automatically within the call to `LoginUserIfNeeded`.
After login, you now have access to the **[full range of Telegram Client APIs](https://corefork.telegram.org/methods)**.
@@ -42,9 +46,11 @@ All those API methods require `using TL;` namespace and are called with an under
# 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.
-This is because WTelegramClient saves (typically in the encrypted file **bin\WTelegram.session**) its state and the authentication keys that were negotiated with Telegram so that you needn't sign-in again every time.
+This is because WTelegramClient saves (typically in the encrypted file **bin\WTelegram.session**) its state
+and the authentication keys that were negotiated with Telegram so that you needn't sign-in again every time.
-That file path is configurable (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.
+That file path is configurable (**session_pathname**), and under various circumstances *(changing user or server address, write permissions)*
+you may want to change it or simply delete the existing session file in order to restart the authentification process.
# Non-interactive configuration
Your next step will probably be to provide a configuration to the client so that the required elements are not prompted through the Console but answered by your program.
@@ -72,42 +78,63 @@ There are other configuration items that are queried to your method but returnin
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
-*(see [FAQ #3](https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#GUI) for WinForms)*
+*(see [FAQ #3](https://wiz0u.github.io/WTelegramClient/FAQ#GUI) for WinForms)*
Returning `""` for verification_code requests the resending of the code through another system (SMS or Call).
Another simple approach is to pass `Environment.GetEnvironmentVariable` as the config callback and define the configuration items as environment variables
*(undefined variables get the default `null` behavior)*.
Finally, if you want to redirect the library logs to your logger instead of the Console, you can install a delegate in the `WTelegram.Helpers.Log` static property.
-Its `int` argument is the log severity, compatible with the [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
->ℹ️ The Telegram API makes extensive usage of base and derived classes, so be ready to use the various syntaxes C# offer to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
+> The Telegram API makes extensive usage of base and derived classes, so be ready to use the various C# syntaxes
+to check/cast base classes into the more useful derived classes (`is`, `as`, `case DerivedType` )
-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.
+All the Telegram API classes/methods are fully documented through Intellisense: Place your mouse over a class/method name,
+or start typing the call arguments to see a tooltip displaying their description, the list of derived classes and a web link to the official API page.
-The Telegram [API object classes](https://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`.
+The Telegram [API object classes](https://corefork.telegram.org/schema) are defined in the `TL` namespace,
+and the [API functions](https://corefork.telegram.org/methods) are available as async methods of `Client`.
-Below is an example of calling the [messages.getAllChats](https://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:
+Below is an example of calling the [messages.getAllChats](https://corefork.telegram.org/method/messages.getAllChats) API function,
+enumerating the various groups/channels the user is in, and then using `client.SendMessageAsync` helper function to easily send a message:
```csharp
using TL;
...
var chats = await client.Messages_GetAllChats();
Console.WriteLine("This user has joined the following:");
foreach (var (id, chat) in chats.chats)
- switch (chat) // example of downcasting to their real classes:
- {
- case Chat basicChat when basicChat.IsActive:
- Console.WriteLine($"{id}: Basic chat: {basicChat.title} with {basicChat.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;
- }
+ if (chat.IsActive)
+ Console.WriteLine($"{id,10}: {chat}");
Console.Write("Type a chat ID to send a message: ");
long chatId = long.Parse(Console.ReadLine());
var target = chats.chats[chatId];
@@ -115,51 +142,68 @@ Console.WriteLine($"Sending a message in chat {chatId}: {target.Title}");
await client.SendMessageAsync(target, "Hello, World");
```
-➡️ You can find lots of useful code snippets in [EXAMPLES.md](https://github.com/wiz0u/WTelegramClient/blob/master/EXAMPLES.md) and in the [Examples subdirectory](https://github.com/wiz0u/WTelegramClient/tree/master/Examples).
+➡️ You can find lots of useful code snippets in [EXAMPLES](https://wiz0u.github.io/WTelegramClient/EXAMPLES)
+and more detailed programs in the [Examples subdirectory](https://github.com/wiz0u/WTelegramClient/tree/master/Examples).
+➡️ Check [the FAQ](https://wiz0u.github.io/WTelegramClient/FAQ#compile) if example codes don't compile correctly on your machine, or other troubleshooting.
# Terminology in Telegram Client API
In the API, Telegram uses some terms/classnames that can be confusing as they differ from the terms shown to end-users:
-- `Channel` : A (large or public) chat group *(sometimes called [supergroup](https://corefork.telegram.org/api/channel#supergroups))* or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (the `broadcast` flag differentiate those)
-- `Chat` : A private [basic chat group](https://corefork.telegram.org/api/channel#basic-groups) with less than 200 members (it may be migrated to a supergroup `Channel` with a new ID when it gets bigger or public, in which case the old `Chat` will still exist but be `deactivated`)
+- `Channel`: A (large or public) chat group *(sometimes called [supergroup](https://corefork.telegram.org/api/channel#supergroups))*,
+or a [broadcast channel](https://corefork.telegram.org/api/channel#channels) (the `broadcast` flag differentiate those)
+- `Chat`: A private [basic chat group](https://corefork.telegram.org/api/channel#basic-groups) with less than 200 members
+(it may be migrated to a supergroup `Channel` with a new ID when it gets bigger or public, in which case the old `Chat` will still exist but will be `deactivated`)
**⚠️ Most chat groups you see are really of type `Channel`, not `Chat`!**
-- chats : In plural or general meaning, it means either `Chat` or `Channel`
-- `Peer` : Either a `Chat`, a `Channel` or a `User`
-- Dialog : Status of chat with a `Peer` *(draft, last message, unread count, pinned...)*. It represents each line from your Telegram chat list.
-- DC (DataCenter) : There are a few datacenters depending on where in the world the user (or an uploaded media file) is from.
-- 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.
+- **chats**: In plural or general meaning, it means either `Chat` or `Channel` *(therefore, no private user discussions)*
+- `Peer`: Either a `Chat`, a `Channel` or a `User`
+- **Dialog**: Status of chat with a `Peer` *(draft, last message, unread count, pinned...)*. It represents each line from your Telegram chat list.
+- **Access Hash**: Telegram requires you to provide a specific `access_hash` for users, channels, and other resources before interacting with them.
+See [FAQ #4](https://wiz0u.github.io/WTelegramClient/FAQ#access-hash) to learn more about it.
+- **DC** (DataCenter): There are a few datacenters depending on where in the world the user (or an uploaded media file) is from.
+- **Session** or **Authorization**: Pairing between a device and a phone number. You can have several active sessions for the same phone number.
+- **Participant**: A member/subscriber of a chat group or channel
# Other things to know
-The Client class also offers an `OnUpdate` event that is triggered when Telegram servers sends Updates (like new messages or status), independently of your API requests. See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs)
+The Client class offers `OnUpdates` and `OnOther` events that are triggered when Telegram servers sends Updates (like new messages or status) or other notifications, independently of your API requests.
+You can also use the [UpdateManager class](https://wiz0u.github.io/WTelegramClient/FAQ#manager) to simplify the handling of such updates.
+See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ListenUpdates.cs?ts=4#L21) and [Examples/Program_ReactorError.cs](https://github.com/wiz0u/WTelegramClient/blob/master/Examples/Program_ReactorError.cs?ts=4#L30)
An invalid API request can result in a `RpcException` being raised, reflecting the [error code and status text](https://revgram.github.io/errors.html) of the problem.
-The other configuration items that you can override include: **session_pathname, session_key, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, user_id**
+To [prevent getting banned](https://wiz0u.github.io/WTelegramClient/FAQ#prevent-ban) during dev, you can connect to [test servers](https://docs.pyrogram.org/topics/test-servers), by adding this line in your Config callback:
+`case "server_address": return "2>149.154.167.40:443"; // test DC`
-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).
+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).
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**
# Library uses and limitations
-This library can be used for any Telegram scenarios including:
+This library can be used for any Telegram scenario including:
- Sequential or parallel automated steps based on API requests/responses
-- Real-time [monitoring](https://github.com/wiz0u/WTelegramClient/blob/master/EXAMPLES.md#updates) of incoming Updates/Messages
-- Download/upload of files/media
+- Real-time [monitoring](https://wiz0u.github.io/WTelegramClient/EXAMPLES#updates) of incoming Updates/Messages
+- [Download](https://wiz0u.github.io/WTelegramClient/EXAMPLES#download)/[upload](https://wiz0u.github.io/WTelegramClient/EXAMPLES#upload) of files/media
+- Exchange end-to-end encrypted messages/files in [Secret Chats](https://wiz0u.github.io/WTelegramClient/EXAMPLES#e2e)
- Building a full-featured interactive client
-It has been tested in a Console app, [in a WinForms app](https://github.com/wiz0u/WTelegramClient/blob/master/FAQ.md#gui),
-[in ASP.NET webservice](https://github.com/wiz0u/WTelegramClient/blob/master/EXAMPLES.md#logging), and in Xamarin/Android.
+It has been tested in a Console app, [in Windows Forms](https://wiz0u.github.io/WTelegramClient/Examples/WinForms_app.zip),
+[in ASP.NET webservice](https://wiz0u.github.io/WTelegramClient/Examples/ASPnet_webapp.zip), and in Xamarin/Android.
-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.
+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.
-Developers feedback is welcome in the Telegram support 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 read all this ReadMe, the [Frequently Asked Questions](https://wiz0u.github.io/WTelegramClient/FAQ),
+the [Examples codes](https://wiz0u.github.io/WTelegramClient/EXAMPLES) and still have questions, feedback is welcome in our Telegram group [@WTelegramClient](https://t.me/WTelegramClient)
-If you like this library, please [consider a donation](http://t.me/WTelegramBot?start=donate).❤ This will help the project keep going.
+If you like this library, you can [buy me a coffee](https://buymeacoffee.com/wizou) ❤ This will help the project keep going.
+
+© 2021-2026 Olivier Marcoux
diff --git a/generator/MTProtoGenerator.cs b/generator/MTProtoGenerator.cs
new file mode 100644
index 0000000..44c7c35
--- /dev/null
+++ b/generator/MTProtoGenerator.cs
@@ -0,0 +1,267 @@
+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 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>(); // 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> Table = new()")
+ .AppendLine("\t\t{");
+ methodsTL
+ .AppendLine("\t\tpublic static readonly Dictionary> 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 ? "