rust-tg-bot 1.0.0-beta.4

Complete Telegram Bot API framework for Rust, inspired by python-telegram-bot
docs.rs failed to build rust-tg-bot-1.0.0-beta.4
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: rust-tg-bot-1.0.0-rc.1

Version: 1.0.0-beta.4 Bot API 9.6 Rust: 1.75+ License: LGPL-3.0 CI codecov docs.rs mdBook Code Style: rustfmt Code Quality: clippy Unsafe: forbidden MSRV: 1.75 Issues


WARNING: This project is in active development (v1.0.0-beta.4). The API is undergoing constant changes as we work toward full feature parity with python-telegram-bot and Rust-native performance optimizations. Use at your own risk -- breaking changes will occur between releases until v1.0.0 stable.


A complete, asynchronous Telegram Bot API framework for Rust, faithfully ported from python-telegram-bot -- the most popular Python library for the Telegram Bot API.

This project carries forward the architecture, handler system, and developer experience that made python-telegram-bot the go-to choice for thousands of bot developers, and transplants it into Rust's async ecosystem with full type safety, zero-cost abstractions, and fearless concurrency.

Heritage

This library stands on the shoulders of python-telegram-bot. The handler hierarchy, filter composition system, ConversationHandler state machine, job queue design, and persistence architecture are all direct ports of their Python counterparts. If you've built bots with python-telegram-bot, you already know how this library works.

We acknowledge and thank the python-telegram-bot maintainers and community for over a decade of work that made this project possible.

Why Rust?

Concern Python Rust
Performance GIL-limited concurrency, interpreted True parallelism, compiled to native code
Memory safety Runtime GC, potential leaks in long-running bots Ownership system prevents leaks at compile time
Type safety Optional type hints, runtime errors Enforced at compile time, no AttributeError at 3 AM
Deployment Requires Python runtime + virtualenv Single static binary, 6.2 MB stripped
Resource usage 57 MB RSS (measured) 15 MB idle / 17 MB load (matches teloxide, see benchmarks)
Concurrency asyncio (single-threaded) tokio (multi-threaded work-stealing)

For bots that handle high volumes of updates, run on constrained hardware (VPS, Raspberry Pi, containers), or need to be deployed without a runtime, Rust is the right tool.

Architecture

The library is organized as a Cargo workspace with four crates:

rust-telegram-bot/
  crates/
    telegram-bot-raw/     # Pure API types and methods (like Python's `telegram` module)
    telegram-bot-ext/     # Application framework (like Python's `telegram.ext`)
    telegram-bot-macros/  # Proc macros (#[derive(BotCommands)])
    telegram-bot/         # Facade crate -- re-exports all three for convenience

rust-tg-bot-raw contains every type and method from Bot API 9.6: Message, Update, User, Chat, inline types, payments, passport, games, stickers, and all API methods on the Bot struct. It depends only on serde, reqwest, and tokio.

rust-tg-bot-ext provides the application framework: ApplicationBuilder, typed handler system, composable filters, ConversationHandler, JobQueue, persistence backends (JSON file, SQLite, Redis, PostgreSQL), rate limiting, webhook support with optional TLS, and callback data caching.

rust-tg-bot-macros provides the #[derive(BotCommands)] proc macro for declarative command handler registration.

rust-tg-bot is the facade crate you add to Cargo.toml. It re-exports everything from all three crates under rust_tg_bot::raw and rust_tg_bot::ext.

Quick Start

1. Create a bot with @BotFather

Open Telegram, message @BotFather, send /newbot, and follow the prompts. Copy the token.

2. Add the dependency

[dependencies]
rust-tg-bot = { git = "https://github.com/HexiCoreDev/rust-telegram-bot" }
tracing-subscriber = "0.3"

3. Write your bot

use rust_tg_bot::ext::prelude::{
    ApplicationBuilder, Arc, CommandHandler, Context, HandlerResult,
    MessageHandler, Update, COMMAND, TEXT,
};

