diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml
index a30001c..de5e145 100644
--- a/.github/workflows/dev.yml
+++ b/.github/workflows/dev.yml
@@ -12,14 +12,16 @@ env:
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: 100
+ fetch-depth: 30
- name: Determine version
run: |
- git fetch --depth=100 --tags
+ git fetch --depth=30 --tags
DESCR_TAG=$(git describe --tags)
COMMITS=${DESCR_TAG#*-}
COMMITS=${COMMITS%-*}
@@ -29,24 +31,37 @@ jobs:
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: Setup .NET
+ # uses: actions/setup-dotnet@v4
+ # with:
+ # dotnet-version: 8.0.x
- name: Pack
- run: dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION "-p:ReleaseNotes=\"$RELEASE_NOTES\"" --output packages
+ 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 ${{secrets.NUGETAPIKEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
+ 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(github.event.head_commit.message) }},
+ "status": "success", "complete": true, "commitMessage": ${{ toJSON(env.RELEASE_NOTES) }},
"message": "{ \"commitId\": \"${{ github.sha }}\", \"buildNumber\": \"${{ env.VERSION }}\", \"repoName\": \"${{ github.repository }}\"}"
}
run: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5d1519e..7bdbcd1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -21,7 +21,8 @@ jobs:
build:
runs-on: ubuntu-latest
permissions:
- contents: write # For git tag
+ contents: write # For git tag
+ id-token: write # enable GitHub OIDC token issuance for this job
steps:
- uses: actions/checkout@v4
with:
@@ -37,20 +38,45 @@ jobs:
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: dotnet pack $PROJECT_PATH --configuration $CONFIGURATION -p:Version=$VERSION "-p:ReleaseNotes=\"$RELEASE_NOTES\"" --output packages
+ 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 ${{secrets.NUGETAPIKEY}} --skip-duplicate --source https://api.nuget.org/v3/index.json
+ 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
index 9a69fd9..efd921a 100644
--- a/.github/workflows/telegram-api.yml
+++ b/.github/workflows/telegram-api.yml
@@ -12,7 +12,7 @@ jobs:
if: contains(github.event.issue.labels.*.name, 'telegram api')
runs-on: ubuntu-latest
steps:
- - uses: dessant/support-requests@v3.0.0
+ - uses: dessant/support-requests@v4
with:
support-label: 'telegram api'
issue-comment: >
@@ -26,3 +26,4 @@ jobs:
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/EXAMPLES.md b/EXAMPLES.md
index e32e10d..d00c4b5 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -210,7 +210,7 @@ that simplifies the download of a photo/document/file once you get a reference t
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._
+_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
@@ -224,6 +224,41 @@ 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
@@ -330,7 +365,7 @@ await Task.Delay(5000);
```csharp
// • Sending a message with custom emojies in Markdown to ourself:
var text = "Vicksy says Hi! ";
-var entities = client.MarkdownToEntities(ref text, premium: true);
+var entities = client.MarkdownToEntities(ref text);
await client.SendMessageAsync(InputPeer.Self, text, entities: entities);
// also available in HTML: 👋
diff --git a/Examples/Program_Heroku.cs b/Examples/Program_Heroku.cs
index 5ae5fe7..4ad3740 100644
--- a/Examples/Program_Heroku.cs
+++ b/Examples/Program_Heroku.cs
@@ -62,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")
@@ -85,7 +83,6 @@ namespace WTelegramClientTest
protected override void Dispose(bool disposing)
{
- _delayedWrite?.Wait();
_sql.Dispose();
}
@@ -97,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;
diff --git a/README.md b/README.md
index 32a183d..05aecf3 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,14 @@
-[](https://corefork.telegram.org/methods)
+[](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_*
+## *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 [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 are welcome](https://buymeacoffee.com/wizou).
+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.
@@ -206,4 +206,4 @@ the [Examples codes](https://wiz0u.github.io/WTelegramClient/EXAMPLES) and still
If you like this library, you can [buy me a coffee](https://buymeacoffee.com/wizou) ❤ This will help the project keep going.
-© 2021-2025 Olivier Marcoux
+© 2021-2026 Olivier Marcoux
diff --git a/generator/MTProtoGenerator.cs b/generator/MTProtoGenerator.cs
index 5cc7719..44c7c35 100644
--- a/generator/MTProtoGenerator.cs
+++ b/generator/MTProtoGenerator.cs
@@ -32,6 +32,7 @@ public class MTProtoGenerator : IIncrementalGenerator
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;")
@@ -46,6 +47,9 @@ public class MTProtoGenerator : IIncrementalGenerator
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)
{
@@ -54,7 +58,6 @@ public class MTProtoGenerator : IIncrementalGenerator
var tldef = symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass == tlDefAttribute);
if (tldef == null) continue;
var id = (uint)tldef.ConstructorArguments[0].Value;
- var inheritBefore = (bool?)tldef.NamedArguments.FirstOrDefault(k => k.Key == "inheritBefore").Value.Value ?? false;
StringBuilder writeTl = new(), readTL = new();
var ns = symbol.BaseType.ContainingNamespace.ToString();
var name = symbol.BaseType.Name;
@@ -80,15 +83,20 @@ public class MTProtoGenerator : IIncrementalGenerator
ns = symbol.ContainingNamespace.ToString();
name = symbol.Name;
if (!namespaces.TryGetValue(ns, out var classes)) namespaces[ns] = classes = [];
- if (name is "_Message" or "RpcResult" or "MsgCopy")
+ 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" && (ns != "TL.Methods" || name == "Ping"))
- tableTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}.ReadTL,");
+ else if (name != "Null")
+ {
+ if (ns == "TL.Methods")
+ methodsTL.AppendLine($"\t\t\t[0x{id:X8}] = {(ns == "TL" ? "" : ns + '.')}{name}{(symbol.IsGenericType ? "