ferogram 0.6.4

Production-grade async Telegram MTProto client: updates, bots, flood-wait, dialogs, messages
Documentation

ferogram

Async Rust client for the Telegram MTProto API.

Crates.io docs.rs License TL Layer Telegram Channel Telegram Chat

Built by Ankit Chaubey

This is the main client crate. It talks to Telegram directly over MTProto, no Bot API proxy in between, and handles auth for both bots and user accounts from the same client builder. You get a dispatcher with composable filters, FSM for multi-step conversations, CDN downloads, middleware, and a raw invoke() escape hatch for anything not wrapped yet.

If you're starting fresh, this is the only crate you need. Everything else in the workspace exists to support it and can be pulled in separately if you need a specific layer on its own. Want the Bot API instead? Take a look at ferobot.

[!NOTE] ferogram is still in active development. It covers major use cases and runs in production, but the API may still shift.

Installation

[dependencies]
ferogram = "0.6.4"
tokio    = { version = "1", features = ["full"] }

Get api_id and api_hash from my.telegram.org.

Optional feature flags:

ferogram = { version = "0.6.4", features = [
    "sqlite-session",  # SqliteBackend via rusqlite
    "libsql-session",  # LibSqlBackend via libsql-client (Turso)
    "html",            # parse_html / generate_html (built-in parser)
    "html5ever",       # parse_html via spec-compliant html5ever
    "derive",          # #[derive(FsmState)]
    "serde",           # serde support on session types
] }

Quick start: bot

use ferogram::{Client, update::Update};

const API_ID: i32 = 0;
const API_HASH: &str = "";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let (client, _) = Client::quick_connect("bot.session", API_ID, API_HASH).await?;

    let mut stream = client.stream_updates();
    while let Some(upd) = stream.next().await {
        if let Update::NewMessage(msg) = upd {
            if !msg.outgoing() {
                msg.reply(msg.text().unwrap_or_default()).await.ok();
            }
        }
    }
    Ok(())
}

Quick start: user account

use ferogram::Client;

const API_ID: i32 = 0;
const API_HASH: &str = "";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let (client, _) = Client::quick_connect("my.session", API_ID, API_HASH).await?;

    client.send_message("me", "Hello from ferogram!").await?;
    Ok(())
}

Examples

19 runnable examples covering everything from sending a message to your Saved Messages to a full FSM order bot.

See examples/README.md for the full list with descriptions and notes on when to use quick_connect vs Client::builder().

Connecting

quick_connect is the fast path. For anything more specific, use the builder:

use ferogram::Client;

let (client, _shutdown) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .session("my.session")
    .catch_up(true)
    .connect()
    .await?;

.catch_up(true) replays missed updates after a reconnect, and .retry_policy(...) / .restart_policy(...) let you customize retry and reconnect behavior. Session storage, transport, and proxy options each have their own section below. Full docs at docs.rs/ferogram.

Dispatcher and filters

use ferogram::filters::{Dispatcher, command, private, text_contains, group, media};

let mut dp = Dispatcher::new();

dp.on_message(command("start"), |msg| async move {
    msg.reply("Hello!").await.ok();
});

dp.on_message(private() & text_contains("help"), |msg| async move {
    msg.reply("Type /start to begin.").await.ok();
});

dp.on_message(group() & media(), |msg| async move {
    // handle media in groups
});

while let Some(upd) = stream.next().await {
    dp.dispatch(upd).await;
}

Filters compose with &, |, !. Built-ins cover command, private, group, channel, text, text_contains, media, photo, document, forwarded, reply, from_user, album, custom, and more. Callback queries and inline queries route through the same dispatcher via on_callback_query / on_inline_query / on_inline_send.

Middleware

dp.middleware(|upd, next| async move {
    tracing::info!("incoming update");
    let result = next.run(upd).await;
    tracing::info!("handler done");
    result
});

Runs in registration order. Call next.run(upd) to pass control forward, or return early to stop the chain.

FSM

use ferogram::{FsmState, fsm::MemoryStorage};
use std::sync::Arc;

#[derive(FsmState, Clone, Debug, PartialEq)]
enum Form { Name, Age }

dp.with_state_storage(Arc::new(MemoryStorage::new()));

dp.on_message_fsm(text(), Form::Name, |msg, state| async move {
    state.set_data("name", msg.text().unwrap()).await.ok();
    state.transition(Form::Age).await.ok();
    msg.reply("How old are you?").await.ok();
});

MemoryStorage is built in. To persist state across restarts, implement StateStorage for Redis, a database, or anything else. State keys scope per-user, per-chat, or per-user-in-chat via StateKeyStrategy. See ferogram-fsm for details.

Session backends

