Skip to content

TimurbekDev/GrammyNet.Core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GrammyNet.Core

A .NET library for building Telegram bots with built-in state management, command routing, and middleware support. Designed for use in ASP.NET Core and other hosted applications.

Features

  • State Management: User and conversation-scoped state with pluggable storage
  • Command Routing: Attribute-based command handlers
  • Conversation Flows: Multi-step conversation support with step tracking
  • Middleware Pipeline: Extensible middleware for logging, authentication, etc.
  • Keyboard Builders: Fluent API for inline and reply keyboards
  • Dependency Injection: Full integration with Microsoft.Extensions.DependencyInjection

Installation

Add the package reference to your project:

<PackageReference Include="GrammyNet.Core" Version="1.0.0" />

Quick Start

Option A: Stateless (No State Management)

If you don't need state management but want attribute-based routing:

using GrammyNet.Core.Commands;
using GrammyNet.Core.Handlers;
using GrammyNet.Core.Keyboard;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;

public class MyBotHandler : BaseStatelessUpdateHandler
{
    public MyBotHandler(ILogger<MyBotHandler> logger) : base(logger) { }

    [Command("start", Description = "Start the bot")]
    public async Task HandleStart(ITelegramBotClient bot, Message message, CancellationToken ct)
    {
        var keyboard = InlineKeyboardBuilder.Create()
            .Button("Help", "help")
            .Button("About", "about")
            .Build();

        await bot.SendMessage(
            message.Chat.Id,
            "Welcome! Choose an option:",
            replyMarkup: keyboard,
            cancellationToken: ct);
    }

    [Command("help", Description = "Show help")]
    public async Task HandleHelp(ITelegramBotClient bot, Message message, CancellationToken ct)
    {
        await bot.SendMessage(message.Chat.Id, "Available commands:\n/start\n/help", cancellationToken: ct);
    }

    [CallbackQuery("help")]
    public async Task HandleHelpCallback(ITelegramBotClient bot, Update update, CancellationToken ct)
    {
        await bot.AnswerCallbackQuery(update.CallbackQuery!.Id);
        await bot.SendMessage(update.CallbackQuery.Message!.Chat.Id, "Help info here!", cancellationToken: ct);
    }

    [CallbackQuery("about*")]
    public async Task HandleAboutCallback(ITelegramBotClient bot, Update update, CancellationToken ct)
    {
        // Matches: about, about_v1, about_details, etc.
        await bot.AnswerCallbackQuery(update.CallbackQuery!.Id, "GrammyNet Bot v1.0");
    }

    [TextMessage]
    public async Task HandleText(ITelegramBotClient bot, Message message, CancellationToken ct)
    {
        await bot.SendMessage(message.Chat.Id, $"You said: {message.Text}", cancellationToken: ct);
    }
}

Configure services (Program.cs):

using GrammyNet.Core.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Stateless handler (inherits BaseStatelessUpdateHandler)
builder.Services.AddGrammyNet<MyBotHandler>(builder.Configuration);

var app = builder.Build();
app.Run();

Option B: With State Management

1. Define Your State Classes

using GrammyNet.Core.State;

// User-scoped state (persists per user across all chats)
public class UserState
{
    public string? Name { get; set; }
    public string? Language { get; set; }
    public int MessageCount { get; set; }
}

// Conversation-scoped state (persists per chat)
public class ChatState : ConversationFlowState
{
    public DateTime? LastActivity { get; set; }
    public List<string> RecentTopics { get; set; } = new();
}

2. Create Your Update Handler

using GrammyNet.Core.Commands;
using GrammyNet.Core.Handlers;
using GrammyNet.Core.State;
using GrammyNet.Core.Keyboard;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;

public class MyBotHandler : BaseUpdateHandler<UserState, ChatState>
{
    public MyBotHandler(ILogger<MyBotHandler> logger) : base(logger) { }

    [Command("start", Description = "Start the bot")]
    public async Task HandleStart(
        ITelegramBotClient bot,
        Message message,
        StateContext<UserState, ChatState> state,
        CancellationToken ct)
    {
        var userState = await state.GetUserStateAsync(ct);
        userState.MessageCount++;

        var keyboard = InlineKeyboardBuilder.Create()
            .Button("Get Help", "help")
            .Button("Settings", "settings")
            .Row()
            .Button("About", "about")
            .Build();

        await bot.SendMessage(
            message.Chat.Id,
            $"Welcome! You've sent {userState.MessageCount} messages.",
            replyMarkup: keyboard,
            cancellationToken: ct);
    }