async fn start(update: Arc<Update>, context: Context) -> HandlerResult {
    let name = update
        .effective_user()
        .map(|u| u.first_name.as_str())
        .unwrap_or("there");
    context
        .reply_text(&update, &format!("Hi {name}! Send me any text and I will echo it back."))
        .await?;
    Ok(())
}

async fn echo(update: Arc<Update>, context: Context) -> HandlerResult {
    let text = update
        .effective_message()
        .and_then(|m| m.text.as_deref())
        .unwrap_or("");
    if !text.is_empty() {
        context.reply_text(&update, text).await?;
    }
    Ok(())
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();

    let token = std::env::var("TELEGRAM_BOT_TOKEN")
        .expect("TELEGRAM_BOT_TOKEN environment variable must be set");

    let app = ApplicationBuilder::new().token(token).build();

    app.add_handler(CommandHandler::new("start", start), 0).await;
    app.add_handler(
        MessageHandler::new(TEXT() & !COMMAND(), echo),
        0,
    ).await;

    println!("Echo bot is running. Press Ctrl+C to stop.");

    if let Err(e) = app.run_polling().await {
        eprintln!("Error running bot: {e}");
    }
}

4. Run it

TELEGRAM_BOT_TOKEN="123456:ABC-DEF" cargo run

Feature Highlights

Composable Filters

Filters use Rust's bitwise operators for composition -- the same mental model as python-telegram-bot, but enforced at compile time:

use rust_tg_bot::ext::prelude::{MessageHandler, TEXT, COMMAND};

// Text messages that are NOT commands
let text_only = TEXT() & !COMMAND();

// Use it with a handler
app.add_handler(
    MessageHandler::new(text_only, my_callback),
    0,
).await;

Over 50 built-in filters are available: TEXT, COMMAND, PHOTO, VIDEO, AUDIO, VOICE, DOCUMENT, LOCATION, CONTACT, ANIMATION, STICKER, POLL, VENUE, GAME, INVOICE, FORWARDED, REPLY, PREMIUM_USER, FORUM, chat type filters, entity filters, regex filters, and more.

Typed Handler Registration

Handlers use strongly-typed constructors and are registered via add_handler:

use rust_tg_bot::ext::prelude::{
    ApplicationBuilder, Arc, CommandHandler, Context, FnHandler,
    HandlerResult, MessageHandler, Update, COMMAND, TEXT,
};

// Command handler -- matches /start
app.add_handler(CommandHandler::new("start", start), 0).await;

// Message handler -- matches text that is not a command
app.add_handler(MessageHandler::new(TEXT() & !COMMAND(), echo), 0).await;

// Callback query handler -- matches inline keyboard button presses
app.add_handler(FnHandler::on_callback_query(button), 0).await;

// Generic function handler with custom predicate
app.add_handler(
    FnHandler::new(|u| u.callback_query.is_some(), my_handler),
    0,
).await;

// Catch-all handler (e.g., for logging)
app.add_handler(FnHandler::on_any(track_users), -1).await;

Builder-based Bot API

Every Bot API method returns a builder with optional parameters as chainable setters. Builders implement IntoFuture, so you can .await directly:

use rust_tg_bot::ext::prelude::{Context, ParseMode};

// Simple -- .await directly
context.bot().send_message(chat_id, "Hello!").await?;

// With optional parameters
context
    .bot()
    .send_message(chat_id, "<b>Bold</b> text")
    .parse_mode(ParseMode::Html)
    .await?;

// Inline keyboard using typed constructors
use rust_tg_bot::ext::prelude::{InlineKeyboardButton, InlineKeyboardMarkup};

let keyboard = InlineKeyboardMarkup::new(vec![
    vec![InlineKeyboardButton::callback("Option 1", "1")],
]);

context
    .bot()
    .send_message(chat_id, "Choose:")
    .reply_markup(keyboard)
    .await?;

// Edit a message
context
    .bot()
    .edit_message_text("Updated text")
    .chat_id(chat_id)
    .message_id(msg_id)
    .await?;

Typed Constants

No more magic strings. All enums are strongly typed:

use rust_tg_bot::ext::prelude::{
    ChatType, Context, MessageEntityType, ParseMode,
};

