# Getting Started with Telegrator --- This guide will walk you through the core concepts and advanced features of **Telegrator** — a modern, aspect-oriented, mediator-based framework for building powerful and maintainable Telegram bots in C#. - [1. Installation](#1-installation) - [2. Framework Mechanics Overview](#2-framework-mechanics-overview) - [2.1. Basic Concepts](#21-basic-concepts) - [2.2. Practice: Minimal Bot](#22-practice-minimal-bot) - [2.3. Working with Filters](#23-working-with-filters) - [2.4. State Management](#24-state-management) - [2.5. Concurrency & Awaiting](#25-concurrency--awaiting) - [2.6. Extensibility](#26-extensibility) - [2.7. Integration](#27-integration) - [3. Step-by-Step Tutorials](#3-step-by-step-tutorials) - [3.1. Minimal Bot Creation](#31-minimal-bot-creation) - [3.2. Command Filtering](#32-command-filtering) - [3.3. State Management Wizard](#33-state-management-wizard) - [3.4. Awaiting CallbackQuery](#34-awaiting-callbackquery) - [3.5. Adding a Custom Filter](#35-adding-a-custom-filter) - [4. Advanced Topics](#4-advanced-topics) - [4.1. Handler Priority](#41-handler-priority) - [4.2. Dependency Injection (DI)](#42-dependency-injection-di) - [4.3. Custom State Keepers](#43-custom-state-keepers) - [4.4. Automatic Handler Discovery](#44-automatic-handler-discovery) - [4.5. Hosting Integration](#45-hosting-integration) - [4.6. Error Handling and Logging](#46-error-handling-and-logging) - [4.7. Performance Optimization](#47-performance-optimization) - [4.8. Best Practices](#48-best-practices) - [5. FAQ & Best Practices](#5-faq--best-practices) - [Q: My handler is not being triggered. What should I do?](#q-my-handler-is-not-being-triggered-what-should-i-do) - [Q: How can I access the `ITelegramBotClient` or the original `Update` object inside a handler?](#q-how-can-i-access-the-itelegrambotclient-or-the-original-update-object-inside-a-handler) - [Q: How do I handle errors?](#q-how-do-i-handle-errors) - [Q: How can I organize my code for a large bot?](#q-how-can-i-organize-my-code-for-a-large-bot) - [6. Links](#6-links) --- ## 1. Installation **Telegrator** is distributed as a NuGet package. You can install it using the .NET CLI, the NuGet Package Manager Console, or by managing NuGet packages in Visual Studio. ### Prerequisites - .NET >= 5.0 `or` .NET Core >= 2.0 `or` Framework >= 4.6.1 (.NET Standart 2.1 compatible) - A Telegram Bot Token from [@BotFather](https://t.me/BotFather). ### .NET CLI ```shell dotnet add package Telegrator ``` ### Package Manager Console ```shell Install-Package Telegrator ``` ### Hosting Integrations - .NET Core >= 8.0 - `Telegrator.Hosting`: For console/background services - `Telegrator.Hosting.Web`: For ASP.NET Core/Webhook (WIP) --- ## 2. Framework Mechanics Overview ### 2.1. Basic Concepts Telegrator is built around several core ideas: - **Aspect-Oriented Handlers**: Each handler is a focused, reusable class that reacts to a specific type of update (message, command, callback, etc.). - **Mediator Pattern**: All updates are routed through a central `UpdateRouter`, which dispatches them to the appropriate handlers based on filters and priorities. - **Filters as Attributes**: Handler classes are decorated with filter attributes that declaratively specify when the handler should run. - **State Management**: Built-in mechanisms for managing user/chat state without external storage. - **Concurrency Control**: Fine-grained control over how many handlers run in parallel, both globally and per-handler. ### 2.2. Practice: Minimal Bot Here's how to create a minimal bot that replies to any private message containing "hello": ```csharp using Telegrator; using Telegrator.Handlers; using Telegrator.Annotations; using Telegram.Bot.Types; using Telegram.Bot.Types.Enums; [MessageHandler] [ChatType(ChatType.Private)] [TextContains("hello", StringComparison.InvariantCultureIgnoreCase)] public class HelloHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("Hello! Nice to meet you!", cancellationToken: cancellation); return Ok; } } class Program { static void Main(string[] args) { TelegratorClient bot = new TelegratorClient(""); bot.Handlers.AddHandler(); bot.StartReceiving(); Console.ReadLine(); } } ``` > **How is it working?** > 1. **`[MessageHandler]`**: This attribute marks `HelloHandler` as a handler for `Message` updates. > 2. **`[ChatType(ChatType.Private)]`**: This filter ensures the handler only runs for private chat messages. > 3. **`[TextContains("hello")]`**: This filter checks if the message contains "hello" (case-insensitive). > 4. **`TelegratorClient`**: The main bot client that manages the connection to Telegram and the update processing pipeline. > 5. **`bot.Handlers.AddHandler()`**: Registers the handler with the bot. > 6. **`bot.StartReceiving()`**: Starts the long-polling loop to fetch updates from Telegram. > 7. **`Reply(...)`**: A helper method that sends a reply to the original message. ### 2.3. Working with Filters Filters are the gatekeepers of your bot logic. They are applied as attributes to handler classes and determine when a handler should be executed. **Common Filters:** - `[CommandAlias("start")]` — Only for the `/start` command - `[TextContains("hello")]` — Message contains "hello" - `[ChatType(ChatType.Private)]` — Only private chats - `[FromUserId(123456789)]` — Only from a specific user - `[HasReply]` — Only if the message is a reply **Combining Filters:** You can combine filters using logical modifiers: - Multiple filters on a handler, by default, are combined with logical AND - `Modifiers = FilterModifier.OrNext` - will combine this and next filter with OR logic - `Modifiers = FilterModifier.Not` - Inverts the filter - This flags can be combined using bit OR (`Modifiers = FilterModifier.Not | FilterModifier.OrNext`) **Example:** ```csharp [MessageHandler] [ChatType(ChatType.Private)] [TextContains("hello", Modifiers = FilterModifier.Not)] public class NotHelloHandler : MessageHandler { // Runs for private messages that do NOT contain "hello" } [MessageHandler] [TextContains("bot", Modifiers = FilterModifier.OrNext)] [Mentioned()] public class NotHelloHandler : MessageHandler { // Runs for messages that contains "bot" or if bot was mentioned using @ } ``` > **How is it working?** > 1. **Multiple Filters**: The handler has two filters that work together with logical AND. > 2. **`[ChatType(ChatType.Private)]`**: Ensures only private chat messages are processed. > 3. **`[TextContains("hello", Modifiers = FilterModifier.Not)]`**: The `Not` modifier inverts the filter, so it matches messages that do NOT contain "hello". > 4. **Combined Logic**: The handler will only run for private messages that don't contain "hello". ### 2.4. State Management Telegrator provides built-in state management for multi-step conversations (wizards, forms, quizzes) without a database. > [!NOTE] > Each type of `StateKeeper`'s (EnumStateKeeper, NumericStateKeeper) is shared beetwen **EVERY** handler in project. **Types of State:** - **NumericState**: Integer-based steps - **StringState**: Named steps - **EnumState**: Enum-based scenarios **How to Use:** 1. Define your state (enum/int/string) 2. Use a state filter attribute on your handler: - `[EnumState(MyEnum.Step1)]` - `[NumericState(1)]` 3. Change state inside the handler using extension methods: - `container.ForwardEnumState()` - `container.ForwardNumericState()` - `container.DeleteEnumState()` **Example:** ```csharp public enum QuizState { Start = SpecialState.NoState, Q1, Q2 } [CommandHandler] [CommandAlias("quiz")] [EnumState(QuizState.Start)] public class StartQuizHandler : CommandHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { container.ForwardEnumState(); await Reply("Quiz started! Question 1: What is the capital of France?"); return Ok; } } [MessageHandler] [EnumState(QuizState.Q1)] public class Q1Handler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { if (Input.Text.Trim().Equals("Paris", StringComparison.InvariantCultureIgnoreCase)) await Reply("Correct!"); else await Reply("Incorrect. The answer is Paris."); container.ForwardEnumState(); await Reply("Question 2: What is 2 + 2?"); return Ok; } } ``` > **How is it working?** > 1. **Enum State Definition**: `QuizState` enum defines the conversation flow with `Start = SpecialState.NoState` indicating no initial state. > 2. **State Filter**: `[EnumState(QuizState.Start)]` ensures the handler only runs when the user is in the "Start" state. > 3. **State Transition**: `container.ForwardEnumState()` moves the user to the next state (Q1). > 4. **Next Handler**: The `Q1Handler` will only run when the user is in state `QuizState.Q1`. > 5. **State Management**: Each handler manages its own state transition, creating a clear conversation flow. ### 2.5. Concurrency & Awaiting **Concurrency Control:** - Limit the number of concurrent executions globally using `MaximumParallelWorkingHandlers` in `TelegramBotOptions` **Awaiting Other Updates:** - Use `AwaitingProvider` to wait for a user's next update (message or callback) inside a handler: ```csharp [CommandHandler] [CommandAlias("ask")] public class AskHandler : CommandHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("What is your name?"); var nextMessage = await container.AwaitMessage().BySenderId(cancellation); await Reply($"Hello, {nextMessage.Text}!"); return Ok; } } ``` > **How is it working?** > 1. **Awaiting Provider**: `container.AwaitMessage()` creates a temporary handler that waits for the next message. > 2. **Sender Filter**: `.BySenderId(cancellation)` ensures only messages from the same user are captured. > 3. **Async Flow**: The handler pauses execution until the user responds, then continues with the conversation. > 4. **Context Preservation**: The original handler context is maintained during the awaiting process. ### 2.6. Extensibility You can extend Telegrator by creating custom filters, attributes, and state keepers. **Custom Filter Attribute Example:** ```csharp using Telegram.Bot.Types; using Telgrator.Attributes; using Telegrator.Handlers; public class AdminOnlyAttribute() : FilterAnnotation { private readonly List _adminIds = []; public void AddAdmin(long id) => _adminIds.Add(id); public void RemoveAdmin(long id) => _adminIds.Remove(id); public override bool CanPass(FilterExecutionContext context) => _adminIds.Contains(context.Input.From?.Id); } [MessageHandler] [AdminOnly] public class AdminHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("Hello, admin!"); return Ok; } } // ... AdminOnlyAttribute.AddAdmin(123456789); bot.StartReceiving(); ``` > **How is it working?** > 1. **Custom Filter**: `AdminOnlyAttribute` inherits from `FilterAnnotation` to create a reusable filter attribute. > 2. **Filter Logic**: `CanPass()` method checks if the message sender's ID matches the admin ID. > 4. **Usage**: The filter is applied as an attribute `[AdminOnly]` to restrict access users that not registered as admins. ### 2.7. Integration Telegrator works in console, hosted applications, --and ASP.NET Core (webhook)-- (WIP) projects. **Console App Example:** ```csharp var bot = new TelegratorClient(""); bot.Handlers.CollectHandlersDomainWide(); bot.StartReceiving(); ``` **Hosting Example:** ```csharp using Telegrator.Hosting; var builder = TelegramBotHost.CreateBuilder(); builder.Handlers.AddHandler(); var host = builder.Build(); await host.StartAsync(); ``` > **How is it working?** > 1. **Console Integration**: `TelegratorClient` provides a simple way to create bots in console applications. > 2. **Domain-Wide Collection**: `CollectHandlersDomainWide()` automatically discovers and registers all handlers in the current assembly. > 3. **ASP.NET Core Integration**: `TelegramBotHost.CreateBuilder()` provides a builder pattern for hosting bots in ASP.NET Core applications. > 4. **Dependency Injection**: Handlers and their dependencies are automatically registered with the DI container. --- ## 3. Step-by-Step Tutorials ### 3.1. Minimal Bot Creation See [Practice: Minimal Bot](#practice-minimal-bot). ### 3.2. Command Filtering Message is considered command if is start with '/' and has not null or empty name. Instead of using the `MessageHandler` for command (such as `/start`) you should use `CommandHandler`. ```csharp [CommandHandler] [CommandAlias("start")] public class StartHandler : CommandHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("Welcome! Use /help to see available commands."); return Ok; } } [CommandHandler] [CommandAlias("help")] public class HelpHandler : CommandHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("Available commands:\n/start - Start the bot\n/help - Show this help"); return Ok; } } [MessageHandler] [TextStartsWith("/", Modifiers = FilterModifier.Not)] public class EchoHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply($"You said: \"{Input.Text}\""); return Ok; } } ``` > **How is it working?** > 1. **Command Handlers**: `[CommandHandler]` and `[CommandAlias]` work together to handle specific commands like `/start` and `/help`. > 2. **Echo Handler**: `[TextStartsWith("/", Modifiers = FilterModifier.Not)]` catches all messages that don't start with "/" (non-commands). > 3. **Handler Separation**: Each command has its own dedicated handler, making the code modular and maintainable. > 4. **Filter Modifiers**: The `Not` modifier inverts the filter logic to exclude command messages. ### 3.3. State Management Wizard ```csharp [CommandHandler] [CommandAlias("wizard")] [NumericState(SpecialState.NoState)] public class StartWizardHandler : CommandHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { container.CreateNumericState(); // This code is not necesary, as "Forward" method can automatically creates state if needed, but its recomended to use container.ForwardNumericState(); await Reply("What is your name?"); return Ok; } } [MessageHandler] [NumericState(1)] public class NameHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { container.ForwardNumericState(); await Reply($"Nice to meet you, {Input.Text}! How old are you?"); return Ok; } } [MessageHandler] [NumericState(2)] public class AgeHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { if (int.TryParse(Input.Text, out int age)) { await Reply($"Thank you! You are {age} years old. Wizard completed!"); container.DeleteNumericState(); } else { await Reply("Please enter a valid age (number)."); } return Ok; } } ``` > **How is it working?** > 1. **Numeric State**: `[NumericState(SpecialState.NoState)]` starts the wizard when no state exists. > 2. **State Creation**: `container.CreateNumericState()` initializes the numeric state for the user. > 3. **State Progression**: `container.ForwardNumericState()` moves to the next step (1, then 2). > 4. **State Cleanup**: `container.DeleteNumericState()` removes the state when the wizard completes. > 5. **Input Validation**: The age handler validates numeric input and provides feedback. ### 3.4. Awaiting CallbackQuery ```csharp [CommandHandler] [CommandAlias("menu")] public class MenuHandler : CommandHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { var keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithCallbackData("Option 1", "option1"), InlineKeyboardButton.WithCallbackData("Option 2", "option2") }); await Reply("Choose an option:", replyMarkup: keyboard, cancellationToken: cancellation); return Ok; } } [CallbackQueryHandler] [CallbackData("option1")] public class Option1Handler : CallbackQueryHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await AnswerCallbackQuery("You selected Option 1!", cancellationToken: cancellation); await EditMessageText("You selected Option 1!"); return Ok; } } [CallbackQueryHandler] [CallbackData("option2")] public class Option2Handler : CallbackQueryHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await AnswerCallbackQuery("You selected Option 2!", cancellationToken: cancellation); await EditMessageText("You selected Option 2!"); return Ok; } } ``` > **How is it working?** > 1. **Inline Keyboard**: `InlineKeyboardMarkup` creates interactive buttons with `CallbackData` identifiers. > 2. **CallbackQuery Handlers**: `[CallbackQueryHandler]` and `[CallbackData]` work together to handle button clicks. > 3. **Response Methods**: `AnswerCallbackQuery()` provides immediate feedback, while `EditMessageText()` updates the message. > 4. **Handler Separation**: Each button option has its own dedicated handler for clean code organization. ### 3.5. Adding a Custom Filter ```csharp public class PremiumUserAttribute : UpdateFilterAttribute { public override Message? GetFilterringTarget(Update update) => update.Message; public override bool CanPass(FilterExecutionContext context) => context.Input.From?.IsPremium == true; } [MessageHandler] [PremiumUser] public class PremiumFeatureHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("This feature is only available for premium users!"); return Ok; } } ``` > **How is it working?** > 1. **Custom Filter**: `PremiumUserAttribute` inherits from `UpdateFilterAttribute` to create a reusable filter. > 2. **Premium Check**: `context.Input.From?.IsPremium == true` checks if the user has Telegram Premium. > 3. **Target Extraction**: `GetFilterringTarget()` extracts the `Message` from the `Update` object. > 4. **Usage**: The filter is applied as `[PremiumUser]` to restrict features to premium users only. --- ## 4. Advanced Topics ### 4.1. Handler Priority By default, handlers are processed in the order they are added. However, you can control the execution order using the `Priority` property in the handler attribute. A greater number means higher priority. ```csharp [MessageHandler(Priority = 1)] // Runs before default priority (0) public class HighPriorityHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("This handler runs first!"); return Ok; } } [MessageHandler(Priority = 0)] // Default priority public class NormalPriorityHandler : MessageHandler { public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { await Reply("This handler runs second!"); return Ok; } } ``` > **How is it working?** > 1. **Priority System**: The `Priority` property in handler attributes controls execution order. > 2. **Higher Priority First**: Handlers with higher priority numbers (1) run before those with lower numbers (0). > 3. **Default Priority**: When not specified, handlers have priority 0. > 4. **Execution Order**: This ensures critical handlers (like admin commands) run before general handlers. ### 4.2. Dependency Injection (DI) Telegrator is designed to work seamlessly with DI containers (e.g., ASP.NET Core). Handlers and their dependencies are automatically registered. ```csharp [MessageHandler] public class MyHandler : MessageHandler { private readonly IMyService _myService; private readonly ILogger _logger; public MyHandler(IMyService myService, ILogger logger) { _myService = myService; _logger = logger; } public override async Task Execute(IHandlerContainer container, CancellationToken cancellation) { _logger.LogInformation("MyHandler executed!"); var result = _myService.DoSomething(); await Reply(result); return Ok; } } ``` > **How is it working?** > 1. **Constructor Injection**: Dependencies (`IMyService`, `ILogger`) are automatically injected by the DI container. > 2. **Service Registration**: When using `Telegrator.Hosting`, services are automatically registered with the DI container. > 3. **Handler Instantiation**: Telegrator creates handler instances through the DI container, resolving all dependencies. > 4. **Logging Integration**: Built-in logging support allows for comprehensive debugging and monitoring. ### 4.3. Custom State Keepers You can implement your own state keeper by inheriting from `StateKeeperBase`. This allows for advanced scenarios (e.g., per-message state, custom key resolution). ### 4.4. Automatic Handler Discovery Telegrator provides automatic discovery and registration of handlers across your entire application domain using the `CollectHandlersDomainWide()` method. **How it works:** - Scans all loaded assemblies in the current domain - Automatically discovers classes decorated with handler attributes - Registers them with the bot without manual registration - **Example:** ```csharp var bot = new TelegratorClient(""); bot.Handlers.CollectHandlersDomainWide(); // Automatically finds and registers all handlers bot.StartReceiving(); ``` **Benefits:** - No need to manually register each handler - Reduces boilerplate code - Ensures all handlers are discovered automatically - Perfect for large applications with many handlers > **How is it working?** > 1. **Domain Scanning**: `CollectHandlersDomainWide()` scans all assemblies loaded in the current AppDomain. > 2. **Reflection Discovery**: Uses reflection to find all classes decorated with handler attributes. > 3. **Automatic Registration**: Each discovered handler is automatically registered with the `HandlersCollection`. > 4. **Handler Types**: Supports all handler types: `MessageHandler`, `CommandHandler`, `CallbackQueryHandler`, etc. ### 4.5. Hosting Integration Telegrator provides seamless integration with .NET's generic host through the `Telegrator.Hosting` package, making it easy to build production-ready bot applications. **Installation:** ```shell dotnet add package Telegrator.Hosting ``` **Dependencies:** - `Microsoft.Extensions.Hosting` - .NET Generic Host - `Microsoft.Extensions.DependencyInjection` - Dependency Injection - `Microsoft.Extensions.Configuration` - Configuration management - `Microsoft.Extensions.Logging` - Logging infrastructure **Core Components:** - `TelegramBotHost` - The main hosted service that manages the bot lifecycle - `TelegramBotHostBuilder` - Builder pattern for configuring the bot host - `TelegramBotOptions` - Configuration options for the bot **Basic Example:** ```csharp var builder = TelegramBotHost.CreateBuilder(); // Configure services builder.Services.AddSingleton(); // Configure handlers builder.Handlers.CollectHandlersDomainWide(); // Building host var host = builder.Build(); await host.Run(); ``` > **How is it working?** > 1. **Generic Host Integration**: `TelegramBotHost` implements `IHost` and integrates with .NET's generic host. > 2. **Lifecycle Management**: The host manages the bot's startup, shutdown, and graceful termination. > 3. **Dependency Injection**: All handlers and services are automatically registered with the DI container. > 4. **Configuration**: Supports standard .NET configuration patterns (appsettings.json, environment variables, etc.). > 5. **Logging**: Integrates with .NET's logging infrastructure for comprehensive monitoring. > 6. **Health Checks**: Can be integrated with .NET's health check system for production monitoring. ### 4.6. Error Handling and Logging You can subscribe to error events or set a custom exception handler: ```csharp bot.UpdateRouter.ExceptionHandler = new DefaultRouterExceptionHandler((client, exception, source, cancellationToken) => { Console.WriteLine($"An error occurred: {exception.Message}"); return Task.CompletedTask; }); ``` > **How is it working?** > 1. **Exception Handler**: `ExceptionHandler` property allows you to set a custom exception handler for the entire bot. > 2. **Error Context**: The handler receives the bot client, exception, source information, and cancellation token. > 3. **Global Error Handling**: This provides a centralized way to handle all exceptions that occur during update processing. > 4. **Logging Integration**: Perfect place to log errors or send notifications to administrators. ### 4.7. Performance Optimization - Use appropriate concurrency limits for resource-intensive operations - Avoid thread-blocking operations in handlers - Use state management for multi-step processes - Use `AwaitingProvider` for complex conversation flows ### 4.8. Best Practices - Organize handlers, filters, and state keepers in separate folders - Use feature modules for large bots - Prefer declarative filters over manual `if` statements - Keep handlers focused and single-responsibility --- ## 5. FAQ & Best Practices ### 5.1. Q: My handler is not being triggered. What should I do? - Check handler registration (use `bot.Handlers.AddHandler()` or domain-wide collection) - Check filter attributes and update types - Enable debug logging ### 5.2. Q: How can I access the `ITelegramBotClient` or the original `Update` object inside a handler? - Use `Client`, `Update`, and `Input` properties in your handler ### 5.3. Q: How do I handle errors? - Set a custom exception handler or subscribe to error events ### 5.4. Q: How can I organize my code for a large bot? - Use folders, feature modules, and namespaces - Keep handlers focused and modular --- ## 6. Links - [API Reference](./TelegramReactive_Api.md) - [Main Repository](https://github.com/Rikitav/Telegrator) - [Wiki & Examples](https://github.com/Rikitav/Telegrator/wiki/) - [NuGet Package](https://www.nuget.org/packages/Telegrator) - [Issues & Discussions](https://github.com/Rikitav/Telegrator/issues) --- > **Feel free to contribute, ask questions, or open issues!** дыкий сишарп