diff --git a/.editorconfig b/.editorconfig index 0cd4bdf1..5bd33ed6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,4 +5,4 @@ indent_size = 2 # Markdown Files [*.md] trim_trailing_whitespace = false -indent_size = 2 +indent_size = 4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 154299d9..e049be8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,24 @@ jobs: run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore - - name: Setup mdBook - uses: peaceiris/actions-mdbook@v2 - with: - mdbook-version: '0.4.35' - - name: Setup nodejs - uses: actions/setup-node@v4 + + - name: Install mdbook and cargo-binstall binaries + uses: taiki-e/install-action@v2 with: - node-version: '18' + tool: mdbook,cargo-binstall + - name: Install mdbook extensions + run: cargo binstall -y mdbook-mermaid mdbook-alerts mdbook-toc + + # - name: Setup mdBook + # uses: peaceiris/actions-mdbook@v2 + # with: + # mdbook-version: '0.4.36' + # - run: cargo install mdbook-alerts + # - name: Setup nodejs + # uses: actions/setup-node@v4 + # with: + # node-version: '18' + # - name: Setup linter and spellchecker cli # run: npm install --location=global markdownlint-cli cspell # - name: Lint markdown files diff --git a/.gitignore b/.gitignore index 38803a18..ffeeb8f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Executables *.exe +*.pdn # msBook compiled book book diff --git a/Examples/.editorconfig b/Examples/.editorconfig index d91f42c5..e0b9a8ee 100644 --- a/Examples/.editorconfig +++ b/Examples/.editorconfig @@ -27,6 +27,16 @@ csharp_empty_block_style = together csharp_max_line_length = 120 # Solution Files + +# CS1998: Async method lacks 'await' operators and will run synchronously +dotnet_diagnostic.CS1998.severity = silent + +# IDE0062: Make local function 'static' +dotnet_diagnostic.IDE0062.severity = silent + +# CA2016: Forward the 'CancellationToken' parameter to methods +dotnet_diagnostic.CA2016.severity = none + [*.sln] indent_style = tab diff --git a/Examples/1/ExampleBot.cs b/Examples/1/ExampleBot.cs deleted file mode 100644 index 5bb14ac5..00000000 --- a/Examples/1/ExampleBot.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ANCHOR: usings -using Telegram.Bot; -using Telegram.Bot.Types; -using Telegram.Bot.Types.Enums; -// ANCHOR_END: usings - -namespace BookExamples.Chapter1; - -internal class ExampleBot -{ - private async Task BookExamples() - { -// ANCHOR: example-bot -using var cts = new CancellationTokenSource(); -var bot = new TelegramBotClient("{YOUR_ACCESS_TOKEN_HERE}", cancellationToken: cts.Token); -bot.StartReceiving(OnUpdate, async (bot, ex, ct) => Console.WriteLine(ex)); - -var me = await bot.GetMeAsync(); -Console.WriteLine($"@{me.Username} is running... Press Enter to terminate"); -Console.ReadLine(); -cts.Cancel(); // stop the bot - -// method that handle updates coming for the bot: -async Task OnUpdate(ITelegramBotClient bot, Update update, CancellationToken ct) -{ - if (update.Message is null) return; // we want only updates about new Message - if (update.Message.Text is null) return; // we want only updates about new Text Message - var msg = update.Message; - Console.WriteLine($"Received message '{msg.Text}' in {msg.Chat}"); - // let's echo back received text in the chat - await bot.SendTextMessageAsync(msg.Chat, $"{msg.From} said: {msg.Text}"); -} -// ANCHOR_END: example-bot - } -} diff --git a/Examples/1/Quickstart.cs b/Examples/1/Quickstart.cs deleted file mode 100644 index dea7476f..00000000 --- a/Examples/1/Quickstart.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ANCHOR: usings -using Telegram.Bot; -// ANCHOR_END: usings - -namespace BookExamples.Chapter1; - -internal class Quickstart -{ - private async Task BookExamples() - { -// ANCHOR: quickstart -var bot = new TelegramBotClient("{YOUR_ACCESS_TOKEN_HERE}"); - -var me = await bot.GetMeAsync(); -Console.WriteLine($"Hello, World! I am user {me.Id} and my name is {me.FirstName}."); -// ANCHOR_END: quickstart - } -} diff --git a/Examples/2/ReplyMarkup.cs b/Examples/2/ReplyMarkup.cs index bb517c3f..a1b95c10 100644 --- a/Examples/2/ReplyMarkup.cs +++ b/Examples/2/ReplyMarkup.cs @@ -10,104 +10,83 @@ namespace BookExamples.Chapter2; internal class ReplyMarkup { - public readonly ITelegramBotClient bot = new TelegramBotClient("{YOUR_ACCESS_TOKEN_HERE}"); + public readonly ITelegramBotClient bot = new TelegramBotClient("YOUR_BOT_TOKEN"); public readonly ChatId chatId = 12345; private async Task SingleRowMarkup() { // ANCHOR: single-row -var buttons = new KeyboardButton[] -{ - "Help me", "Call me ☎️", -}; +var replyMarkup = new ReplyKeyboardMarkup(true) + .AddButtons("Help me", "Call me ☎️"); -var sent = await bot.SendTextMessageAsync(chatId, "Choose a response", - replyMarkup: new ReplyKeyboardMarkup(buttons) { ResizeKeyboard = true }); +var sent = await bot.SendTextMessageAsync(chatId, "Choose a response", replyMarkup: replyMarkup); // ANCHOR_END: single-row } private async Task MultipleRowMarkup() { // ANCHOR: multiple-row -var buttons = new KeyboardButton[][] -{ - new KeyboardButton[] { "Help me" }, - new KeyboardButton[] { "Call me ☎️", "Write me ✉️" }, -}; +var replyMarkup = new ReplyKeyboardMarkup(true) + .AddButton("Help me") + .AddNewRow("Call me ☎️", "Write me ✉️"); -var sent = await bot.SendTextMessageAsync(chatId, "Choose a response", - replyMarkup: new ReplyKeyboardMarkup(buttons) { ResizeKeyboard = true }); +var sent = await bot.SendTextMessageAsync(chatId, "Choose a response", replyMarkup: replyMarkup); // ANCHOR_END: multiple-row } private async Task RequestInfo() { // ANCHOR: request-info -var buttons = new[] -{ - KeyboardButton.WithRequestLocation("Share Location"), - KeyboardButton.WithRequestContact("Share Contact"), -}; +var replyMarkup = new ReplyKeyboardMarkup() + .AddButton(KeyboardButton.WithRequestLocation("Share Location")) + .AddButton(KeyboardButton.WithRequestContact("Share Contact")); -var sent = await bot.SendTextMessageAsync(chatId, "Who or Where are you?", - replyMarkup: new ReplyKeyboardMarkup(buttons)); +var sent = await bot.SendTextMessageAsync(chatId, "Who or Where are you?", replyMarkup: replyMarkup); // ANCHOR_END: request-info } private async Task RemoveKeyboard() { // ANCHOR: remove-keyboard -var sent = await bot.SendTextMessageAsync(chatId, "Removing keyboard", - replyMarkup: new ReplyKeyboardRemove()); +await bot.SendTextMessageAsync(chatId, "Removing keyboard", replyMarkup: new ReplyKeyboardRemove()); // ANCHOR_END: remove-keyboard } private async Task CallbackButtons() { // ANCHOR: callback-buttons -var buttons = new InlineKeyboardButton[][] -{ - new[] // first row - { - InlineKeyboardButton.WithCallbackData("1.1", "11"), - InlineKeyboardButton.WithCallbackData("1.2", "12"), - }, - new[] // second row - { - InlineKeyboardButton.WithCallbackData("2.1", "21"), - InlineKeyboardButton.WithCallbackData("2.2", "22"), - }, -}; +var inlineMarkup = new InlineKeyboardMarkup() + .AddButton("1.1", "11") // first row, first button + .AddButton("1.2", "12") // first row, second button + .AddNewRow() + .AddButton("2.1", "21") // second row, first button + .AddButton("2.2", "22");// second row, second button var sent = await bot.SendTextMessageAsync(chatId, "A message with an inline keyboard markup", - replyMarkup: new InlineKeyboardMarkup(buttons)); + replyMarkup: inlineMarkup); // ANCHOR_END: callback-buttons } private async Task UrlButtons() { // ANCHOR: url-buttons -var buttons = new[] -{ - InlineKeyboardButton.WithUrl("Repository Link", "https://github.com/TelegramBots/Telegram.Bot") -}; +var inlineMarkup = new InlineKeyboardMarkup() + .AddButton(InlineKeyboardButton.WithUrl("Repository Link", "https://github.com/TelegramBots/Telegram.Bot")); var sent = await bot.SendTextMessageAsync(chatId, "A message with an inline keyboard markup", - replyMarkup: new InlineKeyboardMarkup(buttons)); + replyMarkup: inlineMarkup); // ANCHOR_END: url-buttons } private async Task SwitchToInline() { // ANCHOR: switch-to-inline -var buttons = new[] -{ - InlineKeyboardButton.WithSwitchInlineQuery("switch_inline_query"), - InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("switch_inline_query_current_chat"), -}; +var inlineMarkup = new InlineKeyboardMarkup() + .AddButton(InlineKeyboardButton.WithSwitchInlineQuery("switch_inline_query")) + .AddButton(InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("switch_inline_query_current_chat")); var sent = await bot.SendTextMessageAsync(chatId, "A message with an inline keyboard markup", - replyMarkup: new InlineKeyboardMarkup(buttons)); + replyMarkup: inlineMarkup); // ANCHOR_END: switch-to-inline } } diff --git a/Examples/2/SendMessage.cs b/Examples/2/SendMessage.cs index 9d8ad3b7..8b46f1d1 100644 --- a/Examples/2/SendMessage.cs +++ b/Examples/2/SendMessage.cs @@ -7,7 +7,7 @@ namespace Examples.Chapter2; internal class SendMessage { - public readonly ITelegramBotClient bot = new TelegramBotClient("{YOUR_ACCESS_TOKEN_HERE}"); + public readonly ITelegramBotClient bot = new TelegramBotClient("YOUR_BOT_TOKEN"); public readonly ChatId chatId = 12345; public readonly Update update = new (); @@ -90,7 +90,7 @@ private async Task SendPoll() // ANCHOR_END: send-poll // ANCHOR: stop-poll -Poll poll = await bot.StopPollAsync(pollMessage.Chat.Id, pollMessage.MessageId); +Poll poll = await bot.StopPollAsync(pollMessage.Chat, pollMessage.MessageId); // ANCHOR_END: stop-poll } diff --git a/Examples/3/Files.cs b/Examples/3/Files.cs index e835b988..c1e2cdec 100644 --- a/Examples/3/Files.cs +++ b/Examples/3/Files.cs @@ -5,7 +5,7 @@ namespace Examples.Chapter3; internal class Files { - public readonly ITelegramBotClient bot = new TelegramBotClient("{YOUR_ACCESS_TOKEN_HERE}"); + public readonly ITelegramBotClient bot = new TelegramBotClient("YOUR_BOT_TOKEN"); public readonly ChatId chatId = 12345; public readonly Update update = new(); diff --git a/Examples/3/Inline.cs b/Examples/3/Inline.cs index 79e627a7..7bdcd9b9 100644 --- a/Examples/3/Inline.cs +++ b/Examples/3/Inline.cs @@ -10,7 +10,7 @@ namespace BookExamples.Chapter3; internal class Inline { - public readonly ITelegramBotClient bot = new TelegramBotClient("{YOUR_ACCESS_TOKEN_HERE}"); + public readonly ITelegramBotClient bot = new TelegramBotClient("YOUR_BOT_TOKEN"); // ANCHOR: arrays private readonly string[] sites = { "Google", "Github", "Telegram", "Wikipedia" }; private readonly string[] siteDescriptions = diff --git a/Examples/3/LongPolling.cs b/Examples/3/LongPolling.cs deleted file mode 100644 index 17a9817f..00000000 --- a/Examples/3/LongPolling.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Telegram.Bot; - -namespace BookExamples.Chapter3; - -internal class ExampleBot -{ - private async Task LongPolling() - { -// ANCHOR: long-polling -CancellationTokenSource cts = new(); -var bot = new TelegramBotClient("{YOUR BOT TOKEN HERE}", cancellationToken: cts.Token); - -int? offset = null; -while (!cts.IsCancellationRequested) -{ - var updates = await bot.GetUpdatesAsync(offset, timeout: 2); - foreach (var update in updates) - { - offset = update.Id + 1; - try - { - // put your code to handle one Update here. - } - catch (Exception ex) - { - // log exception and continue - } - if (cts.IsCancellationRequested) break; - } -} -// ANCHOR_END: long-polling - } -} diff --git a/Examples/Examples.csproj b/Examples/Examples.csproj index 94ba07ed..5f6362ae 100644 --- a/Examples/Examples.csproj +++ b/Examples/Examples.csproj @@ -11,7 +11,7 @@ - + diff --git a/Examples/nuget.config b/Examples/nuget.config index 7655f5af..aad59706 100644 --- a/Examples/nuget.config +++ b/Examples/nuget.config @@ -2,7 +2,7 @@ - + diff --git a/book.toml b/book.toml index 0b5c7d0c..1d97bf86 100644 --- a/book.toml +++ b/book.toml @@ -1,5 +1,5 @@ [book] -authors = ["Poulad Ashrafpour", "Aleksey Usatov", "karb0f0s", "Miha Zupan"] +authors = ["Poulad Ashrafpour", "Aleksey Usatov", "karb0f0s", "Miha Zupan", "Wizou"] multilingual = false src = "src" title = "A guide to Telegram.Bot library" @@ -9,3 +9,16 @@ language = "en" [output.html] git-repository-url = "https://github.com/TelegramBots/book" additional-css = ["theme/custom.css"] + +[output.html.fold] +enable = true +level = 1 + +[preprocessor.alerts] + +[preprocessor.toc] +command = "mdbook-toc" +renderer = ["html"] + +[output.html.redirect] +"Migration-Guide-to-Version-21.x.html" = "migrate/Version-21.x.html" diff --git a/src/1/docs/NugetPackageManager.jpg b/src/1/docs/NugetPackageManager.jpg deleted file mode 100644 index 0f8cf14d..00000000 Binary files a/src/1/docs/NugetPackageManager.jpg and /dev/null differ diff --git a/src/1/docs/NugetPackageManager.png b/src/1/docs/NugetPackageManager.png new file mode 100644 index 00000000..ed46d93f Binary files /dev/null and b/src/1/docs/NugetPackageManager.png differ diff --git a/src/1/example-bot.md b/src/1/example-bot.md index 3959fd14..7f21a63a 100644 --- a/src/1/example-bot.md +++ b/src/1/example-bot.md @@ -1,18 +1,36 @@ -# Full Example - Your first bot +# Your First Chat Bot -On the [previous page] we got an access token and used the [`getMe`] method to check our setup. +On the [previous page](quickstart.md) we got a bot token and used the [`getMe`](https://core.telegram.org/bots/api#getme) method to check our setup. Now, it is time to make an _interactive_ bot that gets users' messages and replies to them like in this screenshot: ![Example Image](docs/shot-example_bot.jpg) Copy the following code to `Program.cs`. -> ⚠️ Replace `{YOUR_ACCESS_TOKEN_HERE}` with the access token from the [`@BotFather`]. +> ⚠️ Replace `YOUR_BOT_TOKEN` with the bot token obtained from [`@BotFather`]. ```c# -{{#include ../../Examples/1/ExampleBot.cs:usings}} - -{{#include ../../Examples/1/ExampleBot.cs:example-bot}} +using Telegram.Bot; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; + +using var cts = new CancellationTokenSource(); +var bot = new TelegramBotClient("YOUR_BOT_TOKEN", cancellationToken: cts.Token); +var me = await bot.GetMeAsync(); +bot.OnMessage += OnMessage; + +Console.WriteLine($"@{me.Username} is running... Press Enter to terminate"); +Console.ReadLine(); +cts.Cancel(); // stop the bot + +// method that handle messages received by the bot: +async Task OnMessage(Message msg, UpdateType type) +{ + if (msg.Text is null) return; // we only handle Text messages here + Console.WriteLine($"Received {type} '{msg.Text}' in {msg.Chat}"); + // let's echo back received text in the chat + await bot.SendTextMessageAsync(msg.Chat, $"{msg.From} said: {msg.Text}"); +} ``` Run the program: @@ -22,27 +40,21 @@ dotnet run ``` It runs waiting for text messages unless forcefully stopped by pressing Enter. Open a private chat with your bot in -Telegram and send a text message to it. Bot should reply in no time. +Telegram and send a text message to it. Bot should reply immediately. + +By setting `bot.OnMessage`, the bot client starts polling Telegram servers for messages received by the bot. +This is done automatically in the background, so your program continue to execute and we use `Console.ReadLine()` to keep it running until you press Enter. -By invoking [`StartReceiving(...)`] bot client starts fetching updates using [`getUpdates`] method for the bot -from Telegram servers. This operation does not block the caller thread, because it is done on the ThreadPool. We use `Console.ReadLine()` to keep the app running. +When user sends a message, the `OnMessage(...)` method gets invoked with the `Message` object passed as an argument (and the type of update). -When user sends a message, the `OnUpdate(...)` method gets invoked with the `Update` object passed as an argument. We check `Message.Type` and skip the rest if it is not a text message. Finally, we send a text message back to the same chat we got the message from. -The second argument to `StartReceiving` is a lambda method invoked in case of an error that occurred while fetching updates. - -If you take a look at the console, the program outputs the `chatId` value. **Copy the chat id number** to make testing easier -for yourself on the next pages. +If you take a look at the console, the program outputs the `chatId` numeric value. +In a private chat with you, it would be your `userId`, so remember it as it's useful to send yourself messages. ```text -Received a 'text' message in chat 123456789. +Received Message 'test' in Private chat with @You (123456789). ``` - -[previous page]: quickstart.md -[`getMe`]: https://core.telegram.org/bots/api#getme -[`getUpdates`]: https://core.telegram.org/bots/api#getupdates -[`StartReceiving(...)`]: https://github.com/TelegramBots/Telegram.Bot.Extensions.Polling/blob/master/src/Telegram.Bot.Extensions.Polling/Extensions/TelegramBotClientPollingExtensions.cs diff --git a/src/1/full-bot.md b/src/1/full-bot.md new file mode 100644 index 00000000..0353bb93 --- /dev/null +++ b/src/1/full-bot.md @@ -0,0 +1,70 @@ +# Full Example + +On the [previous page](example-bot.md) we got a basic bot reacting to messages via `bot.OnMessage`. + +Now, we are going to set also `bot.OnUpdate` and `bot.OnError` to make a more complete bot + +Modify your `Program.cs` to the following: + +```c# +using Telegram.Bot; +using Telegram.Bot.Polling; +using Telegram.Bot.Types; +using Telegram.Bot.Types.Enums; +using Telegram.Bot.Types.ReplyMarkups; + +using var cts = new CancellationTokenSource(); +var bot = new TelegramBotClient("YOUR_BOT_TOKEN", cancellationToken: cts.Token); +var me = await bot.GetMeAsync(); +bot.OnError += OnError; +bot.OnMessage += OnMessage; +bot.OnUpdate += OnUpdate; + +Console.WriteLine($"@{me.Username} is running... Press Enter to terminate"); +Console.ReadLine(); +cts.Cancel(); // stop the bot + +// method to handle errors in polling or in your OnMessage/OnUpdate code +async Task OnError(Exception exception, HandleErrorSource source) +{ + Console.WriteLine(exception); // just dump the exception to the console +} + +// method that handle messages received by the bot: +async Task OnMessage(Message msg, UpdateType type) +{ + if (msg.Text == "/start") + { + await bot.SendTextMessageAsync(msg.Chat, "Welcome! Pick one direction", + replyMarkup: new InlineKeyboardMarkup().AddButtons("Left", "Right")); + } +} + +// method that handle other types of updates received by the bot: +async Task OnUpdate(Update update) +{ + if (update is { CallbackQuery: { } query }) // non-null CallbackQuery + { + await bot.AnswerCallbackQueryAsync(query.Id, $"You picked {query.Data}"); + await bot.SendTextMessageAsync(query.Message!.Chat, $"User {query.From} clicked on {query.Data}"); + } +} +``` + +Run the program and send `/start` to the bot. +> [!NOTE] +> `/start` is the first message your bot receives automatically when a user interacts in private with the bot for the first time + +The bot will reply with its welcome message and 2 inline buttons for you to choose. + +When you click on a button, your bot receives an Update of type **CallbackQuery** that is not a simple message. +Therefore it will be handled by `OnUpdate` instead. + +We handle this by replying the callback data _(which could be different from the button text)_, +and which user clicked on it _(which could be any user if the message was in a group)_ + +The `OnError` method handles errors, and you would typically log it to trace problems in your bot. + +Look at [the Console example](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Console) in our [Examples repository](https://github.com/TelegramBots/Telegram.Bot.Examples) for an even more complete bot code. + + diff --git a/src/1/quickstart.md b/src/1/quickstart.md index 54cd7f99..325ed782 100644 --- a/src/1/quickstart.md +++ b/src/1/quickstart.md @@ -17,24 +17,27 @@ Bot token is a key that required to authorize the bot and send requests to the B Now that you have a bot, it's time to bring it to life! +> [!NOTE] > We recommend a recent .NET version like .NET 8, but we also support older .NET Framework (4.6.1+), .NET Core (2.0+) or .NET (5.0+) -ne Create a new console project for your bot and add a reference to `Telegram.Bot` package: ```bash dotnet new console -dotnet add package Telegram.Bot --source https://nuget.voids.site/v3/index.json +dotnet nuget add source https://pkgs.dev.azure.com/tgbots/Telegram.Bot/_packaging/release/nuget/v3/index.json -n Telegram.Bot +dotnet add package Telegram.Bot ``` -The code below fetches Bot information based on its access token by calling the Bot API [`getMe`] method. Open `Program.cs` and use the following content: +The code below fetches Bot information based on its bot token by calling the Bot API [`getMe`] method. Open `Program.cs` and use the following content: -> ⚠️ Replace `{YOUR_ACCESS_TOKEN_HERE}` with your access token from the [`@BotFather`]. +> ⚠️ Replace `YOUR_BOT_TOKEN` with your bot token obtained from [`@BotFather`]. ```c# -{{#include ../../Examples/1/Quickstart.cs:usings}} +using Telegram.Bot; -{{#include ../../Examples/1/Quickstart.cs:quickstart}} +var bot = new TelegramBotClient("YOUR_BOT_TOKEN"); +var me = await bot.GetMeAsync(); +Console.WriteLine($"Hello, World! I am user {me.Id} and my name is {me.FirstName}."); ``` Running the program gives you the following output: @@ -45,7 +48,7 @@ dotnet run Hello, World! I am user 1234567 and my name is Awesome Bot. ``` -Great! This bot is self-aware. To make the bot interact with a user, head to the [next page]. +Great! This bot is self-aware. To make the bot react to user messages, head to the [next page]. diff --git a/src/2/README.md b/src/2/README.md index b4c44b12..44bd1bad 100644 --- a/src/2/README.md +++ b/src/2/README.md @@ -9,5 +9,6 @@ - [Document & Animation](send-msg/document-animation-msg.md) - [Native Polls](send-msg/native-polls-msg.md) - [Other Messages](send-msg/other-msg.md) +- [Dealing with chats](chats.md) - [Reply Markup](reply-markup.md) - [Forward, Copy or Delete](forward-copy-delete.md) diff --git a/src/2/chats.md b/src/2/chats.md new file mode 100644 index 00000000..96718f54 --- /dev/null +++ b/src/2/chats.md @@ -0,0 +1,118 @@ +# Dealing with chats + +All messages in Telegram are sent/received on a specific chat. +The `chat.Type` can be one of 4 types: + +- `ChatType.Private`: + A private discussion with a user. The `chat.Id` is the same as the `user.Id` (positive number) +- `ChatType.Group`: + A private chat group with less than 200 users +- `ChatType.Supergroup`: + An advanced chat group, capable of being public, supporting more than 200 users, with specific user/admin rights +- `ChatType.Channel`: + A broadcast type of publishing feed (only admins can write to it) + +Additional notes: +- For groups/channels, the `chat.Id` is a negative number, and the `chat.Title` will be filled. +- For public groups/channels, the `chat.Username` will be filled. +- For private chat with a user, the `chat.FirstName` will be filled, and optionally, the `chat.LastName` and `chat.Username` if the user has one. + +## Calling chat methods + +All methods for dealing with chats _(like sending messages, etc..)_ take a `ChatId` parameter. + +For this parameter, you can pass directly a `long` _(the chat or user ID)_, +or when sending to a public group/channel, you can pass a `"@chatname"` string + +### Getting full info about a chat (`GetChatAsync`) + +Once a bot has joined a group/channel or has started receiving messages from a user, it can use method `GetChatAsync` to get detailed info about that chat/user. + +There are lots of information returned depending on the type of chat, and most are optional and may be unavailable. +Here are a few interesting ones: +* For private chat with a User: + - Birthdate + - Personal channel + - [Business](../4/business.md) information + - Bio +* For groups/channels: + - Description + - default Permissions _(non-administrator access rights)_ + - Linked ChatId _(the associated channel/discussion group for this chat)_ + - IsForum _(This chat group has topics. There is no way to retrieve the list of topics)_ +* Common information for all chats: + - Photo _(use `GetInfoAndDownloadFileAsync` and the `photo.BigFileId` to download it)_ + - Active Usernames _(premium user & public chats can have multiple usernames)_ + - Available reactions in this chat + - Pinned Message _(the most recent one)_ + + +## Receiving chat messages + +See chapter [Getting Updates](../3/updates/README.md) for how to receive updates & messages. + +For groups or private chats, you would receive an update of type `UpdateType.Message` (which means only the field `update.Message` will be set) + +For channel messages, you would receive an update with field `update.ChannelPost`. + +For [business](../4/business) messages, you would receive an update with field `update.BusinessMessage`. + +If someone modifies an existing message, you would receive an update with one of the fields `update.Edited*` + +Note: if you use the `bot.OnMessage` event, this is simplified and you can just check the UpdateType argument. + +> [!IMPORTANT] +> By default, for privacy reasons, bots in groups receive only messages that are targeted at them (reply to their messages, inline messages, or targeted `/commands@botname` with the bot username suffix) +> If you want your bot to receive ALL messages in the group, you can either make it admin, or disable the **Bot Settings** : [**Group Privacy** mode](https://core.telegram.org/bots/features#privacy-mode) in @BotFather + +## Migration to Supergroup + +When you create a private chat group in Telegram, it is usually a `ChatType.Group`. + +If you change settings (like admin rights or making it public), or if members reach 200, +the Group may be migrated into a Supergroup. + +In such case, the Supergroup is like a separate chat with a different ID. +The old Group will have a service message `MigrateToChatId` with the new supergroup ID. +The new Supergroup will have a service message `MigrateFromChatId` with the old group ID. + +## Managing new members in a group + +Bots can't directly add members into a group/channel. +To invite users to join a group/channel, you can send to the users the public link `https://t.me/chatusername` (if chat has a username), or invite links: + +### Invite links + +Invite links are typically of the form `https://t.me/+1234567890aAbBcCdDeEfF` and allow users clicking on them to join the chat. + +You can send those links as a text message or as an `InlineKeyboardButton.WithUrl(...)`. + +If your bot is administrator on a private (or public) group/channel, it can: +- read the (fixed) primary link of the chat: +```csharp +var chatFullInfo = await bot.GetChatAsync(chatId); // you should call this only once +Console.WriteLine(chatFullInfo.InviteLink); +``` +- create new invite links on demand +```csharp +var link = await bot.CreateChatInviteLinkAsync(chatId, "name/reason", ...); +Console.WriteLine(link.InviteLink); +``` + +See also [some other methods for managing invite links](https://core.telegram.org/bots/api#exportchatinvitelink). + +### Detecting new group members and changed member status + +Note: Bots can't detect new channel members + +The simpler approach to detecting new members joining a group is to handle service messages of type `MessageType.NewChatMembers`: the field `message.NewChatMembers` will contain an array of the new User details. +Same for a user leaving the chat, with the `message.LeftChatMember` service message. + +However, under various circumstances (bigger groups, hidden member lists, etc..), these service messages may not be sent out. + +The more complex (and more reliable) approach is instead to handle updates of type `UpdateType.ChatMember`: + +* First you need to enable this specific update type among the `allowedUpdates` parameter when calling `GetUpdatesAsync`, `SetWebhookAsync` or `StartReceiving`+`ReceiverOptions`. +* Typically, you would pass `Update.AllTypes` as the allowedUpdates parameter. +* After that, you will receive an `update.ChatMember` structure for each user changing status with their old & their new status +* The `OldChatMember`/`NewChatMember` status fields can be one of the derived `ChatMember*` class: `Owner`/`Creator`, `Administrator`, `Member`, `Restricted`, `Left`, `Banned`/`Kicked`) diff --git a/src/2/forward-copy-delete.md b/src/2/forward-copy-delete.md index fb632da4..8230a321 100644 --- a/src/2/forward-copy-delete.md +++ b/src/2/forward-copy-delete.md @@ -8,12 +8,15 @@ _Note: When you use the plural form of the copy/forward methods, it will keep Me ## Forward message(s) +[![forward message method](https://img.shields.io/badge/Bot_API_method-forwardMessage-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#forwardmessage) +[![forward messages method](https://img.shields.io/badge/Bot_API_method-forwardMessages-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#forwardmessages) + You can **forward** message(s) from a source chat to a target chat _(it can be the same chat)_. They will appear with a "Forwarded from" header. ```csharp // Forward a single message -await bot.CopyMessageAsync(targetChatId, sourceChatId, messageId); +await bot.ForwardMessageAsync(targetChatId, sourceChatId, messageId); // Forward an incoming message (from the update) onto a target ChatId await bot.ForwardMessageAsync(chatId, update.Message.Chat, update.Message.MessageId); @@ -24,6 +27,9 @@ await bot.ForwardMessagesAsync(targetChatId, sourceChatId, new int[] { 123, 124, ## Copy message(s) +[![copy message method](https://img.shields.io/badge/Bot_API_method-copyMessage-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#copymessage) +[![copy messages method](https://img.shields.io/badge/Bot_API_method-copyMessages-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#copymessages) + If you don't want the "Forwarded from" header, you can instead **copy** the message(s). This will make them look like new messages. @@ -45,6 +51,9 @@ await bot.CopyMessagesAsync(targetChatId, sourceChatId, new int[] { 123, 124, 12 ## Delete message(s) +[![delete message method](https://img.shields.io/badge/Bot_API_method-deleteMessage-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#deletemessage) +[![delete messages method](https://img.shields.io/badge/Bot_API_method-deleteMessages-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#deletemessages) + Finally you can **delete** message(s). This is particularly useful for cleaning unwanted messages in groups. diff --git a/src/2/reply-markup.md b/src/2/reply-markup.md index 9e8c4d6f..91adc317 100644 --- a/src/2/reply-markup.md +++ b/src/2/reply-markup.md @@ -22,7 +22,7 @@ A [`ReplyKeyboardMarkup`] with two buttons in a single row: {{#include ../../Examples/2/ReplyMarkup.cs:single-row}} ``` -> We specify `ResizeKeyboard = true` here to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). +> We specify `true` on the constructor to resize the keyboard vertically for optimal fit (e.g., make the keyboard smaller if there are just two rows of buttons). ### Multi-row keyboard markup @@ -34,8 +34,6 @@ A [`ReplyKeyboardMarkup`] with two rows of buttons: {{#include ../../Examples/2/ReplyMarkup.cs:multiple-row}} ``` -You can use `new List>` instead of `KeyboardButton[][]` if you prefer to build the list dynamically. - ### Request information [`ReplyKeyboardMarkup`] containing buttons for contact and location requests using helper methods `KeyboardButton.WithRequestLocation` and `KeyboardButton.WithRequestContact`: @@ -62,9 +60,14 @@ There are times when you'd prefer to do things without sending any messages to t Unlike custom reply keyboards, pressing buttons on inline keyboards doesn't result in messages sent to the chat. Instead, inline keyboards support buttons that work behind the scenes: [callback buttons](#callback-buttons), [URL buttons](#url-buttons) and [switch to inline buttons](#switch-to-inline-buttons). +You can have several rows and columns of inline buttons of mixed types. + ### Callback buttons -When a user presses a [callback button], no messages are sent to the chat. Instead, your bot simply receives the relevant query. Upon receiving the query, your bot can display some result in a notification at the top of the chat screen or in an alert. In this example we use `InlineKeyboardButton.WithCallbackData` helper method to create a button with a text and callback data. +When a user presses a [callback button], no messages are sent to the chat, and your bot simply receives an `update.CallbackQuery` instead. +Upon receiving this, your bot should answer to that query within 10 seconds, using `AnswerCallbackQueryAsync` _(or else the button gets momentarily disabled)_ + +In this example we use the `AddButton(buttonText, callbackData)` helper, but you can also create such button with `InlineKeyboardButton.WithCallbackData`: ```c# {{#include ../../Examples/2/ReplyMarkup.cs:usings}} @@ -72,8 +75,6 @@ When a user presses a [callback button], no messages are sent to the chat. Inste {{#include ../../Examples/2/ReplyMarkup.cs:callback-buttons}} ``` -You can use `new List>` instead of `InlineKeyboardButton[][]` if you prefer to build the list dynamically. - ### URL buttons Buttons of this type have a small arrow icon to help the user understand that tapping on a [URL button] will open an external link. In this example we use `InlineKeyboardButton.WithUrl` helper method to create a button with a text and url. @@ -97,7 +98,7 @@ Pressing a [switch to inline button] prompts the user to select a chat, opens it [special keyboard]: https://core.telegram.org/bots#keyboards [`ReplyKeyboardMarkup`]: https://core.telegram.org/bots/api/#replykeyboardmarkup [`KeyboardButton`]: https://core.telegram.org/bots/api/#keyboardbutton -[Inline Keyboards]: https://core.telegram.org/bots#inline-keyboards-and-on-the-fly-updating +[Inline Keyboards]: https://core.telegram.org/bots/features#inline-keyboards [callback button]: https://core.telegram.org/bots/2-0-intro#callback-buttons [URL button]: https://core.telegram.org/bots/2-0-intro#url-buttons [switch to inline button]: https://core.telegram.org/bots/2-0-intro#switch-to-inline-buttons diff --git a/src/2/send-msg/native-polls-msg.md b/src/2/send-msg/native-polls-msg.md index 3324e6e0..3e13cf19 100644 --- a/src/2/send-msg/native-polls-msg.md +++ b/src/2/send-msg/native-polls-msg.md @@ -2,7 +2,7 @@ [![native poll tests](https://img.shields.io/badge/Examples-Native_Polls-green.svg?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot/blob/master/test/Telegram.Bot.Tests.Integ/Polls/AnonymousPollTests.cs) -Just as regular users bots can only send native polls to groups and channels, but not to private chats. +Native poll are a special kind of message with question & answers where users can vote. Options can be set to allow multiple answers, vote anonymously, or be a quizz with a correct choice and explanation. ## Send a poll diff --git a/src/2/send-msg/text-msg.md b/src/2/send-msg/text-msg.md index 89305b0f..650f15d9 100644 --- a/src/2/send-msg/text-msg.md +++ b/src/2/send-msg/text-msg.md @@ -7,6 +7,7 @@ Text is a powerful interface for your bot and [`sendMessage`] probably is the mo Text messages are easy to send and fast to display on devices with slower networking. _Don't send boring plain text to users all the time_. Telegram allows you to format the text using HTML or Markdown. +> [!IMPORTANT] > We highly recommend you use HTML instead of Markdown because Markdown has lots of annoying aspects ## Send Text Message diff --git a/src/3/README.md b/src/3/README.md index 6322716f..4a8e076e 100644 --- a/src/3/README.md +++ b/src/3/README.md @@ -1,6 +1,6 @@ # Intermediate -- [Getting Updates](updates/) +- [Working with Updates & Messages](updates/) - [Long Polling](updates/polling.md) - [Webhooks](updates/webhook.md) - [Inline Mode](inline.md) diff --git a/src/3/updates/README.md b/src/3/updates/README.md index 4495cf76..2ea28a60 100644 --- a/src/3/updates/README.md +++ b/src/3/updates/README.md @@ -1,21 +1,80 @@ -# Getting Updates +# Working with Updates & Messages + +## Getting Updates There are two mutually exclusive ways of receiving updates for your bot — the long polling using [`getUpdates`] method on one hand and Webhooks on the other. Telegram is queueing updates until the bot receives them either way, but they will not be kept longer than 24 hours. -- With long polling, the client requests information from the server using [`getUpdates`] method, but with the expectation the server may not respond immediately. If the server has no new information for the client when the poll is received, instead of sending an empty response, the server holds the request open and waits for response information to become available. Once it does have new information, the server immediately sends a response to the client, completing the request. Upon receipt of the server response, the client often immediately issues another server request. +- [With long polling](polling.md), the client is actively requesting updates from the server in a blocking way. The call returns if new updates become available or a timeout has expired. - [Setting a webhook](webhook.md) means you supplying Telegram with a location in the form of an URL, on which your bot listens for updates. Telegram need to be able to connect and post updates to that URL. -To be able to handle webhook updates you'll need a server that: - - Supports IPv4, IPv6 is currently not supported for webhooks. - - Accepts incoming POSTs from subnets 149.154.160.0/20 and 91.108.4.0/22 on port 443, 80, 88, or 8443. - - Is able to handle TLS1.2(+) HTTPS-traffic. - - Provides a supported, non-wildcard, verified or self-signed certificate. - - Uses a CN or SAN that matches the domain you’ve supplied on setup. - - Supplies all intermediate certificates to complete a verification chain. - - You can find more useful information on setting webhook in [Marvin's Marvellous Guide to All Things Webhook](https://core.telegram.org/bots/webhooks) - -Each user interaction with your bot results in new -[Update](https://github.com/TelegramBots/Telegram.Bot/blob/master/src/Telegram.Bot/Types/Update.cs) object. Its fields will be set depending on update type. + +## Update types + +[![Update type](https://img.shields.io/badge/Bot_API_type-Update-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#update) + +Each user interaction with your bot results in an Update object. +It could be about a Message, some changed status, bot-specific queries, etc... +You can use `update.Type` to check which kind of update you are dealing with. + +However this property is slow and just indicates which field of `update` is set, and the other fields are all null. +So it is recommended to instead directly test the fields of Update you want if they are non-null, like this: +```csharp +switch (update) +{ + case { Message: { } msg }: await HandleMessage(msg); break; + case { EditedMessage: { } editedMsg }: await HandleEditedMessage(editedMsg); break; + case { ChannelPost: { } channelMsg }: await HandleChannelMessage(channelMsg); break; + case { CallbackQuery: { } cbQuery }: await HandleCallbackQuery(cbQuery); break; + //... +} +``` + + +## Message types + +[![Message type](https://img.shields.io/badge/Bot_API_type-Message-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#message) + +If the Update is one of the 6 types of update containing a message _(new or edited? channel? business?)_, the contained `Message` object itself can be of various types. + +Like above, you can use `message.Type` to determine the type but it is recommended to directly test the non-null fields of `Message` using `if` or `switch`. + +There are a few dozens of message types, grouped in two main categories: **Content** and **Service** messages + +### Content messages + +These messages represent some actual content that someone posted. + +Depending on which field is set, it can be: +- `Text`: a basic text message _(with its `Entities` for font effects, and `LinkPreviewOptions` for preview info)_ +- `Photo`, `Video`, `Animation` (GIF), `Document` (file), `Audio`, `Voice`, `PaidMedia`: those are media contents which can come with a `Caption` subtext _(and its `CaptionEntities`)_ +- `VideoNote`, `Sticker`, `Dice`, `Game`, `Poll`, `Venue`, `Location`, `Story`: other kind of messages without a caption + +You can use methods `message.ToHtml()` or `message.ToMarkdown()` to convert the text/caption & entities into HTML **(recommended)** or Markdown. + +### Service messages + +All other message types represent some action/status that happened in the chat instead of actual content. + +We are not listing all types here, but it could be for example: +- members joined/left +- pinned message +- chat info/status/topic changed +- [payment](../../4/payments.md)/[passport](../../4/passport/README.md)/giveaway process update +- etc... + +### Common properties + +There are additional properties that gives you information about the context of the message. + +Here are a few important properties: +- `MessageId`: the ID that you will use if you need to reply or call a method acting on this message +- `Chat`: in which chat the message arrived +- `From`: which user posted it +- `Date`: timestamp of the message (in UTC) +- `ReplyToMessage`: which message this is a reply to +- [`ForwardOrigin`](../../2/forward-copy-delete.md#check-if-a-message-is-a-forward): if it is a Forwarded message +- `MediaGroupId`: albums (group of media) are separate consecutive messages having the same MediaGroupId +- `MessageThreadId`: the topic ID for Forum/Topic type chats + ## Example projects diff --git a/src/3/updates/polling.md b/src/3/updates/polling.md index 240b8909..af6b69e8 100644 --- a/src/3/updates/polling.md +++ b/src/3/updates/polling.md @@ -1,10 +1,48 @@ # Long Polling -If you don't want to use our recommended **StartReceiving** helper ([see first example](../../1/example-bot.md)), -you can just call GetUpdateAsync with a timeout in a loop, -such that the call blocks for **up to** X seconds until there is an incoming update +Long Polling is done by calling [getUpdates](https://core.telegram.org/bots/api#getupdates) actively. + +With our library, this can be done in one of three ways: + +## By setting `bot.OnUpdate` (and/or `bot.OnMessage`) +[![Console application](https://img.shields.io/badge/Examples-Console-green?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Console) + +Setting those events will automatically start a background polling system which will call your events accordingly: +- `OnMessage` for updates about messages (new or edited Message, Channel Post or Business Messages) +- `OnUpdate` for all other type of updates (callback, inline-mode, chat members, polls, etc..) + +> [!NOTE] +> If you don't set OnMessage, the OnUpdate event will be triggered for all updates, including messages. + +## By using the `StartReceiving` method (or `ReceiveAsync`) +[![Advanced console application](https://img.shields.io/badge/Examples-Console.Advanced-green?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Console.Advanced) + +Those methods start a polling system which will call your method on incoming updates. + +As arguments, you can pass either lambdas, methods or a class derived from `IUpdateHandler` that implements the handling of Update and Error. + +## By calling `GetUpdatesAsync` manually in a loop + +You can specify a timeout so that the call blocks for **up to** X seconds, waiting for an incoming update Here is an example implementation: ```csharp -{{#include ../../../Examples/3/LongPolling.cs:long-polling}} +int? offset = null; +while (!cts.IsCancellationRequested) +{ + var updates = await bot.GetUpdatesAsync(offset, timeout: 2); + foreach (var update in updates) + { + offset = update.Id + 1; + try + { + // put your code to handle one Update here. + } + catch (Exception ex) + { + // log exception and continue + } + if (cts.IsCancellationRequested) break; + } +} ``` \ No newline at end of file diff --git a/src/3/updates/webhook.md b/src/3/updates/webhook.md index 769ccd39..d1b84364 100644 --- a/src/3/updates/webhook.md +++ b/src/3/updates/webhook.md @@ -1,15 +1,15 @@ # Webhooks [![Webhook guide](https://img.shields.io/badge/Bot_API-Webhook%20guide-blue.svg?style=flat-square)](https://core.telegram.org/bots/webhooks) -[![Webhook ASP.NET example](https://img.shields.io/badge/Examples-ASP.NET%20WebApp-green?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Webhook.MinimalAPIs) -With Webhook, your application gets notified automatically by Telegram when new updates arrive for your bot. +With Webhook, your web application gets notified automatically by Telegram when new updates arrive for your bot. -Your application will receive HTTP POST requests with an Update structure in the body, using specific JSON serialization settings as presented in `Telegram.Bot.Serialization.JsonSerializerOptionsProvider.Options`. +Your application will receive HTTP POST requests with an Update structure in the body, using specific JSON serialization settings `Telegram.Bot.JsonBotAPI.Options`. Below, you will find how to configure an **ASP.NET Core Web API** project to make it work with Telegram.Bot, either with Controllers or Minimal APIs ## ASP.NET Core with Controllers (MVC) +[![ASP.NET example with Controllers](https://img.shields.io/badge/Examples-Webhook.Controllers-green?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Webhook.Controllers) First you need to configure your Web App startup code: - Locate the line `services.AddControllers();` _(in Program.cs or Startup.cs)_ @@ -34,6 +34,7 @@ public async Task HandleUpdate([FromBody] Update update) Good, now skip to [SetWebHookAsync](#setwebhookasync) below ## ASP.NET Core with Minimal APIs +[![ASP.NET example with Minimal APIs](https://img.shields.io/badge/Examples-Webhook.MinimalAPIs-green?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Webhook.MinimalAPIs) First you need to configure your Web App startup code: - Locate the line `builder.Build();` _(in Program.cs)_ @@ -63,7 +64,7 @@ public async Task Post() { Update update; using (var body = await Request.Content.ReadAsStreamAsync()) - update = System.Text.Json.JsonSerializer.Deserialize(body, JsonSerializerOptionsProvider.Options); + update = System.Text.Json.JsonSerializer.Deserialize(body, JsonBotAPI.Options); await HandleUpdate(update); return Ok(); } @@ -78,26 +79,30 @@ await bot.SetWebhookAsync("https://your.public.host:port/bot", allowedUpdates: [ You can now deploy your app to your webapp host machine. +_Note: If you decide to switch back to [Long Polling](polling.md), remember to call `bot.DeleteWebhookAsync()`_ + ## Common issues -- You need a supported certificate - If your host doesn't provide one or you want to develop on your own machine, consider using [ngrok](https://ngrok.com/): -Useful [step-by-step guide](https://medium.com/@oktaykopcak/81c8c4a9a853) +- You need a [supported certificate](https://core.telegram.org/bots/faq#i-39m-having-problems-with-webhooks) + If your host doesn't provide one, or you want to develop on your own machine, consider using [ngrok](https://ngrok.com/): +See this useful [step-by-step guide](https://medium.com/@oktaykopcak/81c8c4a9a853) - You must use HTTPS (TLS 1.2+), IPv4, and ports 443, 80, 88, or 8443 -- [Official webhook guide](https://core.telegram.org/bots/webhooks) +- The [Official webhook guide](https://core.telegram.org/bots/webhooks) gives a lot of details - If your update handler throws an exception or takes too much time to return, Telegram will consider it a temporary failure and will RESEND the same update a bit later. - So you may want to prevent handling the same update.Id twice: + You may want to prevent handling the same update.Id twice: ```csharp if (update.Id <= LastUpdateId) return; LastUpdateId = update.Id; - // put your code to handle the Update here. + // your code to handle the Update here. ``` - Most web hostings will recycle your app after some HTTP inactivity (= stop your app and restart it on the next HTTP request) - To prevent issues like this: + To prevent issues with this: - Search for an Always-On option with your host _(usually not free)_ - Make sure your web app can be safely stopped (saved state) and restarted later (reloading state) - Make sure you don't have critical background code that needs to keep running at all time - Have a service like [cron-job.org](https://cron-job.org/) ping your webapp every 5 minutes to keep it active. _(host will likely still recycle your app after a few days)_ - Host your app on a VPS machine rather than a webapp host. +- Updates are arriving sequentially, is this behaviour correct? + - Check question [Question 28 in the FAQ](../../FAQ.md#28-why-are-my-updates-being-processed-sequentially-when-im-using-webhooks). \ No newline at end of file diff --git a/src/4/README.md b/src/4/README.md index bb7074b4..98f1058c 100644 --- a/src/4/README.md +++ b/src/4/README.md @@ -1,11 +1,22 @@ -# Advanced +# Advanced topics -- [Login Widget](login-widget.md) -- [Passport](passport/) - - [Quickstart](passport/quickstart.md) - - [Files & Documents](passport/files-docs.md) - - [Data Errors](passport/errors.md) - - [RSA Key](passport/key.md) - - [Decryption FAQ](passport/faq.md) - [Proxy](proxy.md) -- [Logging](logging.md) +- [Business Features](business.md) +- [Payments API](payments.md) +- [Mini Apps](webapps.md) +- [Passport](passport/) + +## Telegram Login Widget + +You can use `InlineKeyboardButton.WithLoginUrl` to easily initiate a login connection to your website using the user's Telegram account credentials. +```csharp +replyMarkup: new InlineKeyboardMarkup(InlineKeyboardButton.WithLoginUrl( + "login", new LoginUrl { Url = "https://yourdomain.com/url" })) +``` + +You'll need to associate your website domain with your bot by sending `/setdomain` to `@BotFather`. + +See official documentation about [Telegram Login Widget](https://core.telegram.org/widgets/login) for more information. + +Server-side, you can use our separate repository [`Telegram.Bot.Extensions.LoginWidget`](https://github.com/TelegramBots/Telegram.Bot.Extensions.LoginWidget) +to validate the user credentials, or to generate a Javascript to show the login widget directly on your website. \ No newline at end of file diff --git a/src/4/business.md b/src/4/business.md new file mode 100644 index 00000000..c48bb9c0 --- /dev/null +++ b/src/4/business.md @@ -0,0 +1,54 @@ +# Business Bot Features + +[![Bot Business Mode](https://img.shields.io/badge/Bot_API_Doc-Business_Mode_-blue.svg?style=flat-square)](https://core.telegram.org/bots/features#bots-for-business) + +Several [business features](https://telegram.org/blog/telegram-business) have been added for premium users to Telegram. + +In particular, premium users can now select a bot to act as a [**chatbot on their behalf**](https://telegram.org/blog/telegram-business#chatbots-for-business), +in order to manage/reply to messages from other users _(typically, their business customers)_. + +## BotFather configuration + +First, the bot owner need to talk to [@BotFather](https://t.me/BotFather) and go to the **Bot Settings** to enable **Business Mode** + +In the following sections, we will refer to the premium user using your chatbot as "*the business owner*". + + + +## BusinessConnection update + +Once your chatbot is configured, the business owner has to go to their account settings, under **Telegram Business > Chatbots** and type your bot username. + +At this point, your bot will receive an `update.BusinessConnection` which contains: +- a unique id _(you may want to store this)_ +- details on the User (business owner) +- IsEnabled _(false if the business connection got cancelled)_ +- CanReply _(if the bot can act on behalf of that user in chats that were active in the last 24 hours)_ + +You can retrieve these info again later using `GetBusinessConnectionAsync(id)` + +## BusinessMessage updates + +From now on, your bot will receive updates regarding private messages between users (customers) and the business owner: +- `update.BusinessMessage`: a customer sent a new message to the business owner +- `update.EditedBusinessMessage`: a customer modified one of its message sent to the business owner +- `update.DeletedBusinessMessages`: a customer deleted some messages in private with the business owner + +In these messages/updates, the field `BusinessConnectionId` will tell you which BusinessConnection this applies to +_(useful for context if your chatbot is used by several business owners)_ + +## Acting on behalf of the business owner + +If the business owner enabled "**Reply to message**" during the initial business connection, +your bot can reply or do some other actions on behalf of their user account. + +To do so, you can call many Bot API methods with the optional _businessConnectionId:_ parameter. + +This way your bot can send/edit/pin messages, send chat actions (like _"typing"_), manage polls/live location, as if you were the business owner user. + +Some notes about messages sent on behalf of the business owner: +- They will NOT be marked with your bot name from the customer point of view +- They will be marked with your bot name in the business owner private chat (a banner also appears on top of the chat) +- These features are limited to private chats initiated by customers talking to the business owner. diff --git a/src/4/logging.md b/src/4/logging.md deleted file mode 100644 index ddf2199f..00000000 --- a/src/4/logging.md +++ /dev/null @@ -1,3 +0,0 @@ -# Logging - -> This chapter is not yet written. diff --git a/src/4/login-widget.md b/src/4/login-widget.md deleted file mode 100644 index 431d7635..00000000 --- a/src/4/login-widget.md +++ /dev/null @@ -1,7 +0,0 @@ -# Telegram Login Widget - -Everything related to the [Telegram Login Widget] has been moved to a separate repository: -[`Telegram.Bot.Extensions.LoginWidget`][Login widget repo] - -[Telegram Login Widget]: https://core.telegram.org/widgets/login -[Login widget repo]: https://github.com/TelegramBots/Telegram.Bot.Extensions.LoginWidget diff --git a/src/4/passport/errors.md b/src/4/passport/errors.md index 0000c09e..d2516498 100644 --- a/src/4/passport/errors.md +++ b/src/4/passport/errors.md @@ -1,3 +1,43 @@ # Passport Data Errors -> This chapter is not yet written. +[![setPassportDataErrors method](https://img.shields.io/badge/Bot_API_method-setPassportDataErrors-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#setpassportdataerrors) +[![Passport Element Errors tests](https://img.shields.io/badge/Examples-Passport_Element_Errors-green.svg?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Extensions.Passport/tree/master/test/IntegrationTests/Passport%20Element%20Errors) + +If the passport data you received contains errors, the bot can use the [SetPassportDataErrors](https://core.telegram.org/bots/api#setpassportdataerrors) method to inform the user and request information again. The user will not be able to resend the data, until all errors are fixed. + +Here is an example call using decrypted [credentials](files-docs.md#credentials): + +```csharp +//using Telegram.Bot.Types.Passport; + +PassportElementError[] errors = +{ + new PassportElementErrorDataField + { + Type = EncryptedPassportElementType.Passport, + FieldName = "document_no", + DataHash = credentials.SecureData.Passport.Data.DataHash, + Message = "Invalid passport number" + }, + new PassportElementErrorFrontSide + { + Type = EncryptedPassportElementType.Passport, + FileHash = credentials.SecureData.Passport.FrontSide.FileHash, + Message = "Document scan is redacted" + }, + new PassportElementErrorSelfie + { + Type = EncryptedPassportElementType.Passport, + FileHash = credentials.SecureData.Passport.Selfie.FileHash, + Message = "Take a selfie without glasses" + }, + new PassportElementErrorTranslationFile + { + Type = EncryptedPassportElementType.Passport, + FileHash = credentials.SecureData.Passport.Translation[0].FileHash, + Message = "Document photo is blury" + }, +}; + +await bot.SetPassportDataErrors(passportMessage.From.Id, errors); +``` diff --git a/src/4/passport/files-docs.md b/src/4/passport/files-docs.md index a1db8f7b..bf7b42ca 100644 --- a/src/4/passport/files-docs.md +++ b/src/4/passport/files-docs.md @@ -126,7 +126,8 @@ using (System.IO.Stream stream = System.IO.File.OpenWrite("/path/to/front-side.j } ``` -> **Warning**: This method is convenient to use but gives you the least amount of control over the operations. +> [!WARNING] +> This method is convenient to use but gives you the least amount of control over the operations. ### Reverse Side File diff --git a/src/4/passport/key.md b/src/4/passport/key.md index 451e713b..55b5888f 100644 --- a/src/4/passport/key.md +++ b/src/4/passport/key.md @@ -36,7 +36,8 @@ static RSA GetPrivateKey() { } ``` -> **Note**: You don't necessarily need to have a dependency on the [BouncyCastle package] in your bot project. +> [!NOTE] +> You don't necessarily need to have a dependency on the [BouncyCastle package] in your bot project. > The section below offers a better alternative. ## From RSA Parameters diff --git a/src/4/payments.md b/src/4/payments.md new file mode 100644 index 00000000..74a7ce40 --- /dev/null +++ b/src/4/payments.md @@ -0,0 +1,138 @@ +# Bot Payments API & Telegram Stars + +[![Bot Payments API](https://img.shields.io/badge/Bot_Payments_API-Physical_Goods_-blue.svg?style=flat-square)](https://core.telegram.org/bots/payments) +[![Bot Payments Stars](https://img.shields.io/badge/Bot_Payments_API-Digital_Goods-blue.svg?style=flat-square)](https://core.telegram.org/bots/payments) + +Telegram offers a safe, simple and unified payment system for goods and services. + +Due to Google/Apple policies, there is a distinction between: +- **Digital Goods & Services**, which can be paid using [Telegram Stars](https://telegram.org/blog/telegram-stars) (XTR) only +- **Physical Goods**, which can be paid using real money, and can request more details like a shipping address. + +Both process are similar, so we will demonstrate how to do a Telegram Stars payment (simpler) and give you some info about the difference for Physical Goods. + +## Important notes for physical goods + +Before starting, you need to talk to [@BotFather](https://t.me/BotFather), select one of the supported +[Payment providers](https://core.telegram.org/bots/payments#supported-payment-providers) +(you need to open an account on the provider website), and complete the connection procedure +linking your bot with your provider account. +>It is recommended to start with the [Stripe TEST MODE](https://core.telegram.org/bots/payments#testing-payments-the-39stripe-test-mode-39-provider) +provider so you can test your bot with fake card numbers before going live. + +Price amounts are expressed as integers with some digits at the end for the "decimal" part. +For example, in USD currency there are 2 digits for cents, so 12345 means $123.45 ; With Telegram Stars (XTR), there are no extra digits. +See ["exp" in this table](https://core.telegram.org/bots/payments/currencies.json), to determine the number of decimal digits for each currency. + +## Sending an invoice +[![send invoice method](https://img.shields.io/badge/Bot_API_method-sendInvoice-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#sendinvoice) + +When your bot is ready to issue a payment for the user to complete, it will send an invoice: +```csharp +await bot.SendInvoiceAsync( + chatId: chatId, // same as userId for private chat + title: "Product Title", + description: "Product Detailed Description", + payload: "InternalProductID", // not sent nor shown to user + providerToken: "" // empty string for XTR + currency: "XTR", // 3-letters ISO 4217 currency + prices: [("Price", 500)], // only one price for XTR + photoUrl: "https://cdn.pixabay.com/photo/2012/10/26/03/16/painting-63186_1280.jpg", +); +``` + +Alternatively, you can instead generate an URL for that payment with [`CreateInvoiceLinkAsync`](https://core.telegram.org/bots/api#createinvoicelink). + +With Physical Goods, you can specify [more parameters](https://core.telegram.org/bots/api#sendinvoice) like: +- the `providerToken` obtained by BotFather (something like "1234567:TEST:aBcDeFgHi") +- several price lines detailing the total price +- some suggested tips +- the need for extra information about the user, including a shipping address +- if the price is flexible depending on the shipping address/method + +If your bot supports [Inline Mode](../3/inline.md), you can also [send invoices as inline results](https://core.telegram.org/bots/api#inputinvoicemessagecontent) ("via YourBot"). + +## Handling the `ShippingQuery` Update + +[![Shipping Query Update](https://img.shields.io/badge/Bot_API_update-ShippingQuery-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#shippingquery) +[![Answer Shipping Query method](https://img.shields.io/badge/Bot_API_method-answerShippingQuery-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#answershippingquery) + +This update is received only for Physical Goods, if you specified a flexible price. +Otherwise you can skip to the next section. + +`update.ShippingQuery` would contain information like the current shipping address for the user, and can be received again if the user changes the address. + +You should check if the address is supported, and reply using `bot.AnswerShippingQueryAsync` with an error message or a list of shipping options with associated price lines: +```csharp +var shippingOptions = new List(); +shippingOptions.Add(new() { Title = "DHL Express", Id = "dhl-express", + Prices = [("Shipping", 1200)] }); +shippingOptions.Add(new() { Title = "FedEx Fragile", Id = "fedex-fragile", + Prices = [("Packaging", 500), ("Shipping", 1800)] }); +await bot.AnswerShippingQueryAsync(shippingQuery.Id, shippingOptions); +``` + +## Handling the `PreCheckoutQuery` Update + +[![Pre Checkout Query Update](https://img.shields.io/badge/Bot_API_update-PreCheckoutQuery-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#precheckoutquery) +[![Answer Pre Checkout Query method](https://img.shields.io/badge/Bot_API_method-answerPreCheckoutQuery-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#answerprecheckoutquery) + +This update is received when the user has entered their payment information and confirmed the final Pay button. + +`update.PreCheckoutQuery` contains all the requested information for the order, so you can validate that all is fine before actual payment + +You must reply within 10 seconds with: +```csharp +if (confirm) + await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id); +else + await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id, "Can't process your order: "); +``` + +## Handling the `SuccessfulPayment` Message + +[![Successful Payment Message](https://img.shields.io/badge/Bot_API_message-SuccessfulPayment-blue.svg?style=flat-square)](https://core.telegram.org/bots/api#successfulpayment) + +If you confirmed the order in the step above, Telegram requests the payment with the payment provider. + +If the payment is successfully processed, you will receive a private Message of type `SuccessfulPayment` from the user, and you must then proceed with delivering the goods or services to the user. + +The `message.SuccessfulPayment` structure contains all the same previous information, plus two payment identifiers from Telegram & from the Payment Provider. + +You should store these ChargeId strings for traceability of the transaction in case of dispute, or refund _(possible with [RefundStarPayment](https://core.telegram.org/bots/api#refundstarpayment))_. + +## Full example code for Telegram Stars transaction + +```csharp +using Telegram.Bot; +using Telegram.Bot.Types; + +var bot = new TelegramBotClient("YOUR_BOT_TOKEN"); +bot.OnUpdate += OnUpdate; +Console.ReadKey(); + +async Task OnUpdate(Update update) +{ + switch (update) + { + case { Message.Text: "/start" }: + await bot.SendInvoiceAsync(update.Message.Chat, + "Unlock feature X", "Will give you access to feature X of this bot", "unlock_X", "", + "XTR", [("Price", 200)], photoUrl: "https://cdn-icons-png.flaticon.com/512/891/891386.png"); + break; + case { PreCheckoutQuery: { } preCheckoutQuery }: + if (preCheckoutQuery is { InvoicePayload: "unlock_X", Currency: "XTR", TotalAmount: 200 }) + await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id); + else + await bot.AnswerPreCheckoutQueryAsync(preCheckoutQuery.Id, "Invalid order"); + break; + case { Message.SuccessfulPayment: { } successfulPayment }: + System.IO.File.AppendAllText("payments.log", $"\n{DateTime.Now}: " + + $"User {update.Message.From} paid for {successfulPayment.InvoicePayload}: " + + $"{successfulPayment.TelegramPaymentChargeId} {successfulPayment.ProviderPaymentChargeId}"); + if (successfulPayment.InvoicePayload is "unlock_X") + await bot.SendTextMessageAsync(update.Message.Chat, "Thank you! Feature X is unlocked"); + break; + }; +} +``` \ No newline at end of file diff --git a/src/4/tests.md b/src/4/tests.md deleted file mode 100644 index bba9afd3..00000000 --- a/src/4/tests.md +++ /dev/null @@ -1,155 +0,0 @@ -# Systems Integration Tests - -Integration tests are meant to test the project with real data from Telegram. They are semi-automated tests and tester(s) need to interact with bot for some cases during the test execution. Tests could be used as a playground for exploring Bot API methods. - -## Sample Test Diagnostics Output - -All the test output goes into the supergroup/private chats specified in configurations or interactively during test execution. You can see some samples of test output below. - -Admin bots can change chat photo. - -![Test Case: Set Chat Photo](docs/testcase-chatphoto.jpg) - -Invoices could be paid in private chats. - -![Test Case: Set Chat Photo](docs/testcase-payment.jpg) - -## How Tests Works - -These integration tests are written just like regular unit tests with xUnit framework so they seem to be unit tests. When you run test(s), bot makes a request to Bot API and you should see results(message or service notification) in the chat with bot. - -When you build the solution, you will see them in Test Explorer window. Tests could be run through .NET Core's CLI as well and that's how this project's CI is set up. - -A bot, of course, is needed to test Bot API. This document refers to its user name as _MyTestBot_. - -_Tester_ refers to user name of person testing the bot. Multiple testers could interact with bot during -test execution. If super group chat has other members that are not listed as testers, bot ignores their -messages during test execution. Testers must have user names assigned and their user names should be set -in test configurations before hand. - -All the tests happen in two chats. A Super Group chat and a Private chat with one of the testers. - -Test cases that need tester's interaction to continue, have a limit of usually 2 minutes to wait for receiving an expected update from API. - -Tests could be run individually, in collections, or all at once. All the test collection and test cases within them are ordered and tests will not run in parallel. - -## Test Environment Setup - -Create a Super Group and add bot to it. Promote bot to admin and make sure it has all the permissions. This group needs to have another regular(non-admin) member to be used in tests for chat administration methods(such as Kick, Restrict, Unban). A super group with 2 testers in it, one admin and the other non-admin member, is enough. - -Bot should have some features enabled, usually through BotFather, in order to pass tests. These features are listed below: - -- Inline Queries -- Payment Provider - -For making testing process more convenient, set the following commands for MyTestBot as well. The purpose for these commands is explained in the sections below. - -```text -test - Start test execution - me - Select me for testing admin methods -``` - -## Test Configurations - -You can see a list of configuration keys in `appsettings.json` file. Make a copy of this file and store your configurations there. In addition to `appsettings.json` and `appsettings.Development.json`, environment variables prefixed by `TelegramBot_` are also read into program. - -```bash -cp appsettings.json appsettings.Development.json -``` - -### Required Settings - -Only 2 values must be provided before test execution. - -#### API Token - -This is required for executing any test case. - -```json -{ - "ApiToken": "MyTestBot-API-TOKEN" - /* ... */ -} -``` - -#### Allowed Users - -A comma separated list indicating user name(s) of tester(s). Any update coming from users other than the ones listed here are discarded during test execution. - -```json -{ - /* ... */ - "AllowedUserNames": "tester1, Tester2, TESTER3" - /* ... */ -} -``` - -### Optional Settings - -The following settings are not required for two reasons. Either bot can ask for them during test execution or it is not a required setting for all test cases. - -Bot will ask testers in supergroup/private chat for the necessary information. It would be faster to set all the optional settings as well because it makes testing process faster and less manual. - -> For obtaining values of necessary settings, you can set breakpoints in some test methods and extract values such as chat id or user id. - -#### Supergroup Chat Id - -Bot send messages to this chat in almost all test cases except cases like sending payments that must be to a private chat. - -If not set, before starting any test method, bot waits for a tester to send it a `/test` command in a super group chat (that bot is also a member of). - -```json -{ - /* ... */ - "SuperGroupChatId": -1234567890 - /* ... */ -} -``` - -#### Payment Settings - -##### [Required] Payment Provider Token - -This token is **required** for any test case regarding payments and must be provided before starting tests. - -Consult Telegram API documentations and talk to BotFather to get a test token from a payment provider. - -```json -{ - /* ... */ - "PaymentProviderToken": "MY-PAYMENT-PROVIDER-TOKEN" - /* ... */ -} -``` - -##### TesterPrivateChatId - -Invoices could only be sent to private chats. If not set, bot will wait for a tester to send it `/test` command in a private chat. - -```json -{ - /* ... */ - "TesterPrivateChatId": 1234567890 - /* ... */ -} -``` - -#### Chat Administration - -For this type of tests, bot should be a privileged admin of that super group. Methods such as kick or unban will be performed on a regular (non-admin) tester in that chat. - -If the following 3 settings are not set, bot will ask a tester to send it `/me` command in a private chat with bot. - -- Regular Member's User Id -- Regular Member's User Name -- Regular Member's Private Chat Id - -```json -{ - /* ... */ - "RegularMemberUserId": 1234567890, - "RegularMemberUserName": "tester3", - "RegularMemberPrivateChatId": 1234567890 - /* ... */ -} -``` diff --git a/src/4/webapps.md b/src/4/webapps.md new file mode 100644 index 00000000..3481ba96 --- /dev/null +++ b/src/4/webapps.md @@ -0,0 +1,43 @@ +# Telegram Mini Apps + +[![Mini App bot API](https://img.shields.io/badge/Bot_API_Doc-Mini_Apps-blue.svg?style=flat-square)](https://core.telegram.org/bots/webapps) +[![MiniApp example project](https://img.shields.io/badge/Examples-MiniApp-green?style=flat-square)](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/MiniApp) + +If standard Telegram Bot features aren't enough to fit your needs, +you may want to consider building a [Mini App](https://core.telegram.org/bots/webapps) instead. + +This take the form of an integrated browser window showing directly web pages from your bot WebApp, +so you have more control with HTML/JS to display the interface you like. + + + +Check our [full example project](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/MiniApp) based on Razor pages, and including a clone of the above [@DurgerKingBot](https://t.me/DurgerKingBot) and more demo to test features. + +## Starting Mini-Apps + +Mini Apps can be launched from various ways: +- [Keyboard Buttons](../2/reply-markup.md#custom-keyboards): `KeyboardButton.WithWebApp` +- [Inline Buttons](../2/reply-markup.md#inline-keyboards): `InlineKeyboardButton.WithWebApp` +- Chat menu button (left of user textbox): via @BotFather or `SetChatMenuButtonAsync` +- Inline-mode results with a "Switch to Mini App" button: `AnswerInlineQueryAsync` with parameter `InlineQueryResultsButton.WebApp` +- Direct link like https://t.me/botusername/appname?startapp=command + +## Integration +Your web pages must include this script in the `` part: +```html + +``` + +Your Javascript can then access a [Telegram.WebApp](https://core.telegram.org/bots/webapps#initializing-mini-apps) object supporting many [properties and methods](https://core.telegram.org/bots/webapps#initializing-mini-apps), as well as [event handlers](https://core.telegram.org/bots/webapps#events-available-for-mini-apps). + +In particular, you may want to use your Telegram.Bot backend to validate the authenticity of `Telegram.WebApp.initData`. + +This can be done using our `AuthHelpers.ParseValidateData` method and the bot token, to make sure the requests come from Telegram and obtain information about Telegram user and context. + +## For more details + +To read more about Mini Apps, see + +Visit our example project: \ No newline at end of file diff --git a/src/FAQ.md b/src/FAQ.md index 1fb36e08..f87e6116 100644 --- a/src/FAQ.md +++ b/src/FAQ.md @@ -2,12 +2,15 @@ I recommend you read all of these as you will learn many interesting things. Or you can use Ctrl-F to search for a specific topic. + + ### _1. Can you give me documentation/examples links?_ -- Follow [this installation guide](README.md#-installation) to install the latest versions of the library. -- You are on the [main documentation website](https://telegrambots.github.io/book/). -- Here are [some bot examples](https://github.com/TelegramBots/Telegram.Bot.Examples) -- Search the [official API documentation](https://core.telegram.org/bots/api) and [official FAQ](https://core.telegram.org/bots/faq). +- Follow [this installation guide](https://telegrambots.github.io/book/#-installation) to install the latest versions of the library. +- Here is on the [main documentation website](https://telegrambots.github.io/book/). +- You can find [more bot example projects](https://github.com/TelegramBots/Telegram.Bot.Examples) here +- Search the [official API documentation](https://core.telegram.org/bots/api) and [official bots FAQ](https://core.telegram.org/bots/faq). - check tooltips in your IDE, or navigate with F12 on API methods and read/expand comments. + >If you're C# beginner, you should learn about [async programming](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/). ### _2. My update handler fails or stops executing at some point_ @@ -20,7 +23,7 @@ Not all messages are text messages, `message.Text` could be null (see also `mess So please use a debugger to check the content of your variables or structure fields and make sure your code can handle all cases. ### _4. How to add buttons under a message?_ -Pass an [InlineKeyboardMarkup](https://telegrambots.github.io/book/2/reply-markup.html#inline-keyboards) into the `replyMarkup` parameter when sending the message. You will likely need to create a `List>` for rows&columns +Pass an [InlineKeyboardMarkup](2/reply-markup.md#inline-keyboards) into the `replyMarkup` parameter when sending the message. You will likely need to create a `List>` for rows&columns _See also next question._ ### _5. How to handle a click on such inline buttons?_ @@ -31,33 +34,41 @@ Your code should answer to the query within 10 seconds, using `AnswerCallbackQue ### _6. How to show a popup text to the user?_ It is only possible with inline callback button _(see above questions)_. -In `AnswerCallbackQueryAsync`, pass parameter `showAlert: true` to display as a popup. +Use `AnswerCallbackQueryAsync` with some text, and pass parameter `showAlert: true` to display the text as an alert box instead of a short popup. ### _7. How to fill the input textbox of the user with some text?_ -You can't. The closest you can do is setup a `ReplyKeyboardMarkup` for buttons with pre-made texts under the textbox +There is not a simple direct method for this, but here is what you can try: +- With a [Public username link](https://core.telegram.org/api/links#public-username-links): `t.me/username?text=Hello+World` + _(works only if target is a user/bot and not the current chat)_ +- With a [Share link](https://core.telegram.org/api/links#share-links): `tg://msg_url?url=https://example.com&text=Hello+World` + _(user must select a target first)_ +- With a [Bot deep link](https://core.telegram.org/api/links#bot-links): `t.me/botusername?start=param` + _(param is limited to base64 characters, bot will receive `/start param`)_ +- With a [ReplyKeyboardMarkup](2/reply-markup.md#custom-keyboards): buttons under the textbox to send pre-made texts +- With an [Inline Mode bot](3/inline.md) and `SwitchInlineQuery` inline buttons, you can make the user pre-type the name of your bot followed by some query ### _8. How to fetch previous messages?_ -You can't with Bot API but it's possible with [WTelegramBot](https://www.nuget.org/packages/WTelegramBot). +You can't with Bot API but it's possible with [WTelegramBot](https://www.nuget.org/packages/WTelegramBot#readme-body-tab). Normally, bots only get messages at the moment they are posted. You could archive them all in a database for later retrieval. ### _9. How to fetch a list of all users in chat?_ -You can't with Bot API but it's possible with [WTelegramBot](https://www.nuget.org/packages/WTelegramBot). -Normally, bots can only get the list of administrators (`GetChatAdministratorsAsync`) or detail about one specific member (`GetChatMemberAsync`) +You can't with Bot API but it's possible with [WTelegramBot](https://www.nuget.org/packages/WTelegramBot#readme-body-tab). +Normally, bots can only get the list of admins (`GetChatAdministratorsAsync`) or detail about one specific member (`GetChatMemberAsync`) Alternatively, you can keep track of users by observing new messages in a chat and saving user info into a database. ### _10. How to send a private message to some random user?_ You can't. Bots can only send private messages to users that have already initiated a private chat with your bot. ### _11. How to detect if a user blocked my bot?_ -You would have received an `update.MyChatMember` with `NewChatMember.Status == ChatMemberStatus.Kicked` -If you didn't record that info, you can try to `SendChatActionAsync` and see if it raise an exception. +You would have received an `update.MyChatMember` with `NewChatMember.Status == ChatMemberStatus.Kicked` +If you didn't record that info, you can try to `SendChatActionAsync` and see if it raises an exception. ### _12. How to set a caption to a media group (album)?_ Set the `media.Caption` (and `media.ParseMode`) on the first media ### _13. How to write a bot that make questions/answers with users?_ Either you can code a complex state machine workflow, saving where each user is currently in the discussion. -Or you can just use [YourEasyBot](https://github.com/wiz0u/YourEasyBot) which makes sequential bots very simple to write... _(or one of the [other frameworks](https://github.com/TelegramBots/Telegram.Bot/issues/1072) available for Telegram.Bot)_ +Or you can just use [YourEasyBot](https://github.com/wiz0u/YourEasyBot) which makes sequential bots very simple to write... _(or one of the [other frameworks](https://github.com/TelegramBots/Telegram.Bot/wiki) available for Telegram.Bot)_ ### _14. How to make font effects in message?_ Pass a `ParseMode.Html` _(or `ParseMode.MarkDownV2`)_ to argument `parseMode`. See [formatting options](https://core.telegram.org/bots/api#formatting-options). @@ -70,17 +81,18 @@ A credit-card is necessary but you shouldn't get charged if you stay within quot Other cloud providers might also offer similar services. ### _16. Is there some limitation/maximum about feature X?_ -See https://limits.tginfo.me for a list of limitations. +See for a list of limitations. ### _17. How to populate the bot Menu button / commands list?_ You can either do this via [@BotFather](https://t.me/BotFather) _(static entries)_, or you can use `SetMyCommandsAsync` for more advanced settings ⚠️ This can only be filled with bot commands, starting with a `/` and containing only latin characters `a-z_0-9` ### _18. How to receive `ChatMember` updates?_ -You should specify all update types including ChatMember in `AllowedUpdates` array on `StartReceiving`:`ReceiverOptions` or `SetWebhookAsync` +You should specify all update types **including ChatMember** in `AllowedUpdates` array on `StartReceiving`:`ReceiverOptions` or `SetWebhookAsync` ### _19. How to get rid of past updates when I restart my bot?_ Pass true into `StartReceiving`:`ReceiverOptions`:`DropPendingUpdates` or `SetWebhookAsync`:`dropPendingUpdates` +Alternatively, you can call `await bot.DropPendingUpdatesAsync()` before polling or using [`bot.OnUpdate`](3/updates/polling.md#by-setting-botonupdate-andor-botonmessage). ### _20. Difficulties to upload & send a file/media?_ - Make sure you `await` until the end of the send method before closing the file (a "`using`" clause would close the file on leaving the current { scope } @@ -106,7 +118,7 @@ To post to a specific group, there is an alternative solution: ### _23. How to upgrade my existing code? You keep breaking compatibility!_ A new lead developer (Wizou) is now in charge of the library and commits to reduce code-breaking changes in the future. -Version 21.x of the library have been much improved to facilitate [migration from previous versions](Migration-Guide-to-Version-21.x.md) of the library, and include a lot of helpers/implicit operators to simplify your code. +Version 21.x of the library have been much improved to facilitate [migration from previous versions](migrate/Version-21.x.md) of the library, and include a lot of helpers/implicit operators to simplify your code. ### _24. Can I use several apps/instance to manage my bot?_ You can call API methods (like sending messages) from several instances in parallel @@ -114,7 +126,7 @@ You can call API methods (like sending messages) from several instances in paral ### _25. How do I get the user id from a username?_ -You can't with Bot API but it's possible with [WTelegramBot](https://www.nuget.org/packages/WTelegramBot). +You can't with Bot API but it's possible with [WTelegramBot](https://www.nuget.org/packages/WTelegramBot#readme-body-tab). Alternatively, you could store in database the mapping of `UserId`<->`Username`. Remember that not every user has a username. @@ -123,6 +135,27 @@ Remember that not every user has a username. Your bot has to be added as administrator of the channel. You will then receive the messages as `update.ChannelPost` or `update.EditedChannelPost`. +### _27. How to sent the same media multiple times_ +The first time, you will send the media with a stream (upload). Next times, you will use its **FileId**: +```csharp +var sent = await bot.SendVideoAsync(chatId, stream, ....); +var fileId = sent.Video.FileId + +// next times: +await bot.SendVideoAsync(chatId2, fileId, ...); +``` +For photos, use `sent.Photo[^1].FileId` + +### _28. Why are my updates being processed sequentially when I'm using webhooks?_ +Telegram servers send updates to your webhook sequentially, one at a time. They will not send the next update until you have acknowledged the current one. To acknowledge an update, you must respond with an HTTP Status Code 200. + +Telegram not pushing updates concurrently means that even if you're using webhooks with ASP.NET Core, you won't be able to leverage ASP.NET Core's built-in concurrency features. + +For most use cases and lightweight bots, this sequential processing should not pose a problem. However, if your bot handles long-running processes for some or all updates, it may be beneficial to implement an internal queue using [Channels](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) or a queue implementation such as `Queue` or `ConcurrentQueue`. + + ### This FAQ doesn't have my question on it -Feel free to [join our Telegram group](https://t.me/joinchat/B35YY0QbLfd034CFnvCtCA) and ask your question there +Feel free to [join our Telegram group](https://t.me/joinchat/B35YY0QbLfd034CFnvCtCA) and ask your question there. + +Consider contributing to this FAQ or any other part of the documentation if your question may be of interest for others. \ No newline at end of file diff --git a/src/README.md b/src/README.md index dee005e5..ab22b77e 100644 --- a/src/README.md +++ b/src/README.md @@ -1,30 +1,34 @@ # Telegram Bots Book -[![NuGet](https://img.shields.io/nuget/dt/Telegram.Bot.svg?style=flat-square)](https://nuget.voids.site/packages/Telegram.Bot) +[![NuGet](https://img.shields.io/nuget/dt/Telegram.Bot.svg?style=flat-square)](https://dev.azure.com/tgbots/Telegram.Bot/_artifacts/feed/release/NuGet/Telegram.Bot) [![Repository](https://img.shields.io/github/stars/TelegramBots/Telegram.Bot.svg?style=social&label=Stars)](https://github.com/TelegramBots/Telegram.Bot) **[Telegram.Bot](https://github.com/TelegramBots/Telegram.Bot)** is the most popular .NET client for [Telegram Bot API](https://core.telegram.org/bots/api), allowing [developers to build bots](https://core.telegram.org/bots) for [Telegram](https://www.telegram.org) messaging app. -Telegram Bot API is [officially documented](https://core.telegram.org/bots/api) but this book covers all you need to know to create a -chatbot in .NET. There are also many concrete examples written in C#. -The guides here can even be useful to bot developers using other languages/platforms as it shows best practices -in developing Telegram chatbots with examples. - -➡️ Access the book pages via the Table Of Content (top/left), or start your journey with our [_Quickstart_](1/quickstart.md) guide. +This book covers all you need to know to create a chatbot in .NET, with many concrete examples written in C#. +Begin with our [_Quickstart_](1/quickstart.md), or choose from the Table Of Content (left/top), and don't miss our useful [Frequently Asked Questions](FAQ.md). ## 🧩 Installation -⚠️ _Latest versions of the library are not available on Nuget․org due to false-positive malware detection. We are working with Nuget/ESET teams to resolve this issue._ -In the mean time, latest versions are available on our [special nuget feed](https://nuget.voids.site/packages/Telegram.Bot): `https://nuget.voids.site/v3/index.json` +> [!IMPORTANT] +> _Latest versions of the library are not available on Nuget․org due to false-positive malware detection. We are working with Nuget/ESET teams to resolve this issue._ + +In the mean time, latest versions are available on our [special nuget source](https://dev.azure.com/tgbots/Telegram.Bot/_artifacts/feed/release/NuGet/Telegram.Bot): +`https://pkgs.dev.azure.com/tgbots/Telegram.Bot/_packaging/release/nuget/v3/index.json` -Follow the pictures below to configure the Package source in Visual Studio: -![In Visual Studio](1/docs/NugetPackageManager.jpg) +See the screenshots below to configure the Package source in Visual Studio: +![In Visual Studio](1/docs/NugetPackageManager.png) +and make sure to follow the [Migration Guide for v21.*](migrate/Version-21.x.md) if you have existing bot code. -Alternatively you can set up a `nuget.config` file at the root of your project/solution: +Alternatively you can use command line: +``` +dotnet nuget add source "https://pkgs.dev.azure.com/tgbots/Telegram.Bot/_packaging/release/nuget/v3/index.json" -n Telegram.Bot +``` +Or set up a `nuget.config` file at the root of your project/solution: ```xml - + ``` @@ -41,10 +45,10 @@ This book is filled with ready-to-use snippets of code, but you can also find fu |Visit our|URL| |--|--| -|Nuget feed|https://nuget.voids.site/packages/Telegram.Bot| -|Github repo|https://github.com/TelegramBots/Telegram.Bot| -|Examples repo|https://github.com/TelegramBots/Telegram.Bot.Examples| -|Telegram news channel|https://t.me/tgbots_dotnet| -|Telegram support group|https://t.me/joinchat/B35YY0QbLfd034CFnvCtCA| -|Team page|https://github.com/orgs/TelegramBots/people| +|Nuget feed|| +|Github repo|| +|Examples repo|| +|Telegram news channel|| +|Telegram support group|| +|Team page|| diff --git a/src/SUMMARY.md b/src/SUMMARY.md index d9b98ad5..4c8c1bd9 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -2,7 +2,8 @@ - [Introduction](README.md) - [Quickstart](1/quickstart.md) - - [Full Example](1/example-bot.md) + - [First Chat Bot](1/example-bot.md) + - [Full Example](1/full-bot.md) - [Beginner](2/README.md) - [Sending Messages](2/send-msg/README.md) - [Text](2/send-msg/text-msg.md) @@ -13,10 +14,11 @@ - [Document & Animation](2/send-msg/document-animation-msg.md) - [Native Polls](2/send-msg/native-polls-msg.md) - [Other Messages](2/send-msg/other-msg.md) + - [Dealing with chats](2/chats.md) - [Reply Markup](2/reply-markup.md) - [Forward, Copy or Delete](2/forward-copy-delete.md) - [Intermediate](3/README.md) - - [Getting Updates](3/updates/README.md) + - [Working with Updates](3/updates/README.md) - [Long Polling](3/updates/polling.md) - [Webhooks](3/updates/webhook.md) - [Inline Mode](3/inline.md) @@ -26,18 +28,19 @@ - [Stickers](3/sticker.md) - [Advanced](4/README.md) - [Proxy](4/proxy.md) + - [Business Features](4/business.md) + - [Payments API](4/payments.md) + - [Mini Apps](4/webapps.md) - [Passport](4/passport/README.md) - [Quickstart](4/passport/quickstart.md) - [Files & Documents](4/passport/files-docs.md) - [Data Errors](4/passport/errors.md) - [RSA Key](4/passport/key.md) - [Decryption FAQ](4/passport/faq.md) - - [Login Widget](4/login-widget.md) - - [Systems Integration Tests](4/tests.md) - - [Logging](4/logging.md) -- [Migration guide => v14](Migration-Guide-to-Version-14.x.md) -- [Migration guide => v17](Migration-Guide-to-Version-17.x.md) -- [Migration guide => v18](Migration-Guide-to-Version-18.x.md) -- [Migration guide => v19](Migration-Guide-to-Version-19.x.md) -- [Migration guide => v21](Migration-Guide-to-Version-21.x.md) -- [FAQ](FAQ.md) +- [Frequently Asked Questions](FAQ.md) +- [Migration guides to newer versions](migrate/README.md) + - [Migrate to v21.*](migrate/Version-21.x.md) + - [Migrate to v19.*](migrate/Version-19.x.md) + - [Migrate to v18.*](migrate/Version-18.x.md) + - [Migrate to v17.*](migrate/Version-17.x.md) + - [Migrate to v14.*](migrate/Version-14.x.md) diff --git a/src/migrate/README.md b/src/migrate/README.md new file mode 100644 index 00000000..9da1fcdf --- /dev/null +++ b/src/migrate/README.md @@ -0,0 +1,7 @@ +# Migration guides to newer versions of the library + +- [Migrate to v21.*](Version-21.x.md) +- [Migrate to v19.*](Version-19.x.md) +- [Migrate to v18.*](Version-18.x.md) +- [Migrate to v17.*](Version-17.x.md) +- [Migrate to v14.*](Version-14.x.md) diff --git a/src/Migration-Guide-to-Version-14.x.md b/src/migrate/Version-14.x.md similarity index 100% rename from src/Migration-Guide-to-Version-14.x.md rename to src/migrate/Version-14.x.md diff --git a/src/Migration-Guide-to-Version-17.x.md b/src/migrate/Version-17.x.md similarity index 98% rename from src/Migration-Guide-to-Version-17.x.md rename to src/migrate/Version-17.x.md index 367c6268..d6c76e04 100644 --- a/src/Migration-Guide-to-Version-17.x.md +++ b/src/migrate/Version-17.x.md @@ -70,7 +70,7 @@ catch (OperationCancelledException exception) ## Removal of events -In v17 we removed events and introduced a new way of getting updates with [Telegram.Bot.Extensions.Polling] package. You can find an example in [First Chat Bot](./1/example-bot.md) article. +In v17 we removed events and introduced a new way of getting updates with [Telegram.Bot.Extensions.Polling] package. You can find an example in [First Chat Bot](../1/example-bot.md) article. ## Removal of API methods from `ITelegramBotClient` interface @@ -105,7 +105,7 @@ Also some default enums values were removed, e.g. `ParseMode.Default` since we s ### Constructor accepting IWebProxy -We removed constructor accepting `IWebProxy`. Now you have to configure HttpClient yourself to use proxy. You can find examples in [Working Behind a Proxy](./4/proxy.md#http-proxy) article. +We removed constructor accepting `IWebProxy`. Now you have to configure HttpClient yourself to use proxy. You can find examples in [Working Behind a Proxy](../4/proxy.md#http-proxy) article. ### InputMediaType diff --git a/src/Migration-Guide-to-Version-18.x.md b/src/migrate/Version-18.x.md similarity index 100% rename from src/Migration-Guide-to-Version-18.x.md rename to src/migrate/Version-18.x.md diff --git a/src/Migration-Guide-to-Version-19.x.md b/src/migrate/Version-19.x.md similarity index 100% rename from src/Migration-Guide-to-Version-19.x.md rename to src/migrate/Version-19.x.md diff --git a/src/Migration-Guide-to-Version-21.x.md b/src/migrate/Version-21.x.md similarity index 57% rename from src/Migration-Guide-to-Version-21.x.md rename to src/migrate/Version-21.x.md index 4f7af383..85110f0a 100644 --- a/src/Migration-Guide-to-Version-21.x.md +++ b/src/migrate/Version-21.x.md @@ -3,7 +3,7 @@ Important notes: - Don't bother about version 20, migrate directly to version 21.* - You won't find this version on Nuget: [See this guide to install it in your programs](https://telegrambots.github.io/book/index.html#-installation). -- Version 21.1 supports [Bot API 7.5](https://core.telegram.org/bots/api-changelog#june-18-2024) _(including [Telegram Stars payments](#payments-with-telegram-stars))_ +- Version 21.10 supports [Bot API 7.9](https://core.telegram.org/bots/api#august-14-2024) _(including [Telegram Stars payments](../4/payments.md))_ - Library is now based on System.Text.Json and doesn't depend on NewtonsoftJson anymore. _([See below](#webhooks-with-systemtextjson))_ ## Renamed parameter _replyToMessageId:_ → _replyParameters:_ @@ -66,11 +66,11 @@ Request structures _(types ending with `Request`)_ are NOT the recommended way t They are to be considered as low-level raw access to Bot API structures for advanced programmers, and might change/break at any time in the future. If you have existing code using them, you can use the `MakeRequestAsync` method to send those requests. -(Other methods based on those requests will be removed soon) +_(Other methods based on those requests will be removed soon)_ -## Payments with Telegram Stars +## Payments with [Telegram Stars](https://t.me/BotNews/90) -To make a payment in [Telegram Stars](https://t.me/BotNews/90) with SendInvoiceAsync, set the following parameters: +To make a [payment in Telegram Stars](../4/payments.md) with SendInvoiceAsync, set the following parameters: - `providerToken:` `null` or `""` - `currency:` `"XTR"` - `prices:` with a single price @@ -82,7 +82,7 @@ The library now uses `System.Text.Json` instead of `NewtonsoftJson`. To make it work in your ASP.NET projects, you should now: - Remove package **Microsoft.AspNetCore.Mvc.NewtonsoftJson** from your project dependencies -- Follow our [Webhook page](3/updates/webhook.md) to configure your web app correctly +- Follow our [Webhook page](../3/updates/webhook.md) to configure your web app correctly ## InputPollOption in SendPollAsync @@ -105,7 +105,8 @@ This way, you won't need to pass a cancellationToken to every method call after ## Polling system now catch exceptions in your HandleUpdate code (v21.3) ->⚠️ That's a change of behaviour, but most of you will probably welcome this change +> [!WARNING] +> That's a change of behaviour, but most of you will probably welcome this change If you forgot to wrap your HandleUpdateAsync code in a big `try..catch`, and your code happen to throw an exception, this would previously stop the polling completely. @@ -123,4 +124,56 @@ Now the Polling system will catch your exceptions, pass them to your HandleError >{ > if (source is HandleErrorSource.HandleUpdateError) throw ex; > ... ->``` \ No newline at end of file +>``` + +## New helpers/extensions to simplify your code (v21.5) + +- When replying to a message, you can now simply pass a `Message` for _replyParameters:_ rather than a `Message.MessageId` +- `Update.AllTypes` is a constant array containing all `UpdateType`s. You can pass it for the _allowedUpdates:_ parameter (`GetUpdatesAsync`/`SetWebhookAsync`) +- Message has now 2 extensions methods: `.ToHtml()` and `.ToMarkdown()` to convert the message text/caption and their entities into a simple Html or Markdown string. +- You can also use methods `Markdown.Escape()` and `HtmlText.Escape()` to sanitize reserved characters from strings +- Reply/Inline Keyboard Markup now have construction methods to simplify building keyboards dynamically: +```csharp +var replyMarkup = new InlineKeyboardMarkup() + .AddButton(InlineKeyboardButton.WithUrl("Link to Repository", "https://github.com/TelegramBots/Telegram.Bot")) + .AddNewRow().AddButton("callback").AddButton("caption", "data") + .AddNewRow("with", "three", "buttons") + .AddNewRow().AddButtons("A", "B", InlineKeyboardButton.WithSwitchInlineQueryCurrentChat("switch")); +``` +- Same for ReplyKeyboardMarkup (and you can use `new ReplyKeyboardMarkup(true)` to resize keyboard) + +As [previously announced](#request-structures), the Request-typed methods are gone. +But you can still send Request structures via the `MakeRequestAsync` method. + +## Simplified polling with events (v21.7) + +Instead of `StartReceiving`/`ReceiveAsync` system, you can now simply set 2 or 3 events on the botClient: +- `bot.OnMessage += ...` to receive Message updates +- `bot.OnUpdate += ...` to receive other updates _(or all updates if you don't set `OnMessage`)_ +- `bot.OnError += ...` to handle errors/exceptions during polling or your handlers + +Note: Second argument to `OnMessage` event specifies which kind of update it was (_edited_, _channel_ or _business_ message?) + +When you assign those events, polling starts automatically, you don't have anything to do. +Polling will stop when you remove (`-=`) your events, or when you cancel the [global cancellation token](#global-cancellation-token-v212) + +You can also use `await bot.DropPendingUpdatesAsync()` before setting those events to ignore past updates. +The [Console example project](https://github.com/TelegramBots/Telegram.Bot.Examples/tree/master/Console) has been updated to demonstrate these events. + +## Automatic retrying API calls in case of "Too Many Requests" (v21.9) + +If Telegram servers fail on your API call with this error and ask you to to retry in less than 60 seconds, TelegramBotClient will now automatically wait for the requested delay and retry sending the same request up to 3 times. + +This is configurable using Options on the constructor: +```csharp +using var cts = new CancellationTokenSource(); +var options = new TelegramBotClientOptions(token) { RetryThreshold = 120, RetryCount = 2 }; +var bot = new TelegramBotClient(options, cancellationToken: cts.Token); +``` + +*️⃣ This is a change of behavior compared to previous versions, but probably a welcome one. +To disable this system and keep the same behavior as previous versions, use `RetryThreshold = 0` + +> Notes for advanced users: +> - If this happens while uploading files (InputFile streams), the streams will be reset to their start position in order to be sent again +> - If your streams are non-seekable _(no problem with MemoryStream/FileStream)_, the full HTTP request to Bot API will be buffered before the first sending _(so it can lead to a temporary use of memory if you're sending big files)_ diff --git a/theme/custom.css b/theme/custom.css index 3f1d4cd7..c9722cc4 100644 --- a/theme/custom.css +++ b/theme/custom.css @@ -1,3 +1,13 @@ :root { --content-max-width: 900px; } + +/* mdbook margins are way too big */ +h2, h3 { + margin-block-start: 1em; + margin-block-end: 0.5em; +} +h4, h5 { + margin-block-start: 0.8em; + margin-block-end: 0.4em; +}