// Parse modes
context.bot().send_message(chat_id, text)
    .parse_mode(ParseMode::Html)
    .await?;

// Entity types
if entity.entity_type == MessageEntityType::BotCommand { /* ... */ }

// Chat types
if chat.chat_type != ChatType::Private { /* ... */ }

Handler Groups

Handlers are organized into numbered groups. Within a group, the first matching handler wins. Across groups, processing continues unless a handler returns HandlerResult::Stop:

use rust_tg_bot::ext::prelude::{
    Arc, CommandHandler, Context, FnHandler, HandlerResult,
    MessageHandler, Update, COMMAND, TEXT,
};

// Group -1: always runs first (e.g., user tracking)
app.add_handler(FnHandler::on_any(track_users), -1).await;

// Group 0: command handlers
app.add_handler(CommandHandler::new("start", start), 0).await;
app.add_handler(CommandHandler::new("help", help), 0).await;

// Group 1: message handlers
app.add_handler(
    MessageHandler::new(TEXT() & !COMMAND(), echo),
    1,
).await;

Typed Data Access

The Context provides typed read and write guards for bot-wide, per-user, and per-chat data:

use rust_tg_bot::ext::prelude::{Arc, Context, HandlerResult, Update};

async fn handle(update: Arc<Update>, context: Context) -> HandlerResult {
    // Read bot-wide data
    let bd = context.bot_data().await;
    let name = bd.get_str("bot_name");
    let count = bd.get_i64("total_messages");

    // Write bot-wide data with typed setters
    let mut bd = context.bot_data_mut().await;
    bd.set_str("last_user", "Alice");
    bd.set_i64("total_messages", count.unwrap_or(0) + 1);
    bd.add_to_id_set("user_ids", user_id);

    Ok(())
}

ConversationHandler State Machine

Multi-step conversations with automatic state tracking, timeouts, nested conversations, and persistence:

use rust_tg_bot::ext::handlers::conversation::ConversationHandler;
use std::time::Duration;

#[derive(Clone, Hash, Eq, PartialEq)]
enum State { AskName, AskAge, AskBio }

let conv = ConversationHandler::builder()
    .entry_point(start_step)
    .state(State::AskName, vec![name_step])
    .state(State::AskAge, vec![age_step])
    .state(State::AskBio, vec![bio_step])
    .fallback(cancel_step)
    .conversation_timeout(Duration::from_secs(300))
    .persistent(true)
    .name("registration".to_string())
    .build();

Features ported from python-telegram-bot:

  • Per-chat/per-user/per-message conversation keys
  • Re-entry support
  • map_to_parent for nested conversations
  • Automatic timeout with configurable handlers
  • Non-blocking callbacks with state revert on failure
  • Persistence integration

Webhook Support

The simplest way to run in webhook mode -- the framework handles the axum server, secret token validation, and update dispatching internally:

use rust_tg_bot::ext::updater::WebhookConfig;

let config = WebhookConfig::new("https://your.domain/telegram")
    .port(8000)
    .url_path("/telegram")
    .secret_token("my-secret-token");

app.run_webhook(config).await?;

See webhook_bot.rs for a complete working example, or custom_webhook_bot.rs for adding custom routes alongside the webhook.

Job Queue Scheduling

Schedule one-shot, repeating, daily, and monthly jobs using tokio timers and a builder pattern:

use rust_tg_bot::ext::job_queue::{JobQueue, JobCallbackFn, JobContext};
use std::sync::Arc;
use std::time::Duration;

let jq = Arc::new(JobQueue::new());

// One-shot: fire after 30 seconds
jq.once(callback, Duration::from_secs(30))
    .name("reminder")
    .chat_id(chat_id)
    .start()
    .await;

// Repeating: every 60 seconds
jq.repeating(callback, Duration::from_secs(60))
    .name("heartbeat")
    .start()
    .await;

// Daily at 09:00 UTC on weekdays
use chrono::NaiveTime;
let time = NaiveTime::from_hms_opt(9, 0, 0).unwrap();
jq.daily(callback, time, &[1, 2, 3, 4, 5])
    .name("morning_report")
    .start()
    .await;