Client::builder().session("bot.session")                                              // binary file (default)
Client::builder().in_memory()                                                         // no persistence
Client::builder().session_string(env::var("SESSION")?)                               // base64 string
Client::builder().session_backend(Arc::new(SqliteBackend::open("s.db")?))            // sqlite
Client::builder().session_backend(Arc::new(LibSqlBackend::remote(url, token).await?)) // turso

The base64 string backend is useful for serverless or containers where writing to disk isn't an option. To bring your own, implement SessionBackend from ferogram-session.

Transport and proxy

use ferogram::TransportKind;

Client::builder().transport(TransportKind::Abridged)    // default
Client::builder().transport(TransportKind::Obfuscated)  // DPI bypass, plain MTProxy secrets
Client::builder().transport(TransportKind::FakeTls)     // TLS camouflage, 0xee secrets

// MTProxy from a t.me link
Client::builder().proxy_link("https://t.me/proxy?server=HOST&port=PORT&secret=SECRET")

// SOCKS5
Client::builder().socks5("127.0.0.1:1080")

// Race transports, use first to connect
Client::builder().probe_transport(true)

// Fall back through DoH + Telegram special-config if TCP is blocked
Client::builder().resilient_connect(true)

See ferogram-connect for the framing layer underneath.

Raw API

When the high-level API isn't enough, client.invoke() takes any TL function directly (current layer exposed as tl::LAYER). It's the escape hatch, not the normal path, but it's always there:

use ferogram::tl;

let req = tl::functions::bots::SetBotCommands {
    scope: tl::enums::BotCommandScope::Default(tl::types::BotCommandScopeDefault {}),
    lang_code: "en".into(),
    commands: vec![tl::enums::BotCommand::BotCommand(tl::types::BotCommand {
        command: "start".into(),
        description: "Start the bot".into(),
    })],
};
client.invoke(&req).await?;
client.invoke_on_dc(2, &req).await?;  // target a specific DC

See ferogram-tl-types for all generated types and functions.

Error handling

use ferogram::{InvocationError, RpcError};

match client.send_message("@peer", "Hi").await {
    Ok(()) => {}
    Err(InvocationError::Rpc(RpcError { code, message, .. })) => {
        eprintln!("Telegram error {code}: {message}");
    }
    Err(InvocationError::Io(e)) => eprintln!("I/O: {e}"),
    Err(e) => eprintln!("{e}"),
}

FLOOD_WAIT is handled automatically. To disable it:

use ferogram::retry::NoRetries;
Client::builder().retry_policy(Arc::new(NoRetries))

Shutdown

let (client, shutdown) = Client::builder()...connect().await?;

shutdown.cancel();   // graceful
client.disconnect(); // immediate

What's covered

See FEATURES.md for the full list with method signatures. If something is missing, open a feature request or drop by t.me/FerogramChat. the raw API is always there in the meantime.

Secret chats (end-to-end encrypted) are fully implemented but not published to crates.io yet. The plan is to release once there is enough community demand for it.

Voice and video calls

Group audio, video, and P2P calling are now fully implemented. To get started, check out the tgcalls crate and its examples in the tgcalls repository. It provides seamless integration between ferogram and the official ntgcalls Rust bindings for building Telegram voice and video calling applications.

Crates

Most people only need this crate. But each crate in the workspace is independently publishable if you need just one layer.

Crate What it does
ferogram High-level client. Auth, messaging, media, dispatcher, FSM, middleware.
ferogram-session Session types and pluggable storage backends (file, memory, SQLite, LibSQL, base64).
ferogram-fsm FSM state storage and context. StateStorage trait, MemoryStorage, StateContext.
ferogram-parsers Telegram Markdown and HTML entity parsers.
ferogram-derive #[derive(FsmState)] proc macro.
ferogram-mtsender DC connection pool and retry policy. AutoSleep, NoRetries, CircuitBreaker.
ferogram-connect Raw TCP, MTProto framing, obfuscation, SOCKS5, MTProxy, gzip.
ferogram-mtproto MTProto 2.0 session, DH key exchange, message framing, PFS key binding.
ferogram-crypto AES-IGE, RSA, SHA, Diffie-Hellman, PQ factorization, auth key derivation.
ferogram-tl-types Auto-generated TL types, functions, and enums (tracks tl::LAYER).
ferogram-tl-gen Build-time code generator from TL AST to Rust source.
ferogram-tl-parser Parses .tl schema text into a Definition AST.

The rough dependency chain (build-critical path only):

ferogram
└ ferogram-mtsender
  └ ferogram-connect
    ├ ferogram-mtproto
    │ ├ ferogram-tl-types
    │ │ └ (build) ferogram-tl-gen
    │ │   └ (build) ferogram-tl-parser
    │ └ ferogram-crypto
    └ ferogram-crypto

Testing

cargo test --workspace
cargo test --workspace --all-features

Community

License

MIT OR Apache-2.0. See LICENSE-MIT and LICENSE-APACHE.

Usage must comply with Telegram's API Terms of Service.