blazegram
Declarative Telegram bot framework for Rust.
One screen at a time. Zero garbage in chat. Direct MTProto over persistent TCP.
Blazes skips the HTTP Bot API entirely. It holds a single persistent TCP socket to Telegram's datacenter via grammers MTProto — ~5 ms per call instead of ~50 ms, 2 GB file uploads instead of 50 MB, no middleman server.
On top of that, blazegram introduces the Screen — a declarative description of what the user
should see right now. When you call navigate(), a Virtual Chat Differ computes the minimal
set of Telegram API calls (edit, delete, send) to transition from current state to the new one.
You never manage message IDs.
Quick start
[]
= "0.4"
= { = "1", = ["full"] }
use ;
async
First launch authenticates via MTProto and creates a .session file.
Subsequent starts reconnect in under 100 ms.
The differ
Every navigate() call runs through the differ before touching the network:
callback (button press) → edit in place (1 API call)
user sent text / command → delete old + send (2–3 calls)
content identical → nothing (0 calls)
If the user typed something between screens, the old bot message is scrolled out of view — editing it would be invisible. The differ detects this and switches to delete + send. Active progressive streams are auto-cancelled before diffing, so no concurrent edits race.
Screens
# use *;
// simple text
text.build;
// text + keyboard
text
.keyboard
.build;
// photo with caption + keyboard
builder
.photo
.caption
.keyboard
.done
.build;
// multi-message screen
builder
.text.done
.photo.caption.done
.build;
push() / pop() give you a navigation stack (capped at 20 levels):
ctx.push.await?;
ctx.pop.await?;
Forms
builder
.text_step
.validator
.done
.integer_step.min.max.done
.choice_step
.confirm_step
.on_complete
.build
Validation errors auto-delete the bad input and show a 3 s toast. Cancel/back buttons are built-in.
Progressive updates
Stream edits to one message, auto-throttled to respect Telegram rate limits.
If navigate() is called before finalize(), the stream is cancelled automatically.
let h = ctx.progressive.await?;
h.update.await;
h.finalize.await?;
Reply mode
For conversational bots that don't need chat cleanup:
ctx.reply.await?; // sends
ctx.reply.await?; // edits
ctx.reply.await?; // edits
User messages are not deleted. Next handler call starts a fresh message.
State
ctx.set;
let n: i32 = ctx.get.unwrap_or;
// or typed:
let p: Profile = ctx.state;
ctx.set_state;
Backends: in-memory (default), memory + snapshot (.snapshot("state.bin")),
redb (.redb_store("bot.redb"), pure Rust, ACID, default feature),
Redis (.redis_store("redis://..."), feature redis).
Per-chat state is capped at 1 000 keys by default (configurable via .max_state_keys()).
Tracked bot messages are capped at 100 per chat. Oldest entries are evicted automatically.
Frozen & permanent messages
// frozen: survives navigate(), differ won't touch it
let sent = ctx.send_text.await?;
ctx.freeze_message;
// permanent: never tracked, never deleted
ctx.send_permanent.await?;
Inline mode, i18n, templates
// inline
.on_inline
// i18n — auto-detected from user.language_code
// locales/en.json: { "hi": "Hello, { $name }!" }
let text = ctx.t_with;
// templates
let html = render;
Middleware & testing
builder
.middleware
.middleware
.middleware
.run.await;
use TestApp;
async
No network. MockBotApi records every API call for assertions.
handler! macro
Eliminates Box::pin(async move { ... }) boilerplate:
handler! // commands, callbacks
handler! // on_input
form_handler! // form completion
Good to know
Unrecognized messages are deleted by default to keep the chat clean.
Disable with .delete_unrecognized(false) or register .on_unrecognized(handler!(...)).
reply() messages are tracked by the differ. Switching from reply() to navigate()
cleans up old replies. Freeze them if you want them to persist.
Rate limiting is adaptive: global (30 rps), per-chat (1 rps private, 20/min groups),
with automatic FLOOD_WAIT retry and exponential backoff. answer_callback_query bypasses the limiter.
Entity fallback: if HTML entities fail (ENTITY_BOUNDS_INVALID), the executor
automatically retries as plain text.
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 60+ async methods (trait, mockable)
│
▼
grammers MTProto → Telegram DC (persistent TCP)
Per-chat mutex guarantees sequential update processing. No race conditions.
License
MIT