// Monthly on the 1st at midnight
jq.monthly(callback, time, 1)
    .name("monthly_summary")
    .start()
    .await;

Every job returns a Job handle for cancellation, status checking, and enable/disable toggling.

Persistence

Swap between backends without changing application code:

JSON file (human-readable, great for development):

use rust_tg_bot::ext::persistence::json_file::JsonFilePersistence;
use rust_tg_bot::ext::prelude::ApplicationBuilder;

let persistence = JsonFilePersistence::new("bot_data", true, false);
let app = ApplicationBuilder::new()
    .token(token)
    .persistence(Box::new(persistence))
    .build();

SQLite (production-ready, WAL mode, atomic writes):

use rust_tg_bot::ext::persistence::sqlite::SqlitePersistence;
use rust_tg_bot::ext::prelude::ApplicationBuilder;

let persistence = SqlitePersistence::open("bot.db").unwrap();
let app = ApplicationBuilder::new()
    .token(token)
    .persistence(Box::new(persistence))
    .build();

Custom backend -- implement the BasePersistence trait:

use rust_tg_bot::ext::persistence::base::BasePersistence;
use std::collections::HashMap;

#[derive(Debug)]
struct RedisPersistence { /* ... */ }

impl BasePersistence for RedisPersistence {
    async fn get_user_data(&self) -> PersistenceResult<HashMap<i64, JsonMap>> { /* ... */ }
    async fn get_chat_data(&self) -> PersistenceResult<HashMap<i64, JsonMap>> { /* ... */ }
    // ... implement all trait methods
}

Feature Flags

[dependencies]
# Default: polling support only
rust-tg-bot = { git = "https://github.com/HexiCoreDev/rust-telegram-bot" }

# Everything
rust-tg-bot = { git = "https://github.com/HexiCoreDev/rust-telegram-bot", features = ["full"] }

# Pick what you need
rust-tg-bot = { git = "https://github.com/HexiCoreDev/rust-telegram-bot", features = [
    "webhooks",              # axum-based webhook server
    "webhooks-tls",          # TLS auto-configuration for webhooks
    "job-queue",             # Scheduled job execution
    "persistence-json",      # JSON file persistence
    "persistence-sqlite",    # SQLite persistence
    "persistence-redis",     # Redis persistence
    "persistence-postgres",  # PostgreSQL persistence (JSONB)
    "rate-limiter",          # API rate limiting
    "macros",                # #[derive(BotCommands)]
] }

Comparison

Feature rust-tg-bot python-telegram-bot teloxide
Bot API version 9.6 9.5 9.2
Language Rust Python Rust
Async runtime tokio asyncio tokio
Handler system 22 typed handlers + FnHandler 22 handler types Dispatcher + handler chains
Filter composition &, |, ^, ! operators Same operators Predicate combinators
ConversationHandler Full port (timeouts, nesting, persistence) Full dialogue macro
Job queue Built-in (tokio timers) APScheduler wrapper External
Persistence JSON file, SQLite, Redis, PostgreSQL, custom trait Pickle, Dict, custom Community crates
Webhook support axum tornado / starlette axum / warp
Type safety Compile-time Runtime (optional hints) Compile-time
Memory idle (measured) 15 MB 57 MB 15 MB
Memory under load (measured) 17 MB 60 MB 17 MB
Binary size (stripped) 6.2 MB Requires Python runtime 6.6 MB
Minimum version Rust 1.75 Python 3.10 Rust 1.68
Builder pattern IntoFuture (directly awaitable) Keyword args Method chains
Typed constants ParseMode::Html ParseMode.HTML String-based
Maturity v1.0.0-beta.4 (new) Mature (10+ years) Mature (3+ years)

Examples

The crates/telegram-bot/examples/ directory contains complete, runnable examples:

