diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md deleted file mode 100644 index 4815ada..0000000 --- a/GETTING_STARTED.md +++ /dev/null @@ -1,526 +0,0 @@ -# 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!** \ No newline at end of file