Kippo 1.0.4
dotnet add package Kippo --version 1.0.4
NuGet\Install-Package Kippo -Version 1.0.4
<PackageReference Include="Kippo" Version="1.0.4" />
<PackageVersion Include="Kippo" Version="1.0.4" />
<PackageReference Include="Kippo" />
paket add Kippo --version 1.0.4
#r "nuget: Kippo, 1.0.4"
#:package Kippo@1.0.4
#addin nuget:?package=Kippo&version=1.0.4
#tool nuget:?package=Kippo&version=1.0.4
π€ Kippo
Build Telegram Bots with Elegance
A lightweight, attribute-based framework for creating powerful Telegram bots in .NET
with session management, middleware support, and intuitive routing.
Why Kippo? β’ What's New β’ Installation β’ Get Started β’ Documentation
β¨ Why Kippo?
π― Simple & Intuitive
Write bot handlers with clean attributes. No complex routing configuration or boilerplate code.
πΎ Smart Sessions
Built-in session management tracks user state and data automatically across conversations.
π Extensible
Add custom middleware for logging, auth, rate limiting, or any behavior you need.
β¨οΈ Beautiful Keyboards
Fluent API for creating reply and inline keyboards with minimal code.
π Production Ready
Seamless ASP.NET Core integration with dependency injection and hosting support.
π¦ Get Started Fast
Install via NuGet and have your bot running in under 5 minutes.
π What's New in 1.0.4
π Production-Ready Improvements
Thread-Safety
- β
Session storage now uses
ConcurrentDictionaryfor safe concurrent access - β Session data dictionary is thread-safe by default
- β No more race conditions under high load
Dependency Injection
- β Automatic service injection in handler methods
- β Full support for scoped services (DbContext, EF Core, etc.)
- β Service scope created per request automatically
Error Handling & Logging
- β
Integrated
ILoggersupport throughout the framework - β Detailed error messages with context for debugging
- β Automatic error logging with stack traces
- β Duplicate command registration warnings
Performance
- β
Optimized network usage with
AllowedUpdatesconfiguration - β 85% reduction in unnecessary update types
- β Extended update type support (EditedMessage, MyChatMember, etc.)
Developer Experience
- β Clear exception messages instead of NullReferenceException
- β Better null-safety for Message.Text and CallbackQuery.Data
- β Service resolution errors with helpful guidance
π New: Service Injection Example
public class MyHandler : BotUpdateHandler
{
// Inject services directly into handler methods!
[Command("users")]
public async Task GetUsers(Context context, IUserService userService)
{
var users = await userService.GetAllUsersAsync();
await context.Reply($"Total users: {users.Count}");
}
// Works with scoped services too (DbContext, etc.)
[Command("save")]
public async Task SaveData(Context context, AppDbContext db)
{
var user = new User { Name = "John" };
db.Users.Add(user);
await db.SaveChangesAsync();
await context.Reply("β
Saved!");
}
}
π§ Breaking Changes
Minor: ISessionStore interface now requires DeleteAsync method:
public interface ISessionStore
{
Task<Session> GetAsync(long chatId);
Task SaveAsync(long chatId, Session session);
Task<bool> DeleteAsync(long chatId); // New in 1.0.4
}
Migration: If you have a custom session store, simply add:
public Task<bool> DeleteAsync(long chatId)
{
// Your implementation
return Task.FromResult(true);
}
π¦ Installation
Via .NET CLI
dotnet add package Kippo
Via Package Manager Console
Install-Package Kippo
Via PackageReference
<PackageReference Include="Kippo" Version="1.0.4" />
π Getting Started
Step 1: Get Your Bot Token
- Open Telegram and search for @BotFather
- Send
/newbotand follow the instructions - Copy your bot token (looks like
123456789:ABCdefGHIjklMNOpqrsTUVwxyz)
Step 2: Create a New ASP.NET Core Project
dotnet new web -n MyTelegramBot
cd MyTelegramBot
dotnet add package Kippo
Step 3: Configure Your Bot Token
Add to appsettings.json:
{
"Kippo": {
"BotToken": "YOUR_BOT_TOKEN_HERE"
},
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
Step 4: Create Your Bot Handler
Create a new file MyBotHandler.cs:
using Kippo.Attribute;
using Kippo.Contexs;
using Kippo.Handlers;
using Kippo.Keyboard;
using Kippo.Middleware;
using Kippo.SessionStorage;
public class MyBotHandler : BotUpdateHandler
{
[Command("start")]
public async Task Start(Context context)
{
var keyboard = ReplyKeyboardBuilder.Create()
.Button("π Say Hello")
.Button("β Help")
.Resize()
.Build();
await context.Reply(
"π€ Welcome! I'm your new bot.\n\n" +
"Choose an option below:",
keyboard
);
}
[Text(Pattern = "π Say Hello")]
public async Task SayHello(Context context)
{
var username = context.Update.Message?.From?.FirstName ?? "friend";
await context.Reply($"Hello, {username}! π");
}
[Command("help")]
[Text(Pattern = "β Help")]
public async Task Help(Context context)
{
await context.Reply(
"π *Available Commands*\n\n" +
"/start - Start the bot\n" +
"/help - Show this message\n" +
"/menu - Show inline menu"
);
}
[Command("menu")]
public async Task ShowMenu(Context context)
{
var keyboard = InlineKeyboardBuilder.Create()
.Button("β
Option 1", "opt_1")
.Button("β
Option 2", "opt_2")
.Row()
.UrlButton("π GitHub", "https://github.com")
.Build();
await context.Reply(
"Choose an option:",
keyboard
);
}
[CallbackQuery("opt_*")]
public async Task HandleOption(Context context)
{
var option = context.Update.CallbackQuery!.Data!.Replace("opt_", "");
// Answer callback to remove loading state
await context.Callback.Answer($"You selected Option {option}!");
await context.Reply($"β
You chose Option {option}");
}
[Text]
public async Task HandleText(Context context)
{
await context.Reply($"You said: _{context.Message.Text}_");
}
}
Step 5: Register Kippo in Program.cs
Replace the contents of Program.cs:
using Kippo.Extensions;
using Kippo.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Register Kippo with your bot handler
builder.Services.AddKippo<MyHandler>(builder.Configuration)
.AddKippoMiddleware<SessionMiddleware>();//For Session Management
var app = builder.Build();
app.Run();
Step 6: Run Your Bot
dotnet run
π Your bot is now live! Open Telegram and send /start to your bot.
π Documentation
π·οΈ Routing with Attributes
Kippo uses attributes to route updates to your handler methods:
Commands
Handle bot commands (messages starting with /):
[Command("start")]
public async Task Start(Context context)
{
await context.Reply("Welcome! π");
}
// With description
[Command("settings", Description = "Bot settings")]
public async Task Settings(Context context)
{
await context.Reply("βοΈ Settings");
}
Text Messages
Handle text messages with pattern matching:
// Handle all text messages
[Text]
public async Task HandleAnyText(Context context)
{
await context.Reply($"You said: {context.Message.Text}");
}
// Match specific text
[Text(Pattern = "Hello")]
public async Task SayHello(Context context)
{
await context.Reply("Hi there! π");
}
// Match text containing substring
[Text(Contains = "help")]
public async Task ShowHelp(Context context)
{
await context.Reply("Need help? Ask me anything!");
}
// Match with regex
[Text(Regex = @"^\d+$")]
public async Task HandleNumbers(Context context)
{
await context.Reply("That's a number!");
}
Callback Queries
Handle inline keyboard button clicks:
// Exact match
[CallbackQuery("confirm")]
public async Task HandleConfirm(Context context)
{
await context.Callback.Answer("Confirmed!");
await context.Reply("β
Action confirmed");
}
// Prefix match (use wildcard *)
[CallbackQuery("page_*")]
public async Task HandlePage(Context context)
{
var page = context.Callback.Data.Replace("page_", "");
await context.Callback.Answer();
await context.Reply($"Showing page {page}");
}
// Match any callback
[CallbackQuery("*")]
public async Task HandleAnyCallback(Context context)
{
await context.Callback.Answer();
}
Multiple Attributes
Handlers can respond to multiple triggers:
[Command("cancel")]
[Text(Pattern = "Cancel")]
[Text(Pattern = "β Cancel")]
public async Task Cancel(Context context)
{
await context.Reply("β Operation cancelled");
}
πΎ Session Management
Track user state and data across conversations:
Basic Session Usage
[Command("register")]
public async Task StartRegistration(Context context)
{
// Set conversation state
context.Session!.State = "awaiting_name";
// Store data
context.Session.Data["started_at"] = DateTime.Now;
await context.Reply("What's your name?");
}
[Text(State = "awaiting_name")]
public async Task HandleName(Context context)
{
var name = context.Message.Text;
// Save to session
context.Session!.Data["name"] = name;
context.Session.State = "awaiting_age";
await context.Reply($"Nice to meet you, {name}! How old are you?");
}
[Text(State = "awaiting_age")]
public async Task HandleAge(Context context)
{
if (int.TryParse(context.Message.Text, out var age))
{
context.Session!.Data["age"] = age;
context.Session.State = null; // Clear state
var name = context.Session.Data["name"];
await context.Reply($"β
Registration complete!\nName: {name}, Age: {age}");
}
else
{
await context.Reply("β Please enter a valid number");
}
}
Session Properties
// State - track conversation flow
context.Session.State = "awaiting_input";
// Data - store any serializable data
context.Session.Data["key"] = value;
context.Session.Data["user_id"] = 12345;
context.Session.Data["preferences"] = new { theme = "dark", lang = "en" };
// Retrieve data
var name = context.Session.Data["name"];
var age = (int)context.Session.Data["age"];
// Check if key exists
if (context.Session.Data.ContainsKey("name"))
{
// ...
}
// Clear session
context.Session.State = null;
context.Session.Data.Clear();
β¨οΈ Building Keyboards
Create interactive keyboards with fluent API:
Reply Keyboards
Keyboards that appear at the bottom of the chat:
var keyboard = ReplyKeyboardBuilder.Create()
.Button("Option 1")
.Button("Option 2")
.Row() // Start new row
.Button("Option 3")
.Button("Option 4")
.Resize() // Auto-resize to fit
.OneTime() // Hide after button press
.Build();
await context.Reply("Choose an option:", keyboard);
// Remove keyboard
await context.Reply("Keyboard removed", new ReplyKeyboardRemove());
Inline Keyboards
Keyboards attached to messages with callback buttons:
var keyboard = InlineKeyboardBuilder.Create()
.Button("β
Yes", "answer_yes")
.Button("β No", "answer_no")
.Row()
.Button("π View Stats", "stats")
.Row()
.UrlButton("π Visit Website", "https://example.com")
.Build();
await context.Reply("Do you agree?", keyboard);
Advanced Inline Keyboard:
var keyboard = InlineKeyboardBuilder.Create()
// Callback buttons
.Button("π Home", "home")
.Button("βοΈ Settings", "settings")
.Row()
// URL buttons
.UrlButton("π Docs", "https://docs.example.com")
.Row()
// Pagination
.Button("β¬
οΈ", "page_prev")
.Button("1 / 10", "page_info")
.Button("β‘οΈ", "page_next")
.Build();
await context.Reply("Main Menu", keyboard);
οΏ½ Dependency Injection
New in 1.0.4: Inject services directly into your handler methods!
Method Parameter Injection
The framework automatically resolves services from the DI container:
public class MyHandler : BotUpdateHandler
{
[Command("profile")]
public async Task ShowProfile(Context context, IUserService userService)
{
// userService is automatically injected
var user = await userService.GetUserAsync(context.ChatId);
await context.Reply($"π€ {user.Name}");
}
[Command("stats")]
public async Task ShowStats(
Context context,
IUserService userService,
IAnalyticsService analytics)
{
// Multiple services can be injected
var userCount = await userService.GetCountAsync();
var stats = await analytics.GetStatsAsync();
await context.Reply($"π Users: {userCount}\nViews: {stats.Views}");
}
}
Scoped Services Support
Works seamlessly with scoped services like Entity Framework DbContext:
// Register scoped service in Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddScoped<IUserService, UserService>();
// Use in handlers
public class MyHandler : BotUpdateHandler
{
[Command("save")]
public async Task SaveUser(Context context, AppDbContext db)
{
// New scope created automatically per request
var user = new User
{
TelegramId = context.ChatId,
Name = context.Update.Message?.From?.FirstName
};
db.Users.Add(user);
await db.SaveChangesAsync();
await context.Reply("β
User saved to database!");
}
}
Service Lifetimes
- β Singleton - Shared across all requests
- β Scoped - New instance per update (recommended for DbContext)
- β Transient - New instance every time
// Program.cs
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddTransient<IEmailService, EmailService>();
Constructor Injection (Alternative)
You can also use IServiceScopeFactory in the constructor:
public class MyHandler : BotUpdateHandler
{
private readonly IServiceScopeFactory _scopeFactory;
public MyHandler(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
[Command("data")]
public async Task GetData(Context context)
{
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<IDataService>();
var data = await service.GetDataAsync();
await context.Reply($"Data: {data}");
}
}
Recommendation: Use method parameter injection for cleaner code!
οΏ½π Middleware
Extend Kippo with custom middleware that executes before handlers:
Built-in Middleware
SessionMiddleware- Automatic session loading/saving (recommended)
Creating Custom Middleware
using Kippo.Contexs;
using Kippo.Middleware;
public class LoggingMiddleware : IBotMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(Context context, Func<Task> next)
{
var userId = context.Update.Message?.From?.Id ??
context.Update.CallbackQuery?.From?.Id;
_logger.LogInformation("π¨ Update from user {UserId}", userId);
await next(); // Continue to next middleware/handler
_logger.LogInformation("β
Update processed");
}
}
Registering Middleware
In Program.cs:
using Kippo.Extensions;
using Kippo.Middleware;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKippo<MyBotHandler>(builder.Configuration)
.AddKippoMiddleware<LogginMiddleware>()
.AddKippoMiddleware<SessionMiddleware>();
var app = builder.Build();
app.Run();
Example: Authentication Middleware
public class AuthMiddleware : IBotMiddleware
{
private readonly HashSet<long> _allowedUsers = new() { 123456789, 987654321 };
public async Task InvokeAsync(Context context, Func<Task> next)
{
var userId = context.Update.Message?.From?.Id ??
context.Update.CallbackQuery?.From?.Id;
if (userId.HasValue && _allowedUsers.Contains(userId.Value))
{
await next(); // User authorized
}
else
{
await context.Reply("π« Access denied");
}
}
}
Example: Rate Limiting Middleware
public class RateLimitMiddleware : IBotMiddleware
{
private readonly Dictionary<long, DateTime> _lastRequest = new();
private readonly TimeSpan _cooldown = TimeSpan.FromSeconds(2);
public async Task InvokeAsync(Context context, Func<Task> next)
{
var userId = context.Update.Message?.From?.Id ??
context.Update.CallbackQuery?.From?.Id;
if (!userId.HasValue)
{
await next();
return;
}
if (_lastRequest.TryGetValue(userId.Value, out var lastTime))
{
if (DateTime.Now - lastTime < _cooldown)
{
await context.Reply("β³ Please wait before sending another message");
return;
}
}
_lastRequest[userId.Value] = DateTime.Now;
await next();
}
}
π¨ Context API
The Context object provides access to everything you need:
public async Task MyHandler(Context context)
{
// Bot client
var bot = context.BotClient;
var me = await bot.GetMeAsync();
// Update information
var update = context.Update;
var updateType = update.Type;
// Message data
var message = context.Message;
var text = context.Message.Text;
var chatId = context.ChatId;
// User information
var user = context.Update.Message?.From;
var userId = user?.Id;
var username = user?.Username;
// Session
context.Session!.State = "processing";
context.Session.Data["key"] = "value";
// Send messages
await context.Reply("Simple text");
await context.Reply("Text with keyboard", keyboard);
// Callback queries
await context.Callback.Answer();
await context.Callback.Answer("Notification text", showAlert: true);
// Get callback data
var data = context.Callback.Data;
}
π― Advanced Examples
Multi-Step Registration Flow
Complete example with validation and state management:
[Command("register")]
public async Task StartRegistration(Context context)
{
context.Session!.State = "awaiting_age";
var keyboard = ReplyKeyboardBuilder.Create()
.Button("Cancel β")
.Resize()
.Build();
await context.Reply("π€ Let's register! What's your age?", keyboard);
}
[Text(State = "awaiting_age")]
public async Task AskAge(Context context)
{
if (context.Message.Text == "Cancel β")
{
await Cancel(context);
return;
}
if (!int.TryParse(context.Message.Text, out var age) || age < 13 || age > 120)
{
await context.Reply("β Please enter a valid age (13-120)");
return;
}
context.Session!.Data["age"] = age;
context.Session.State = "awaiting_name";
await context.Reply("β
Great! What's your name?");
}
[Text(State = "awaiting_name")]
public async Task AskName(Context context)
{
var name = context.Message.Text;
if (string.IsNullOrWhiteSpace(name) || name.Length < 2)
{
await context.Reply("β Please enter a valid name (min 2 chars)");
return;
}
context.Session!.Data["name"] = name;
context.Session.State = "awaiting_country";
var keyboard = InlineKeyboardBuilder.Create()
.Button("πΊπΈ USA", "country_usa")
.Button("π¬π§ UK", "country_uk")
.Row()
.Button("π©πͺ Germany", "country_de")
.Button("π«π· France", "country_fr")
.Build();
await context.Reply($"Nice to meet you, {name}! Where are you from?", keyboard);
}
[CallbackQuery("country_*")]
public async Task HandleCountry(Context context)
{
var country = context.Callback.Data.Replace("country_", "").ToUpper();
context.Session!.Data["country"] = country;
context.Session.State = null;
await context.Callback.Answer();
var name = context.Session.Data["name"];
var age = context.Session.Data["age"];
await context.Reply(
$"π Registration Complete!\n\n" +
$"Name: {name}\n" +
$"Age: {age}\n" +
$"Country: {country}"
);
}
[Command("cancel")]
[Text(Pattern = "Cancel β")]
public async Task Cancel(Context context)
{
context.Session!.State = null;
context.Session.Data.Clear();
await context.Reply("β Cancelled", new ReplyKeyboardRemove());
}
Custom Session Storage
Replace in-memory storage with persistent storage:
using Kippo.SessionStorage;
using System.Text.Json;
public class FileSessionStorage : ISessionStore
{
private readonly string _storagePath;
public FileSessionStorage(string storagePath = "./sessions")
{
_storagePath = storagePath;
Directory.CreateDirectory(_storagePath);
}
public async Task<Session> GetAsync(long chatId)
{
var filePath = Path.Combine(_storagePath, $"{chatId}.json");
if (!File.Exists(filePath))
return new Session { ChatId = chatId };
var json = await File.ReadAllTextAsync(filePath);
return JsonSerializer.Deserialize<Session>(json)
?? new Session { ChatId = chatId };
}
public async Task SaveAsync(long chatId, Session session)
{
var filePath = Path.Combine(_storagePath, $"{chatId}.json");
var json = JsonSerializer.Serialize(session, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json);
}
}
// Register in Program.cs
builder.Services.AddSingleton<ISessionStore, FileSessionStorage>();
π‘ Example Project
Check out the KippoGramm sample project for a complete working example:
cd KippoGramm
# Add your bot token to appsettings.json
dotnet run
Features demonstrated:
- β Multi-step registration with validation
- β State-based routing
- β Reply and inline keyboards
- β Callback query handling with wildcards
- β Session data persistence
- β Custom logging middleware
π Requirements
| Component | Version |
|---|---|
| .NET | 8.0, 9.0, or 10.0 |
| Bot Token | Get from @BotFather |
π€ Contributing
Contributions are welcome! Here's how:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Support
<table> <tr> <td align="center">
π Report Issues
</td> <td align="center">
π¬ Discussions
</td> <td align="center">
π Telegram API
</td> </tr> </table>
π Acknowledgments
- Built with Telegram.Bot library
- Inspired by modern web frameworks
- Thanks to all contributors
<div align="center">
π Ready to Build?
Install Kippo and create your bot in 5 minutes!
dotnet add package Kippo
Made with β€οΈ for the .NET Community
</div>
NuGet Package Manager CLI
dotnet add package Kippo
Package Manager Console
Install-Package Kippo
PackageReference (add to your .csproj)
<PackageReference Include="Kippo" Version="1.0.0" />
π Quick Start
Get your Telegram bot running in 3 simple steps:
Get your Telegram bot running in 3 simple steps:
<details open> <summary><b>Step 1:</b> Configure Your Bot Token</summary>
<br>
Add your Telegram bot token to appsettings.json:
{
"Kippo": {
"BotToken": "YOUR_BOT_TOKEN_HERE"
}
}
π‘ Get your bot token from @BotFather on Telegram
</details>
<details open> <summary><b>Step 2:</b> Create Your Bot Handler</summary>
<br>
Create a class that inherits from BotUpdateHandler:
using Kippo.Attribute;
using Kippo.Contexs;
using Kippo.Handlers;
using Kippo.Keyboard;
public class MyHandler : BotUpdateHandler
{
public MyHandler(ISessionStore sessionStore, IEnumerable<IBotMiddleware> middlewares)
: base(sessionStore, middlewares) { }
[Command("start")]
public async Task Start(Context context)
{
var keyboard = ReplyKeyboardBuilder.Create()
.Button("Get Started π")
.Button("Help β")
.Resize()
.Build();
await context.Reply("Welcome to my bot! π", keyboard);
}
[Command("help")]
public async Task Help(Context context)
{
await context.Reply(
"π *Available Commands*\n\n" +
"/start - Start the bot\n" +
"/help - Show this message"
);
}
[Text]
public async Task HandleText(Context context)
{
await context.Reply($"You said: _{context.Message.Text}_");
}
}
</details>
<details open> <summary><b>Step 3:</b> Register Kippo in Your Application</summary>
<br>
In Program.cs:
using Kippo.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKippo<MyHandler>(builder.Configuration);
var app = builder.Build();
app.Run();
</details>
<div align="center">
π That's it! Your bot is now running!
</div>
π Documentation
π Documentation
π·οΈ Attributes
Kippo provides powerful attributes for routing updates to your handlers:
<table> <tr> <td width="33%">
[Command]
Handle bot commands
[Command("start")]
public async Task Start(Context ctx)
{
await ctx.Reply("Hello! π");
}
[Command("settings",
Description = "Configure bot")]
public async Task Settings(Context ctx)
{
// Handle /settings
}
</td> <td width="33%">
[Text]
Handle text messages
// All text messages
[Text]
public async Task HandleText(Context ctx)
{
await ctx.Reply($"You: {ctx.Message.Text}");
}
// State-specific
[Text(State = "awaiting_name")]
public async Task HandleName(Context ctx)
{
var name = ctx.Message.Text;
ctx.Session.Data["name"] = name;
}
</td> <td width="33%">
[CallbackQuery]
Handle inline keyboard callbacks
[CallbackQuery("btn_yes")]
public async Task HandleYes(Context ctx)
{
await ctx.Reply("You clicked Yes!");
}
// Pattern matching
[CallbackQuery("product_")]
public async Task HandleProduct(Context ctx)
{
var id = ctx.Update
.CallbackQuery.Data
.Replace("product_", "");
}
</td> </tr> </table>
πΎ Session Management
Track user state and data across conversations with built-in session support:
[Command("register")]
public async Task StartRegistration(Context context)
{
// Set the conversation state
context.Session.State = "awaiting_age";
// Store metadata
context.Session.Data["started_at"] = DateTime.Now;
context.Session.Data["step"] = 1;
await context.Reply("π€ Let's get you registered!\n\nHow old are you?");
}
[Text(State = "awaiting_age")]
public async Task HandleAge(Context context)
{
if (!int.TryParse(context.Message.Text, out var age) || age < 13)
{
await context.Reply("β Please enter a valid age (13+)");
return;
}
context.Session.Data["age"] = age;
context.Session.State = "awaiting_name";
context.Session.Data["step"] = 2;
await context.Reply("β
Great! What's your name?");
}
[Text(State = "awaiting_name")]
public async Task HandleName(Context context)
{
var name = context.Message.Text;
var age = context.Session.Data["age"];
context.Session.State = null; // Clear state
await context.Reply(
$"π Registration complete!\n\n" +
$"Name: {name}\n" +
$"Age: {age}"
);
}
β¨οΈ Keyboard Builders
Create beautiful, interactive keyboards with a fluent API:
<table> <tr> <td width="50%">
Reply Keyboard
var keyboard = ReplyKeyboardBuilder.Create()
.Button("π Create")
.Button("π List")
.Row() // New row
.Button("βοΈ Settings")
.Button("β Cancel")
.Resize() // Auto-resize
.OneTime() // Hide after use
.Build();
await context.Reply(
"Choose an action:",
keyboard
);
</td> <td width="50%">
Inline Keyboard
var keyboard = InlineKeyboardBuilder.Create()
.Button("β
Confirm", "confirm")
.Button("β Cancel", "cancel")
.Row()
.Button("π Contact", "contact")
.Row()
.UrlButton("π Website", "https://example.com")
.Build();
await context.Reply(
"Please confirm your action:",
keyboard
);
</td> </tr> </table>
π Middleware
Extend Kippo's functionality with custom middleware. Middleware executes in a pipeline before your handlers.
Built-in Middleware:
SessionMiddleware- Automatic session loading and saving (auto-registered)
How to Register Middleware
using Kippo.Extensions;
using Kippo.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Register custom middleware BEFORE AddKippo
builder.Services.AddSingleton<IBotMiddleware, LoggingMiddleware>();
builder.Services.AddSingleton<IBotMiddleware, SessionMiddleware>();
// Then register Kippo with your handler
builder.Services.AddKippo<MyHandler>(builder.Configuration);
var app = builder.Build();
app.Run();
Creating Custom Middleware
using Kippo.Middleware;
using Kippo.Contexs;
public class LoggingMiddleware : IBotMiddleware
{
private readonly ILogger<LoggingMiddleware> _logger;
public LoggingMiddleware(ILogger<LoggingMiddleware> logger)
{
_logger = logger;
}
public async Task InvokeAsync(Context context, Func<Task> next)
{
var userId = context.Update.Message?.From?.Id ??
context.Update.CallbackQuery?.From?.Id;
_logger.LogInformation(
"π¨ Update from user {UserId}: {Type}",
userId,
context.Update.Type
);
await next(); // Continue to next middleware/handler
_logger.LogInformation("β
Update processed");
}
}
Middleware Pipeline Flow:
Update β SessionMiddleware β LoggingMiddleware β Your Handler β Response
π¨ Context API
The Context object is your gateway to bot interactions:
public async Task ExampleHandler(Context context)
{
// π€ Bot client access
var botInfo = await context.Client.GetMeAsync();
// π¬ Update information
var updateType = context.Update.Type;
var message = context.Message;
var user = context.User;
// πΎ Session management
context.Session.State = "processing";
context.Session.Data["key"] = "value";
// π¬ Messaging methods
await context.Reply("Simple message");
await context.Reply("Message with keyboard", keyboard);
await context.EditMessage("Updated text");
await context.DeleteMessage();
// π Send files
await context.Client.SendPhotoAsync(
context.ChatId,
InputFile.FromUri("https://example.com/photo.jpg")
);
}
π― Advanced Usage
<details> <summary><b>Custom Session Storage</b></summary>
<br>
By default, Kippo uses InMemorySessionStorage. For production, implement ISessionStore for persistent storage:
using Kippo.SessionStorage;
using System.Text.Json;
public class FileSessionStorage : ISessionStore
{
private readonly string _storagePath;
public FileSessionStorage(string storagePath = "./sessions")
{
_storagePath = storagePath;
Directory.CreateDirectory(_storagePath);
}
public async Task<Session> GetAsync(long chatId)
{
var filePath = Path.Combine(_storagePath, $"{chatId}.json");
if (!File.Exists(filePath))
return new Session { ChatId = chatId };
var json = await File.ReadAllTextAsync(filePath);
return JsonSerializer.Deserialize<Session>(json)
?? new Session { ChatId = chatId };
}
public async Task SaveAsync(long chatId, Session session)
{
var filePath = Path.Combine(_storagePath, $"{chatId}.json");
var json = JsonSerializer.Serialize(session, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json);
}
}
// Register in Program.cs
builder.Services.AddSingleton<ISessionStore, FileSessionStorage>();
builder.Services.AddKippo<MyHandler>(builder.Configuration);
</details>
<details> <summary><b>Multiple Attributes & Pattern Matching</b></summary>
<br>
Combine multiple attributes for flexible routing:
// Handle both command and text
[Command("cancel")]
[Text(Pattern = "Cancel")]
[Text(Pattern = "β Cancel")]
public async Task Cancel(Context context)
{
context.Session.State = null;
context.Session.Data.Clear();
await context.Reply("β
Operation cancelled.");
}
// Pattern-based callback handling
[CallbackQuery("page_")]
public async Task HandlePagination(Context context)
{
var page = int.Parse(
context.Update.CallbackQuery.Data.Replace("page_", "")
);
await ShowPage(context, page);
}
</details>
<details> <summary><b>Authentication Middleware</b></summary>
<br>
Create middleware for user authentication:
public class AuthMiddleware : IBotMiddleware
{
private readonly HashSet<long> _allowedUsers = new()
{
123456789, // Admin user IDs
987654321
};
public async Task InvokeAsync(Context context, Func<Task> next)
{
var userId = context.User?.Id;
if (userId.HasValue && _allowedUsers.Contains(userId.Value))
{
await next(); // User is authorized
}
else
{
await context.Reply(
"π« Access denied. You are not authorized to use this bot."
);
}
}
}
</details>
<details> <summary><b>Rate Limiting</b></summary>
<br>
Prevent spam with rate limiting middleware:
public class RateLimitMiddleware : IBotMiddleware
{
private readonly Dictionary<long, DateTime> _lastRequest = new();
private readonly TimeSpan _cooldown = TimeSpan.FromSeconds(2);
public async Task InvokeAsync(Context context, Func<Task> next)
{
var userId = context.User?.Id;
if (!userId.HasValue) return;
if (_lastRequest.TryGetValue(userId.Value, out var lastTime))
{
if (DateTime.Now - lastTime < _cooldown)
{
await context.Reply("β³ Please wait before sending another message.");
return;
}
}
_lastRequest[userId.Value] = DateTime.Now;
await next();
}
}
</details>
π‘ Examples
π‘ Examples
The KippoGramm sample project demonstrates real-world bot usage:
<table> <tr> <td>
β Features Demonstrated
- π Multi-step registration with validation
- πΎ Session state management
- β¨οΈ Reply & Inline keyboards
- π Callback query handling
- π― Command & pattern routing
- βοΈ Message editing
- π Multiple attributes per handler
- π Data persistence across conversations
</td> <td>
ποΈ Project Structure
KippoGramm/
βββ Program.cs # Setup & middleware
βββ MyHandler.cs # Bot handlers
βββ appsettings.json # Configuration
π Run the Example
cd KippoGramm
# Add your token to appsettings.json
dotnet run
</td> </tr> </table>
Example Flow in MyHandler:
/startβ Shows main menu with reply keyboard- User clicks "π Register" β Starts registration flow
- Bot asks for age β Validates input
- Bot asks for name β Validates input
- Bot shows country selection β Inline keyboard with callbacks
- User selects country β Registration complete with summary
/infoβ Shows saved user data from session
π Requirements
| Component | Version |
|---|---|
| π£ .NET | 10.0 or higher |
| π€ Telegram Bot Token | Get from @BotFather |
| π¦ Dependencies | Automatically installed via NuGet |
π€ Contributing
We welcome contributions! Here's how you can help:
<table> <tr> <td width="25%">
π Report Bugs
Found a bug? Open an issue
</td> <td width="25%">
π‘ Suggest Features
Have an idea? Start a discussion
</td> <td width="25%">
π§ Submit PRs
Want to contribute code? Fork & submit a PR!
</td> <td width="25%">
π Improve Docs
Help make our docs better!
</td> </tr> </table>
Before contributing:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
π License
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2026 Kippo Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction...
π Support & Resources
<table> <tr> <td width="33%" align="center">
π« Issues & Bugs
Report bugs and technical issues
</td> <td width="33%" align="center">
π¬ Discussions
Ask questions and share ideas
</td> <td width="33%" align="center">
π Documentation
Official Telegram Bot API docs
</td> </tr> </table>
π Acknowledgments
- Built with β€οΈ using Telegram.Bot library
- Inspired by modern web frameworks like ASP.NET Core
- Thanks to all contributors
β Star History
If you find Kippo useful, please consider giving it a star! β
<div align="center">
π Ready to build your bot?
Get Started β’ View Examples β’ Read Docs
Made with β€οΈ for the .NET Community
</div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Hosting (>= 10.0.2)
- Microsoft.Extensions.Logging (>= 10.0.2)
- Telegram.Bot (>= 22.8.1)
-
net8.0
- Microsoft.Extensions.Hosting (>= 8.0.1)
- Microsoft.Extensions.Logging (>= 8.0.1)
- Telegram.Bot (>= 22.8.1)
-
net9.0
- Microsoft.Extensions.Hosting (>= 9.0.0)
- Microsoft.Extensions.Logging (>= 9.0.0)
- Telegram.Bot (>= 22.8.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v1.0.4: Production-ready improvements - Thread-safe sessions with ConcurrentDictionary, automatic service injection in handler methods, scoped service support, integrated ILogger, optimized network usage with AllowedUpdates, better error messages and null-safety. Breaking: ISessionStore.DeleteAsync method required.