Добавьте файлы проекта.

This commit is contained in:
2025-07-24 23:19:59 +04:00
commit 33d1f6218a
168 changed files with 15035 additions and 0 deletions
+526
View File
@@ -0,0 +1,526 @@
# 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<T>` (which I'll call `container` for simplicity) to change the user's state, e.g., `container.ForwardEnumState<YourEnum>()`.
#### 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>(QuizState.Start)]
public class StartQuizHandler : MessageHandler
{
public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
{
// Create the state for the user and move it to the first question.
container.ForwardNumericState<QuizState>(); // 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>(QuizState.ExpectingAnswer1)]
public class Answer1Handler : MessageHandler
{
public override async Task Execute(IAbstractHandlerContainer<Message> 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<QuizState>(); // 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>(QuizState.ExpectingAnswer2)]
public class Answer2Handler : MessageHandler
{
public override async Task Execute(IAbstractHandlerContainer<Message> 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<QuizState>();
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<QuizState>()`**: 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<QuizState>()`**: This moves the user's state to the next value in the enum sequence.
- **`container.DeleteEnumState<QuizState>()`**: 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<Message> 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 "<YOUR_BOT_TOKEN>" with your actual bot token.
var bot = new ReactiveClient("<YOUR_BOT_TOKEN>");
// Automatically discover and add all **public** handlers from the current assembly.
bot.Handlers.CollectHandlersDomainWide();
// Or, add a specific handler manually:
// bot.Handlers.AddHandler<StartHandler>();
// 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<Message> 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<Message> 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<Message> 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<Message> 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<Message> 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<T>`. 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<Message>
{
private readonly long _adminId;
public AdminOnlyAttribute(long adminId)
{
_adminId = adminId;
}
public override Message? GetFilterringTarget(Update update) => update.Message;
public override bool CanPass(FilterExecutionContext<Message> 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<MyHandler> _logger;
// Dependencies are injected automatically.
public MyHandler(IMyService myService, ILogger<MyHandler> logger)
{
_myService = myService;
_logger = logger;
}
public override async Task Execute(IAbstractHandlerContainer<Message> 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<Message> 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<MyHandler>()`. 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<T>` 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!**