Expand description
§blazegram
Declarative Telegram bot framework for Rust.
One screen at a time. Zero garbage in chat. Direct MTProto over persistent TCP.
§Why blazegram?
| HTTP Bot API | blazegram | |
|---|---|---|
| Latency | ~50 ms per call (2 hops) | ~5 ms (direct MTProto) |
| File uploads | 50 MB limit | 2 GB |
| Connection | New HTTP per call | Persistent TCP socket |
| Message management | Manual IDs everywhere | Automatic diffing |
| Chat cleanup | You delete manually | Auto-managed |
blazegram holds a single persistent TCP socket to Telegram’s datacenter via grammers MTProto — no webhook server, no middleman, no HTTP overhead.
On top of that, it introduces the Screen abstraction: declare what the user should see, and a Virtual Chat Differ computes the minimal set of API calls to get there.
§Quick start
[dependencies]
blazegram = "0.4"
tokio = { version = "1", features = ["full"] }use blazegram::{handler, prelude::*};
#[tokio::main]
async fn main() {
App::builder("BOT_TOKEN")
.command("start", handler!(ctx => {
ctx.navigate(
Screen::text("home", "<b>Pick a side.</b>")
.keyboard(|kb| kb
.button("Light", "pick:light")
.button("Dark", "pick:dark"))
.build()
).await
}))
.callback("pick", handler!(ctx => {
let side = ctx.callback_param().unwrap_or_default();
ctx.navigate(
Screen::text("chosen", format!("You chose <b>{side}</b>."))
.keyboard(|kb| kb.button_row("Back", "menu"))
.build()
).await
}))
.run().await;
}First launch authenticates via MTProto and creates a .session file.
Subsequent starts reconnect in under 100 ms.
§Core concepts
§Screens & the Differ
A Screen is a declarative snapshot of what the user should see. Call navigate() and the
differ handles everything:
callback (button press) → edit in place (1 API call)
user sent text / command → delete old + send (2–3 calls)
content identical → nothing (0 calls)No message IDs. No “should I edit or re-send?” logic. No stale buttons lingering in chat.
// text + keyboard
Screen::text("menu", "Pick one:")
.keyboard(|kb| kb
.button_row("A", "pick:a")
.button_row("B", "pick:b"))
.build();
// photo with caption
Screen::builder("gallery")
.photo("https://example.com/pic.jpg")
.caption("Nice shot")
.keyboard(|kb| kb.button_row("Next", "next"))
.done()
.build();
// multi-message screen
Screen::builder("receipt")
.text("Order confirmed.").done()
.photo("https://example.com/qr.png").caption("QR code").done()
.build();§Navigation stack
push() / pop() give you a navigation stack (capped at 20 levels) —
back buttons work out of the box:
ctx.push(detail_screen).await?; // push new screen
ctx.pop(|prev_id| make_screen(prev_id)).await?; // pop back§Features
§🔲 Keyboards & Grids
Fluent keyboard builder with automatic row management:
.keyboard(|kb| kb
.button("A", "a").button("B", "b").button("C", "c").row() // 3 buttons in one row
.button_row("Full width", "full") // single-button row
.grid(items, 3, |item| (item.name, item.id)) // auto-grid from iterator
.pagination(page, total, "page") // ← [2/5] →
.nav_back("menu") // localized back button
.confirm_cancel("OK", "ok", "Cancel", "cancel") // confirm/cancel pair
)§📄 Pagination
One-liner paginated lists with navigation buttons:
let paginator = Paginator::new(items, 5); // 5 per page
let screen = paginated_screen(
"items", // screen ID
"Your items", // title
&paginator, // paginator
|i, item| (item.name.clone(), format!("view:{i}")), // formatter
"page", // callback prefix for ←/→
"menu", // back callback
);
ctx.navigate(screen).await?;← [2/5] → buttons auto-generated. Handles empty lists. Labels localized via i18n.
§📝 Multi-step forms
Declarative form wizard with validation, type coercion, and auto-generated keyboards:
Form::builder("signup")
.text_step("name", "name", "Your name?")
.validator(|s| if s.len() < 2 { Err("Too short".into()) } else { Ok(()) })
.done()
.integer_step("age", "age", "Age?").min(13).max(120).done()
.choice_step("plan", "plan", "Pick a plan:", vec![("Free", "free"), ("Pro", "pro")])
.confirm_step(|d| format!("Name: {}\nAge: {}", d["name"], d["age"]))
.on_complete(form_handler!(ctx, data => {
ctx.navigate(Screen::text("done", "Welcome aboard.").build()).await
}))
.build()Bad input auto-deleted, error shown as 3 s toast, cancel button built-in.
§⚡ Progressive updates (streaming)
Stream edits to a single message, auto-throttled to respect Telegram rate limits. Perfect for LLM streaming, progress bars, live dashboards:
let h = ctx.progressive(Screen::text("t", "Loading...").build()).await?;
h.update(Screen::text("t", "Loading... 40%").build()).await;
h.update(Screen::text("t", "Loading... 80%").build()).await;
h.finalize(Screen::text("t", "Done ✅").build()).await?;If navigate() is called before finalize(), the stream is cancelled automatically — no races.
§💬 Reply mode
For conversational bots (LLM wrappers, support bots) that don’t need chat cleanup:
ctx.reply(Screen::text("r", "thinking...").build()).await?; // sends new message
ctx.reply(Screen::text("r", "thinking... ok").build()).await?; // edits same message
ctx.reply(Screen::text("r", "Here you go.").build()).await?; // edits same message
// next handler call → fresh messageUser messages are not deleted. Combine with freeze_message() to keep important messages
across navigate() transitions.
§💾 State management
Typed per-chat state with zero boilerplate:
// key-value
ctx.set("counter", &42);
let n: i32 = ctx.get("counter").unwrap_or(0);
// or full typed state
#[derive(Serialize, Deserialize, Default)]
struct Profile { xp: u64, level: u32 }
let p: Profile = ctx.state();
ctx.set_state(&Profile { xp: 100, level: 2 });4 backends, same API:
| Backend | Setup | Persistence |
|---|---|---|
| In-memory | default | none |
| Memory + snapshot | .snapshot("state.bin") | periodic flush to disk |
| redb | .redb_store("bot.redb") | pure Rust, ACID, zero C deps |
| Redis | .redis_store("redis://...") | multi-instance, feature redis |
§🌍 i18n
FTL-based with automatic user language detection:
// locales/en.ftl: greeting = Hello, { $name }!
// locales/ru.ftl: greeting = Привет, { $name }!
let text = ctx.t_with("greeting", &[("name", "World")]);
// → "Hello, World!" or "Привет, World!" depending on user.language_codeFramework labels (back, next, cancel) are auto-localized.
§📡 Broadcast
Mass-message all users with built-in rate limiting and optional dismiss button:
let screen = Screen::text("update", "🎉 New feature!").build();
let result = broadcast(&bot, &store, screen, BroadcastOptions::default().hideable()).await;
// result.sent = 1523, result.blocked = 12, result.failed = 0§🔌 Inline mode
Declarative result builders with auto-pagination:
.on_inline(handler!(ctx, query, offset => {
let results = search(&query).iter().map(|r|
InlineResult::article(&r.id)
.title(&r.title)
.description(&r.summary)
.screen(Screen::text("r", &r.body).build())
.build()
).collect();
let answer = InlineAnswer::new(results).per_page(20).cache_time(60);
let (page, next) = answer.paginate(&offset);
ctx.answer_inline(page.into_iter().map(|r| r.clone().into()).collect(), Some(next), Some(60), false).await
}))§🛡️ Middleware
Composable middleware chain — auth, throttle, logging, analytics:
App::builder("TOKEN")
.middleware(LoggingMiddleware)
.middleware(ThrottleMiddleware::new(5, Duration::from_secs(1)))
.middleware(AuthMiddleware::new(vec![UserId(123456)]))
.run().await;§🧪 Testing
Full test harness with MockBotApi — no network, no tokens:
#[tokio::test]
async fn test_start() {
let app = TestApp::new();
let reply = app.send_command("/start").await;
assert!(reply.text.contains("Pick a side"));
}
#[tokio::test]
async fn test_callback() {
let app = TestApp::new();
app.send_command("/start").await;
let reply = app.press_button("pick:dark").await;
assert!(reply.text.contains("dark"));
}Simulate any update type: text, callbacks, photos, voice, stickers, locations, payments, member joins/leaves.
§💳 Payments (Stars & Fiat)
// Send invoice (Telegram Stars)
ctx.send_invoice(Invoice {
title: "Premium".into(),
description: "Unlock premium features".into(),
payload: "premium_1".into(),
currency: "XTR".into(),
prices: vec![("Premium".into(), 100)],
provider_token: None, // None = Stars
..Default::default()
}).await?;
// Handle checkout
.on_pre_checkout(handler!(ctx => { ctx.approve_checkout().await }))
.on_successful_payment(handler!(ctx => {
ctx.navigate(Screen::text("ty", "Thanks for your purchase! 🎉").build()).await
}))§Good to know
Unrecognized messages are deleted by default to keep the chat clean.
Disable with .delete_unrecognized(false).
Rate limiting is adaptive: global (30 rps), per-chat (1 rps private, 20/min groups),
with automatic FLOOD_WAIT retry. answer_callback_query bypasses the limiter.
Entity fallback: if HTML formatting fails, the executor automatically retries as plain text.
handler! macro eliminates Box::pin(async move { ... }) boilerplate:
handler!(ctx => { ... }) // commands, callbacks
handler!(ctx, text => { ... }) // on_input
form_handler!(ctx, data => { ... }) // form completion§Architecture
Handlers .command() / .callback() / .on_input()
│
▼
Ctx navigate() / push() / pop() / reply()
│
▼
Differ old msgs + new Screen → minimal ops
│
▼
Executor FLOOD_WAIT retry, entity fallback
│
▼
BotApi 70+ async methods (trait, mockable)
│
▼
grammers MTProto → Telegram DC (persistent TCP)Per-chat mutex guarantees sequential update processing. No race conditions.
§License
MIT
Modules§
- adapter
- grammers MTProto adapter implementing
BotApi. - app
- App builder and main event loop. App — the main entry point for building a Blazegram bot.
- bot_api
BotApitrait — 60+ async methods abstracting Telegram API calls.- broadcast
- Broadcast messages to multiple chats. Broadcast — mass messaging with rate limiting and dismiss button.
- ctx
- Handler context — the single object handlers interact with. Ctx — handler context. The single object handlers interact with.
- differ
- Virtual Chat Differ — computes minimal API operations for screen transitions. Virtual Chat Differ — the heart of Blazegram.
- error
- Error types for API calls and handler logic.
- executor
- Diff operation executor — applies edit/delete/send with retry and fallback. DiffOp executor — applies diff operations via BotApi.
- file_
cache - File ID cache — avoid re-uploading the same media. Cache mapping content hashes to Telegram file IDs.
- file_
session - File-backed session — MemorySession + JSON persistence. Zero SQLite.
File-backed session storage —
MemorySession+ periodic JSON persistence. - form
- Multi-step form wizards with validation. Form Wizard — declarative multi-step forms.
- grammers_
adapter - Re-export for backward compatibility with pre-0.4 code that used
blazegram::grammers_adapter::*. - i18n
- FTL-based i18n with
{ $var }interpolation. I18n — full localization system. - inline
- Inline query results with builder API. Inline mode support — declarative result builders and auto-pagination.
- keyboard
- Inline keyboard builder with buttons, rows, grids, navigation.
- macros
- Ergonomic handler macros (
handler!,form_handler!). Ergonomic macros for handler registration. - markup
- Markup processor:
*bold*_italic_→ Telegram HTML, plus HTML builder helpers andescape(). Markup processor: CleanBot-style markdown → HTML, plus HTML helpers. - metrics
- Prometheus-style counters and histograms. Framework metrics for observability.
- middleware
- Middleware trait + built-in logging, analytics, throttle. Middleware system.
- mock
- Mock BotApi for unit tests without Telegram. Mock BotApi for testing.
- pagination
- Paginated lists with auto-generated navigation buttons. Pagination helper.
- prelude
- Re-exports everything you need:
use blazegram::prelude::*;Prelude — import everything you need withuse blazegram::prelude::*. - progressive
- Progressive screen updates (streaming, progress bars). Auto-cancelled on navigate(). Progressive Screens — stream content updates to a message in real-time.
- rate_
limiter - Token-bucket rate limiter with automatic FLOOD_WAIT handling. Adaptive, multi-layer rate limiter for Telegram bots.
- redb_
store - Pure-Rust persistent state store (redb). Zero C deps, no SQLite conflicts.
Requires the
redbfeature (enabled by default). Persistent state store backed byredb— a pure-Rust, single-file, ACID embedded database. - redis_
store - Redis-backed state store (requires
redisfeature). Redis-backed state store. - router
- Command/callback/input router with prefix matching. Router — dispatches incoming updates to handlers.
- screen
- Screen builder — declarative UI for chat messages.
- serializer
- Per-chat lock guaranteeing sequential update processing. Chat Serializer — per-chat lock guaranteeing sequential update processing.
- state
- State storage trait + in-memory store with snapshot support. State storage trait and in-memory implementation with versioned snapshots.
- template
- Template engine:
{{ var }},{% if %},{% for %}. Simple template engine with auto-escaping. - testing
- TestApp harness for integration-style tests. Testing utilities — simulate bot interactions without Telegram.
- types
- Core types: ChatId, MessageId, Screen identifiers, message content, parsed updates. Core types: ChatId, MessageId, Screen identifiers, message content, parsed updates.
Macros§
- form_
handler - Create a form completion handler.
- handler
- Create a handler closure that wraps the body in
Box::pin(async move { ... }).