Library for creating Telegram bot interfaces based on Telegram.Bot
Visit the repository with a demo project of a photo editor bot Telegram.Bot.UI.Demo
- π Different bot operation modes:
- Long Polling
- WebHook via controller
- Built-in WebHook server
- πΌοΈ Text templating system
- π¦ Resource loader (texts, images, etc.)
- π Nested interface pages
- β¨οΈ Built-in command parser
- π‘οΈ User permissions management system (useful for bans)
β οΈ Safe bot shutdown mechanism (waits for all critical operations to complete)- ποΈ Page wallpaper support (via web preview)
- π Built-in license agreement acceptance mechanism
- π§° Rich library of interactive menu components
The library provides numerous interactive components:
MenuCheckbox
- Checkboxes for enabling/disabling optionsMenuCheckboxGroup
- Group of checkboxes for multiple selectionMenuCheckboxModal
- Modal window with checkboxes (separate page)MenuCommand
- Button for triggering custom actionsMenuLink
- Link to external resources, channels, chatsMenuNavigatePanel
- Navigation between menu pages (in development)MenuOpenPege
- Opening other interface pagesMenuRadio
- Radio buttons for selecting one of several optionsMenuRadioModal
- Modal window with radio buttonsMenuSplit
- Element separator (line break)MenuSwitch
- Carousel option switch (one button)
The Telegram.Bot.UI package is available via NuGet!
dotnet add package Telegram.Bot.UI
A separate instance of the user class is created for each user, where you can store state, work with the database, configure localization and interface:
public class MyBotUser : BaseBotUser
{
public LanguageView languageView { get; private set; }
public UserAgreementView userAgreementView { get; private set; }
public InformationView informationView { get; private set; }
public MyBotUser(IBotWorker worker, long chatId, ITelegramBotClient client, CancellationToken token) :
base(worker, chatId, client, token)
{
// Setting up pages
languageView = new(this);
userAgreementView = new(this);
informationView = new(this);
parseMode = ParseMode.Html;
}
public override void Begin() {
// These values can be retrieved from the database
localization.code = "en";
acceptLicense = false;
}
public override async Task HandleCommandAsync(string cmd, string[] arguments, Message message) {
switch (cmd) {
case "hello":
case "info":
case "start": {
await informationView.SendPageAsync();
}
break;
case "lang": {
await languageView.SendPageAsync();
}
break;
case "ping": {
await SendTextMessageAsync("`pong`", mode: ParseMode.MarkdownV2);
}
break;
}
}
public override Task<bool> HandlePermissiveAsync(Message message) {
// Prohibit private chats
return Task.FromResult(message.Chat.Type != ChatType.Private);
}
public override async Task HandleAcceptLicense(Message message) {
// License must be accepted first
await userAgreementView.SendPageAsync();
}
public override async Task HandleErrorAsync(Exception exception) {
// Log the error and send it in response
Console.WriteLine(exception.ToString());
await SendTextMessageAsync($"<pre>{EscapeText(exception.ToString(), ParseMode.Html)}</pre>", mode: ParseMode.Html);
}
}
A simple way for a quick start:
var bot = new BotWorkerPulling<MyBotUser>((worker, chatId, client, token) => {
return new MyBotUser(worker, chatId, client, token);
}) {
botToken = "TELEGRAM_BOT_TOKEN",
resourcePath = Path.Combine("Resources", "View"),
localizationPack = LocalizationPack.FromJson(new FileInfo(Path.Combine("Resources", "Lang", "Lang.json")))
};
await bot.StartAsync();
- Wait! But the polling mode is slow! I want a webhook!
- No problem! This can be implemented like this!
var bot = new BotWorkerWebHook<MyBotUser>((worker, chatId, client, token) => {
return new MyBotUser(worker, chatId, client, token);
}) {
botToken = "TELEGRAM_BOT_TOKEN",
botSecretToken = "WEBHOOK_SECRET_TOKEN",
botHostAddress = "https://mybot.com",
botRoute = "TelegramBot/webhook",
resourcePath = Path.Combine("Resources", "View"),
localizationPack = LocalizationPack.FromJson(new FileInfo(Path.Combine("Resources", "Lang", "Lang.json")))
};
await bot.StartAsync();
builder.Services.AddSingleton(bot);
Controller for handling requests:
[ApiController]
[Route("[controller]")]
public class TelegramBotController : ControllerBase {
private readonly BotWorkerWebHook<MyBotUser> bot;
public TelegramBotController(BotWorkerWebHook<MyBotUser> bot) {
this.bot = bot;
}
[HttpPost("webhook")]
public async Task<IActionResult> Post([FromBody] Update update) {
await bot.UpdateHandlerAsync(update);
return Ok();
}
}
For console applications or when integration with ASP.NET is not possible:
- Damn! I hate WebApi and all that DI! I want a simple console application with webhook!
- Don't worry! This is also possible!
var bot = new BotWorkerWebHookServer<MyBotUser>((worker, chatId, client, token) => {
return new MyBotUser(worker, chatId, client, token);
}) {
botToken = "TELEGRAM_BOT_TOKEN",
botSecretToken = "WEBHOOK_SECRET_TOKEN",
botHostAddress = "https://mybot.com",
port = 80,
botRoute = "webhook",
resourcePath = Path.Combine("Resources", "View"),
localizationPack = LocalizationPack.FromJson(new FileInfo(Path.Combine("Resources", "Lang", "Lang.json")))
};
await bot.StartAsync();
The library uses the concept of pages (classes inheriting from MessagePage
) to represent bot interface elements.
public class LanguageView : MessagePage {
public override string pageResource => "Language"; // There should be a folder with pageResource name in resourcePath (Resources/View/Language)
public override string title => $"{flags[botUser.localization.code]} " + "{{ 'Language select' | L }}"; // | L - Built-in localization method
private MenuRadio languageRadio;
private Dictionary<string, string> flags { get; init; } = new() {
["ru"] = "π·πΊ",
["en"] = "πΊπΈ"
};
public LanguageView(BaseBotUser botUser) : base(botUser) {
languageRadio = MenuRadio(MenuSelector.FromArray(new[] {
("English", "en"),
("Π ΡΡΡΠΊΠΈΠΉ", "ru")
}));
using var context = ((MyBotUser)botUser).Context();
var userTable = ((MyBotUser)botUser).GetUserTable(context);
languageRadio.Select(userTable.language);
languageRadio.onSelect += select => {
using var context = ((MyBotUser)botUser).Context();
var userTable = ((MyBotUser)botUser).GetUserTable(context);
((MyBotUser)botUser).localization.code = select.id;
userTable.language = select.id;
context.SaveChanges();
};
}
public override string? RequestMessageResource() => $"description-{botUser.localization.code}";
public override List<ButtonsPage> RequestPageComponents() {
return ButtonsPage.Page([
[languageRadio]
]);
}
}
public class UserAgreementView : MessagePage {
public override string pageResource => "UserAgreement";
public override string title => "{{ 'User agreement' | L }}";
private MenuRadio languageRadio;
private MenuCommand acceptCommand;
public UserAgreementView(BaseBotUser botUser) : base(botUser) {
languageRadio = MenuRadio(MenuSelector.FromArray(new[] {
("English", "en"),
("Π ΡΡΡΠΊΠΈΠΉ", "ru")
}));
using var context = ((MyBotUser)botUser).Context();
var userTable = ((MyBotUser)botUser).GetUserTable(context);
languageRadio.Select(userTable.language);
languageRadio.onSelect += select => {
using var context = ((MyBotUser)botUser).Context();
var userTable = ((MyBotUser)botUser).GetUserTable(context);
((MyBotUser)botUser).localization.code = select.id;
userTable.language = select.id;
context.SaveChanges();
};
acceptCommand = MenuCommand("{{ 'I agree' | L }}");
acceptCommand.onClick += async (callbackQueryId, messageId, chatId) => {
using var context = ((MyBotUser)botUser).Context();
var userTable = ((MyBotUser)botUser).GetUserTable(context);
userTable.acceptLicense = true;
((MyBotUser)botUser).acceptLicense = true;
context.SaveChanges();
// Delete current page
await botUser.DeleteMessageAsync(messageId);
// Send welcome page after accepting the agreement
await ((MyBotUser)botUser).informationView.SendPageAsync();
};
}
public override string? RequestMessageResource() => $"description-{botUser.localization.code}";
public override List<ButtonsPage> RequestPageComponents() {
return ButtonsPage.Page([
[languageRadio],
[acceptCommand]
]);
}
}
public class InformationView : MessagePage {
public override string pageResource => "Information";
public override string title => "{{ 'Information' | L }}";
public InformationView(BaseBotUser botUser) : base(botUser) { }
public override string? RequestMessageResource() => $"description-{botUser.localization.code}";
public override object? RequestModel() => new {
me = botUser.chatId // Now can be used in the templating engine
};
public override List<ButtonsPage> RequestPageComponents() {
return ButtonsPage.Page([
[
MenuLink("https://t.me/MyBotSupport", "π {{ 'Support' | L }}"),
MenuOpenSubPege(((MyBotUser)botUser).languageView)
]
]);
}
}
Localization uses a simple JSON format:
[
{
"en": "Support",
"ru": "ΠΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ°"
},
{
"en": "I agree",
"ru": "Π― ΡΠΎΠ³Π»Π°ΡΠ΅Π½"
},
{
"en": "Language select",
"ru": "ΠΡΠ±ΠΎΡ ΡΠ·ΡΠΊΠ°"
},
{
"en": "Information",
"ru": "ΠΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ"
},
{
"en": "User agreement",
"ru": "ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΡΠΊΠΎΠ΅ ΡΠΎΠ³Π»Π°ΡΠ΅Π½ΠΈΠ΅"
}
]
Resources are organized in folders by page name:
Resources/
βββ View/
β βββ Language/ # pageResource = "Language"
β β βββ text/
β β β βββ description-en.md
β β β βββ description-ru.md
β β βββ image/
β β βββ background.png
β βββ UserAgreement/ # pageResource = "UserAgreement"
β β βββ text/
β β βββ description-en.md
β β βββ description-ru.md
β βββ Information/ # pageResource = "Information"
β βββ text/
β βββ description-en.md
β βββ description-ru.md
βββ Lang/
βββ Lang.json # File with localizations
π Hello, {{ me }}!
π ΠΡΠΈΠ²Π΅Ρ, {{ me }}!