();`
61 | - Add the following PollingService.cs class:
62 | ```csharp
63 | using Telegram.Bot;
64 | using Telegram.Bot.Polling;
65 |
66 | public class PollingService(ITelegramBotClient bot, UpdateHandler updateHandler) : BackgroundService
67 | {
68 | protected override async Task ExecuteAsync(CancellationToken stoppingToken)
69 | {
70 | await bot.ReceiveAsync(updateHandler, new ReceiverOptions { AllowedUpdates = [], DropPendingUpdates = true }, stoppingToken);
71 | }
72 | }
73 | ```
74 | - Make sure to implement your UpdateHandler class as deriving from `Telegram.Bot.Polling.IUpdateHandler`
75 | - Remember to `DeleteWebhookAsync` from Telegram Bot API when switching to WTelegramBot
76 | - You should also make sure your hosting service won't stop/recycle your app after some HTTP inactivity timeout.
77 | _(some host providers have an "always on" option, or alternatively you can ping your service with an HTTP request every 5 min to keep it alive)_
78 |
79 |
--------------------------------------------------------------------------------
/Examples/ConsoleApp/ConsoleApp.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | latest
7 | enable
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Examples/ConsoleApp/Program.cs:
--------------------------------------------------------------------------------
1 | // ----------------------------------------------------------------------------------------------
2 | // This example demonstrates a lot of things you cannot normally do with Telegram.Bot / Bot API
3 | // ----------------------------------------------------------------------------------------------
4 | using System.Text;
5 | using Telegram.Bot;
6 | using Telegram.Bot.Types;
7 | using Telegram.Bot.Types.Enums;
8 |
9 | // This code needs these 3 variables in Project Properties > Debug > Launch Profiles > Environment variables
10 | // Get your Api Id/Hash from https://my.telegram.org/apps
11 | int apiId = int.Parse(Environment.GetEnvironmentVariable("ApiId")!);
12 | string apiHash = Environment.GetEnvironmentVariable("ApiHash")!;
13 | string botToken = Environment.GetEnvironmentVariable("BotToken")!;
14 |
15 | StreamWriter WTelegramLogs = new StreamWriter("WTelegramBot.log", true, Encoding.UTF8) { AutoFlush = true };
16 | WTelegram.Helpers.Log = (lvl, str) => WTelegramLogs.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{"TDIWE!"[lvl]}] {str}");
17 |
18 | // Using SQLite DB for storage. Other DBs below (remember to add/uncomment the adequate PackageReference in .csproj)
19 | using var connection = new Microsoft.Data.Sqlite.SqliteConnection(@"Data Source=WTelegramBot.sqlite");
20 | //SQL Server: using var connection = new Microsoft.Data.SqlClient.SqlConnection(@"Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=PATH_TO.mdf;Integrated Security=True;Connect Timeout=60");
21 | //MySQL: using var connection = new MySql.Data.MySqlClient.MySqlConnection(@"Data Source=...");
22 | //PosgreSQL: using var connection = new Npgsql.NpgsqlConnection(@"Data Source=...");
23 |
24 | using var bot = new WTelegram.Bot(botToken, apiId, apiHash, connection);
25 | // use new WTelegramBotClient(...) instead, if you want the power of WTelegram with Telegram.Bot compatibility for existing code
26 | // use new TelegramBotClient(...) instead, if you just want Telegram.Bot classic code
27 | var my = await bot.GetMe();
28 | Console.WriteLine($"I am @{my.Username}");
29 |
30 | // get details about a user via the public username (even if not in discussion with bot)
31 | if (await bot.InputUser("@spotifysavebot") is { user_id: var userId })
32 | {
33 | var userDetails = await bot.GetChat(userId);
34 | var full = (TL.Users_UserFull)userDetails.TLInfo!;
35 | var tlUser = full.users[userId];
36 | var fullUser = full.full_user;
37 | if (tlUser.flags.HasFlag(TL.User.Flags.bot)) Console.WriteLine($"{tlUser} is a bot");
38 | if (tlUser.flags.HasFlag(TL.User.Flags.scam)) Console.WriteLine($"{tlUser} is reported as scam");
39 | if (tlUser.flags.HasFlag(TL.User.Flags.verified)) Console.WriteLine($"{tlUser} is verified");
40 | if (tlUser.flags.HasFlag(TL.User.Flags.restricted)) Console.WriteLine($"{tlUser} is restricted: {tlUser.restriction_reason?[0].reason}");
41 | if (fullUser.bot_info is { commands: { } botCommands })
42 | {
43 | Console.WriteLine($"{tlUser} has {botCommands.Length} bot commands:");
44 | foreach (var command in botCommands)
45 | Console.WriteLine($" /{command.command,-20} {command.description}");
46 | }
47 | }
48 |
49 | //---------------------------------------------------------------------------------------
50 | // get details about a public chat (even if bot is not a member of that chat)
51 | var chatDetails = await bot.GetChat("@tdlibchat");
52 | if (chatDetails.TLInfo is TL.Messages_ChatFull { full_chat: TL.ChannelFull channelFull })
53 | {
54 | Console.WriteLine($"@{chatDetails.Username} has {channelFull.participants_count} members, {channelFull.online_count} online");
55 | if (channelFull.slowmode_seconds > 0)
56 | Console.WriteLine($"@{chatDetails.Username} has slowmode enabled: {channelFull.slowmode_seconds} seconds");
57 | if (channelFull.available_reactions is TL.ChatReactionsAll { flags: TL.ChatReactionsAll.Flags.allow_custom })
58 | Console.WriteLine($"@{chatDetails.Username} allows custom emojis as reactions");
59 | }
60 |
61 | //---------------------------------------------------------------------------------------
62 | // get list of members (you can increase the limit but Telegram might also impose a limit anyway)
63 | var members = await bot.GetChatMemberList(chatDetails.Id, limit: 1000);
64 | Console.WriteLine($"I fetched the info of {members.Length} members");
65 |
66 | //---------------------------------------------------------------------------------------
67 | // get a range of posted messages
68 | var messages = await bot.GetMessagesById("@tginfoen", Enumerable.Range(1904, 5));
69 | Console.WriteLine($"I fetched {messages.Count} messages from @tginfoen:");
70 | foreach (var m in messages)
71 | Console.WriteLine($" {m.MessageId}: {m.Type}");
72 |
73 | //---------------------------------------------------------------------------------------
74 | // show some message info not accessible in Bot API
75 | var msg = messages[0];
76 | var tlMsg = msg.TLMessage() as TL.Message;
77 | Console.WriteLine($"Info for message {tlMsg.id}: Views = {tlMsg.views} Shares = {tlMsg.forwards} Pinned = {tlMsg.flags.HasFlag(TL.Message.Flags.pinned)}");
78 |
79 | Console.WriteLine("___________________________________________________\n");
80 | Console.WriteLine("I'm listening now. Send me a command in private or in a group where I am... Or press Escape to exit");
81 | await bot.DropPendingUpdates();
82 | bot.WantUnknownTLUpdates = true;
83 | bot.OnError += (e, s) => Console.Error.WriteLineAsync(e.ToString());
84 | bot.OnMessage += OnMessage;
85 | bot.OnUpdate += OnUpdate;
86 | while (Console.ReadKey(true).Key != ConsoleKey.Escape) { }
87 | Console.WriteLine("Exiting...");
88 |
89 |
90 | async Task OnMessage(WTelegram.Types.Message msg, UpdateType type)
91 | {
92 | if (msg.Text == null) return;
93 | var text = msg.Text.ToLower();
94 | // commands accepted:
95 | if (text == "/start")
96 | {
97 | //---> It's easy to reply to a message by giving its id to replyParameters: (was broken in Telegram.Bot v20.0.0)
98 | await bot.SendMessage(msg.Chat, $"Hello, {msg.From}!\nTry commands /pic /react /lastseen /getchat /setphoto", replyParameters: msg);
99 | }
100 | else if (text == "/pic")
101 | {
102 | //---> It's easy to send a file by id or by url by just passing the string: (was broken in Telegram.Bot v19.0.0)
103 | await bot.SendPhoto(msg.Chat, "https://picsum.photos/310/200.jpg"); // easily send file by URL or FileID
104 | }
105 | else if (text == "/react")
106 | {
107 | //---> It's easy to send reaction emojis by just giving the emoji string or id
108 | await bot.SetMessageReaction(msg.Chat, msg.MessageId, ["👍"]);
109 | }
110 | else if (text == "/lastseen")
111 | {
112 | //---> Show more user info that is normally not accessible in Bot API:
113 | var tlUser = msg.From?.TLUser();
114 | await bot.SendMessage(msg.Chat, $"Your last seen is: {tlUser?.status?.ToString()?[13..]}");
115 | }
116 | else if (text == "/getchat")
117 | {
118 | var chat = await bot.GetChat(msg.Chat);
119 | //---> Demonstrate how to serialize structure to Json, and post it in code
120 | var dump = System.Text.Json.JsonSerializer.Serialize(chat, JsonBotAPI.Options);
121 | dump = $"{TL.HtmlText.Escape(dump)}
";
122 | await bot.SendMessage(msg.Chat, dump, parseMode: ParseMode.Html);
123 | }
124 | else if (text == "/setphoto")
125 | {
126 | var prevPhotos = await bot.GetUserProfilePhotos(my.Id);
127 | var jpegData = await new HttpClient().GetByteArrayAsync("https://picsum.photos/256/256.jpg");
128 | await bot.SetMyPhoto(InputFile.FromStream(new MemoryStream(jpegData)));
129 | await bot.SendMessage(msg.Chat, "New bot profile photo set. Check my profile to see it. Restoring it in 20 seconds");
130 | if (prevPhotos.TotalCount > 0)
131 | {
132 | await Task.Delay(20000);
133 | await bot.SetMyPhoto(prevPhotos.Photos[0][^1].FileId); // restore previous photo
134 | await bot.SendMessage(msg.Chat, "Bot profile photo restored");
135 | }
136 | }
137 | }
138 |
139 | Task OnUpdate(WTelegram.Types.Update update)
140 | {
141 | if (update.Type == UpdateType.Unknown)
142 | {
143 | //---> Show some update types that are unsupported by Bot API but can be handled via TLUpdate
144 | if (update.TLUpdate is TL.UpdateDeleteChannelMessages udcm)
145 | Console.WriteLine($"{udcm.messages.Length} message(s) deleted in {bot.Chat(udcm.channel_id)?.Title}");
146 | else if (update.TLUpdate is TL.UpdateDeleteMessages udm)
147 | Console.WriteLine($"{udm.messages.Length} message(s) deleted in user chat or small private group");
148 | else if (update.TLUpdate is TL.UpdateReadChannelOutbox urco)
149 | Console.WriteLine($"Someone read {bot.Chat(urco.channel_id)?.Title} up to message {urco.max_id}");
150 | }
151 | return Task.CompletedTask;
152 | }
153 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wiz0u/WTelegramBot/373c09ccee04b0997de3d6eae87a7a7b4d22678d/LICENSE.txt
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://core.telegram.org/bots/api)
2 | [](https://www.nuget.org/packages/WTelegramBot/)
3 | [](https://www.nuget.org/packages/WTelegramBot/absoluteLatest)
4 | [](https://www.buymeacoffee.com/wizou)
5 |
6 | # Powerful Telegram Bot API library for .NET
7 |
8 | WTelegramBot is a full rewrite in pure C# of Telegram Bot API server, presenting the same methods as the Telegram.Bot library for easy [migration](https://github.com/wiz0u/WTelegramBot/blob/master/CHANGES.md).
9 |
10 | The library is built on top of [WTelegramClient](https://wiz0u.github.io/WTelegramClient) to connect directly to Telegram Client API and gives you additional control over your bot, updates and call methods normally impossible to use with Bot API.
11 |
12 | ## Advantages of WTelegram.Bot
13 |
14 | Using class `WTelegram.Bot` you have access to a clean set of developer-friendly methods to easily access the Bot API
15 |
16 | You can also call Client API methods that are possible for bots but not accessible from Bot API!
17 | Some examples:
18 | - Fetch past messages of group/channel
19 | - Get group/channel members list
20 | - Resolve user/chat usernames
21 | - Get full details of users/chats
22 | - Send/receive big files
23 | - Connect using a MTProxy
24 | - Change the bot's profile picture
25 |
26 | You also get access to raw Updates information from Client API, in addition to the usual Bot API updates.
27 | They contain more information than the limited set of Bot API updates!
28 | Some examples:
29 | - Detect deletion of messages _(not always immediate)_
30 | - Get more info on message media _(like date of original media upload, sticker duration, ...)_
31 | - Notification when your messages were read in a group
32 |
33 | See the [Example app](https://github.com/wiz0u/WTelegramBot/tree/master/Examples/ConsoleApp) for a nice demonstration of features.
34 |
35 | ➡️ There are still a lot of restrictions to bots, even via Client API, so don't expect to be able to do many fancy things
36 |
37 |
38 | ## Difference between classes `WTelegram.Bot` and `TelegramBotClient`
39 |
40 | The library contains a compatibility layer as `Telegram.Bot.WTelegramBotClient` inheriting from WTelegram.Bot.
41 | [Click here to easily migrate](https://github.com/wiz0u/WTelegramBot/blob/master/CHANGES.md) your existing Telegram.Bot code.
42 |
43 | If you're not migrating an existing codebase, it is recommended that you use `WTelegram.Bot` class directly.
44 | Here are the main differences:
45 | * The method names don't have the *Async suffix (even though they should still be invoked with `await`) so they are more close to official [Bot API method names](https://core.telegram.org/bots/api#available-methods).
46 | * The optional parameters follow a more logical order for developers, with the more rarely used optional parameters near the end.
47 | * There is no CancellationToken parameter because it doesn't make sense to abort an immediate TCP request to Client API.
48 | _(Even with HTTP Bot API, it didn't make much sense: You can use cancellationToken.ThrowIfCancellationRequested() at various points of your own code if you want it to be cancellable)_
49 | * In case of an error, WTelegram.Bot will throw `WTelegram.WTException` like `TL.RpcException` showing the raw Telegram error, instead of an ApiRequestException
50 | * `WTelegram.Bot` and `WTelegramBotClient` are `IDisposable`, so remember to call `.Dispose()`
51 |
52 | ## How to access the advanced features?
53 |
54 | The [Example app](https://github.com/wiz0u/WTelegramBot/tree/master/Examples/ConsoleApp) demonstrates all of the features below.
55 |
56 | On each Update/Message/User/Chat you receive, there is an extra field named "`TL...`" that contains the corresponding raw Client API structure, which may contain extra information not transcribed into the Bot API
57 |
58 | You can also enable property `WantUnknownTLUpdates` to receive updates that usually would have been silently ignored by Bot API
59 | (they will be posted as Update of type Unknown with the TLUpdate field filled)
60 |
61 | Some extended API calls can be made via `WTelegram.Bot` special methods:
62 | - `GetChatMemberList`: fetch a list of chat members
63 | - `GetMessagesById`: fetch posted messages (or range of messages) based on their message IDs
64 | - `InputUser`: can resolve a username into a user ID that you can then use with GetChat
65 | - `GetChat`: can obtain details about any group/channel based on their public name, or a user ID resolved by InputUser
66 | - `SetMyPhoto`: change the bot's profile picture
67 |
68 | Other extended API calls not usually accessible to Bot API can be made via the `Bot.Client` property which is the underlying [WTelegramClient](https://wiz0u.github.io/WTelegramClient/) instance.
69 | * This way, you can use new features available only in Client API latest layers without waiting months for it to be available in Bot API
70 |
71 | For more information about calling Client API methods, you can read that [library's documentation](https://wiz0u.github.io/WTelegramClient/EXAMPLES)
72 | or search through the [official Client API documentation](https://corefork.telegram.org/methods),
73 | but make sure to look for the mention "**Bots can use this method**" (other methods can't be called).
74 |
75 | > Note: If you want to experiment with these, you'll need to add a `using TL;` on top of your code, and these calls might throw `TL.RpcException` instead of `ApiRequestException`
76 |
77 | Some other `WTelegram.Bot` methods (for example, beginning with Input*) and extension methods can help you convert Bot API ids or structure to/from Client API.
78 |
79 |
80 | ## Help with the library
81 |
82 | This library is still quite new but I tested it extensively to make sure it covers all of the Bot API successfully.
83 |
84 | If you have questions about the (official) Bot API methods from TelegramBotClient, you can ask them in [Telegram.Bot support chat](https://t.me/joinchat/B35YY0QbLfd034CFnvCtCA).
85 |
86 | If your question is more specific to WTelegram.Bot, or an issue with library behaviour, you can ask them in [@WTelegramClient](https://t.me/WTelegramClient).
87 |
88 | If you like this library, you can [buy me a coffee](https://www.buymeacoffee.com/wizou) ❤ This will help the project keep going.
89 |
90 | © 2021-2025 Olivier Marcoux
91 |
--------------------------------------------------------------------------------
/WTelegramBot.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33530.505
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTelegramBot", "src\WTelegramBot.csproj", "{1237F115-59C4-4085-B724-F4B512B39ADD}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp", "Examples\ConsoleApp\ConsoleApp.csproj", "{1D2C245C-21DE-450B-9B2E-2E541A2B746A}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {1237F115-59C4-4085-B724-F4B512B39ADD}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {1D2C245C-21DE-450B-9B2E-2E541A2B746A}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {5E0CD176-0F53-4816-A223-B779B9EF9660}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wiz0u/WTelegramBot/373c09ccee04b0997de3d6eae87a7a7b4d22678d/logo.png
--------------------------------------------------------------------------------
/src/Bot.Updates.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using TL;
3 | using Chat = WTelegram.Types.Chat;
4 | using Message = WTelegram.Types.Message;
5 | using Update = WTelegram.Types.Update;
6 | using User = WTelegram.Types.User;
7 |
8 | namespace WTelegram;
9 |
10 | public partial class Bot
11 | {
12 | /// Converts Client API TL.Update to Bot Telegram.Bot.Types.Update
13 | protected async Task MakeUpdate(TL.Update update)
14 | {
15 | switch (update)
16 | {
17 | case UpdateNewMessage unm:
18 | if (unm.message is TL.Message msg && msg.flags.HasFlag(TL.Message.Flags.out_)) return null;
19 | bool isChannelPost = (await ChatFromPeer(unm.message.Peer))?.Type == ChatType.Channel;
20 | if (NotAllowed(isChannelPost ? UpdateType.ChannelPost : UpdateType.Message)) return null;
21 | var message = await MakeMessageAndReply(unm.message);
22 | if (message == null) return null;
23 | return isChannelPost ? new Update { ChannelPost = message, TLUpdate = update }
24 | : new Update { Message = message, TLUpdate = update };
25 | case UpdateEditMessage uem:
26 | if (uem.message is TL.Message emsg && emsg.flags.HasFlag(TL.Message.Flags.out_)) return null;
27 | isChannelPost = (await ChatFromPeer(uem.message.Peer))?.Type == ChatType.Channel;
28 | if (NotAllowed(isChannelPost ? UpdateType.ChannelPost : UpdateType.Message)) return null;
29 | return isChannelPost ? new Update { EditedChannelPost = await MakeMessageAndReply(uem.message), TLUpdate = update }
30 | : new Update { EditedMessage = await MakeMessageAndReply(uem.message), TLUpdate = update };
31 | case UpdateBotInlineQuery ubiq:
32 | if (NotAllowed(UpdateType.InlineQuery)) return null;
33 | return new Update
34 | {
35 | InlineQuery = new InlineQuery
36 | {
37 | Id = ubiq.query_id.ToString(),
38 | From = await UserOrResolve(ubiq.user_id),
39 | Query = ubiq.query,
40 | Offset = ubiq.offset,
41 | ChatType = ubiq.peer_type switch
42 | {
43 | InlineQueryPeerType.SameBotPM => ChatType.Sender,
44 | InlineQueryPeerType.PM or InlineQueryPeerType.BotPM => ChatType.Private,
45 | InlineQueryPeerType.Chat => ChatType.Group,
46 | InlineQueryPeerType.Megagroup => ChatType.Supergroup,
47 | InlineQueryPeerType.Broadcast => ChatType.Channel,
48 | _ => null,
49 | },
50 | Location = ubiq.geo.Location()
51 | },
52 | TLUpdate = update
53 | };
54 | case UpdateBotInlineSend ubis:
55 | if (NotAllowed(UpdateType.ChosenInlineResult)) return null;
56 | return new Update
57 | {
58 | ChosenInlineResult = new ChosenInlineResult
59 | {
60 | ResultId = ubis.id,
61 | From = await UserOrResolve(ubis.user_id),
62 | Location = ubis.geo.Location(),
63 | InlineMessageId = ubis.msg_id.InlineMessageId(),
64 | Query = ubis.query,
65 | },
66 | TLUpdate = update
67 | };
68 | case UpdateBotCallbackQuery ubcq:
69 | if (NotAllowed(UpdateType.CallbackQuery)) return null;
70 | return new Update
71 | {
72 | CallbackQuery = new CallbackQuery
73 | {
74 | Id = ubcq.query_id.ToString(),
75 | From = await UserOrResolve(ubcq.user_id),
76 | Message = await GetMIMessage(await ChatFromPeer(ubcq.peer, true), ubcq.msg_id, replyToo: true),
77 | ChatInstance = ubcq.chat_instance.ToString(),
78 | Data = ubcq.data.NullOrUtf8(),
79 | GameShortName = ubcq.game_short_name
80 | },
81 | TLUpdate = update
82 | };
83 | case UpdateInlineBotCallbackQuery ubicq:
84 | if (NotAllowed(UpdateType.CallbackQuery)) return null;
85 | return new Update
86 | {
87 | CallbackQuery = new CallbackQuery
88 | {
89 | Id = ubicq.query_id.ToString(),
90 | From = await UserOrResolve(ubicq.user_id),
91 | InlineMessageId = ubicq.msg_id.InlineMessageId(),
92 | ChatInstance = ubicq.chat_instance.ToString(),
93 | Data = ubicq.data.NullOrUtf8(),
94 | GameShortName = ubicq.game_short_name
95 | },
96 | TLUpdate = update
97 | };
98 | case UpdateChannelParticipant uchp:
99 | if (NotAllowed((uchp.new_participant ?? uchp.prev_participant)?.UserId == BotId ? UpdateType.MyChatMember : UpdateType.ChatMember)) return null;
100 | return MakeUpdate(new ChatMemberUpdated
101 | {
102 | Chat = await ChannelOrResolve(uchp.channel_id),
103 | From = await UserOrResolve(uchp.actor_id),
104 | Date = uchp.date,
105 | OldChatMember = uchp.prev_participant.ChatMember(await UserOrResolve((uchp.prev_participant ?? uchp.new_participant)!.UserId)),
106 | NewChatMember = uchp.new_participant.ChatMember(await UserOrResolve((uchp.new_participant ?? uchp.prev_participant)!.UserId)),
107 | InviteLink = await MakeChatInviteLink(uchp.invite),
108 | ViaJoinRequest = uchp.invite is ChatInvitePublicJoinRequests,
109 | ViaChatFolderInviteLink = uchp.flags.HasFlag(UpdateChannelParticipant.Flags.via_chatlist)
110 | }, update);
111 | case UpdateChatParticipant ucp:
112 | if (NotAllowed(ucp.new_participant.UserId == BotId ? UpdateType.MyChatMember : UpdateType.ChatMember)) return null;
113 | return MakeUpdate(new ChatMemberUpdated
114 | {
115 | Chat = await ChatOrResolve(ucp.chat_id),
116 | From = await UserOrResolve(ucp.actor_id),
117 | Date = ucp.date,
118 | OldChatMember = ucp.prev_participant.ChatMember(await UserOrResolve(ucp.prev_participant.UserId)),
119 | NewChatMember = ucp.new_participant.ChatMember(await UserOrResolve(ucp.new_participant.UserId)),
120 | InviteLink = await MakeChatInviteLink(ucp.invite)
121 | }, update);
122 | case UpdateBotStopped ubs:
123 | if (NotAllowed(ubs.user_id == BotId ? UpdateType.MyChatMember : UpdateType.ChatMember)) return null;
124 | var user = await UserOrResolve(ubs.user_id);
125 | var cmMember = new ChatMemberMember { User = user };
126 | var cmBanned = new ChatMemberBanned { User = user };
127 | return MakeUpdate(new ChatMemberUpdated
128 | {
129 | Chat = user.Chat(),
130 | From = user,
131 | Date = ubs.date,
132 | OldChatMember = ubs.stopped ? cmMember : cmBanned,
133 | NewChatMember = ubs.stopped ? cmBanned : cmMember
134 | }, update);
135 | case UpdateMessagePoll ump:
136 | if (NotAllowed(UpdateType.Poll)) return null;
137 | return new Update { Poll = MakePoll(ump.poll, ump.results), TLUpdate = update };
138 | case UpdateMessagePollVote umpv:
139 | if (NotAllowed(UpdateType.PollAnswer)) return null;
140 | return new Update
141 | {
142 | PollAnswer = new Telegram.Bot.Types.PollAnswer
143 | {
144 | PollId = umpv.poll_id.ToString(),
145 | VoterChat = umpv.peer is PeerChannel pc ? await ChannelOrResolve(pc.channel_id) : null,
146 | User = umpv.peer is PeerUser pu ? await UserOrResolve(pu.user_id) : null,
147 | OptionIds = [.. umpv.options.Select(o => (int)o[0])]
148 | },
149 | TLUpdate = update
150 | };
151 | case TL.UpdateBotChatInviteRequester ubcir:
152 | if (NotAllowed(UpdateType.ChatJoinRequest)) return null;
153 | return new Update
154 | {
155 | ChatJoinRequest = new Telegram.Bot.Types.ChatJoinRequest
156 | {
157 | Chat = (await ChatFromPeer(ubcir.peer))!,
158 | From = await UserOrResolve(ubcir.user_id),
159 | Date = ubcir.date,
160 | Bio = ubcir.about,
161 | UserChatId = ubcir.user_id,
162 | InviteLink = await MakeChatInviteLink(ubcir.invite)
163 | },
164 | TLUpdate = update
165 | };
166 | case TL.UpdateBotShippingQuery ubsq:
167 | if (NotAllowed(UpdateType.ShippingQuery)) return null;
168 | return new Update
169 | {
170 | ShippingQuery = new Telegram.Bot.Types.Payments.ShippingQuery
171 | {
172 | Id = ubsq.query_id.ToString(),
173 | From = await UserOrResolve(ubsq.user_id),
174 | InvoicePayload = Encoding.UTF8.GetString(ubsq.payload),
175 | ShippingAddress = ubsq.shipping_address.ShippingAddress()
176 | },
177 | TLUpdate = update
178 | };
179 | case TL.UpdateBotPrecheckoutQuery ubpq:
180 | if (NotAllowed(UpdateType.PreCheckoutQuery)) return null;
181 | return new Update
182 | {
183 | PreCheckoutQuery = new Telegram.Bot.Types.Payments.PreCheckoutQuery
184 | {
185 | Id = ubpq.query_id.ToString(),
186 | From = await UserOrResolve(ubpq.user_id),
187 | Currency = ubpq.currency,
188 | TotalAmount = (int)ubpq.total_amount,
189 | InvoicePayload = Encoding.UTF8.GetString(ubpq.payload),
190 | ShippingOptionId = ubpq.shipping_option_id,
191 | OrderInfo = ubpq.info.OrderInfo()
192 | },
193 | TLUpdate = update
194 | };
195 | case TL.UpdateBotBusinessConnect ubbc:
196 | if (NotAllowed(UpdateType.BusinessConnection)) return null;
197 | return new Update { BusinessConnection = await MakeBusinessConnection(ubbc.connection), TLUpdate = update };
198 | case TL.UpdateBotNewBusinessMessage ubnbm:
199 | if (NotAllowed(UpdateType.BusinessMessage)) return null;
200 | var replyToMessage = await MakeMessage(ubnbm.reply_to_message);
201 | if (replyToMessage != null) replyToMessage.BusinessConnectionId = ubnbm.connection_id;
202 | message = await MakeMessageAndReply(ubnbm.message, replyToMessage, ubnbm.connection_id);
203 | return message == null ? null : new Update { BusinessMessage = message, TLUpdate = update };
204 | case TL.UpdateBotEditBusinessMessage ubebm:
205 | if (NotAllowed(UpdateType.EditedBusinessMessage)) return null;
206 | replyToMessage = await MakeMessage(ubebm.reply_to_message);
207 | if (replyToMessage != null) replyToMessage.BusinessConnectionId = ubebm.connection_id;
208 | message = await MakeMessageAndReply(ubebm.message, replyToMessage, ubebm.connection_id);
209 | return message == null ? null : new Update { EditedBusinessMessage = message, TLUpdate = update };
210 | case TL.UpdateBotDeleteBusinessMessage ubdbm:
211 | if (NotAllowed(UpdateType.DeletedBusinessMessages)) return null;
212 | return new Update
213 | {
214 | DeletedBusinessMessages = new BusinessMessagesDeleted
215 | {
216 | BusinessConnectionId = ubdbm.connection_id,
217 | Chat = await ChatFromPeer(ubdbm.peer, true),
218 | MessageIds = ubdbm.messages
219 | },
220 | TLUpdate = update
221 | };
222 | case TL.UpdateBotMessageReaction ubmr:
223 | if (NotAllowed(UpdateType.MessageReaction)) return null;
224 | return new Update
225 | {
226 | MessageReaction = new MessageReactionUpdated
227 | {
228 | Chat = await ChatFromPeer(ubmr.peer, true),
229 | MessageId = ubmr.msg_id,
230 | User = await UserFromPeer(ubmr.actor),
231 | ActorChat = await ChatFromPeer(ubmr.actor),
232 | Date = ubmr.date,
233 | OldReaction = [.. ubmr.old_reactions.Select(TypesTLConverters.ReactionType)],
234 | NewReaction = [.. ubmr.new_reactions.Select(TypesTLConverters.ReactionType)],
235 | },
236 | TLUpdate = update
237 | };
238 | case TL.UpdateBotMessageReactions ubmrs:
239 | if (NotAllowed(UpdateType.MessageReactionCount)) return null;
240 | return new Update
241 | {
242 | MessageReactionCount = new MessageReactionCountUpdated
243 | {
244 | Chat = await ChatFromPeer(ubmrs.peer, true),
245 | MessageId = ubmrs.msg_id,
246 | Date = ubmrs.date,
247 | Reactions = [.. ubmrs.reactions.Select(rc => new Telegram.Bot.Types.ReactionCount { Type = rc.reaction.ReactionType(), TotalCount = rc.count })],
248 | },
249 | TLUpdate = update
250 | };
251 | case TL.UpdateBotChatBoost ubcb:
252 | bool expired = ubcb.boost.expires < ubcb.boost.date;
253 | if (NotAllowed(expired ? UpdateType.RemovedChatBoost : UpdateType.ChatBoost)) return null;
254 | var cb = new ChatBoostUpdated
255 | {
256 | Chat = await ChatFromPeer(ubcb.peer, true),
257 | Boost = await MakeBoost(ubcb.boost)
258 | };
259 | return new Update
260 | {
261 | ChatBoost = expired ? null : cb,
262 | RemovedChatBoost = !expired ? null : new ChatBoostRemoved
263 | {
264 | Chat = cb.Chat,
265 | BoostId = cb.Boost.BoostId,
266 | RemoveDate = cb.Boost.AddDate,
267 | Source = cb.Boost.Source,
268 | },
269 | TLUpdate = update
270 | };
271 | case TL.UpdateBotPurchasedPaidMedia ubppm:
272 | if (NotAllowed(UpdateType.PurchasedPaidMedia)) return null;
273 | return new Update
274 | {
275 | PurchasedPaidMedia = new PaidMediaPurchased
276 | {
277 | From = await UserOrResolve(ubppm.user_id),
278 | PaidMediaPayload = ubppm.payload,
279 | },
280 | TLUpdate = update
281 | };
282 | //TL.UpdateDraftMessage seems used to update ourself user info
283 | default:
284 | return null;
285 | }
286 | }
287 |
288 | private Update? MakeUpdate(ChatMemberUpdated chatMember, TL.Update update) => chatMember.NewChatMember?.User.Id == BotId
289 | ? new Update { MyChatMember = chatMember, TLUpdate = update }
290 | : new Update { ChatMember = chatMember, TLUpdate = update };
291 |
292 | [return: NotNullIfNotNull(nameof(invite))]
293 | private async Task MakeChatInviteLink(ExportedChatInvite? invite)
294 | => invite switch
295 | {
296 | null => null,
297 | ChatInvitePublicJoinRequests => null,
298 | ChatInviteExported cie => new ChatInviteLink
299 | {
300 | InviteLink = cie.link,
301 | Creator = await UserOrResolve(cie.admin_id),
302 | CreatesJoinRequest = cie.flags.HasFlag(ChatInviteExported.Flags.request_needed),
303 | IsPrimary = cie.flags.HasFlag(ChatInviteExported.Flags.permanent),
304 | IsRevoked = cie.flags.HasFlag(ChatInviteExported.Flags.revoked),
305 | Name = cie.title,
306 | ExpireDate = cie.expire_date.NullIfDefault(),
307 | MemberLimit = cie.usage_limit.NullIfZero(),
308 | PendingJoinRequestCount = cie.flags.HasFlag(ChatInviteExported.Flags.has_requested) ? cie.requested : null,
309 | SubscriptionPeriod = cie.subscription_pricing?.period,
310 | SubscriptionPrice = cie.subscription_pricing is { amount: var amount } ? (int)amount : null,
311 | },
312 | _ => throw new WTException("Unexpected ExportedChatInvite: " + invite)
313 | };
314 |
315 | /// User or a stub on failure
316 | public async Task UserOrResolve(long userId)
317 | {
318 | lock (_users)
319 | if (_users.TryGetValue(userId, out var user))
320 | return user;
321 | try
322 | {
323 | var users = await Client.Users_GetUsers(new InputUser(userId, 0));
324 | if (users.Length != 0 && users[0] is TL.User user)
325 | lock (_users)
326 | return _users[userId] = user.User();
327 | }
328 | catch (RpcException) { }
329 | return new User { Id = userId, FirstName = "" };
330 | }
331 |
332 | /// null if peer is not PeerUser ; User or a stub on failure
333 | private async Task UserFromPeer(Peer peer) => peer is not PeerUser pu ? null : await UserOrResolve(pu.user_id);
334 |
335 | private async Task ChannelOrResolve(long id)
336 | {
337 | if (Chat(id) is { } chat)
338 | return chat;
339 | try
340 | {
341 | var chats = await Client.Channels_GetChannels(new InputChannel(id, 0));
342 | if (chats.chats.TryGetValue(id, out var chatBase))
343 | lock (_chats)
344 | return _chats[id] = chatBase.Chat();
345 | }
346 | catch (RpcException) { }
347 | return new Chat { Id = ZERO_CHANNEL_ID - id, Type = ChatType.Supergroup };
348 | }
349 |
350 | private async Task ChatOrResolve(long chatId)
351 | {
352 | if (Chat(chatId) is { } chat)
353 | return chat;
354 | try
355 | {
356 | var chats = await Client.Messages_GetChats(chatId);
357 | if (chats.chats.TryGetValue(chatId, out var chatBase))
358 | lock (_chats)
359 | return _chats[chatId] = chatBase.Chat();
360 | }
361 | catch (RpcException) { }
362 | return new Chat { Id = -chatId, Type = ChatType.Group };
363 | }
364 |
365 | private async Task ChatFromPeer(Peer? peer, [DoesNotReturnIf(true)] bool allowUser = false) => peer switch
366 | {
367 | null => null,
368 | PeerUser pu => allowUser ? (await UserOrResolve(pu.user_id)).Chat() : null,
369 | PeerChannel pc => await ChannelOrResolve(pc.channel_id),
370 | _ => await ChatOrResolve(peer.ID),
371 | };
372 |
373 | private async Task ChatFromPeer(InputPeer peer) => peer switch
374 | {
375 | InputPeerUser pu => (await UserOrResolve(pu.user_id)).Chat(),
376 | InputPeerChannel ipc => await ChannelOrResolve(ipc.channel_id),
377 | _ => await ChatOrResolve(peer.ID)
378 | };
379 |
380 | /// Handle UpdatesBase returned by various Client API and build the returned Bot Message
381 | protected async Task PostedMsg(Task updatesTask, InputPeer peer, string? text = null, Message? replyToMessage = null, string? bConnId = null)
382 | {
383 | var updates = await updatesTask;
384 | updates.UserOrChat(_collector);
385 | if (updates is UpdateShortSentMessage sent)
386 | return await FillTextAndMedia(new Message
387 | {
388 | Id = sent.id,
389 | From = await UserOrResolve(BotId),
390 | Date = sent.date,
391 | Chat = await ChatFromPeer(peer)!,
392 | ReplyToMessage = replyToMessage
393 | }, text, sent.entities, sent.media);
394 | foreach (var update in updates.UpdateList)
395 | {
396 | switch (update)
397 | {
398 | case UpdateNewMessage { message: { } message }: return (await MakeMessageAndReply(message, replyToMessage))!;
399 | case UpdateNewScheduledMessage { message: { } schedMsg }: return (await MakeMessageAndReply(schedMsg, replyToMessage))!;
400 | case UpdateEditMessage { message: { } editMsg }: return (await MakeMessageAndReply(editMsg, replyToMessage))!;
401 | case UpdateBotNewBusinessMessage { message: { } bizMsg }: return (await MakeMessageAndReply(bizMsg, replyToMessage, bConnId))!;
402 | }
403 | }
404 | throw new WTException("Failed to retrieve sent message");
405 | }
406 |
407 | private async Task PostedMsgs(Task updatesTask, int nbMsg, long startRandomId, Message? replyToMessage, string? bConnId = null)
408 | {
409 | var updates = await updatesTask;
410 | updates.UserOrChat(_collector);
411 | var result = new List(nbMsg);
412 | foreach (var update in updates.UpdateList)
413 | {
414 | Message? msg = null;
415 | switch (update)
416 | {
417 | case UpdateNewMessage { message: TL.Message message }: msg = await MakeMessageAndReply(message, replyToMessage); break;
418 | case UpdateNewScheduledMessage { message: TL.Message schedMsg }: msg = await MakeMessageAndReply(schedMsg, replyToMessage); break;
419 | case UpdateBotNewBusinessMessage { message: { } bizMsg } biz: msg = await MakeMessageAndReply(bizMsg, replyToMessage, bConnId); break;
420 | }
421 | if (msg != null) result.Add(msg);
422 | }
423 | return [.. result.OrderBy(msg => msg.MessageId)];
424 | }
425 |
426 | /// Converts Client API TL.MessageBase to Bot Telegram.Bot.Types.Message and assign the ReplyToMessage/ExternalReply
427 | public async Task MakeMessageAndReply(MessageBase? msgBase, Message? replyToMessage = null, string? bConnId = null)
428 | {
429 | var msg = await MakeMessage(msgBase);
430 | if (msg == null) return null;
431 | msg.BusinessConnectionId = bConnId;
432 | if (msgBase?.ReplyTo == null) return msg;
433 | if (msgBase.ReplyTo is MessageReplyHeader reply_to)
434 | {
435 | if (replyToMessage != null)
436 | msg.ReplyToMessage = replyToMessage;
437 | else if (reply_to.reply_to_msg_id > 0 && reply_to.reply_from == null)
438 | {
439 | var replyToPeer = reply_to.reply_to_peer_id ?? msgBase.Peer;
440 | msg.ReplyToMessage = await GetMessage(await ChatFromPeer(replyToPeer, true), reply_to.reply_to_msg_id);
441 | }
442 | else if (reply_to.reply_to_top_id > 0)
443 | msg.ReplyToMessage = await GetMessage(await ChatFromPeer(msgBase.Peer, true), reply_to.reply_to_top_id);
444 | if (reply_to.reply_from?.date > default(DateTime))
445 | {
446 | var ext = await FillTextAndMedia(new Message(), null, null!, reply_to.reply_media);
447 | msg.ExternalReply = new ExternalReplyInfo
448 | {
449 | MessageId = reply_to.reply_to_msg_id,
450 | Chat = await ChatFromPeer(reply_to.reply_to_peer_id),
451 | HasMediaSpoiler = ext.HasMediaSpoiler,
452 | LinkPreviewOptions = ext.LinkPreviewOptions,
453 | Origin = (await MakeOrigin(reply_to.reply_from))!,
454 | Animation = ext.Animation, Audio = ext.Audio, Contact = ext.Contact, Dice = ext.Dice, Document = ext.Document,
455 | Game = ext.Game, Giveaway = ext.Giveaway, GiveawayWinners = ext.GiveawayWinners, Invoice = ext.Invoice,
456 | Location = ext.Location, Photo = ext.Photo, Poll = ext.Poll, Sticker = ext.Sticker, Story = ext.Story,
457 | Venue = ext.Venue, Video = ext.Video, VideoNote = ext.VideoNote, Voice = ext.Voice, PaidMedia = ext.PaidMedia
458 | };
459 | }
460 | if (reply_to.quote_text != null)
461 | msg.Quote = new TextQuote
462 | {
463 | Text = reply_to.quote_text,
464 | Entities = MakeEntities(reply_to.quote_entities),
465 | Position = reply_to.quote_offset,
466 | IsManual = reply_to.flags.HasFlag(MessageReplyHeader.Flags.quote)
467 | };
468 | if (msg.IsTopicMessage |= reply_to.flags.HasFlag(MessageReplyHeader.Flags.forum_topic))
469 | msg.MessageThreadId = reply_to.reply_to_top_id > 0 ? reply_to.reply_to_top_id : reply_to.reply_to_msg_id;
470 | }
471 | else if (msgBase.ReplyTo is MessageReplyStoryHeader mrsh)
472 | msg.ReplyToStory = new Story
473 | {
474 | Chat = await ChatFromPeer(mrsh.peer, true),
475 | Id = mrsh.story_id
476 | };
477 | return msg;
478 | }
479 |
480 | /// Converts Client API TL.MessageBase to Bot Telegram.Bot.Types.Message
481 | [return: NotNullIfNotNull(nameof(msgBase))]
482 | protected async Task MakeMessage(MessageBase? msgBase)
483 | {
484 | switch (msgBase)
485 | {
486 | case TL.Message message:
487 | var msg = new WTelegram.Types.Message
488 | {
489 | TLMessage = message,
490 | Id = message.flags.HasFlag(TL.Message.Flags.from_scheduled) ? 0 : message.id,
491 | From = await UserFromPeer(message.from_id),
492 | SenderChat = await ChatFromPeer(message.from_id),
493 | Date = message.date,
494 | Chat = await ChatFromPeer(message.peer_id, allowUser: true),
495 | AuthorSignature = message.post_author,
496 | ReplyMarkup = message.reply_markup.InlineKeyboardMarkup(),
497 | SenderBoostCount = message.from_boosts_applied > 0 ? message.from_boosts_applied : null,
498 | SenderBusinessBot = User(message.via_business_bot_id),
499 | IsFromOffline = message.flags2.HasFlag(TL.Message.Flags2.offline),
500 | EffectId = message.flags2.HasFlag(TL.Message.Flags2.has_effect) ? message.effect.ToString() : null,
501 | PaidStarCount = message.paid_message_stars.IntIfPositive()
502 | };
503 | if (message.fwd_from is { } fwd)
504 | {
505 | msg.ForwardOrigin = await MakeOrigin(fwd);
506 | msg.IsAutomaticForward = msg.Chat.Type == ChatType.Supergroup && await ChatFromPeer(fwd.saved_from_peer) is Chat { Type: ChatType.Channel } && fwd.saved_from_msg_id != 0;
507 | }
508 | await FixMsgFrom(msg, message.from_id, message.peer_id);
509 | if (message.via_bot_id != 0) msg.ViaBot = await UserOrResolve(message.via_bot_id);
510 | if (message.edit_date != default) msg.EditDate = message.edit_date;
511 | if (message.flags.HasFlag(TL.Message.Flags.noforwards)) msg.HasProtectedContent = true;
512 | if (message.grouped_id != 0) msg.MediaGroupId = message.grouped_id.ToString();
513 | return CacheMessage(await FillTextAndMedia(msg, message.message, message.entities, message.media, message.flags.HasFlag(TL.Message.Flags.invert_media)), msgBase);
514 | case TL.MessageService msgSvc:
515 | msg = new WTelegram.Types.Message
516 | {
517 | TLMessage = msgSvc,
518 | Id = msgSvc.id,
519 | From = await UserFromPeer(msgSvc.from_id),
520 | SenderChat = await ChatFromPeer(msgSvc.from_id),
521 | Date = msgSvc.date,
522 | Chat = await ChatFromPeer(msgSvc.peer_id, allowUser: true),
523 | };
524 | if (msgSvc.action is MessageActionTopicCreate)
525 | {
526 | msg.IsTopicMessage = true;
527 | msg.MessageThreadId = msgSvc.id;
528 | }
529 | await FixMsgFrom(msg, msgSvc.from_id, msgSvc.peer_id);
530 | if (await MakeServiceMessage(msgSvc, msg) == null) return CacheMessage(null, msgBase);
531 | return CacheMessage(msg, msgBase);
532 | case null:
533 | return null;
534 | default:
535 | return CacheMessage(new WTelegram.Types.Message
536 | {
537 | TLMessage = msgBase,
538 | Id = msgBase.ID,
539 | Chat = await ChatFromPeer(msgBase.Peer, allowUser: true)!,
540 | }, msgBase);
541 | }
542 |
543 | async Task FixMsgFrom(Message msg, Peer from_id, Peer peer_id)
544 | {
545 | if (msg.From == null)
546 | switch (msg.Chat.Type)
547 | {
548 | case ChatType.Channel: break;
549 | case ChatType.Private:
550 | msg.From = await UserFromPeer(peer_id);
551 | break;
552 | default:
553 | if (from_id == null)
554 | {
555 | msg.From = GroupAnonymousBot;
556 | msg.SenderChat = msg.Chat;
557 | }
558 | else if (msg.IsAutomaticForward == true)
559 | msg.From = ServiceNotification;
560 | break;
561 | }
562 | }
563 | }
564 |
565 | private async Task MakeOrigin(MessageFwdHeader fwd)
566 | {
567 | MessageOrigin? origin = fwd.from_id switch
568 | {
569 | PeerUser pu => new MessageOriginUser { SenderUser = await UserOrResolve(pu.user_id) },
570 | PeerChat pc => new MessageOriginChat { SenderChat = await ChatOrResolve(pc.chat_id), AuthorSignature = fwd.post_author },
571 | PeerChannel pch => new MessageOriginChannel
572 | {
573 | Chat = await ChannelOrResolve(pch.channel_id),
574 | AuthorSignature = fwd.post_author,
575 | MessageId = fwd.channel_post
576 | },
577 | _ => fwd.from_name != null ? new MessageOriginHiddenUser { SenderUserName = fwd.from_name } : null
578 | };
579 | if (origin != null) origin.Date = fwd.date;
580 | return origin;
581 | }
582 |
583 | private async Task FillTextAndMedia(Message msg, string? text, TL.MessageEntity[] entities, MessageMedia media, bool invert_media = false)
584 | {
585 | switch (media)
586 | {
587 | case null:
588 | if (entities?.Any(e => e is MessageEntityUrl or MessageEntityTextUrl) == true)
589 | msg.LinkPreviewOptions = new LinkPreviewOptions { IsDisabled = true };
590 | msg.Text = text;
591 | msg.Entities = MakeEntities(entities);
592 | return msg;
593 | case MessageMediaWebPage mmwp:
594 | msg.LinkPreviewOptions = mmwp.LinkPreviewOptions(invert_media);
595 | msg.Text = text;
596 | msg.Entities = MakeEntities(entities);
597 | return msg;
598 | case MessageMediaDocument { document: TL.Document document } mmd:
599 | if (mmd.flags.HasFlag(MessageMediaDocument.Flags.spoiler)) msg.HasMediaSpoiler = true;
600 | msg.ShowCaptionAboveMedia = invert_media;
601 | var thumb = document.LargestThumbSize;
602 | if (mmd.flags.HasFlag(MessageMediaDocument.Flags.voice))
603 | {
604 | var audio = document.GetAttribute();
605 | msg.Voice = new Telegram.Bot.Types.Voice
606 | {
607 | FileSize = document.size,
608 | Duration = (int)(audio?.duration + 0.5 ?? 0.0),
609 | MimeType = document.mime_type
610 | }.SetFileIds(document.ToFileLocation(), document.dc_id);
611 | }
612 | else if (mmd.flags.HasFlag(MessageMediaDocument.Flags.round))
613 | {
614 | var video = document.GetAttribute();
615 | msg.VideoNote = new Telegram.Bot.Types.VideoNote
616 | {
617 | FileSize = document.size,
618 | Length = video?.w ?? 0,
619 | Duration = (int)(video?.duration + 0.5 ?? 0.0),
620 | Thumbnail = thumb?.PhotoSize(document.ToFileLocation(thumb), document.dc_id)
621 | }.SetFileIds(document.ToFileLocation(), document.dc_id);
622 | }
623 | else if (mmd.flags.HasFlag(MessageMediaDocument.Flags.video))
624 | msg.Video = document.Video(mmd);
625 | else if (document.GetAttribute() is { } audio)
626 | {
627 | msg.Audio = new Telegram.Bot.Types.Audio
628 | {
629 | FileSize = document.size,
630 | Duration = (int)(audio?.duration + 0.5 ?? 0.0),
631 | Performer = audio?.performer,
632 | Title = audio?.title,
633 | FileName = document.Filename,
634 | MimeType = document.mime_type,
635 | Thumbnail = thumb?.PhotoSize(document.ToFileLocation(thumb), document.dc_id)
636 | }.SetFileIds(document.ToFileLocation(), document.dc_id);
637 | }
638 | else if (document.GetAttribute() is { } sticker)
639 | {
640 | msg.Sticker = await MakeSticker(document, sticker);
641 | }
642 | else
643 | {
644 | msg.Document = document.Document(thumb);
645 | if (document.GetAttribute() != null)
646 | msg.Animation = MakeAnimation(msg.Document!, document.GetAttribute());
647 | }
648 | break;
649 | case MessageMediaPhoto { photo: TL.Photo photo } mmp:
650 | if (mmp.flags.HasFlag(MessageMediaPhoto.Flags.spoiler)) msg.HasMediaSpoiler = true;
651 | msg.ShowCaptionAboveMedia = invert_media;
652 | msg.Photo = photo.PhotoSizes();
653 | break;
654 | case MessageMediaVenue mmv:
655 | msg.Venue = new Venue
656 | {
657 | Location = mmv.geo.Location(),
658 | Title = mmv.title,
659 | Address = mmv.address,
660 | FoursquareId = mmv.provider == "foursquare" ? mmv.venue_id : null,
661 | FoursquareType = mmv.provider == "foursquare" ? mmv.venue_type : null,
662 | GooglePlaceId = mmv.provider == "gplaces" ? mmv.venue_id : null,
663 | GooglePlaceType = mmv.provider == "gplaces" ? mmv.venue_id : null
664 | };
665 | break;
666 | case MessageMediaContact mmc:
667 | msg.Contact = new Telegram.Bot.Types.Contact
668 | {
669 | PhoneNumber = mmc.phone_number,
670 | FirstName = mmc.first_name,
671 | LastName = mmc.last_name,
672 | UserId = mmc.user_id,
673 | Vcard = mmc.vcard,
674 | };
675 | break;
676 | case MessageMediaGeo mmg:
677 | msg.Location = mmg.geo.Location();
678 | break;
679 | case MessageMediaGeoLive mmgl:
680 | msg.Location = mmgl.geo.Location();
681 | msg.Location.LivePeriod = mmgl.period;
682 | msg.Location.Heading = mmgl.flags.HasFlag(MessageMediaGeoLive.Flags.has_heading) ? mmgl.heading : null;
683 | msg.Location.ProximityAlertRadius = mmgl.flags.HasFlag(MessageMediaGeoLive.Flags.has_proximity_notification_radius) ? mmgl.proximity_notification_radius : null;
684 | break;
685 | case MessageMediaPoll { poll: TL.Poll poll, results: TL.PollResults pollResults }:
686 | msg.Poll = MakePoll(poll, pollResults);
687 | return msg;
688 | case MessageMediaDice mmd:
689 | msg.Dice = new Dice { Emoji = mmd.emoticon, Value = mmd.value };
690 | return msg;
691 | case MessageMediaInvoice mmi:
692 | msg.Invoice = new Telegram.Bot.Types.Payments.Invoice
693 | {
694 | Title = mmi.title,
695 | Description = mmi.description,
696 | StartParameter = mmi.start_param,
697 | Currency = mmi.currency,
698 | TotalAmount = (int)mmi.total_amount
699 | };
700 | return msg;
701 | case MessageMediaGame mmg:
702 | msg.Game = new Telegram.Bot.Types.Game
703 | {
704 | Title = mmg.game.title,
705 | Description = mmg.game.description,
706 | Photo = mmg.game.photo.PhotoSizes()!,
707 | Text = text.NullIfEmpty(),
708 | TextEntities = MakeEntities(entities)
709 | };
710 | if (mmg.game.document is TL.Document doc && doc.GetAttribute() != null)
711 | {
712 | thumb = doc.LargestThumbSize;
713 | msg.Game.Animation = MakeAnimation(doc.Document(thumb)!, doc.GetAttribute());
714 | }
715 | return msg;
716 | case MessageMediaStory mms:
717 | msg.Story = new Story
718 | {
719 | Chat = await ChatFromPeer(mms.peer, true),
720 | Id = mms.id
721 | };
722 | break;
723 | case MessageMediaGiveaway mmg:
724 | msg.Giveaway = new Giveaway
725 | {
726 | Chats = await mmg.channels.Select(ChannelOrResolve).WhenAllSequential(),
727 | WinnersSelectionDate = mmg.until_date,
728 | WinnerCount = mmg.quantity,
729 | OnlyNewMembers = mmg.flags.HasFlag(MessageMediaGiveaway.Flags.only_new_subscribers),
730 | HasPublicWinners = mmg.flags.HasFlag(MessageMediaGiveaway.Flags.winners_are_visible),
731 | PrizeDescription = mmg.prize_description,
732 | CountryCodes = mmg.countries_iso2,
733 | PremiumSubscriptionMonthCount = mmg.months.NullIfZero(),
734 | PrizeStarCount = ((int)mmg.stars).NullIfZero(),
735 | };
736 | break;
737 | case MessageMediaGiveawayResults mmgr:
738 | msg.GiveawayWinners = new GiveawayWinners
739 | {
740 | Chat = await ChannelOrResolve(mmgr.channel_id),
741 | GiveawayMessageId = mmgr.launch_msg_id,
742 | WinnersSelectionDate = mmgr.until_date,
743 | WinnerCount = mmgr.winners_count,
744 | Winners = await mmgr.winners.Select(UserOrResolve).WhenAllSequential(),
745 | AdditionalChatCount = mmgr.additional_peers_count,
746 | PremiumSubscriptionMonthCount = mmgr.months,
747 | UnclaimedPrizeCount = mmgr.unclaimed_count,
748 | OnlyNewMembers = mmgr.flags.HasFlag(MessageMediaGiveawayResults.Flags.only_new_subscribers),
749 | WasRefunded = mmgr.flags.HasFlag(MessageMediaGiveawayResults.Flags.refunded),
750 | PrizeDescription = mmgr.prize_description,
751 | PrizeStarCount = ((int)mmgr.stars).NullIfZero(),
752 | };
753 | break;
754 | case MessageMediaPaidMedia mmpm:
755 | msg.PaidMedia = new PaidMediaInfo
756 | {
757 | StarCount = (int)mmpm.stars_amount,
758 | PaidMedia = [.. mmpm.extended_media.Select(TypesTLConverters.PaidMedia)]
759 | };
760 | break;
761 | default:
762 | break;
763 | }
764 | if (text != "") msg.Caption = text;
765 | msg.CaptionEntities = MakeEntities(entities);
766 | return msg;
767 | }
768 |
769 | private async Task