Telegram Bots Book
Telegram.Bot is the most popular .NET client for Telegram Bot API, allowing developers to build bots for Telegram messaging app.
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, or choose from the Table Of Content (left/top), and don't miss our useful Frequently Asked Questions.
🧩 Installation
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 feed: https://nuget.voids.site/v3/index.json
See the screenshots below to configure the Package source in Visual Studio:
and make sure to follow the Migration Guide for v21.* if you have existing bot code.
Alternatively you can use command line: dotnet nuget add source https://nuget.voids.site/v3/index.json
Or set up a nuget.config
file at the root of your project/solution:
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="nuget.voids.site" value="https://nuget.voids.site/v3/index.json" />
</packageSources>
</configuration>
🪄 More examples
This book is filled with ready-to-use snippets of code, but you can also find full project examples at our Telegram.Bot.Examples Github repository, featuring:
- Simple Console apps (long polling)
- Webhook ASP.NET example (with Controllers or Minimal APIs)
- Full-featured advanced solution
- Serverless Functions implementations
🔗 More useful links
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 |
Quickstart
Bot Father
Before you start, you need to talk to @BotFather
on Telegram.
Create a new bot, acquire the bot token and get back here.
Bot token is a key that required to authorize the bot and send requests to the Bot API. Keep your token secure and store it safely, it can be used to control your bot. It should look like this:
1234567:4TT8bAc8GHUspu3ERYn-KGcvsvGB9u_n4ddy
Hello World
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+)
Create a new console project for your bot and add a reference to Telegram.Bot
package:
dotnet new console
dotnet nuget add source https://nuget.voids.site/v3/index.json
dotnet add package Telegram.Bot
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_BOT_TOKEN
with your bot token obtained from@BotFather
.
using Telegram.Bot;
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:
dotnet run
Hello, World! I am user 1234567 and my name is Awesome Bot.
Great! This bot is self-aware. To make the bot react to user messages, head to the next page.
Your First Chat Bot
On the previous page we got a bot token and used the 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:
Copy the following code to Program.cs
.
⚠️ Replace
YOUR_BOT_TOKEN
with the bot token obtained from [@BotFather
].
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:
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 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.
When user sends a message, the OnMessage(...)
method gets invoked with the Message
object passed as an argument (and the type of update).
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.
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.
Received Message 'test' in Private chat with @You (123456789).
Full Example
On the previous page 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:
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 in our Examples repository for an even more complete bot code.
Beginner
Sending Messages
There are many different types of message that a bot can send. Fortunately, methods for sending such messages are similar. Take a look at these examples:
Sending text message
await bot.SendTextMessageAsync(chatId, "Hello, World!");
Sending sticker message
await bot.SendStickerAsync(chatId, "https://telegrambots.github.io/book/docs/sticker-dali.webp");
Sending video message
await bot.SendVideoAsync(chatId, "https://telegrambots.github.io/book/docs/video-hawk.mp4");
Text Messages and More
Text is a powerful interface for your bot and sendMessage
probably is the most used method of the Telegram Bot API.
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
The code snippet below sends a message with multiple parameters that looks like this:
You can use this code snippet in the event handler from Example Bot page and use
chatId
or put thechatId
value if you know it.
var message = await bot.SendTextMessageAsync(chatId, "Trying <b>all the parameters</b> of <code>sendMessage</code> method",
parseMode: ParseMode.Html,
protectContent: true,
replyParameters: update.Message.MessageId,
replyMarkup: new InlineKeyboardMarkup(
InlineKeyboardButton.WithUrl("Check sendMessage method", "https://core.telegram.org/bots/api#sendmessage")));
The method SendTextMessageAsync
of .NET Bot Client maps to sendMessage
on Telegram's Bot API. This method sends a
text message and returns the message object sent.
text
is written in HTML format and parseMode
indicates that. You can also write in Markdown or plain text.
By passing protectContent
we prevent the message (and eventual media) to be copiable/forwardable elsewhere.
It's a good idea to make it clear to a user the reason why the bot is sending this message and that's why we pass the user's
message id for replyParameters
.
You have the option of specifying a replyMarkup
when sending messages.
Reply markups are explained in details later in this book.
Here we used an Inline Keyboard Markup with a button that attaches to the message itself. Clicking that opens
sendMessage
method documentation in the browser.
The Sent Message
Almost all of the methods for sending messages return you the message you just sent. Let's have a look at this object. Add this statement after the previous code.
Console.WriteLine(
$"{message.From.FirstName} sent message {message.MessageId} " +
$"to chat {message.Chat.Id} at {message.Date}. " +
$"It is a reply to message {message.ReplyToMessage.MessageId} " +
$"and has {message.Entities.Length} message entities.");
Output should look similar to this:
Awesome bot sent message 123 to chat 123456789 at 8/21/18 11:25:09 AM. It is a reply to message 122 and has 2 message entities.
There are a few things to note.
Date and time is in UTC format and not your local timezone.
Convert it to local time by calling message.Date.ToLocalTime()
method.
Message Entity refers to those formatted parts of the text: all the parameters in bold and
sendMessage in mono-width font.
Property message.Entities
holds the formatting information and message.EntityValues
gives you the actual value.
For example, in the message we just sent:
message.Entities.First().Type == MessageEntityType.Bold
message.EntityValues.First() == "all the parameters"
Try putting a breakpoint in the code to examine all the properties on a message objects you get.
Photo and Sticker Messages
You can provide the source file for almost all multimedia messages (e.g. photo, video) in 3 ways:
- Uploading a file with the HTTP request
- HTTP URL for Telegram to get a file from the internet
file_id
of an existing file on Telegram servers (recommended)
Examples in this section show all three. You will learn more about them later on when we discuss file upload and download.
Photo
Sending a photo is simple. Here is an example:
var message = await bot.SendPhotoAsync(chatId, "https://telegrambots.github.io/book/docs/photo-ara.jpg",
caption: "<b>Ara bird</b>. <i>Source</i>: <a href=\"https://pixabay.com\">Pixabay</a>", parseMode: ParseMode.Html);
Caption
Multimedia messages can optionally have a caption attached to them. Here we sent a caption in HTML format. A user can click on Pixabay in the caption to open its URL in the browser.
Similar to message entities discussed before, caption entities on Message
object are the result of
parsing formatted(Markdown or HTML) caption text.
Try inspecting these properties in debug mode:
message.Caption
: caption in plain text without formattingmessage.CaptionEntities
: info about special entities in the captionmessage.CaptionEntityValues
: text values of mentioned entities
Photo Message
The message
returned from this method represents a photo message because message.Photo
has a value.
Its value is a PhotoSize
array with each element representing the same photo in different dimensions.
If your bot needs to send this photo again at some point, it is recommended to store this array
so you can reuse the file_id
value.
Here is how message.Photo
array looks like in JSON:
[
{
"file_id": "AgADBAADDqgxG-QDDVCm5JVvld7MN0z6kBkABCQawlb-dBXqBZUEAAEC",
"file_size": 1254,
"width": 90,
"height": 60
},
{
"file_id": "AgADBAADDqgxG-QDDVCm5JVvld7MN0z6kBkABAKByRnc22RmBpUEAAEC",
"file_size": 16419,
"width": 320,
"height": 213
},
{
"file_id": "AgADBAADDqgxG-QDDVCm5JVvld7MN0z6kBkABHezqGiNOz9yB5UEAAEC",
"file_size": 57865,
"width": 640,
"height": 426
}
]
Sticker
Telegram stickers are fun and our bot is about to send its very first sticker. Sticker files should be in WebP format.
This code sends the same sticker twice. First by passing HTTP URL to a WebP sticker file and
second by reusing FileId
of the same sticker on Telegram servers.
var message1 = await bot.SendStickerAsync(chatId, "https://telegrambots.github.io/book/docs/sticker-fred.webp");
var message2 = await bot.SendStickerAsync(chatId, message1.Sticker!.FileId);
Try inspecting the sticker1.Sticker
property. It is of type Sticker
and its schema looks similar to a photo.
There is more to stickers and we will talk about them in greater details later.
Audio and Voice Messages
These two types of messages are pretty similar. Audio is MP3-encoded file that can be played in music player. A voice file has OGG format and is not shown in music player.
Audio
This is the code to send an MP3 soundtrack. You might be wondering why some parameters are commented out? That's because this MP3 file has metadata on it and Telegram does a good job at reading it.
var message = await bot.SendAudioAsync(chatId, "https://telegrambots.github.io/book/docs/audio-guitar.mp3"
// , performer: "Joel Thomas Hunger", title: "Fun Guitar and Ukulele", duration: 91 // optional
);
And a user can see the audio in Music Player:
Method returns an audio message. Let's take a look at the value of message.Audio
property in JSON format:
{
"duration": 91,
"mime_type": "audio/mpeg",
"title": "Fun Guitar and Ukulele",
"performer": "Joel Thomas Hunger",
"file_id": "CQADBAADKQADA3oUUKalqDOOcqesAg",
"file_size": 1102154
}
Voice
A voice message is an OGG audio file. Let's send it differently this time by uploading the file from disk alongside with an HTTP request.
To run this example, download the NFL Commentary voice file to your disk.
A value is passed for duration
because Telegram can't figure that out from a file's metadata.
⚠️ Replace
/path/to/voice-nfl_commentary.ogg
with an actual file path.
await using Stream stream = System.IO.File.OpenRead("/path/to/voice-nfl_commentary.ogg");
var message = await bot.SendVoiceAsync(chatId, stream, duration: 36);
A voice message is returned from the method. Inspect the message.Voice
property to learn more.
Video and Video Note Messages
You can send MP4 files as a regular video or a video note. Other video formats may be sent as documents.
Video
Videos, like other multimedia messages, can have caption, reply, reply markup, and etc. You can optionally specify the duration and resolution of the video.
In the example below, we send a video of a 10 minute countdown and expect the Telegram clients to stream that long video instead of downloading it completely. We also set a thumbnail image for our video.
await bot.SendVideoAsync(chatId, "https://telegrambots.github.io/book/docs/video-countdown.mp4",
thumbnail: "https://telegrambots.github.io/book/2/docs/thumb-clock.jpg", supportsStreaming: true);
Check the Bot API docs for
sendVideo
method to learn more about video size limits and the thumbnail images.
User should be able to seek through the video without the video being downloaded completely.
Video Note
Video notes, shown in circles to the user, are usually short (1 minute or less) with the same width and height.
You can send a video note only by uploading the video file or reusing the file_id
of another video note.
Sending video note by its HTTP URL is not supported currently.
Download the Sea Waves video to your disk for this example.
await using Stream stream = System.IO.File.OpenRead("/path/to/video-waves.mp4");
await bot.SendVideoNoteAsync(chatId, stream,
duration: 47, length: 360); // value of width/height
Album Messages
Using sendMediaGroup
method you can send a group of photos, videos, documents or audios as an album. Documents and audio files can be only grouped in an album with messages of the same type.
var messages = await bot.SendMediaGroupAsync(chatId, new IAlbumInputMedia[]
{
new InputMediaPhoto("https://cdn.pixabay.com/photo/2017/06/20/19/22/fuchs-2424369_640.jpg"),
new InputMediaPhoto("https://cdn.pixabay.com/photo/2017/04/11/21/34/giraffe-2222908_640.jpg"),
});
Document and Animation Messages
Send documents
Use sendDocument
method to send general files.
await bot.SendDocumentAsync(chatId, "https://telegrambots.github.io/book/docs/photo-ara.jpg",
caption: "<b>Ara bird</b>. <i>Source</i>: <a href=\"https://pixabay.com\">Pixabay</a>", parseMode: ParseMode.Html);
Send animations
Use sendAnimation
method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
await bot.SendAnimationAsync(chatId, "https://telegrambots.github.io/book/docs/video-waves.mp4",
caption: "Waves");
Native Poll Messages
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
This is the code to send a poll to a chat.
var pollMessage = await bot.SendPollAsync("@channel_name",
"Did you ever hear the tragedy of Darth Plagueis The Wise?",
new InputPollOption[]
{
"Yes for the hundredth time!",
"No, who`s that?"
});
You can optionally send a keyboard with a poll, both an inline or a regular one.
You'll get the message with Poll
object inside it.
Stop a poll
To close a poll you need to know original chat and message ids of the poll that you got from calling SendPollAsync
method.
Let's close the poll that we sent in the previous example:
Poll poll = await bot.StopPollAsync(pollMessage.Chat, pollMessage.MessageId);
You can add an inline keyboard when you close a poll.
As a result of the request you'll get the the final poll state with property Poll.IsClosed
set to true.
If you'll try to close a forwarded poll using message and chat ids from the received message even if your bot is the author of the poll you'll get an ApiRequestException
with message Bad Request: poll can't be stopped
. Polls originated from channels is an exception since forwarded messages originated from channels contain original chat and message ids inside properties Message.ForwardFromChat.Id
and Message.ForwardFromMessageId
.
Also if you'll try to close an already closed poll you'll get ApiRequestException
with message Bad Request: poll has already been closed
.
Other Messages
There are other kind of message types which are supported by the client. In the following paragraphs we will look how to send contacts, venues or locations.
Contact
This is the code to send a contact. Mandatory are the parameters chatId
, phoneNumber
and firstName
.
await bot.SendContactAsync(chatId, phoneNumber: "+1234567890", firstName: "Han", lastName: "Solo");
If you want to send a contact as vCard you can achieve this by adding a valid vCard string
as value for the optional parameter vCard
as seen in the given example below.
await bot.SendContactAsync(chatId, phoneNumber: "+1234567890", firstName: "Han",
vcard: "BEGIN:VCARD\n" +
"VERSION:3.0\n" +
"N:Solo;Han\n" +
"ORG:Scruffy-looking nerf herder\n" +
"TEL;TYPE=voice,work,pref:+1234567890\n" +
"EMAIL:hansolo@mfalcon.com\n" +
"END:VCARD");
Venue
The code snippet below sends a venue with a title and a address as given parameters:
await bot.SendVenueAsync(chatId, latitude: 50.0840172f, longitude: 14.418288f,
title: "Man Hanging out", address: "Husova, 110 00 Staré Město, Czechia");
Location
The difference between sending a location and a venue is, that the venue requires a title and address. A location can be any given point as latitude and longitude.
The following snippet shows how to send a location with the mandatory parameters:
await bot.SendLocationAsync(chatId, latitude: 33.747252f, longitude: -112.633853f);
Reply Markup
Telegram provides two types of reply markup: Custom keyboards and Inline keyboards.
Custom keyboards
Whenever your bot sends a message, it can pass along a special keyboard with predefined reply options. Regular keyboards are represented by ReplyKeyboardMarkup
object. You can request a contact or location information from the user with KeyboardButton
or send a poll. Regular button will send predefined text to the chat.
Keyboard is an array of button rows, each represented by an array of KeyboardButton
objects. KeyboardButton
supports text and emoji.
By default, custom keyboards are displayed until a new keyboard is sent by a bot.
Single-row keyboard markup
A ReplyKeyboardMarkup
with two buttons in a single row:
// using Telegram.Bot.Types.ReplyMarkups;
var replyMarkup = new ReplyKeyboardMarkup(true)
.AddButtons("Help me", "Call me ☎️");
var sent = await bot.SendTextMessageAsync(chatId, "Choose a response", replyMarkup: replyMarkup);
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
A ReplyKeyboardMarkup
with two rows of buttons:
// using Telegram.Bot.Types.ReplyMarkups;
var replyMarkup = new ReplyKeyboardMarkup(true)
.AddButton("Help me")
.AddNewRow("Call me ☎️", "Write me ✉️");
var sent = await bot.SendTextMessageAsync(chatId, "Choose a response", replyMarkup: replyMarkup);
Request information
ReplyKeyboardMarkup
containing buttons for contact and location requests using helper methods KeyboardButton.WithRequestLocation
and KeyboardButton.WithRequestContact
:
// using Telegram.Bot.Types.ReplyMarkups;
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: replyMarkup);
Remove keyboard
To remove keyboard you have to send an instance of ReplyKeyboardRemove
object:
// using Telegram.Bot.Types.ReplyMarkups;
await bot.SendTextMessageAsync(chatId, "Removing keyboard", replyMarkup: new ReplyKeyboardRemove());
Inline keyboards
There are times when you'd prefer to do things without sending any messages to the chat. For example, when your user is changing settings or flipping through search results. In such cases you can use Inline Keyboards that are integrated directly into the messages they belong to.
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, URL buttons and 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, 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
:
// using Telegram.Bot.Types.ReplyMarkups;
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: inlineMarkup);
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.
// using Telegram.Bot.Types.ReplyMarkups;
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: inlineMarkup);
Switch to Inline buttons
Pressing a switch to inline button prompts the user to select a chat, opens it and inserts the bot's username into the input field. You can also pass a query that will be inserted along with the username – this way your users will immediately get some inline results they can share. In this example we use InlineKeyboardButton.WithSwitchInlineQuery
and InlineKeyboardButton.WithSwitchInlineQueryCurrentChat
helper methods to create buttons which will insert the bot's username in the chat's input field.
// using Telegram.Bot.Types.ReplyMarkups;
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: inlineMarkup);
Forward, Copy or Delete messages
You can forward, copy, or delete a single message, or even a bunch of messages in one go.
You will need to provide the source messageId(s), the source chatId and eventually the target chatId.
Note: When you use the plural form of the copy/forward methods, it will keep Media Groups (albums) as such.
Forward message(s)
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.
// Forward a single message
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);
// Forward a bunch of messages from a source ChatId to a target ChatId, using a list of their message ids
await bot.ForwardMessagesAsync(targetChatId, sourceChatId, new int[] { 123, 124, 125 });
Copy message(s)
If you don't want the "Forwarded from" header, you can instead copy the message(s).
This will make them look like new messages.
// Copy a single message
await bot.CopyMessageAsync(targetChatId, sourceChatId, messageId);
// Copy an incoming message (from the update) onto a target ChatId
await bot.CopyMessageAsync(targetChatId, update.Message.Chat, update.Message.MessageId);
// Copy a media message and change its caption at the same time
await bot.CopyMessageAsync(targetChatId, update.Message.Chat, update.Message.MessageId,
caption: "New <b>caption</b> for this media", parseMode: ParseMode.Html);
// Copy a bunch of messages from a source ChatId to a target ChatId, using a list of their message ids
await bot.CopyMessagesAsync(targetChatId, sourceChatId, new int[] { 123, 124, 125 });
Delete message(s)
Finally you can delete message(s).
This is particularly useful for cleaning unwanted messages in groups.
// Delete a single message
await bot.DeleteMessageAsync(chatId, messageId);
// Delete an incoming message (from the update)
await bot.CopyMessageAsync(update.Message.Chat, update.Message.MessageId);
// Delete a bunch of messages, using a list of their message ids
await bot.DeleteMessagesAsync(chatId, new int[] { 123, 124, 125 });
Check if a message is a forward
When receiving an update about a message, you can check if that message is "Forwarded from" somewhere,
by checking if Message.ForwardOrigin
is set:
Console.WriteLine(update.Message.ForwardOrigin switch
{
MessageOriginChannel moc => $"Forwarded from channel {moc.Chat.Title}",
MessageOriginUser mou => $"Forwarded from user {mou.SenderUser}",
MessageOriginHiddenUser mohu => $"Forwarded from hidden user {mohu.SenderUserName}",
MessageOriginChat moch => $"Forwarded on behalf of {moch.SenderChat}",
_ => "Not forwarded"
});
Intermediate
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 is actively requesting updates 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. -
Setting a webhook 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
Each user interaction with your bot results in new Update object. Its fields will be set depending on update type.
Example projects
Long polling
- Console application. Demonstrates a basic bot with some commands.
- Advanced console application. Demonstrates the use of many advanced programming features.
Webhook
- ASP.NET Core web application with Minimal APIs
- ASP.NET Core web application with Controllers
- Azure Functions
- AWS Lambda
Long Polling
Long Polling is done by calling getUpdates actively.
With our library, this can be done in one of three ways:
By setting bot.OnUpdate
(and/or bot.OnMessage
)
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
)
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:
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;
}
}
Webhooks
With Webhook, your 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 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)
First you need to configure your Web App startup code:
- Locate the line
services.AddControllers();
(in Program.cs or Startup.cs) - If you're using .NET 6.0 or more recent, add the line:
services.ConfigureTelegramBotMvc();
- For older .NET versions, add the line:
services.ConfigureTelegramBot<Microsoft.AspNetCore.Mvc.JsonOptions>(opt => opt.JsonSerializerOptions);
Next, in a controller class (like BotController.cs), you need to add an action for the updates. Typically:
[HttpPost]
public async Task HandleUpdate([FromBody] Update update)
{
// put your code to handle one Update here.
}
Good, now skip to SetWebHookAsync below
ASP.NET Core with Minimal APIs
First you need to configure your Web App startup code:
- Locate the line
builder.Build();
(in Program.cs) - Above it, insert the line:
builder.Services.ConfigureTelegramBot<Microsoft.AspNetCore.Http.Json.JsonOptions>(opt => opt.SerializerOptions);
Next, you need to map an action for the updates. Typically:
app.MapPost("/bot", (Update update) => HandleUpdate(update));
...
async Task HandleUpdate(Update update)
{
// put your code to handle one Update here.
}
Good, now skip to SetWebHookAsync below
Old ASP.NET 4.x support
For older .NET Framework usage, you may use the following code:
public async Task<IHttpActionResult> Post()
{
Update update;
using (var body = await Request.Content.ReadAsStreamAsync())
update = System.Text.Json.JsonSerializer.Deserialize<Update>(body, JsonBotAPI.Options);
await HandleUpdate(update);
return Ok();
}
SetWebHookAsync
Your update handler code is ready, now you need to instruct Telegram to send updates to your URL, by running:
var bot = new TelegramBotClient("YOUR_BOT_TOKEN");
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, 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: Useful step-by-step guide
- You must use HTTPS (TLS 1.2+), IPv4, and ports 443, 80, 88, or 8443
- Official webhook guide
- 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.
You may want to prevent handling the same update.Id twice:if (update.Id <= LastUpdateId) return; LastUpdateId = update.Id; // 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:- 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 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.
Inline Mode
Telegram bots can be queried directly in the chat or via inline queries.
To use inline queries in your bot, you need to set up inline mode by command:
Import Telegram.Bot.Types.InlineQueryResults
namespace for inline query types.
There are two types that allow you to work with inline queries - InlineQuery
and ChosenInlineResult
:
switch (update.Type)
{
case UpdateType.InlineQuery:
await OnInlineQueryReceived(bot, update.InlineQuery!);
break;
case UpdateType.ChosenInlineResult:
await OnChosenInlineResultReceived(bot, update.ChosenInlineResult!);
break;
};
InlineQuery
Suppose we have two arrays:
private readonly string[] sites = { "Google", "Github", "Telegram", "Wikipedia" };
private readonly string[] siteDescriptions =
{
"Google is a search engine",
"Github is a git repository hosting",
"Telegram is a messenger",
"Wikipedia is an open wiki"
};
So we can handle inline queries this way:
async Task OnInlineQueryReceived(ITelegramBotClient bot, InlineQuery inlineQuery)
{
var results = new List<InlineQueryResult>();
var counter = 0;
foreach (var site in sites)
{
results.Add(new InlineQueryResultArticle(
$"{counter}", // we use the counter as an id for inline query results
site, // inline query result title
new InputTextMessageContent(siteDescriptions[counter])) // content that is submitted when the inline query result title is clicked
);
counter++;
}
await bot.AnswerInlineQueryAsync(inlineQuery.Id, results); // answer by sending the inline query result list
}
InlineQueryResult
is an abstract type used to create a response for inline queries. You can use these result types for inline queries: InlineQueryResultArticle
for articles, InlineQueryResultPhoto
for photos, etc.
ChosenInlineResult
This type helps to handle chosen inline result. For example, you may want to know which result users chose:
Task OnChosenInlineResultReceived(ITelegramBotClient bot, ChosenInlineResult chosenInlineResult)
{
if (uint.TryParse(chosenInlineResult.ResultId, out var resultId) // check if a result id is parsable and introduce variable
&& resultId < sites.Length)
{
Console.WriteLine($"User {chosenInlineResult.From} has selected site: {sites[resultId]}");
}
return Task.CompletedTask;
}
To use the feature you need to enable "inline feedback" in BotFather by /setinlinefeedback
command:
Final result:
Working with Files
Downloading files
First, read the documentation on getFile
method.
To download file you have to know its file identifier - FileId
.
Finding the file identifier
Telegram Bot API has several object types, representing file:
PhotoSize
, Animation
, Audio
, Document
, Video
, VideoNote
, Voice
, Sticker
.
The file identifier for each file type can be found in their FileId
property (e.g. Message.Audio.FileId
).
The exception is photos, which represented as an array of PhotoSize[]
objects.
For each photo Telegram sends you a set of PhotoSize
objects - available resolutions, you can choose from.
Generally, you will want the highest quality - the last PhotoSize
object in the array.
With LINQ, this boils down to Message.Photo.Last().FileId
.
Downloading a file
Downloading a file from Telegram is done in two steps:
- Get file information with
getFile
method. ResultingFile
object containsFilePath
from which we can download the file. - Downloading the file.
var fileId = update.Message.Photo.Last().FileId;
var fileInfo = await bot.GetFileAsync(fileId);
var filePath = fileInfo.FilePath;
The URL from which you can now download the file is https://api.telegram.org/file/bot<token>/<FilePath>
.
To download file you can use DownloadFileAsync
function:
const string destinationFilePath = "../downloaded.file";
await using Stream fileStream = System.IO.File.Create(destinationFilePath);
await bot.DownloadFileAsync(filePath, fileStream);
For your convenience the library provides you a helper function that does both - GetInfoAndDownloadFileAsync
:
const string destinationFilePath = "../downloaded.file";
await using Stream fileStream = System.IO.File.Create(destinationFilePath);
var file = await bot.GetInfoAndDownloadFileAsync(fileId, fileStream);
Uploading files
First, read the documentation on sending files.
Upload local file
To upload local file open stream and call one of the file-sending functions:
await using Stream stream = System.IO.File.OpenRead("../hamlet.pdf");
var message = await bot.SendDocumentAsync(chatId, document: InputFile.FromStream(stream, "hamlet.pdf"),
caption: "The Tragedy of Hamlet,\nPrince of Denmark");
Be aware of limitation for this method - 10 MB max size for photos, 50 MB for other files.
Upload file by file identifier
If the file is already stored somewhere on the Telegram servers, you don't need to reupload it: each file object has a FileId
property. Simply pass this FileId
as a parameter instead of uploading. There are no limits for files sent this way.
var fileId = update.Message.Photo.Last().FileId;
var message = await bot.SendPhotoAsync(chatId, fileId);
Upload by URL
Provide Telegram with an HTTP URL for the file to be sent. Telegram will download and send the file. 5 MB max size for photos and 20 MB max for other types of content.
var message = await bot.SendPhotoAsync(chatId, "https://cdn.pixabay.com/photo/2017/04/11/21/34/giraffe-2222908_640.jpg");
Stickers
Sticker
Telegram stickers are fun and our bot is about to send its very first sticker. Sticker files should be in WebP format.
This code sends the same sticker twice. First by passing HTTP URL to a WebP sticker file and
second by reusing FileId
of the same sticker on Telegram servers.
var message1 = await bot.SendStickerAsync(chatId, "https://telegrambots.github.io/book/docs/sticker-fred.webp");
var message2 = await bot.SendStickerAsync(chatId, message1.Sticker!.FileId);
Try inspecting the sticker1.Sticker
property. It is of type [Sticker
] and its schema looks similar to a photo.
Advanced topics
Working Behind a Proxy
TelegramBotClient
allows you to use a proxy for Bot API connections. This guide covers using three different proxy solutions.
If you are in a country, such as Iran, where HTTP and SOCKS proxy connections to Telegram servers are blocked, consider using a VPN, using Tor Network, or hosting your bot in other jurisdictions.
HTTP Proxy
You can configure HttpClient
with WebProxy
and pass it to the Bot client.
// using System.Net;
// using System.Net.Http;
WebProxy webProxy = new (Host: "https://example.org", Port: 8080)
{
// Credentials if needed:
Credentials = new NetworkCredential("USERNAME", "PASSWORD")
};
HttpClient httpClient = new (
new HttpClientHandler { Proxy = webProxy, UseProxy = true, }
);
var bot = new TelegramBotClient("YOUR_API_TOKEN", httpClient);
SOCKS5 Proxy
As of .NET 6, SocketsHttpHandler
is able to use Socks4, Socks4a and Socks5 proxies!
// using System.Net;
// using System.Net.Http;
WebProxy proxy = new ("socks5://127.0.0.1:9050")
{
Credentials = new NetworkCredential("USERNAME", "PASSWORD")
};
HttpClient httpClient = new (
new SocketsHttpHandler { Proxy = proxy, UseProxy = true, }
);
var bot = new TelegramBotClient("YOUR_API_TOKEN", httpClient);
SOCKS5 Proxy over Tor
Warning: Use for Testing only!
Do not use this method in a production environment as it has high network latency and poor bandwidth.
Using Tor, a developer can avoid network restrictions while debugging and testing the code before a production release.
-
Install Tor Browser
-
Open the
torcc
file with a text editor (Found inTor Browser\Browser\TorBrowser\Data\Tor
) -
Add the following lines: (configurations are described below)
EntryNodes {NL} ExitNodes {NL} StrictNodes 1 SocksPort 127.0.0.1:9050
-
Look at the Socks5 proxy example above.
-
Start the Tor Browser
Usage:
// using System.Net;
// using System.Net.Http;
WebProxy proxy = new ("socks5://127.0.0.1:9050");
HttpClient httpClient = new (
new SocketsHttpHandler { Proxy = proxy, UseProxy = true }
);
var bot = new TelegramBotClient("YOUR_API_TOKEN", httpClient);
Note that Tor has to be active at all times for the bot to work.
Configurations in torcc
EntryNodes {NL}
ExitNodes {NL}
StrictNodes 1
These three lines make sure you use nodes from the Netherlands as much as possible to reduce latency.
SocksPort 127.0.0.1:9050
This line tells tor to listen on port 9050 for any socks connections. You can change the port to anything you want (9050 is just the default), only make sure to use the same port in your code.
Telegram Mini Apps
If standard Telegram Bot features aren't enough to fit your needs, you may want to consider building a Mini App 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.
Mini Apps can be launched from various ways:
- Keyboard Buttons:
KeyboardButton.WithWebApp
- Inline Buttons:
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 parameterInlineQueryResultsButton.WebApp
- Direct link like https://t.me/botusername/appname?startapp=command
To read more about Mini Apps, see https://core.telegram.org/bots/webapps
Telegram Passport
Telegram Passport is a unified authorization method for services that require personal identification. As a bot developer, you can use it to receive confidential user data in an end-to-end encrypted fashion. There are several Know Your Customer(KYC) solutions that have already added support for Telegram Passport.
This guide is targeted at bot developers and assumes the audience is already familiar with:
Telegram Passport - Quickstart
This guide teaches the basics of working with Telegram Passport. See the complete version of the code at Quickstart project. Code snippets on this page are in the context of that project.
Package
You need to add Telegram.Bot.Extensions.Passport
extension package to your project
in addition to the core package (Telegram.Bot
).
⭐️ Star the Telegram.Bot.Extensions.Passport project on GitHub 👍
dotnet add package Telegram.Bot.Extensions.Passport
Encryption Keys
You don't really need to generate any RSA key. Use our sample keys for this demo.
Send the public key to @BotFather using /setpublickey
command:
Copy this public key and send it to BotFather.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0VElWoQA2SK1csG2/sY/
wlssO1bjXRx+t+JlIgS6jLPCefyCAcZBv7ElcSPJQIPEXNwN2XdnTc2wEIjZ8bTg
BlBqXppj471bJeX8Mi2uAxAqOUDuvGuqth+mq7DMqol3MNH5P9FO6li7nZxI1FX3
9u2r/4H4PXRiWx13gsVQRL6Clq2jcXFHc9CvNaCQEJX95jgQFAybal216EwlnnVV
giT/TNsfFjW41XJZsHUny9k+dAfyPzqAk54cgrvjgAHJayDWjapq90Fm/+e/DVQ6
BHGkV0POQMkkBrvvhAIQu222j+03frm9b2yZrhX/qS01lyjW4VaQytGV0wlewV6B
FwIDAQAB
-----END PUBLIC KEY-----
Now Telegram client app can encrypt the data for your bot using this key.
Request Information
Bot waits for a text message from user. Once it receives a text message, it generates an authorization request link and sends that to the user.
Authorization Request
A passport authorization request means that the bot should ask the user to open a tg://resolve
URI in the browser
with specific parameters in its query string.
You can alternatively have a button in an HTML page on your website for that.
Type AuthorizationRequestParameters helps you in creating such an URI.
AuthorizationRequestParameters authReq = new AuthorizationRequestParameters(
botId: 123456, // bot user ID
publicKey: "...", // public key in PEM format. same as the key above.
nonce: "unique nonce for this request",
scope: new PassportScope(new[] { // a PassportScope object
new PassportScopeElementOne("address"),
new PassportScopeElementOne("phone_number")
})
);
In SendAuthorizationRequestAsync method, we ask for address
and phone_number
scopes.
Then, we generate the query string and ask user to open the link.
You might be wondering what is the magic in here?
https://telegrambots.github.io/Telegram.Bot.Extensions.Passport/redirect.html
This web page redirects user to tg://resolve
URI, appending whatever query string was passed to it.
If a user is using an Android device, the URI will start with
tg:
instead of the defaulttg://
.
Passport Data
You, the user, should now be redirected to the Telegram Passport screen in your Telegram client app. Enter your password and log in.
Note that the app will ask you to register if this is the first time you are using Telegram Passport.
Fill in the address and phone number data. Click on the Authorize button at the end.
At this point, your Telegram client app encrypts the actual Telegram Passport data (e.g. address) using the AES algorithm, and then encrypts the info required for decryption using your bot's public RSA key. Finally, it sends the result of both encryptions to Telegram servers.
Data Decryption
Your bot now receives a new message update with the encrypted Passport data. The user is also notified in the chat:
Let's decrypt that gibberish to get the information. That's what DecryptPassportDataAsync method does.
Step 1: Credentials
You can't just access the encrypted data in the message.passport_data.data
array.
Required parameters for their decryption are in the message.passport_data.credentials
object.
But that credentials object is encrypted using bot's public key!
We first take the bot's private key this time and decrypt the credentials.
There are more details about importing a key in PEM format on the RSA Key page.
IDecrypter decrypter = new Decrypter();
Credentials credentials = decrypter.DecryptCredentials(
message.PassportData.Credentials, // EncryptedCredentials object
GetRsaPrivateKey() // private key as an RSA object
);
Step 2: Nonce
There is a nonce
property on the credentials (now decrypted) object.
In order to prevent certain attacks, ensure its value is exactly the same as the nonce you set in the authorization request.
Read more about nonce on Wikipedia.
Step 3: Residential Address
It's finally time to see the user's address.
We are looking for an encrypted element with type of address in message.passport_data.data
array.
Also, decryption parameters for that are in credentials.secure_data.address.data
.
Here is how the decryption magic happens:
EncryptedPassportElement addressElement = message.PassportData.Data.Single(
el => el.Type == PassportEnums.Scope.Address
);
ResidentialAddress address = decrypter.DecryptData<ResidentialAddress>(
encryptedData: addressElement.Data,
dataCredentials: credentials.SecureData.Address.Data
);
DecryptData method does 3 tasks here:
- Decrypts the data into a JSON-serialized string
- Verifies that the data hashes match
- Converts from JSON to a .NET object
Step 4: Phone Number
Values for phone number and email address are not end-to-end encrypted in Telegram Passport and Telegram stores these values after being verified.
There is no need for decryption at this point.
Just find the element with the type of phone_number in the message.passport_data.data
array.
Information Demo
At the end, bot sends some of the information received to the user for demo purposes.
Passport Files and Documents
We use the driver's license scope here to show decryption of ID document data and passport files for front side scan, reverse side scan, selfie photo, and translation scan. That should cover most of the field types in Telegram Passport.
Sections below are referring to the test methods in Driver's License Scope Tests collection. Here are the steps:
- Authorization Request
- Driver's License Info
- Passport Message
- Credentials
- ID Document Data
- Passport File
Authorization Request
We start by generating an authorization URI. Since a driver's license is considered as a proof of identity, we ask for optional data selfie with document and translation document scan as well.
Driver's License Info
As a user, provide information for the required fields: front side, reverse side, and document number. Also, test methods here expect a selfie photo and a file for translation scan.
Click the Authorize button at the end.
Passport Message
This test method checks for a Passport message with a driver's license element on it.
Credentials
We decrypt credentials using the RSA private key and verify that the same nonce is used.
RSA key = EncryptionKey.ReadAsRsa();
IDecrypter decrypter = new Decrypter();
Credentials credentials = decrypter.DecryptCredentials(
passportData.Credentials,
key
);
bool isSameNonce = credentials.Nonce == "Test nonce for driver's license";
ID Document Data
In our test case, there is only 1 item in the message.passport_data.data
array and that's the encrypted element for
the driver's license scope.
We can get information such as document number and expiry date for the license from that element:
IdDocumentData licenseDoc = decrypter.DecryptData<IdDocumentData>(
encryptedData: element.Data,
dataCredentials: credentials.SecureData.DriverLicense.Data
);
Passport File
Passport file is an encrypted JPEG file on Telegram servers. You need to download the passport file and decrypt it using its accompanying file credentials to see the actual JPEG file content. In this section we try to demonstrate different use cases that you might have for such files.
No matter the method used, the underlying decryption logic is the same. It really comes down to your decision on working with streams vs. byte arrays. IDecrypter gives you both options.
Front Side File
A pretty handy extension method is used here to stream writing the front side file to disk. Method DownloadAndDecryptPassportFileAsync does a few things:
- Makes an HTTP request to fetch the encrypted file's info using its passport file_id
- Makes an HTTP request to download the encrypted file using its file_path
- Decrypts the encrypted file
- Writes the actual content to the destination stream
File encryptedFileInfo;
using (System.IO.Stream stream = System.IO.File.OpenWrite("/path/to/front-side.jpg"))
{
encryptedFileInfo = await bot.DownloadAndDecryptPassportFileAsync(
element.FrontSide, // PassportFile object for front side
credentials.SecureData.DriverLicense.FrontSide, // front side FileCredentials
stream // destination stream for writing the JPEG content to
);
}
warning
This method is convenient to use but gives you the least amount of control over the operations.
Reverse Side File
Previous method call is divided into two operations here for reverse side of the license. Streams are used here as well.
File encryptedFileInfo;
using (System.IO.Stream
encryptedContent = new System.IO.MemoryStream(element.ReverseSide.FileSize),
decryptedFile = System.IO.File.OpenWrite("/path/to/reverse-side.jpg")
) {
// fetch the encrypted file info and download it to memory
encryptedFileInfo = await bot.GetInfoAndDownloadFileAsync(
element.ReverseSide.FileId, // file_id of passport file for reverse side
encryptedContent // stream to copy the encrypted file into
);
// ensure memory stream is at the beginning before reading from it
encryptedContent.Position = 0;
// decrypt the file and write it to disk
await decrypter.DecryptFileAsync(
encryptedContent,
credentials.SecureData.DriverLicense.ReverseSide, // reverse side FileCredentials
decryptedFile // destination stream for writing the JPEG content to
);
}
Selfie File
We deal with selfie photo as a byte array. This is essentially the same operation as done above via streams. We also post the selfie photo to a chat.
// fetch the info of the passport file(selfie) residing on Telegram servers
File encryptedFileInfo = await bot.GetFileAsync(element.Selfie.FileId);
// download the encrypted file and get its bytes
byte[] encryptedContent;
using (System.IO.MemoryStream
stream = new System.IO.MemoryStream(encryptedFileInfo.FileSize)
)
{
await bot.DownloadFileAsync(encryptedFileInfo.FilePath, stream);
encryptedContent = stream.ToArray();
}
// decrypt the content and get bytes of the actual selfie photo
byte[] selfieContent = decrypter.DecryptFile(
encryptedContent,
credentials.SecureData.DriverLicense.Selfie
);
// send the photo to a chat
using (System.IO.Stream stream = new System.IO.MemoryStream(selfieContent)) {
await bot.SendPhotoAsync(
123456,
stream,
"selfie with driver's license"
);
}
Translation File
A bot can request certified English translations of a document. Translations are also encrypted passport files so their decryption is no different from others passport files.
Assuming that the user sends one translation scan only for the license, we receive the translation passport file object in
message.passport_data.data[0].translation[0]
and its accompanying file credentials object in
credentials.secure_data.driver_license.translation[0]
.
File gets written to disk as a byte array.
PassportFile passportFile = element.Translation[0];
FileCredentials fileCreds = credentials.SecureData.DriverLicense.Translation[0];
// fetch passport file info
File encryptedFileInfo = await bot.GetFileAsync(passportFile.FileId);
// download encrypted file and get its bytes
byte[] encryptedContent;
using (System.IO.MemoryStream
stream = new System.IO.MemoryStream(encryptedFileInfo.FileSize)
)
{
await bot.DownloadFileAsync(encryptedFileInfo.FilePath, stream);
encryptedContent = stream.ToArray();
}
// decrypt the content and get bytes of the actual selfie photo
byte[] content = decrypter.DecryptFile(
encryptedContent,
fileCreds
);
// write the file to disk
await System.IO.File.WriteAllBytesAsync("/path/to/translation.jpg", content);
Passport Data Errors
This chapter is not yet written.
Import RSA Key
In order to decrypt the credentials you need to provide the private RSA key to DecryptCredentials method. If you have the RSA key in PEM format, you cannot simply instantiate an RSA .NET object from it. Here we discuss two ways of importing your PEM private key.
From PEM Format
This is the easier option and recommended for development time only. We can generate an RSA .NET object from an RSA Key in PEM format using the BouncyCastle package.
dotnet add package BouncyCastle
Code snippet here shows the conversion from a PEM file to the needed RSA object.
// using System.IO;
// using System.Security.Cryptography;
// using Org.BouncyCastle.Crypto;
// using Org.BouncyCastle.Crypto.Parameters;
// using Org.BouncyCastle.OpenSsl;
// using Org.BouncyCastle.Security;
static RSA GetPrivateKey() {
string privateKeyPem = File.ReadAllText("/path/to/private-key.pem");
PemReader pemReader = new PemReader(new StringReader(privateKeyPem));
AsymmetricCipherKeyPair keyPair = (AsymmetricCipherKeyPair) pemReader.ReadObject();
RSAParameters rsaParameters = DotNetUtilities
.ToRSAParameters(keyPair.Private as RsaPrivateCrtKeyParameters);
RSA rsa = RSA.Create(rsaParameters);
return rsa;
}
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
We recommend to JSON-serialize RSAParameters of your key and create an RSA object using its values without any dependency on the BouncyCastle package in production deployment.
Copy EncryptionKeyUtility and EncryptionKeyParameters files from our Quickstart project. Those help with serialization.
You still need to use BouncyCastle only once to read the RSA key in PEM format and serialize its parameters:
// ONLY ONCE: read the RSA private key and serialize its parameters to JSON
static void WriteRsaParametersToJson() {
string privateKeyPem = System.IO.File.ReadAllText("/path/to/private-key.pem");
string json = EncryptionKeyUtility.SerializeRsaParameters(privateKeyPem);
System.IO.File.WriteAllText("/path/to/private-key-params.json", json);
}
// Now, read the JSON file and create an RSA instance
static RSA GetRsaKey() {
string json = System.IO.File.ReadAllText("/path/to/private-key-params.json");
return EncryptionKeyUtility.GetRsaKeyFromJson(json);
}
Content of private-key-params.json
will look similar to this:
{
"E": "AQAB",
"M": "0VElW...Fw==",
"P": "56Mdiw...i7FSwDaM=",
"Q": "51UN2sd...J44NTf0=",
"D": "nrXEeOl2Ky...JIQ==",
"DP": "KZYZWbsy.../lk60=",
"DQ": "Y25KgzPj...AdBd0=",
"IQ": "0153...N6Y="
}
It's worth mentioning that EncryptionKeyParameters is just a copy of RSAParameters struct. There are inconsistencies in serialization of RSAParameters type on different .NET platforms and that's why we use our own EncryptionKeyParameters type for serialization.
For instance, compare
RSAParameters
implementations on .NET Framework and .NET Core.
Telegram Passport Data Decryption - FAQ
What is PassportDataDecryptionException
Methods on IDecrypter
might throw PassportDataDecryptionException
exception
if an error happens during decryption.
The exception message tells you what went wrong but there is not much you can do to resolve it.
Maybe let your user know the issue and ask for Passport data again.
It is important to pass each piece of encrypted data, e.g. Id Document, Passport File, etc., with the right accompanying credentials to decryption methods.
Spot the problem in this code decrypting driver's license files:
byte[] selfieContent = decrypter.DecryptFile(
encSelfieContent, // byte array of encrypted selfie file
credentials.SecureData.DriverLicense.FrontSide // WRONG! use selfie file credentials
);
// throws PassportDataDecryptionException: "Data hash mismatch at position 123."
Telegram Login Widget
Everything related to the Telegram Login Widget has been moved to a separate repository:
Telegram.Bot.Extensions.LoginWidget
Frequently Asked Questions
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?
- 2. My update handler fails or stops executing at some point
- 3. Apparently my update handler gets a
NullReferenceException
- 4. How to add buttons under a message?
- 5. How to handle a click on such inline buttons?
- 6. How to show a popup text to the user?
- 7. How to fill the input textbox of the user with some text?
- 8. How to fetch previous messages?
- 9. How to fetch a list of all users in chat?
- 10. How to send a private message to some random user?
- 11. How to detect if a user blocked my bot?
- 12. How to set a caption to a media group (album)?
- 13. How to write a bot that make questions/answers with users?
- 14. How to make font effects in message?
- 15. Where can I host my bot online for cheap/free?
- 16. Is there some limitation/maximum about feature X?
- 17. How to populate the bot Menu button / commands list?
- 18. How to receive
ChatMember
updates? - 19. How to get rid of past updates when I restart my bot?
- 20. Difficulties to upload & send a file/media?
- 21. How to fetch all medias from an album/media group ?
- 22. How to send a custom emoji❓
- 23. How to upgrade my existing code? You keep breaking compatibility!
- 24. Can I use several apps/instance to manage my bot?
- 25. How do I get the user id from a username?
- 26. How to receive messages from channels?
- 27. How to sent the same media multiple times
- This FAQ doesn't have my question on it
1. Can you give me documentation/examples links?
- Follow this installation guide to install the latest versions of the library.
- Here is on the main documentation website.
- You can find more bot example projects here
- Search the official API documentation and official 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.
2. My update handler fails or stops executing at some point
You likely have an exception somewhere. You should place a try..catch
around your whole update handler.
Also, you should learn to use a debugger and go step-by-step through your code to understand where and why an exception is raised. See next question.
3. Apparently my update handler gets a NullReferenceException
Not all updates are about an incoming Message, so update.Message
could be null. (see also update.Type
)
Not all messages are text messages, message.Text
could be null (see also message.Type
). etc...
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 into the replyMarkup
parameter when sending the message. You will likely need to create a List<List<InlineKeyboardButton>>
for rows&columns
See also next question.
5. How to handle a click on such inline buttons?
For buttons with callback data, your update handler should handle update.CallbackQuery
.
(Remember that not all updates are about update.Message
. See question #3)
Your code should answer to the query within 10 seconds, using AnswerCallbackQueryAsync
(or else the button gets momentarily disabled)
6. How to show a popup text to the user?
It is only possible with inline callback button (see above questions).
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
8. How to fetch previous messages?
You can't with Bot API but it's possible with WTelegramBot.
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.
Normally, bots can only get the list of administrators (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 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 which makes sequential bots very simple to write... (or one of the other frameworks 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.
⚠️ I highly recommend you choose HTML formatting because MarkDownV2 has A LOT of annoyingly reserved characters and you will regret it later.
15. Where can I host my bot online for cheap/free?
I would recommend you make an ASP.NET webhook bot and host it on some WebApp hosting service.
For example, Azure WebApp Service has a F1 Free plan including 1 GB disk, 1 GB ram, 60 minutes of daily cumulated active CPU usage (more than enough for most bots without heavy use). And publishing to Azure is very easy from VS.
A credit-card is necessary but you shouldn't get charged if you stay within quotas.
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.
17. How to populate the bot Menu button / commands list?
You can either do this via @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
19. How to get rid of past updates when I restart my bot?
Pass true into StartReceiving
:ReceiverOptions
:DropPendingUpdates
or SetWebhookAsync
:dropPendingUpdates
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 } - If you just filled a
MemoryStream
, make sure to rewind it to the beginning withms.Position = 0;
before sending - If you send a media group, make sure you specify different filenames on
InputFile.FromStream
21. How to fetch all medias from an album/media group ?
Medias in a media group are received as separate consecutive messages having the same MediaGroupId
property. You should collect them progressively as you receive those messages.
There is no way to know how many medias are in the album, so:
- look for consecutive messages in that chat with same
MediaGroupId
and stop when it's not the same - stop after 10 media in the group (maximum)
- use a timeout of a few seconds not receiving new messages in that chat to determine the end
22. How to send a custom emoji❓
⚠️ It costs about ~$5,000 !! 😱
- First you need to buy a reserved username on Fragment.
- Then you need to pay an additional upgrade fee of 1K TON to apply that username to your bot.
- Finally, your bot can now post custom emojis using specific HTML or Markdown syntax (or entity).
To post to a specific group, there is an alternative solution:
- Have premium members boost your group to Level 4.
- Then you can assign a custom emoji pack to your group that your members AND bots can use freely in group messages.
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 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
BUT only one instance can call method GetUpdates (or else you will receive Telegram API Error 409: Conflict: terminated by other getUpdates request)
25. How do I get the user id from a username?
You can't with Bot API but it's possible with WTelegramBot.
Alternatively, you could store in database the mapping of UserId
<->Username
.
Remember that not every user has a username.
26. How to receive messages from channels?
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:
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
This FAQ doesn't have my question on it
Feel free to join our Telegram group and ask your question there
Migration guides to newer versions of the library
Migration guide for version 21.x
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.
- Version 21.6 supports Bot API 7.7 (including Telegram Stars payments)
- Library is now based on System.Text.Json and doesn't depend on NewtonsoftJson anymore. (See below)
Renamed parameter replyToMessageId: → replyParameters:
That parameter was renamed and you can still pass a messageId for simple replies.
Or you can pass a ReplyParameters structure for more advanced reply configuration.
Renamed parameter disableWebPagePreview: → linkPreviewOptions:
That parameter was renamed and you can still pass true
to disable web preview.
Or you can pass a LinkPreviewOptions structure for more precise preview configuration.
Changed bool?
→ bool
Many boolean parameters or fields are now simply of type bool
.
In most cases, it shouldn't impact your existing code, or rather simplify it. Previously null
values are now just false
.
Changed ParseMode?
→ ParseMode
When you don't need to specify a ParseMode, just pass default
or ParseMode.None
.
Better backward-compatibility and simplification of code
We added/restored features & implicit conversions that make your code simpler:
InputFile
: just pass astring
/Stream
for file_id/url/stream content (as was possible in previous versions of Telegram.Bot)InputMedia*
: just pass anInputFile
when you don't need to associate caption or suchMessageId
: auto-converts to/fromint
(and also fromMessage
)ReactionType
: just pass astring
when you want to send an emojiReactionType
: just pass along
when you want to send a custom emoji (id)- Some other obvious implicit conversion operators for structures containing a single property
- No more enforcing
init;
properties, so you can adjust the content of fields as you wish or modify a structure returned by the API (before passing it back to the API if you want) - No more JSON "required properties" during deserialization, so your old saved JSON files won't break if a field is added/renamed.
- Restored some
MessageType
enum values that were removed (renamed) recently (easier compatibility)
MaybeInaccessibleMessage
This class hierarchy was introduced in Bot API 7.0 and broke existing code and added unnecessary complexity.
This was removed in our library v21 and you will just receive directly a Message (as before).
To identify an "inaccessible message", you can just check message.Type == MessageType.Unknown
or message.Date == default
.
Chat and ChatFullInfo
In previous versions, the big Chat
structure contained many fields that were filled only after a call to GetChatAsync.
This structure is now split into Chat
and ChatFullInfo
structures.
The new Chat
structure contains only common fields that are always filled.
The new ChatFullInfo
structure inherits from Chat
and is returned only by GetChatAsync method, with all the extra fields.
Request structures
Request structures (types ending with Request
) are NOT the recommended way to use the library in your projects.
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)
Payments with Telegram Stars
To make a payment in Telegram Stars with SendInvoiceAsync, set the following parameters:
providerToken:
null
or""
currency:
"XTR"
prices:
with a single price- no tip amounts
Webhooks with System.Text.Json
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 to configure your web app correctly
InputPollOption in SendPollAsync
SendPollAsync now expect an array of InputPollOption instead of string.
But we added an implicit conversion from string to InputPollOption, so the change is minimal:
// before:
await bot.SendPollAsync(chatId, "question", new[] { "answer1", "answer2" });
// after:
await bot.SendPollAsync(chatId, "question", new InputPollOption[] { "answer1", "answer2" });
Global cancellation token (v21.2)
You can now specify a global CancellationToken
directly in TelegramBotClient constructor.
This way, you won't need to pass a cancellationToken to every method call after that (if you just need one single cancellation token for stopping your bot)
Polling system now catch exceptions in your HandleUpdate code (v21.3)
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.
Now the Polling system will catch your exceptions, pass them to your HandleErrorAsync method and continue the polling.
In previous versions of the library:
- ReceiveAsync would throw out the exception (therefore stopping the polling)
- StartReceiving would pass the exception to HandlePollingErrorAsync and silently stop the polling
If you still want the previous behaviour, have your HandleErrorAsync start like this:
Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, HandleErrorSource source, CancellationToken ct) { if (source is HandleErrorSource.HandleUpdateError) throw ex; ...
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 aMessage.MessageId
Update.AllTypes
is a constant array containing allUpdateType
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()
andHtmlText.Escape()
to sanitize reserved characters from strings - Reply/Inline Keyboard Markup now have construction methods to simplify building keyboards dynamically:
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, 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 updatesbot.OnUpdate += ...
to receive other updates (or all updates if you don't setOnMessage
)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
You can also use await bot.DropPendingUpdatesAsync()
before setting those events to ignore past updates.
The Console example project has been updated to demonstrate these events.
Migration guide for version 19.0
Topics in Groups
New topics functionality allow bots interact with users in topic specified by messageThreadId
parameter.
We try to keep our Bot API implementation as close to Telegram Bot API as possible. This means, that the new messageThreadId
now the first optional parameter for a variety of methods.
Consider to use named parameters to avoid confusion with changed parameter order.
-Message message = await bot.SendTextMessageAsync(
- _fixture.SupergroupChat.Id,
- "Please click on *Notify* button.",
- cancellationToken);
+Message message = await bot.SendTextMessageAsync(
+ chatId: _fixture.SupergroupChat.Id,
+ text: "Please click on *Notify* button.",
+ messageThreadId: threadId,
+ cancellationToken: cancellationToken);
New InputFile Hierarchy
Old InputMedia*
class hierarchy poorly reflected actual file-related APIs.
We removed old hierarchy of InputFile
related classes such as InputOnlineFile
, InputTelegramFile
, InputFileStream
, etc., and also removed all implicit casts to them. From now on you should explicitly specify one of file types: InputFileStream
for Stream
content, InputFileUrl
for URL and InputFileId
if you want to use existing file_id
. For convenience the base InputType
class has factory methods to create the correct types:
InputFile.FromStream(Stream stream, string? fileName = default)
for streamsInputFile.FromString(string urlOrFileId)
for URLs or file idsInputFile.FromUri(Uri url)
- for URLs as stringsInputFile.FromUri(string url)
- for URLs asURI
sInputFile.FromFileId(string fileId)
- for file ids
The migration scheme looks like that:
Previous method | New method |
---|---|
new InputTelegramType(string) | InputFile.FromId(string) , InputFile.FromString(string) |
new InputTelegramType(Stream, string?) | InputFile.FromStream(Stream, string?) |
new InputFileStream(Stream) | InputFile.FromStream(Stream) |
new InputOnlineFile(string) | InputFile.FromId(string) , InputFile.FromString(string) , InputFile.FromString(string) , InputFile.FromUrl(string) , InputFile.FromUrl(Uri) |
new InputOnlineFile(Stream, string?) | InputFile.FromStream(Stream, string?) |
raw Stream | InputFile.FromStream(Stream) |
raw string | InputFile.FromString(string) |
raw URI | InputFile.FromUrl(URI) |
ChatId implicit conversion
Implicit conversion from ChatId
to string
was removed due to complaints and problems it caused. The migration path is to explicitly call ChatId.ToString()
method.
Stickers
- All methods and types with animated, static and video sticker distinction were removed and replaced with a single set of sticker related methods per new Bot API updates:
AddAnimatedStickerToSetAsync
,AddStaticStickerToSetAsync
,AddVideoStickerToSetAsync
, etc. Remove the wordsStatic
,Animated
andVideo
from sticker related methods in your code - Associated emojies and masks were moved to a separate type
InputSticker
, use them there instead, consult the official Bot API docs for a more detailed information
.NET Core 3.1 removed as a separate target framework
Since .NET Core 3.1 LTS status is not officialy supported anymore we changed the target to netstandard2.0
and net6.0
instead. If you're using .NET Core 3.1 or .NET 5 runtimes you need to use the build for netstandard2.0
instead. If you relied on IAsyncEnumerable
implementation of poller you need to move to .NET 6 instead.
Other changes
Message.Type
returnsMessageType.Animation
when the message contains anAnimation
, useMessageType.Animation
instead ofMessageType.Document
to check if the message contains an animation- Property
CanSendMediaMessages
was removed from the typesChatMemberRestricted
andChatPermissions
and replaced with more granular permissions, use them instead - Removed method
GetChatMembersCountAsync
, useGetChatMemberCountAsync
- Removed method
KickChatMemberAsync
, useBanChatMemberAsync
- Properties and types
VoiceChatEnded
,VoiceChatParticipantsInvited
,VoiceChatScheduled
,VoiceChatStarted
removed, use methods and types which start withVideo*
instead - All propties with the word
Thumb
in them were renamed to contain the wordThumbnail
per new Bot API updates - A new type
InlineQueryResultsButton
is used instead ofSwitchPmText
andSwitchPmParameter
properties, consult the official Bot API docs
Migration guide for version 18.0
Most breaking changes in v18 come from new Bot API changes, such as:
- In Bot API 6.0
voice_chat*
related message properties were deprecated in favour ofvideo_chat*
with the same semantics and shape. - With introduction of video stickers in Bot API 5.7 we needed a way
to separate methods for different sticker types. So static .WEBP
*StickerSet*
methods and requests were given aStatic
prefix. - Removed
untilDate
parameter fromTelegramBotClientExtensions.BanChatSenderChatAsync
method andUntilDate
property fromBanChatSenderChatRequest
class. - As of the next update some users will be able to upload up to 4GB files, so we changed
FileBase.FileSize
type tolong?
. - A new way of configuring the client.
ApiRequestEventArgs
contains full request data.
Complete list of changes is available in CHANGELOG
1. Removal of VoiceChat*
properties in Message
object
Telegram renamed voice_chat_*
properties in the Message
class and with video_chat_*
onces so we replaced
corresponding MessageType
enum members with the new ones.
Following properties in Message
class and corresponding enum members in MessageType
enum were changed:
-VoiceChatScheduled
-VoiceChatStarted
-VoiceChatEnded
-VoiceChatParticipantsInvited
+VideoChatScheduled
+VideoChatStarted
+VideoChatEnded
+VideoChatParticipantsInvited
Also property CanManageVoiceChats
in ChatMemberAdministrator
and PromoteChatMemberRequest
classes was renamed to
CanManageVideoChats
.
2. Renaming static sticker methods and classes
With introduction of video stickers in Bot API 5.7 we needed a way
to separate methods for different sticker types. We already used Animated
and Video
suffix for methods related to animated
and video stickers so we decided to do the same for the static stickers:
- Classes
CreateNewStickerSetRequest
andAddStickerToSetRequest
were renamed toCreateNewStaticStickerSetRequest
andAddStaticStickerToSetRequest
. - Methods
CreateNewStickerSetAsync
andAddStickerToSetAsync
where renamed toCreateStaticNewStickerSetAsync
andAddStaticStickerToSetAsync
.
3. Removal of untilDate
parameter from BanChatSenderChatAsync
method and UntilDate
property from BanChatSenderChatRequest
class
The untilDate
parameter from TelegramBotClientExtensions.BanChatSenderChatAsync
method and UntilDate
property from BanChatSenderChatRequest
class were removed from the Bot API.
4. Lifting of the FileSize limit
As of the next update some users will be able to upload up to 4GB files, so we changed FileBase.FileSize
type to long?
to accommodate this change.
5. A new way of client configuration
Starting with this release client configuration parameters should be passed through TelegramBotClientOptions
class.
You need to create an instance of TelegramBotClientOptions
and pass it to the client:
using Telegram.Bot;
var options = new TelegramBotClientOptions(
token: "<token>"
// pass an optional baseUrl if you want to use a custom bot server
baseUrl: "https://custombotserverdomain.com",
// pass an optional flag `true` if you want to use test environment
useTestEnvironment = true
);
var client = new TelegramBotClient(options);
If you don't know about test environment you can read more about it in the official documentation.
If you don't need extra configuration options you can still use the constructor that accepts a token and an instance of HttpClient
:
var client = new TelegramBotClient("<token>");
6. Polling functionality in the core library
The latest biggest change which is not a breaking one, but nevertheless worth a note: deprecation of
Telegram.Bot.Extensions.Polling
package.
All the functionality from the package was merged into the core library under namespace Telegram.Bot.Polling
.
Name of the method HandleErrorAsync
in IUpdateHandler
interface was quite confusing from the beginning since a lot
of people assumed they can handle all errors in it, but in reality it's used only for handling errors during polling.
We decided to give it a more appropriate name: HandlePollingErrorAsync
.
Migration guide for version 17.0
There are several breaking changes in v17:
- New exceptions handling logic
- Removal of update and message events
- Removal of API methods from
ITelegramBotClient
interface and moving them into extension methods in the same namespace (that shouldn't break anyone's sources as long as they don't employ reflection or make their own interface implementations) - Working with default enum values
These are the most user facing breaking changes you should be aware of during migration.
Let's dive deep on the migrations.
New exceptions handling logic
v17 brings a new base type for exceptions: RequestException
. ApiRequestException
inherits from RequestException
and is thrown only when an actual error response with the correct body is received from the Bot API. In other situations RequestException
will be thrown instead containing actual exception as InnerException
if there is one, e.g. serialization or connection-related exceptions.
If you used ApiRequestException
and HttpRequestException
to handle most exception now you have to replace HttpRequestException
with RequestException
and look for the inner exception. All valid errors with JSON body from Telegram are now thrown as ApiRequestException
including 429: Too Many Requests
.
Since 5XX
responses don't usually include correct JSON body they are thrown as RequestException
with HttpRequestException
inside.
Look at the following example on how to handle different kinds of exceptions. You might not need to implement everything as you see, it's there only for demonstration purposes.
try
{
await bot.SendTextMessageAsync(chatId, "Hello");
}
catch (ApiRequestException exception)
{
switch (exception.StatusCode)
{
case 400:
// Handle incorrect requests exceptions
break;
case 401:
// Handle incorrect bot token exception (revoked tokens)
break;
case 403:
// Handle authorization exceptions (blocked users, unaccessible chats, etc)
break;
case 429:
// Handle rate limiting exception
break;
default:
// Handle other errors with valid json body: it includes status code and description of the error
break;
}
}
catch (RequestException exception)
{
if (exception.InnerException is HttpRequestException httpRequestException)
{
// Handle connection exceptions or 5XX exceptions from the Bot API
}
else if (exception.InnerException is JsonSerializationException serializationException)
{
// Handle serialization exception when a request or a response can't be serialized for some reason
}
else
{
// Handle all other exceptions
}
}
catch (OperationCancelledException exception)
{
// Handle cancellation exception, e.g. when CancellationToken is cancelled
}
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 article.
Removal of API methods from ITelegramBotClient
interface
This change shouldn't affect most users, the methods are still there, but instead of being implementations of the interface they are now extension methods. It makes the interface leaner and easier to implement for custom clients and for decorators (e.g. rate limiters implemented as decorators). There isn't really a migration path for those who used these for some reason.
Working with default enum values
We changed how we work with enums. The most notable change is the default value: there is none, all our enums are now start with 1 (exception UpdateType
and MessageType
since they are not a part of the Bot API and we fully control these). 0
value is left unassigned for a purpose: if we encounter an unknown value in the response from the Bot API we assign 0
as its value.
Let's imagine that Telegram adds new MessageEntity
value. From now on all unknown values can be handled in the default
case of switch
statement.
MessageEntity entity = message.Entities.First();
switch (entity.Type)
{
case MessageEntityType.Username:
// ...
break;
case MessageEntityType.Command:
// ...
break;
default:
// All unknown values will go there
break;
}
Also some default enums values were removed, e.g. ParseMode.Default
since we started using nullable types for every optional value and ParseMode.Default
lost it's use. If a message doesn't have any markup you'll receive null
in places where ParseMode
type was used or if you want to explicitly indicate an absence of markup pass null
instead.
Other breaking changes
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 article.
InputMediaType
Property Type
in IInputMedia
was changed to an enum InputMediaType
for easier discoverability. So if you relied string values like photo
, video
, animation
and so on now you need to switch to using enums. As a result you'll get autocomplete in IDEs and more predictability of what types of input media there are.
EncryptedPassportElementType
Property Type
of EncryptedPassportElement
was replace with an enum for the same reason with EncryptedPassportElementType
enum.
ChatMember
As part of Bot API 5.3 implementation ChatMember
type was split into a hierarchy of types with a discriminator field Status
. If you need to access some data from the derived class you should use pattern matching or type casting like this:
ChatMember member = ... //;
if (chatMember is ChatMemberKicked kickedMember)
{
// now you can access properties of a kicked chat member
if (kickedMember.Until is not null)
{
// do something with the value of Until
}
}
ChatId
Fields Identifier
and Username
are now get-only properties. It shouldn't break most people's code as it's not a source breaking change. If you used reflection to find these fields you should to look for properties now.
InlineQueryResultBase
Type InlineQueryResultBase
is renamed to InlineQueryResult
to match Bot API type hierarchy.
Nullability
From now on all properties that are optional will use nullable types, e.g. int?
, string?
, because default values of such properties might be an actual values and isn't distinguishable from a lack of value. From now if a property is null
you can be sure that it's value was not present in a response from the Bot API.
ReplyKeyboardMarkup
Since ResizeKeyboard
and OneTimeKeyboard
are optional, we removed them from ReplyKeyboardMarkup
constructor. You have to use object initialization syntax to configure these properties:
var replyKeyboardMarkup = new ReplyKeyboardMarkup(
new KeyboardButton[][]
{
new KeyboardButton[] { "1.1", "1.2" },
new KeyboardButton[] { "2.1", "2.2" },
})
{
ResizeKeyboard = true
};
Migration guide for version 14.x
Date and Time
All DateTime
values are now in UTC format. Here are some examples of usage:
// Use UTC time when making a request
await bot.KickChatMemberAsync(
chatId: -9876,
userId: 1234,
untilDate: DateTime.UtcNow.AddDays(2)
);
// Convert to local time (not recommended though)
DateTime localTime = update.Message.Date.ToLocalTime();
Keyboard Buttons
Many keyboard button types are removed from project. It is more convenient to use factory methods on KeyboardButton
and InlineKeyboardButton
classes.
Here are some examples:
// Message having an inline keyboard button with URL that redirects to a page
await bot.SendTextMessageAsync(
chatId: -9876,
text: "Check out the source code",
replyMarkup: new InlineKeyboardMarkup(
InlineKeyboardButton.WithUrl("Repository", "https://github.com/TelegramBots/Telegram.Bot")
)
);
// Message to a private chat having a 2-row reply keyboard
await bot.SendTextMessageAsync(
chatId: 1234,
text: "Share your contact & location",
replyMarkup: new ReplyKeyboardMarkup(
new [] { KeyboardButton.WithRequestContact("Share Contact") },
new [] { KeyboardButton.WithRequestLocation("Share Location") },
)
);
GetFileAsync()
Downloading a file from Telegram Bot API has 2 steps (see docs here):
- Get file info by calling
getFile
- Download file from
https://api.telegram.org/file/bot<token>/<file_path>
GetFileAsync()
is replaced by 3 methods. Method GetInfoAndDownloadFileAsync()
looks very similar to old GetFileAsync()
:
// Gets file info and saves it to "path/to/file.pdf"
using (var fileStream = System.IO.File.OpenWrite("path/to/file.pdf"))
{
File fileInfo = await bot.GetInfoAndDownloadFileAsync(
fileId: "BsdfgLg4Khdlsn-bldBD",
destination: fileStream
);
}
Note that calling the method
GetInfoAndDownloadFileAsync()
results in 2 HTTP requests (steps 1 and 2 above) being sent to the Bot API.
There are two more methods that assist you with downloading files:
// New version of GetFileAsync() only gets the file info (step 1)
File fileInfo = await bot.GetFileAsync("BsdfgLg4Khdlsn-bldBD");
// Download file from server (step 2)
using (var fileStream = System.IO.File.OpenWrite("path/to/file.pdf")) {
await bot.DownloadFileAsync(
filePath: fileInfo.FilePath,
destination: fileStream
);
}
GetUpdatesAsync()
, SetWebhookAsync()
Value All
is removed from enum Telegram.Bot.Types.Enums.UpdateType
. In order to get all kind of updates, pass an empty list such as Array.Empty<UpdateType>()
for allowedUpdates
argument.
SetWebhookAsync()
Parameter url
is required. If you intend to remove the webhook, it is recommended to use DeleteWebhookAsync()
instead. However, you could achieve the same result by passing string.Empty
value to url
argument.
AnswerInlineQueryAsync()
and InlineQueryResult
Classes InlineQueryResultNew
and InlineQueryResultCache
are removed. InlineQueryResult
has become the only shared base type for all inline query result classes.
Many shared and redundant properties are removed. This might require significant changes to your .cs
files if your bot is in inline mode. Fortunately, all input query results now have constructors with only the required properties as their parameters. This is the preferred way to instantiate input query result instances e.g.:
Instead of:
// bad way. easy to get exceptions
var documentResult = new InlineQueryResultDocument
{
Id = "some-id",
Url = "https://example.com/document.pdf",
Title = "Some title",
MimeType = "application/pdf"
};
You should use:
// good way
var documentResult = new InlineQueryResultDocument(
id: "some-id",
documentUrl: "https://example.com/document.pdf",
title: "Some title",
mimeType: "application/pdf"
);
SendMediaGroupAsync()
InputMediaType
is renamed to InputMedia
.
ToDo
Inline Message Overloads
Many inline message methods have been replaced with their overloads.
EditInlineMessageTextAsync
-->EditMessageTextAsync
ToDo
FileToSend
New classes have replaced FileToSend
struct.
InputFileStream
:InputTelegramFile
:InputOnlineFile
:
In many cases, you can use implicit casting to pass parameters.
Stream stream = System.IO.File.OpenRead("photo.png");
var message = await bot.SendPhotoAsync("chat id", stream);
string fileId = "file_id on Telegram servers";
var message = await bot.SendPhotoAsync("chat id", fileId);
ToDo. implicit casts
UpdateType
and MessageType
Values in these two enums are renamed e.g. UpdateType.MessageUpdate
is now UpdateType.Message
.
MessageType.Service
is removed. Now each type of message has its own MessageType
value e.g. when a chat member leaves a group, corresponding update contains a message type of MessageType.ChatMemberLeft
value.
VideoNote
Properties Width
and Height
are removed. Vide notes are squared and Length
property represents both width and height.
Constructor Parameters Instead of Public Setters
Many types now have the required parameters in their constructors. To avoid running into problems or getting exceptions, we recommend providing all required values in the constructor e.g.:
//bad way:
var markup = new InlineKeyboardMarkup
{
Keyboard = buttonsArray,
ResizeKeyboard = true
};
// better:
var markup = new InlineKeyboardMarkup(buttonsArray)
{
ResizeKeyboard = true
};