Files
Telegrator/docs/GETTING_STARTED.md
T
2025-09-15 21:19:53 +04:00

27 KiB

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

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.0 compatible)
  • A Telegram Bot Token from @BotFather.

.NET CLI

dotnet add package Telegrator

Package Manager Console

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":

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(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("Hello! Nice to meet you!", cancellationToken: cancellation);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var bot = new TelegratorClient("<YOUR_BOT_TOKEN>");
        bot.Handlers.AddHandler<HelloHandler>();
        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<HelloHandler>(): 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:

  • [CommandAllias("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:

[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>(MyEnum.Step1)]
    • [NumericState(1)]
  3. Change state inside the handler using extension methods:
    • container.ForwardEnumState<MyEnum>()
    • container.ForwardNumericState()
    • container.DeleteEnumState<MyEnum>()

Example:

public enum QuizState
{
    Start = SpecialState.NoState, Q1, Q2
}

[CommandHandler]
[CommandAllias("quiz")]
[EnumState<QuizState>(QuizState.Start)]
public class StartQuizHandler : CommandHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        container.ForwardEnumState<QuizState>();
        await Reply("Quiz started! Question 1: What is the capital of France?");
    }
}

[MessageHandler]
[EnumState<QuizState>(QuizState.Q1)]
public class Q1Handler : 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.");

        container.ForwardEnumState<QuizState>();
        await Reply("Question 2: What is 2 + 2?");
    }
}

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>(QuizState.Start)] ensures the handler only runs when the user is in the "Start" state.
  3. State Transition: container.ForwardEnumState<QuizState>() 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:
[CommandHandler]
[CommandAllias("ask")]
public class AskHandler : CommandHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("What is your name?");
        var nextMessage = await container.AwaitMessage().BySenderId(cancellation);
        await Reply($"Hello, {nextMessage.Text}!");
    }
}

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:

using Telegram.Bot.Types;
using Telgrator.Attributes;
using Telegrator.Handlers;

public class AdminOnlyAttribute() : FilterAnnotation<Message>
{ 
    private readonly List<long> _adminIds = [];

    public void AddAdmin(long id) => _adminIds.Add(id);
    public void RemoveAdmin(long id) => _adminIds.Remove(id);
    
    public override bool CanPass(FilterExecutionContext<Message> context)
        => _adminIds.Contains(context.Input.From?.Id);
}

[MessageHandler]
[AdminOnly]
public class AdminHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("Hello, admin!");
    }
}

// ...
AdminOnlyAttribute.AddAdmin(123456789);
bot.StartReceiving();

How is it working?

  1. Custom Filter: AdminOnlyAttribute inherits from FilterAnnotation<Message> to create a reusable filter attribute.
  2. Filter Logic: CanPass() method checks if the message sender's ID matches the admin ID.
  3. 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:

var bot = new TelegratorClient("<YOUR_BOT_TOKEN>");
bot.Handlers.CollectHandlersDomainWide();
bot.StartReceiving();

Hosting Example:

using Telegrator.Hosting;

var builder = TelegramBotHost.CreateBuilder();
builder.Handlers.AddHandler<StartHandler>();
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.

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.

[CommandHandler]
[CommandAllias("start")]
public class StartHandler : CommandHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("Welcome! Use /help to see available commands.");
    }
}

[CommandHandler]
[CommandAllias("help")]
public class HelpHandler : CommandHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("Available commands:\n/start - Start the bot\n/help - Show this help");
    }
}

[MessageHandler]
[TextStartsWith("/", Modifiers = FilterModifier.Not)]
public class EchoHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply($"You said: \"{Input.Text}\"");
    }
}

How is it working?

  1. Command Handlers: [CommandHandler] and [CommandAllias] 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

[CommandHandler]
[CommandAllias("wizard")]
[NumericState(SpecialState.NoState)]
public class StartWizardHandler : CommandHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> 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?");
    }
}

[MessageHandler]
[NumericState(1)]
public class NameHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        container.ForwardNumericState();
        await Reply($"Nice to meet you, {Input.Text}! How old are you?");
    }
}

[MessageHandler]
[NumericState(2)]
public class AgeHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> 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).");
        }
    }
}

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

[CommandHandler]
[CommandAllias("menu")]
public class MenuHandler : CommandHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> 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);
    }
}

[CallbackQueryHandler]
[CallbackData("option1")]
public class Option1Handler : CallbackQueryHandler
{
    public override async Task Execute(IAbstractHandlerContainer<CallbackQuery> container, CancellationToken cancellation)
    {
        await AnswerCallbackQuery("You selected Option 1!", cancellationToken: cancellation);
        await EditMessageText("You selected Option 1!");
    }
}

[CallbackQueryHandler]
[CallbackData("option2")]
public class Option2Handler : CallbackQueryHandler
{
    public override async Task Execute(IAbstractHandlerContainer<CallbackQuery> container, CancellationToken cancellation)
    {
        await AnswerCallbackQuery("You selected Option 2!", cancellationToken: cancellation);
        await EditMessageText("You selected Option 2!");
    }
}

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

public class PremiumUserAttribute : UpdateFilterAttribute<Message>
{
    public override Message? GetFilterringTarget(Update update) => update.Message;
    public override bool CanPass(FilterExecutionContext<Message> context) => context.Input.From?.IsPremium == true;
}

[MessageHandler]
[PremiumUser]
public class PremiumFeatureHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("This feature is only available for premium users!");
    }
}

How is it working?

  1. Custom Filter: PremiumUserAttribute inherits from UpdateFilterAttribute<Message> 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.

[MessageHandler(Priority = 1)] // Runs before default priority (0)
public class HighPriorityHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("This handler runs first!");
    }
}

[MessageHandler(Priority = 0)] // Default priority
public class NormalPriorityHandler : MessageHandler
{
    public override async Task Execute(IAbstractHandlerContainer<Message> container, CancellationToken cancellation)
    {
        await Reply("This handler runs second!");
    }
}

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.

[MessageHandler]
public class MyHandler : MessageHandler
{
    private readonly IMyService _myService;
    private readonly ILogger<MyHandler> _logger;
    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);
    }
}

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<TKey, TState>. 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:

var bot = new TelegratorClient("<YOUR_BOT_TOKEN>");
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:

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:

var builder = TelegramBotHost.CreateBuilder();

// Configure services
builder.Services.AddSingleton<IMyService, MyService>();

// 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:

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<MyHandler>() 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


Feel free to contribute, ask questions, or open issues!