    [Command("register", Description = "Start registration")]
    public async Task HandleRegister(
        ITelegramBotClient bot,
        Message message,
        StateContext<UserState, ChatState> state,
        CancellationToken ct)
    {
        var chatState = await state.GetConversationStateAsync(ct);
        chatState.StartFlow("register_name");

        await bot.SendMessage(
            message.Chat.Id,
            "Let's get you registered! What's your name?",
            cancellationToken: ct);
    }

    [ConversationStep("register_name")]
    public async Task HandleRegisterName(
        ITelegramBotClient bot,
        Message message,
        StateContext<UserState, ChatState> state,
        CancellationToken ct)
    {
        var userState = await state.GetUserStateAsync(ct);
        var chatState = await state.GetConversationStateAsync(ct);

        userState.Name = message.Text;
        chatState.GoToStep("register_language");

        var keyboard = ReplyKeyboardBuilder.Create()
            .Button("English").Button("Spanish")
            .Row()
            .Button("French").Button("German")
            .OneTime()
            .Build();

        await bot.SendMessage(
            message.Chat.Id,
            $"Nice to meet you, {userState.Name}! What language do you prefer?",
            replyMarkup: keyboard,
            cancellationToken: ct);
    }

    [ConversationStep("register_language")]
    public async Task HandleRegisterLanguage(
        ITelegramBotClient bot,
        Message message,
        StateContext<UserState, ChatState> state,
        CancellationToken ct)
    {
        var userState = await state.GetUserStateAsync(ct);
        var chatState = await state.GetConversationStateAsync(ct);

        userState.Language = message.Text;
        chatState.EndFlow();

        await bot.SendMessage(
            message.Chat.Id,
            $"Registration complete!\nName: {userState.Name}\nLanguage: {userState.Language}",
            replyMarkup: new Telegram.Bot.Types.ReplyMarkups.ReplyKeyboardRemove(),
            cancellationToken: ct);
    }

    [CallbackQuery("help")]
    public async Task HandleHelpCallback(
        ITelegramBotClient bot,
        Update update,
        CancellationToken ct)
    {
        await bot.AnswerCallbackQuery(update.CallbackQuery!.Id, "Help is on the way!");
        await bot.SendMessage(
            update.CallbackQuery.Message!.Chat.Id,
            "Here's how to use this bot:\n/start - Start\n/register - Register",
            cancellationToken: ct);
    }

    [CallbackQuery("settings*")]
    public async Task HandleSettingsCallback(
        ITelegramBotClient bot,
        Update update,
        CancellationToken ct)
    {
        // Matches: settings, settings_theme, settings_notifications, etc.
        await bot.AnswerCallbackQuery(update.CallbackQuery!.Id);
        await bot.SendMessage(
            update.CallbackQuery.Message!.Chat.Id,
            "Settings menu coming soon!",
            cancellationToken: ct);
    }
}

3. Configure Services

// Program.cs or Startup.cs
using GrammyNet.Core.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Stateful handler (inherits BaseUpdateHandler<TUserState, TConversationState>)
builder.Services.AddGrammyNet<UserState, ChatState, MyBotHandler>(builder.Configuration);

var app = builder.Build();
app.Run();

4. Add Configuration

// appsettings.json
{
  "GrammyNet": {
    "BotToken": "YOUR_BOT_TOKEN_HERE"
  }
}

State Management

User State vs Conversation State

  • User State: Scoped to a Telegram user ID. Persists across all chats.
  • Conversation State: Scoped to a chat ID. Useful for group-specific data.

Accessing State

public async Task SomeHandler(StateContext<UserState, ChatState> state, CancellationToken ct)
{
    // Get states (lazy loaded)
    var userState = await state.GetUserStateAsync(ct);
    var chatState = await state.GetConversationStateAsync(ct);

    // Modify state
    userState.MessageCount++;

    // State is automatically saved after handler completes
    // Or save manually:
    await state.SaveUserStateAsync(ct);

    // Clear state
    await state.ClearUserStateAsync(ct);
}

Conversation Flows

Use ConversationFlowState for multi-step conversations:

