# Getting Started with Telegrator --- This guide will walk you through the core concepts and features of **Telegrator**. - [1. Introduction](#1-introduction) - [2. Key Concepts](#2-key-concepts) - [3. Installation](#3-installation) - [4. Your First Bot: A "Hello, World!" Example](#4-your-first-bot-a-hello-world-example) - [5. Step-by-Step Tutorial](#5-step-by-step-tutorial) - [6. Advanced Topics](#6-advanced-topics) - [7. FAQ / Troubleshooting](#7-faq--troubleshooting) - [8. Links](#8-links) --- ## 1. Introduction Welcome to **Telegrator** — a modern, aspect-oriented, mediator-based framework for building powerful and maintainable Telegram bots in C#. - **Why Telegrator?** - Decentralized logic: no more monolithic state machines. - Flexible filtering and handler composition. - Advanced state management and concurrency control. - Inspired by AOP, but tailored for practical bot development. --- ## 2. Key Concepts At the core of **Telegrator** are a few fundamental concepts that enable its power and flexibility. Understanding them is key to building robust and scalable bots. ### Mediator: The Central Dispatcher The framework is built around the **Mediator pattern**. Every incoming update from Telegram (like a message, a button click, or a user joining a chat) is first received by a central `UpdateRouter`. This router acts as a mediator, responsible for dispatching the update to the appropriate handlers. This decouples the update receiving logic from the processing logic, making the system clean and maintainable. ### Handlers: The Logic Processors A **Handler** is a class that contains the logic for processing a specific type of update. For example, you might have a `WelcomeHandler` for new chat members, a `CommandHandler` for slash commands, or a `CallbackQueryHandler` for button clicks. Each handler is a small, focused, and reusable component. * **`MessageHandler`**: For processing text messages. * **`CallbackQueryHandler`**: For handling button clicks from `InlineKeyboardMarkup`. * **`AnyUpdateHandler`**: A catch-all handler for any type of update. ### Filters: The Gatekeepers **Filters** are attributes that you apply to your handler classes to specify *when* a handler should be executed. They act as gatekeepers, ensuring that a handler only runs if the incoming update meets certain criteria. - **`[Command("/start")]`**: Triggers the handler only for the `/start` command. - **`[MessageText(Contains = "hello")]`**: Triggers when a message contains the word "hello". - **`[MessageChat(Is = ChatType.Private)]`**: Triggers only for messages from private chats. - **`[RepliedMessage]`**: Triggers only when the message is a reply to another message. Filters can be combined with logical operators (`OrNext`, `Not`) to create complex and precise routing rules without writing messy `if/else` statements. ### State Management: The Memory **State Management** in Telegrator is a powerful feature for creating multi-step conversations (wizards, forms, quizzes) without a database. It works by using special **filter attributes** that make handlers execute only when a user or chat is in a specific state. The core idea is that you don't interact with a "StateKeeper" object directly. Instead: 1. You define a state machine using a C# `enum` or simple `int` values. 2. You use `[EnumState(YourEnum.SomeState)]` or `[NumericState(1)]` attributes to filter which handler should run at which step. 3. Inside a handler, you use extension methods on the `IAbstractHandlerContainer` (which I'll call `container` for simplicity) to change the user's state, e.g., `container.ForwardEnumState()`. #### Example: A Multi-Step Quiz Let's build a simple two-question quiz. **1. Define the Quiz States:** ```csharp // Enums/QuizState.cs public enum QuizState { // We use the SpecialState enum to represent the "no state" condition. // This is the default state for all users. Start = SpecialState.NoState, ExpectingAnswer1, ExpectingAnswer2 } ``` **2. Build the Handlers:** The `/quiz` command will start the process. It will only trigger for users who are not currently in a quiz (`QuizState.Start`). ```csharp // Handlers/QuizHandler.cs using Telegrator.Annotations.StateKeeping; [MessageHandler] [CommandAllias("quiz")] // This handler only runs if the user's state for QuizState is the default (NoState). [EnumState(QuizState.Start)] public class StartQuizHandler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { // Create the state for the user and move it to the first question. container.ForwardNumericState(); // If state isnt created, creates default state and formards its value. QuizState.Start -> QuizState.ExpectingAnswer1 await Reply("Quiz started! Question 1: What is the capital of France?"); } } ``` Now, create handlers for each expected answer. They will only trigger if the user is in the correct state. ```csharp // Handlers/QuizAnswerHandlers.cs [MessageHandler] // This handler only runs if the user's state is ExpectingAnswer1. [EnumState(QuizState.ExpectingAnswer1)] public class Answer1Handler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { if (Input.Text.Trim().Equals("Paris", StringComparison.InvariantCultureIgnoreCase)) { await Reply("Correct!"); } else { await Reply("Incorrect. The answer is Paris."); } // Move to the next state. container.ForwardEnumState(); // Moves state to ExpectingAnswer2 await Reply("Question 2: What is 2 + 2?"); } } [MessageHandler] // This handler only runs if the user's state is ExpectingAnswer2. [EnumState(QuizState.ExpectingAnswer2)] public class Answer2Handler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { if (Input.Text.Trim() == "4") { await Reply("Correct!"); } else { await Reply("Incorrect. The answer is 4."); } // The quiz is over, so we delete the user's state. container.DeleteEnumState(); await Reply("Quiz finished!"); } } ``` **How it Works:** - **`[EnumState(QuizState.Start)]`**: This is a **filter**. It checks the user's current state for the `QuizState` enum. `SpecialState.NoState` is a conventional way to say "the user has no state set yet". - **`container.CreateEnumState()`**: This initializes the state for the current user (or chat, depending on the key resolver) and sets it to the first actual value of the enum (`Start`). - **`container.ForwardEnumState()`**: This moves the user's state to the next value in the enum sequence. - **`container.DeleteEnumState()`**: This removes the state for the user, effectively resetting them to `SpecialState.NoState`. This declarative, attribute-based approach keeps your handler logic clean and focused on a single task, while the framework manages the complexity of the state machine. --- ## 3. 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 6.0 SDK or later. - A Telegram Bot Token from [@BotFather](https://t.me/BotFather). ### .NET CLI ```shell dotnet add package Telegrator ``` ### Package Manager Console ```shell Install-Package Telegrator ``` The framework also has integrations for different hosting models, which can be installed separately: - **`Telegrator.Hosting`**: For console applications and background services. - **`Telegrator.Hosting.Web`**: For ASP.NET Core applications and Webhook support. ```shell # For console apps dotnet add package Telegrator.Hosting # For ASP.NET Core apps dotnet add package Telegrator.Hosting.Web ``` --- ## 4. Your First Bot: A "Hello, World!" Example Let's create a simple bot that replies with "Hello, {FirstName}!" when a user sends the `/start` command. ### 1. Create the Handler First, create a new class that inherits from `MessageHandler`. This class will contain the logic for handling the message. ```csharp // Handlers/StartHandler.cs using System.Threading; using System.Threading.Tasks; using Telegram.Bot.Types; using Telegrator.Handlers; using Telegrator.Annotations; namespace Handlers; // This attribute registers the class as a message handler. [MessageHandler] // This filter ensures that the message's text equals to "Hello". [TextEquals("Hello")] public class StartHandler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { // Get the user's first name from the incoming message. var firstName = Input.From?.FirstName ?? "User"; // Reply to the user. await Reply($"Hello, {firstName}!", cancellationToken: cancellation); } } ``` ### 2. Set Up and Run the Bot Next, in your application's entry point (e.g., `Program.cs`), create an instance of `ReactiveClient`, add your handler, and start listening for updates. ```csharp // Program.cs using System; using System.Threading; using Telegrator; using Handlers; // Assuming your handler is in the "Handlers" namespace class Program { static void Main(string[] args) { // Replace "" with your actual bot token. var bot = new ReactiveClient(""); // Automatically discover and add all **public** handlers from the current assembly. bot.Handlers.CollectHandlersDomainWide(); // Or, add a specific handler manually: // bot.Handlers.AddHandler(); // Start receiving updates from Telegram. bot.StartReceiving(); Console.WriteLine("Bot started. Press any key to exit."); Console.ReadKey(); } } ``` ### What's Happening? 1. **`[MessageHandler]`**: This attribute marks `StartHandler` as a handler for `Message` updates. 2. **`[TextEquals("Hello")]`**: This is a **Filter**. It tells the `UpdateRouter` to only execute this handler if the message text is equals `Hello`. 3. **`ReactiveClient`**: This is the main bot client. It manages the connection to Telegram and the update processing pipeline. 4. **`bot.Handlers.CollectHandlersDomainWide()`**: This convenient extension method scans your project for all classes marked with handler attributes (like `[MessageHandler]`) and registers them automatically. 5. **`bot.StartReceiving()`**: This method starts the long-polling loop to fetch updates from Telegram and passes them to the `UpdateRouter`. 6. **`Reply(...)`**: This is a helper method from the base `MessageHandler` class that simplifies sending a reply to the original message. --- ## 5. Step-by-Step Tutorial This tutorial will guide you through building a bot that demonstrates common features like command handling, filtering, and waiting for user input. ### 5.1 The Problem: An Overly Eager Echo Bot Let's start with two simple handlers: one for the `/start` command and one to echo messages. ```csharp // StartHandler.cs [CommandHandler] [CommandAllias("start")] public class StartHandler : CommandHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { await Reply("Welcome! Send me any message and I will echo it back."); } } // EchoHandler.cs [MessageHandler] public class EchoHandler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { await Reply($"You said: {Input.Text}"); } } ``` If you run this bot and send `/start`, you'll get two replies! 1. "Welcome! Send me any message and I will echo it back." (from `StartHandler`) 2. "You said: /start" (from `EchoHandler`) This happens because `EchoHandler` has no filters, so it triggers for *every* message, including the `/start` command. ### 5.2 The Traditional Fix: `if` Statements The classic way to solve this is to add a check inside the `EchoHandler`. ```csharp // EchoHandler.cs (with an if-statement) [MessageHandler] public class EchoHandler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { // Manually ignore commands. if (Input.Text.StartsWith("/")) { return; } await Reply($"You said: {Input.Text}"); } } ``` This works, but it clutters our handler with boilerplate logic. As you add more commands and conditions, this approach becomes messy. ### 5.3 The Reactive Fix: Declarative Filters **Telegrator** lets you solve this cleanly using filter attributes. We can tell the `EchoHandler` to only trigger if the message is *not* a command. ```csharp // EchoHandler.cs (the reactive way) using Telegrator.Attributes; // For FilterModifier [MessageHandler] // This filter ensures the handler only runs for messages that are NOT the command. [TextStartsWith("/", Modifiers = FilterModifier.Not)] public class EchoHandler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { await Reply($"You said: {Input.Text}"); } } ``` Now, the `EchoHandler` will correctly ignore the any command without any `if` statements in the handler body. The routing logic is declared right where it belongs: on the class itself. ### 5.4 Waiting for Input with `AwaitingProvider` What if you want to ask a question and wait for the user's *next* message? You could use the state management system, but for a simple one-off question, that's overkill. The `AwaitingProvider` is perfect for this. Let's create a `/question` command that asks for the user's name and then greets them with the name they provide. ```csharp // QuestionHandler.cs [CommandHandler] [CommandAllias("question")] public class QuestionHandler : CommandHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { await Reply("What is your name?"); // Await the user's next message. // We apply a filter to ensure we only catch messages from the same user in the same chat. var nextMessage = await Container.AwaitMessage().BySenderId(cancellation); await Client.SendMessage(Input.Chat, $"Hello, {nextMessage.Text}!"); } } ``` **How it works:** 1. The handler first sends the question "What is your name?". 2. `AwaitMessage()` temporarily registers an internal handler that waits for the next `Message` sended by this user. This pattern is extremely powerful for creating dynamic, interactive conversations without the complexity of a full state machine. --- ## 6. Advanced Topics This section covers more advanced features of the framework. ### Handler Concurrency and Priority By default, handlers are executed 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)] // This will run before handlers with the default priority (0) public class HighPriorityHandler : MessageHandler { // ... } ``` ### Creating Custom Filters You can create your own filters by inheriting from `UpdateFilterAttribute`. This is useful for encapsulating complex or reusable filtering logic. Let's create a filter that only allows messages from a specific user ID. ```csharp // Filters/AdminOnlyFilter.cs using Telegram.Bot.Types; using Telegrator.Attributes; public class AdminOnlyAttribute : UpdateFilterAttribute { private readonly long _adminId; public AdminOnlyAttribute(long adminId) { _adminId = adminId; } public override Message? GetFilterringTarget(Update update) => update.Message; public override bool CanPass(FilterExecutionContext context) { return context.Input.From?.Id == _adminId; } } // Usage in a handler: [MessageHandler] [AdminOnly(123456789)] // Replace with your actual admin ID public class AdminCommandHandler : MessageHandler { // ... } ``` ### Integration with Dependency Injection **Telegrator** is designed to work seamlessly with dependency injection (DI) containers like the one built into ASP.NET Core. When using the `Telegrator.Hosting` or `Telegrator.Hosting.Web` packages, handlers and their dependencies are automatically registered with the DI container. This means you can inject any registered service directly into your handler's constructor. ```csharp [MessageHandler] public class MyHandler : MessageHandler { private readonly IMyService _myService; private readonly ILogger _logger; // Dependencies are injected automatically. public MyHandler(IMyService myService, ILogger logger) { _myService = myService; _logger = logger; } public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { _logger.LogInformation("MyHandler executed!"); var result = _myService.DoSomething(); await Reply(result); } } ``` ### Concurrency Control You can limit the number of concurrent executions for a specific handler by setting the `concurrency` parameter in the handler attribute. This is useful for preventing race conditions or for managing resource-intensive operations. ```csharp // This handler will only allow one execution at a time for a given chat. [MessageHandler(concurrency: 1)] public class SlowHandler : MessageHandler { public override async Task Execute(IAbstractHandlerContainer container, CancellationToken cancellation) { await Task.Delay(5000, cancellation); // Simulate a long-running operation await Reply("Done!"); } } ``` --- ## 7. FAQ / Troubleshooting ### Q: My handler is not being triggered. What should I do? - **Check Handler Registration**: Ensure you are calling `bot.Handlers.AddHandlers()` or `bot.Handlers.AddHandler()`. If you are using DI, make sure the assembly containing your handlers is being scanned. - **Check Filters**: Double-check your filter attributes. A common mistake is a typo in a command or text filter. Remember that filters are combined with a logical AND by default. If you have multiple filters, the update must pass all of them. - **Check Update Type**: Make sure your handler is for the correct update type. A `MessageHandler` will not be triggered by a `CallbackQuery` update. - **Enable Debug Logging**: You can subscribe to the `UpdateRouter.OnUpdate` and `UpdateRouter.OnHandlerEnter` events to see how updates are being processed in real-time. ### Q: How can I access the `ITelegramBotClient` or the original `Update` object inside a handler? The base `AbstractUpdateHandler` class provides access to these: - **`Client`**: The `ITelegramBotClient` instance. - **`Update`**: The raw `Update` object. - **`Input`**: The specific update payload (e.g., `Message`, `CallbackQuery`). ### Q: How do I handle errors? You can subscribe to the `UpdateRouter.OnError` event to receive notifications about exceptions that occur during update processing. This is a good place to log errors or send notifications to an administrator. ```csharp bot.UpdateRouter.OnError += (sender, args) => { Console.WriteLine($"An error occurred: {args.Exception.Message}"); return Task.CompletedTask; }; ``` ### Q: How can I organize my code for a large bot? - **Folders**: Organize your handlers, filters, and state keepers into separate folders (e.g., `Handlers/Commands`, `Handlers/Callbacks`, `StateKeepers`). - **Feature Modules**: For very large bots, consider structuring your code into "feature modules", where each module is a separate class library containing all the related handlers, filters, and services for a specific feature. --- ## 8. Links - [API Reference](./TelegramReactive_Api.md) - [Main Repository](https://github.com/Rikitav/Telegrator) - [Wiki & Examples](https://github.com/Rikitav/Telegrator/wiki/) --- > **Feel free to contribute, ask questions, or open issues!**