Example Description Python equivalent
echo_bot Echoes text messages back to the user echobot.py
webhook_bot Webhook mode with run_webhook() -- simplest production setup N/A
inline_keyboard Inline keyboard with callback queries inlinekeyboard.py
timer_bot Job queue: delayed messages, cancellation timerbot.py
conversation_bot Multi-step conversation with state machine conversationbot.py
raw_api_bot Direct Bot API usage without the ext framework N/A
context_types_bot Typed data access: bot_data, chat_data, user tracking contexttypesbot.py
custom_webhook_bot Custom axum routes alongside the Telegram webhook N/A
bench_bot Benchmark bot matching PTB/teloxide feature set N/A

Run any example:

TELEGRAM_BOT_TOKEN="your-token" cargo run -p rust-tg-bot --example echo_bot

Webhook examples require the webhooks feature:

TELEGRAM_BOT_TOKEN="your-token" WEBHOOK_URL="https://your.domain" \
    cargo run -p rust-tg-bot --example webhook_bot --features webhooks

Project Status

Current: v1.0.0-beta.4 -- API complete, stabilizing

What is implemented:

  • All Bot API 9.6 types and methods (281 types, 168 method builders)
  • ApplicationBuilder with typestate pattern
  • Typed handler system (CommandHandler, MessageHandler, FnHandler, and more)
  • 50+ composable filters with &, |, ^, ! operators
  • ConversationHandler with full state machine, timeouts, nesting, and persistence
  • JobQueue with one-shot, repeating, daily, and monthly scheduling (builder pattern)
  • JSON file, SQLite, Redis, and PostgreSQL persistence backends
  • Typed data access guards (DataReadGuard, DataWriteGuard)
  • Polling and webhook (axum) update delivery with optional TLS
  • Callback data caching
  • Rate limiter wired into request pipeline
  • Defaults system for parse mode, link preview, etc.
  • 90+ type constructors for ergonomic API type creation
  • Context shortcuts: reply_html, reply_photo, reply_document, reply_sticker, reply_location
  • answer_callback_query() and edit_callback_message_text() on Context
  • Arc dispatch with bounded update channel (capacity 64)
  • #[non_exhaustive] on all 344+ public types/enums for forward compatibility
  • #[derive(BotCommands)] proc macro for declarative command registration
  • #![warn(missing_docs)] on all crates

Build & Test

  • 385+ tests passing, zero clippy warnings
  • 25 roundtrip serialization tests, 9 proptest filter tests, 10 persistence stress tests
  • GitHub Actions CI: check, test, clippy, format, examples, docs (stable + MSRV 1.75)
  • Release pipeline: cross-compile binaries + crates.io publish
  • Measured performance: 6.2 MB binary (stripped), 15 MB idle / 17 MB RSS under load (release) -- matches teloxide, beats it on binary size

Forward Compatibility

Unlike python-telegram-bot's api_kwargs (which captures unknown JSON fields into a dict), RTB uses Rust's #[non_exhaustive] attribute on all 344+ public types and enums. This means:

  • New fields added by Telegram are silently dropped on deserialization until the library is updated.
  • New enum variants can be added without breaking downstream code.
  • Downstream crates cannot construct types via struct expressions — they must use constructors or serde_json::from_value().

This is a deliberate trade-off: #[non_exhaustive] provides compile-time forward compatibility guarantees that api_kwargs cannot, at the cost of not preserving unknown fields on roundtrip. For most bot use cases, unknown fields are irrelevant; if you need to inspect raw JSON, use serde_json::from_str::<serde_json::Value>() directly.

Roadmap

  • Publish to crates.io
  • Comprehensive cargo doc documentation with #[deny(missing_docs)]
  • Integration tests against real Bot API payloads
  • Benchmarks with criterion (throughput, memory, latency)
  • Webhook TLS auto-configuration
  • Passport decryption utilities
  • Payment flow helpers
  • Bot API forward-compatibility layer (auto-update from spec)

Documentation

Generate API docs locally:

cargo doc --open --no-deps

Contributing

Contributions of all sizes are welcome. Please see CONTRIBUTING.md for guidelines.

License

Licensed under the GNU Lesser General Public License v3.0.

You may copy, distribute, and modify the software provided that modifications are described and licensed for free under LGPL-3.0. Derivative works (including modifications or anything statically linked to the library) can only be redistributed under LGPL-3.0, but applications that use the library don't have to be.

Links