public class ChatState : ConversationFlowState
{
    // Your additional properties
}

// Start a flow
chatState.StartFlow("step1");

// Move to next step
chatState.GoToStep("step2");

// Store temporary data
chatState.SetData("email", "user@example.com");
var email = chatState.GetData<string>("email");

// End the flow
chatState.EndFlow();

// Check if flow is active
if (chatState.IsFlowActive) { }

Command Routing

Available Attributes

// Command handler (e.g., /start, /help)
[Command("start", Description = "Start the bot")]

// Callback query handler
[CallbackQuery("action")]           // Exact match
[CallbackQuery("prefix*")]          // Prefix match
[CallbackQuery("*")]                // Match all

// Text message handler
[TextMessage]                       // All text messages
[TextMessage(Pattern = @"\d+")]     // Regex pattern

// Conversation step handler
[ConversationStep("step_name")]

Handler Parameters

Handlers can accept any combination of these parameters (order doesn't matter):

  • ITelegramBotClient - The bot client
  • Update - The full update object
  • Message - The message (for message updates)
  • CallbackQuery - The callback query (for callback updates)
  • StateContext<TUserState, TConversationState> - State context
  • CancellationToken - Cancellation token

Keyboard Builders

Inline Keyboard

var keyboard = InlineKeyboardBuilder.Create()
    .Button("Option 1", "callback_1")
    .Button("Option 2", "callback_2")
    .Row()
    .UrlButton("Visit Website", "https://example.com")
    .Build();

Reply Keyboard

var keyboard = ReplyKeyboardBuilder.Create()
    .Button("Yes").Button("No")
    .Row()
    .ContactButton("Share Contact")
    .LocationButton("Share Location")
    .OneTime()
    .Resize()
    .Placeholder("Choose an option...")
    .Build();

Custom State Storage

Implement IStateStorage<T> for custom storage (Redis, database, etc.):

public class RedisStateStorage<TState> : IStateStorage<TState> 
    where TState : class, new()
{
    public Task<TState> GetStateAsync(string key, CancellationToken ct) { }
    public Task SetStateAsync(string key, TState state, CancellationToken ct) { }
    public Task DeleteStateAsync(string key, CancellationToken ct) { }
    public Task<bool> ExistsAsync(string key, CancellationToken ct) { }
}

// Register with custom storage
services.AddGrammyNetWithCustomStorage<
    UserState, 
    ChatState, 
    MyBotHandler,
    RedisStateStorage<UserState>,
    RedisStateStorage<ChatState>>(configuration);

Middleware

Create custom middleware for cross-cutting concerns:

public class AuthMiddleware<TUserState, TConversationState> 
    : IMiddleware<TUserState, TConversationState>
    where TUserState : class, new()
    where TConversationState : class, new()
{
    public async Task InvokeAsync(
        ITelegramBotClient botClient,
        Update update,
        StateContext<TUserState, TConversationState> stateContext,
        UpdateDelegate next,
        CancellationToken cancellationToken)
    {
        // Pre-processing
        if (!IsAuthorized(stateContext.UserId))
        {
            await botClient.SendMessage(stateContext.ChatId, "Unauthorized");
            return; // Don't call next()
        }

        // Call next middleware
        await next();

        // Post-processing
    }
}

Project Structure

GrammyNet.Core/
├── Commands/
│   ├── CommandAttribute.cs      # Handler attributes
│   └── CommandRouter.cs         # Attribute-based routing
├── Extensions/
│   ├── ServiceCollectionExtension.cs
│   └── TelegramBotClientExtensions.cs
├── Handlers/
│   ├── BaseUpdateHandler.cs     # Base class for handlers
│   ├── IUpdateHandler.cs        # Stateful handler interface
│   └── StatefulUpdateHandlerAdapter.cs
├── Keyboard/
│   └── InlineKeyboardBuilder.cs # Keyboard builders
├── Middleware/
│   ├── IMiddleware.cs
│   ├── LoggingMiddleware.cs
│   └── MiddlewarePipeline.cs
├── Services/
│   └── StatefulBotBackgroundService.cs
├── State/
│   ├── ConversationStep.cs      # Flow state base
│   ├── InMemoryStateStorage.cs  # Default storage
│   ├── IStateManager.cs
│   ├── IStateStorage.cs
│   └── StateContext.cs
└── README.md

License

MIT License

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages