layer 0.4.5

Ergonomic Telegram MTProto library โ€” auto-generated TL types, raw API access, session management
Documentation

โšก layer

A modular, production-grade async Rust library for the Telegram MTProto protocol.

Developed By Ankit Chaubey

Built with curiosity, caffeine, and a lot of Rust compiler errors ๐Ÿฆ€

GitHub Website

Crates.io Downloads docs.rs Guide

License Rust 2024 TL Layer Tokio Build PRs Welcome

Telegram Channel Telegram Chat

Pre-production (0.x.x) โ€” APIs may change between minor versions. Review the CHANGELOG before upgrading.


Table of Contents


๐Ÿงฉ What is layer?

layer is a hand-crafted, bottom-up async Rust implementation of the Telegram MTProto protocol.

Every core piece โ€” the .tl schema parser, the AES-IGE cipher, the Diffie-Hellman key exchange, the MTProto session, the async typed update stream โ€” is written from scratch, owned by this project, and fully understood. The async runtime and a handful of well-known utilities (tokio, flate2, getrandom) come from the ecosystem, because that's good engineering.

The goal was never "yet another Telegram SDK." It was: what happens if you sit down and build every piece yourself, and truly understand why it works?


๐Ÿ’ก What makes layer unique?

Most Telegram libraries are thin wrappers around generated code or ports from other languages. layer is different.

Built from first principles. The .tl schema parser, the AES-IGE cipher, the Diffie-Hellman key exchange, and the MTProto framing are all implemented from scratch โ€” not borrowed from a C++ library or wrapped behind FFI. Every algorithm is understood and owned by this project.

Modular workspace architecture. layer is not a monolith. Each concern lives in its own focused crate: schema parsing, code generation, cryptographic primitives, the protocol session, and the high-level client are all separate, versioned, independently usable pieces.

A full escape hatch. Every one of Telegram's 2,329 Layer 224 API methods is accessible via client.invoke() with the fully-typed TL schema โ€” even if no high-level wrapper exists yet. You never hit a wall.

Unique session flexibility. layer ships with binary file, in-memory, string (base64), SQLite, and libsql/Turso session backends out of the box โ€” and supports custom SessionBackend implementations for any other storage (Redis, Postgres, S3, etc.).

Android / Termux tested. The reconnect logic, backoff parameters, and socket handling are tuned for mobile conditions. layer is actively developed and tested on Android via Termux.

No unsafe, pure async Rust. The entire stack from cryptographic primitives to the high-level client is safe Rust, running on Tokio.


๐Ÿ—๏ธ Crate Overview

layer is a workspace of focused crates. Most users only ever need layer-client.

Crate Version Description
layer-client crates.io High-level async client: auth, send, receive, media, bots
layer-tl-types crates.io All Layer 224 constructors, functions, and enums (2,329 definitions)
layer-mtproto crates.io MTProto session, DH exchange, message framing, transports
layer-crypto crates.io AES-IGE, RSA, SHA, Diffie-Hellman, auth key derivation
layer-tl-gen crates.io Build-time Rust code generator from the TL AST
layer-tl-parser crates.io Parses .tl schema text into an AST
layer-app โŒ Interactive demo binary (not published)
layer-connect โŒ Raw DH connection demo (not published)
layer/
โ”œโ”€โ”€ layer-tl-parser/      .tl schema text โ†’ AST
โ”œโ”€โ”€ layer-tl-gen/         AST โ†’ Rust source (build-time codegen)
โ”œโ”€โ”€ layer-tl-types/       Auto-generated types, functions & enums (Layer 224)
โ”œโ”€โ”€ layer-crypto/         AES-IGE, RSA, SHA, auth key derivation, PQ factorization
โ”œโ”€โ”€ layer-mtproto/        MTProto session, DH handshake, framing, transport
โ”œโ”€โ”€ layer-client/         High-level async Client API  โ† you are here
โ”œโ”€โ”€ layer-connect/        Demo: raw DH + getConfig
โ””โ”€โ”€ layer-app/            Demo: interactive login + update stream

The full API reference lives at docs.rs/layer-client. The narrative guide lives at github.ankitchaubey.in/layer.


๐Ÿ“ฆ Installation

Add to your Cargo.toml:

[dependencies]
layer-client = "0.4.5"
tokio        = { version = "1", features = ["full"] }

Get your api_id and api_hash from my.telegram.org โ€” every Telegram client needs them.

Optional feature flags:

# SQLite session persistence (stores auth key in a local .db file)
layer-client = { version = "0.4.5", features = ["sqlite-session"] }

# libsql / Turso remote or embedded database session
layer-client = { version = "0.4.5", features = ["libsql-session"] }

# Hand-rolled HTML entity parser (parse_html / generate_html)
layer-client = { version = "0.4.5", features = ["html"] }

# Spec-compliant html5ever tokenizer โ€” replaces the built-in html parser
layer-client = { version = "0.4.5", features = ["html5ever"] }

Note: layer-client re-exports layer_tl_types as layer_client::tl, so you usually do not need to add layer-tl-types as a direct dependency.


โšก The Minimal Bot โ€” 15 Lines

This is the least code you need to have a working, update-receiving Telegram bot running with layer.

use layer_client::{Client, Config, update::Update};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let (client, _shutdown) = Client::connect(Config {
        session_path: "bot.session".into(),
        api_id:   std::env::var("API_ID")?.parse()?,
        api_hash: std::env::var("API_HASH")?,
        ..Default::default()
    }).await?;

    client.bot_sign_in(&std::env::var("BOT_TOKEN")?).await?;
    client.save_session().await?;

    let mut stream = client.stream_updates();
    while let Some(Update::NewMessage(msg)) = stream.next().await {
        if let (false, Some(text), Some(peer)) = (msg.outgoing(), msg.text(), msg.peer_id()) {
            client.send_message_to_peer(peer.clone(), &format!("Echo: {text}")).await?;
        }
    }
    Ok(())
}

No trait objects, no callbacks, no dyn Handler. Just an async loop and pattern matching. That's the whole bot.

๐Ÿ“– Read more in the Bot Quick Start guide โ†’


๐Ÿ‘ค Quick Start โ€” User Account

use layer_client::{Client, Config, SignInError};
use std::io::{self, BufRead};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (client, _shutdown) = Client::connect(Config {
        session_path: "my.session".into(),
        api_id:       12345,
        api_hash:     "your_api_hash".into(),
        ..Default::default()
    })
    .await?;

    if !client.is_authorized().await? {
        let phone = "+1234567890";
        let token = client.request_login_code(phone).await?;

        print!("Enter code: ");
        let stdin = io::stdin();
        let code  = stdin.lock().lines().next().unwrap()?;

        match client.sign_in(&token, &code).await {
            Ok(name) => println!("Welcome, {name}!"),
            Err(SignInError::PasswordRequired(t)) => {
                // 2FA โ€” read password and call check_password
                client.check_password(*t, "my_2fa_password").await?;
            }
            Err(e) => return Err(e.into()),
        }
        client.save_session().await?;
    }

    let me = client.get_me().await?;
    println!("Logged in as: {}", me.first_name.unwrap_or_default());

    // Send a message to Saved Messages
    client.send_message("me", "Hello from layer! ๐Ÿ‘‹").await?;

    // Or send to any peer
    client.send_message_to_peer("@username", "Hello!").await?;

    Ok(())
}

After the first successful login the session is persisted to my.session. Subsequent runs skip the phone/code flow entirely.

๐Ÿ“– Full user account guide โ†’


๐Ÿค– Quick Start โ€” Bot

use layer_client::{Client, Config, update::Update};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let (client, _shutdown) = Client::connect(Config {
        session_path: "bot.session".into(),
        api_id:       12345,
        api_hash:     "your_api_hash".into(),
        ..Default::default()
    })
    .await?;

    if !client.is_authorized().await? {
        client.bot_sign_in("1234567890:ABCdef...").await?;
        client.save_session().await?;
    }

    let me = client.get_me().await?;
    println!("@{} is online", me.username.as_deref().unwrap_or("bot"));

    let mut stream = client.stream_updates();
    while let Some(update) = stream.next().await {
        match update {
            Update::NewMessage(msg) if !msg.outgoing() => {
                if let Some(peer) = msg.peer_id() {
                    client
                        .send_message_to_peer(
                            peer.clone(),
                            &format!("You said: {}", msg.text().unwrap_or("")),
                        )
                        .await?;
                }
            }
            Update::CallbackQuery(cb) => {
                client
                    .answer_callback_query(cb.query_id, Some("โœ… Done!"), false)
                    .await?;
            }
            _ => {}
        }
    }
    Ok(())
}

Spawning per-update tasks

For production bots the update loop should never block. Spawn each update into its own task:

use layer_client::{Client, update::Update};
use std::sync::Arc;

// Wrap in Arc so it can be moved into spawned tasks
let client = Arc::new(client);
let mut stream = client.stream_updates();

while let Some(update) = stream.next().await {
    let c = client.clone();
    tokio::spawn(async move {
        if let Err(e) = handle_update(update, &c).await {
            eprintln!("handler error: {e}");
        }
    });
}

async fn handle_update(
    update: Update,
    client: &Client,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    match update {
        Update::NewMessage(msg) if !msg.outgoing() => {
            if let Some(peer) = msg.peer_id() {
                client.send_message_to_peer(peer.clone(), "๐Ÿ‘‹").await?;
            }
        }
        _ => {}
    }
    Ok(())
}

๐Ÿ“– Full production bot guide โ†’


๐Ÿ”จ ClientBuilder

The fluent ClientBuilder is the cleanest way to configure a connection when you need more than defaults:

use layer_client::Client;

let (client, _shutdown) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .session("my.session")          // BinaryFileBackend at this path
    .catch_up(true)                 // replay missed updates on reconnect
    .connect()
    .await?;

Use .session_string(s) for portable base64 sessions (no file on disk):

let session = std::env::var("SESSION").unwrap_or_default();

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

Use .socks5(host, port) for a proxy:

let (client, _shutdown) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .session("proxy.session")
    .socks5("127.0.0.1", 1080)
    .connect()
    .await?;

๐Ÿ“– ClientBuilder reference โ†’


๐Ÿ”‘ String Sessions โ€” Portable Auth

A string session encodes the entire auth state (auth key, DC, peer cache) into a single printable base64 string. Store it in an environment variable, a database column, a secret manager โ€” anywhere.

// โ”€โ”€ Export from any running client โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
let session_string = client.export_session_string().await?;
println!("{session_string}");  // save this somewhere safe

// โ”€โ”€ Restore later โ€” no phone/code needed โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
let (client, _shutdown) = Client::with_string_session(&session_string).await?;

// Or via builder
let (client, _shutdown) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .session_string(session_string)
    .connect()
    .await?;

String sessions are ideal for serverless deployments, CI/CD bots, and any environment where writing files is inconvenient.

๐Ÿ“– Session backends guide โ†’


๐Ÿ“ก Update Stream

client.stream_updates() returns an UpdateStream that yields typed updates:

let mut stream = client.stream_updates();
while let Some(update) = stream.next().await {
    // ...
}

stream_updates() is cheap and can be called multiple times. Each call returns an independent receiver. Use Arc<Client> and clone it into spawned tasks.

Update variants

use layer_client::update::Update;

match update {
    // โ”€โ”€ Messages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    Update::NewMessage(msg)     => { /* new incoming message */ }
    Update::MessageEdited(msg)  => { /* existing message was edited */ }
    Update::MessageDeleted(del) => { /* one or more messages were deleted */ }

    // โ”€โ”€ Bot interactions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    Update::CallbackQuery(cb)   => { /* inline button was pressed */ }
    Update::InlineQuery(iq)     => { /* @bot query in inline mode */ }
    Update::InlineSend(is)      => { /* user selected an inline result */ }

    // โ”€โ”€ Presence โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    Update::UserTyping(action)  => { /* typing / uploading / recording */ }
    Update::UserStatus(status)  => { /* contact went online / offline */ }

    // โ”€โ”€ Raw passthrough โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    Update::Raw(raw)            => { /* any unmapped TL update */ }

    _ => {}  // Update is #[non_exhaustive] โ€” always add a fallback
}

Important: Update is #[non_exhaustive]. Always include _ => {} to stay forward-compatible as new variants are added.

IncomingMessage API

IncomingMessage is the type of NewMessage and MessageEdited:

Update::NewMessage(msg) => {
    msg.id()          // i32 โ€” unique message ID in the chat
    msg.text()        // Option<&str> โ€” text or caption
    msg.peer_id()     // Option<&tl::enums::Peer> โ€” the chat this message is in
    msg.sender_id()   // Option<&tl::enums::Peer> โ€” who sent it
    msg.outgoing()    // bool โ€” was this sent by us?
    msg.date()        // i32 โ€” Unix timestamp
    msg.edit_date()   // Option<i32> โ€” last edit timestamp
    msg.mentioned()   // bool โ€” are we mentioned?
    msg.silent()      // bool โ€” no notification?
    msg.pinned()      // bool โ€” is the message currently pinned?
    msg.post()        // bool โ€” is this a channel post (no sender)?
    msg.raw           // tl::enums::Message โ€” full TL object for everything else
}

๐Ÿ“– Incoming message reference โ†’


๐Ÿ’ฌ Messaging

Send text

The simplest send methods accept any impl Into<PeerRef> โ€” a &str username, "me" for Saved Messages, a tl::enums::Peer clone, or a numeric ID:

// By username
client.send_message("@username", "Hello!").await?;

// To Saved Messages
client.send_message("me", "Note to self").await?;

// By TL Peer (from an incoming message)
if let Some(peer) = msg.peer_id() {
    client.send_message_to_peer(peer.clone(), "Reply!").await?;
}

// To self โ€” shorthand for "me"
client.send_to_self("Reminder: buy milk ๐Ÿฅ›").await?;

InputMessage builder

InputMessage gives you full control over every send option:

use layer_client::{InputMessage, parsers::parse_markdown};
use layer_client::keyboard::InlineKeyboard;

let (text, entities) = parse_markdown("**Bold** and `code`");

let kb = InlineKeyboard::new()
    .row()
    .callback("โœ… Confirm", b"confirm")
    .url("๐Ÿ”— Docs", "https://docs.rs/layer-client")
    .build();

client
    .send_message_to_peer_ex(
        peer.clone(),
        &InputMessage::text(text)
            .entities(entities)         // formatted text
            .reply_to(Some(msg_id))     // reply to a specific message
            .silent(true)               // no notification
            .no_webpage(true)           // suppress link preview
            .keyboard(kb),              // attach inline keyboard
    )
    .await?;

Edit, forward, delete

// Edit
client.edit_message(peer.clone(), message_id, "Updated text").await?;

// Forward messages between peers
client.forward_messages(
    from_peer.clone(),
    to_peer.clone(),
    &[message_id_1, message_id_2],
).await?;

// Delete (also removes from the other side if you have permission)
client.delete_messages(peer.clone(), &[message_id]).await?;

Pin and unpin

// Pin a message (notify: true sends a "pinned message" service message)
client.pin_message(peer.clone(), message_id, true).await?;

// Get the current pinned message
let pinned = client.get_pinned_message(peer.clone()).await?;

// Unpin a specific message
client.unpin_message(peer.clone(), message_id).await?;

// Unpin all at once
client.unpin_all_messages(peer.clone()).await?;

Scheduled messages

use std::time::{SystemTime, UNIX_EPOCH};

// Schedule for 1 hour from now
let schedule_ts = (SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap()
    .as_secs() + 3600) as i32;

client
    .send_message_to_peer_ex(
        peer.clone(),
        &InputMessage::text("Reminder! โฐ").schedule_date(Some(schedule_ts)),
    )
    .await?;

// List all scheduled messages in a chat
let scheduled = client.get_scheduled_messages(peer.clone()).await?;

// Cancel a scheduled message
client.delete_scheduled_messages(peer.clone(), &[scheduled_msg_id]).await?;

Chat actions and typing

use layer_tl_types as tl;

// Start a "typing..." indicator
client.send_chat_action(
    peer.clone(),
    tl::enums::SendMessageAction::SendMessageTypingAction,
    None,  // top_msg_id โ€” None for normal chats, Some(id) for forum topics
).await?;

// Mark all messages as read
client.mark_as_read(peer.clone()).await?;

// Clear all @mention badges
client.clear_mentions(peer.clone()).await?;

๐Ÿ“– Full messaging reference โ†’


๐Ÿ“Ž Media

Upload

use layer_client::media::UploadedFile;

// Upload from bytes โ€” small files sequentially
let uploaded: UploadedFile = client
    .upload_file("photo.jpg", file_bytes.as_ref())
    .await?;

// Upload from bytes โ€” parallel chunks (faster for large files)
let uploaded = client
    .upload_file_concurrent("video.mp4", video_bytes.as_ref())
    .await?;

// Upload from an async reader (e.g. a file on disk)
use tokio::fs::File;
let f = File::open("document.pdf").await?;
let uploaded = client
    .upload_stream("document.pdf", f)
    .await?;

// Send the uploaded file to a peer
client.send_file(peer.clone(), uploaded, /* as_photo */ false).await?;

// Send multiple files as an album in one call
client.send_album(peer.clone(), vec![uploaded_a, uploaded_b]).await?;

Download

// Download directly to a file path (streaming, no full memory buffer)
client
    .download_media_to_file(&message_media, "output.jpg")
    .await?;

// Download to Vec<u8> โ€” sequential
let bytes: Vec<u8> = client.download_media(&message_media).await?;

// Download to Vec<u8> โ€” parallel chunks
let bytes: Vec<u8> = client.download_media_concurrent(&message_media).await?;

// Use the Downloadable trait for Photos, Documents, Stickers
use layer_client::media::{Photo, Downloadable};
let photo = Photo::from_message(&msg.raw)?;
let bytes = client.download(&photo).await?;

๐Ÿ“– Media guide โ†’


โŒจ๏ธ Keyboards and Reply Markup

Inline keyboards

use layer_client::keyboard::InlineKeyboard;

let kb = InlineKeyboard::new()
    .row()
        .callback("๐Ÿ‘ Like",    b"like")
        .callback("๐Ÿ‘Ž Dislike", b"dislike")
    .row()
        .url("๐Ÿ”— Open docs", "https://docs.rs/layer-client")
        .switch_inline("๐Ÿ” Search", "query")
    .build();

client
    .send_message_to_peer_ex(peer.clone(), &InputMessage::text("Vote!").keyboard(kb))
    .await?;

Available button types: callback, url, url_auth, switch_inline, switch_elsewhere, webview, simple_webview, request_phone, request_geo, request_poll, request_quiz, game, buy, copy_text.

Reply keyboards

use layer_client::keyboard::ReplyKeyboard;

let kb = ReplyKeyboard::new()
    .row()
        .text("๐Ÿ“ธ Photo")
        .text("๐Ÿ“„ Document")
    .row()
        .text("โŒ Cancel")
    .resize(true)
    .single_use(true)
    .build();

client
    .send_message_to_peer_ex(peer.clone(), &InputMessage::text("Choose:").keyboard(kb))
    .await?;

Answer callback queries

Update::CallbackQuery(cb) => {
    let data = cb.data().unwrap_or("");
    match data {
        b"like"    => client.answer_callback_query(cb.query_id, Some("โค๏ธ Liked!"), false).await?,
        b"dislike" => client.answer_callback_query(cb.query_id, Some("๐Ÿ‘Ž Noted"),   false).await?,
        _          => client.answer_callback_query(cb.query_id, None,              false).await?,
    }
}

Pass alert: true as the third argument to show a popup alert instead of a toast.

Inline mode

use layer_tl_types as tl;

Update::InlineQuery(iq) => {
    let q   = iq.query().to_string();
    let qid = iq.query_id;

    let results = vec![
        tl::enums::InputBotInlineResult::InputBotInlineResult(
            tl::types::InputBotInlineResult {
                id: "1".into(), r#type: "article".into(),
                title: Some("Result title".into()),
                description: Some(q.clone()),
                url: None, thumb: None, content: None,
                send_message: tl::enums::InputBotInlineMessage::Text(
                    tl::types::InputBotInlineMessageText {
                        no_webpage: false, invert_media: false,
                        message: q, entities: None, reply_markup: None,
                    },
                ),
            },
        ),
    ];

    // cache_time: 30s, is_personal: false, next_offset: None
    client.answer_inline_query(qid, results, 30, false, None).await?;
}

๐Ÿ“– Keyboards guide โ†’


๐Ÿ–Š๏ธ Text Formatting

Markdown

use layer_client::parsers::{parse_markdown, generate_markdown};

// Parse markdown โ†’ plain text + message entities
let (text, entities) = parse_markdown("**Bold**, `code`, _italic_, [link](https://example.com)");

// Send with formatting
client
    .send_message_to_peer_ex(
        peer.clone(),
        &InputMessage::text(text).entities(entities),
    )
    .await?;

// Go the other way: entities + plain text โ†’ markdown string
let md = generate_markdown(&plain_text, &entities);

HTML

Enable the html or html5ever feature flag:

layer-client = { version = "0.4.5", features = ["html"] }
use layer_client::parsers::{parse_html, generate_html};

let (text, entities) = parse_html("<b>Bold</b> and <code>monospace</code>");

client
    .send_message_to_peer_ex(peer.clone(), &InputMessage::text(text).entities(entities))
    .await?;

// Always available, no feature flag needed
let html_str = generate_html(&plain_text, &entities);

๐Ÿ“– Formatting reference โ†’


๐Ÿ’ฅ Reactions

InputReactions is the typed builder for reactions:

use layer_client::reactions::InputReactions;

// Single emoji reaction
client.send_reaction(peer.clone(), message_id, InputReactions::emoticon("๐Ÿ‘")).await?;

// Custom premium emoji
client.send_reaction(peer.clone(), message_id, InputReactions::custom_emoji(1234567890)).await?;

// Big animated reaction
client.send_reaction(peer.clone(), message_id, InputReactions::emoticon("๐Ÿ”ฅ").big()).await?;

// Remove all reactions
client.send_reaction(peer.clone(), message_id, InputReactions::remove()).await?;

๐Ÿ“– Reactions guide โ†’


โŒ› Typing Guard (RAII)

TypingGuard is a RAII wrapper that automatically starts and stops typing/uploading indicators:

use layer_client::TypingGuard;
use layer_tl_types as tl;

async fn handle_long_task(client: &Client, peer: tl::enums::Peer) -> anyhow::Result<()> {
    // Typing indicator starts immediately and is renewed every ~4 seconds
    let _typing = TypingGuard::start(
        client,
        peer.clone(),
        tl::enums::SendMessageAction::SendMessageTypingAction,
    )
    .await?;

    // Do expensive work โ€” user sees "typing..."
    do_expensive_work().await;

    // _typing is dropped here โ†’ Telegram sees the indicator stop
    Ok(())
}

Convenience constructors for common actions:

// Typing
let _t = client.typing(peer.clone()).await?;

// Uploading document
let _t = client.uploading_document(peer.clone()).await?;

// Recording video
let _t = client.recording_video(peer.clone()).await?;

// Typing in a specific forum topic
let _t = client.typing_in_topic(peer.clone(), topic_id).await?;

๐Ÿ“– Typing guard reference โ†’


๐Ÿ‘ฅ Participants and Chat Management

Fetch participants

use layer_client::participants::Participant;

// Fetch up to N participants at once
let participants: Vec<Participant> = client.get_participants(peer.clone(), 100).await?;

// Paginated lazy iterator โ€” works for very large groups
let mut iter = client.iter_participants(peer.clone());
while let Some(p) = iter.next(&client).await? {
    println!("{}", p.user.first_name.as_deref().unwrap_or(""));
}

// Search within a group
let results = client.search_peer(peer.clone(), "John").await?;

Ban, kick, promote

use layer_client::participants::{BanRights, AdminRightsBuilder};

// Kick (ban + immediate unban)
client.kick_participant(peer.clone(), user_id).await?;

// Ban with custom rights and optional expiry
client
    .ban_participant(
        peer.clone(),
        user_id,
        BanRights::new()
            .no_messages(true)
            .no_media(true)
            .until(expiry_unix_timestamp),
    )
    .await?;

// Promote to admin with specific rights
client
    .promote_participant(
        peer.clone(),
        user_id,
        AdminRightsBuilder::new()
            .post_messages(true)
            .delete_messages(true)
            .ban_users(true)
            .title("Moderator"),
    )
    .await?;

// Get a user's current permissions in a channel
let perms = client.get_permissions(peer.clone(), user_id).await?;

Profile photos

// Fetch the first page of profile photos
let photos = client.get_profile_photos(user_id, 0, 10).await?;

// Lazy iterator across all pages
let mut iter = client.iter_profile_photos(user_id);
while let Some(photo) = iter.next(&client).await? {
    let bytes = client.download(&photo).await?;
}

Join and leave

// Join a public group or channel by username
client.join_chat("@somegroup").await?;

// Accept a private invite link
client.accept_invite_link("https://t.me/joinchat/AbCdEfG...").await?;

// Leave and delete a dialog from the dialog list
client.delete_dialog(peer.clone()).await?;

๐Ÿ“– Participants guide โ†’


๐Ÿ” Search

In-chat search

SearchBuilder is a chainable builder for messages.search:

use layer_tl_types::enums::MessagesFilter;

let results = client
    .search(peer.clone(), "hello world")
    .min_date(1_700_000_000)
    .max_date(1_720_000_000)
    .filter(MessagesFilter::InputMessagesFilterPhotos)
    .limit(50)
    .fetch(&client)
    .await?;

for msg in results {
    println!("[{}] {}", msg.id, msg.message);
}

Global search

GlobalSearchBuilder searches across all chats:

let results = client
    .search_global_builder("rust async")
    .broadcasts_only(true)       // channels only
    .min_date(1_700_000_000)
    .limit(30)
    .fetch(&client)
    .await?;

๐Ÿ“– Search guide โ†’


๐Ÿ“œ Dialogs and Iterators

// Fetch the first N dialogs
let dialogs = client.get_dialogs(50).await?;
for d in &dialogs {
    println!("{} โ€” {} unread", d.title(), d.unread_count());
}

// Lazy dialog iterator (all dialogs, paginated)
let mut iter = client.iter_dialogs();
while let Some(dialog) = iter.next(&client).await? {
    println!("{}", dialog.title());
}

// Lazy message iterator for a specific peer
let mut iter = client.iter_messages(peer.clone());
while let Some(msg) = iter.next(&client).await? {
    println!("{}", msg.message);
}

// Fetch messages by ID
let messages = client.get_messages_by_id(peer.clone(), &[100, 101, 102]).await?;

// Fetch the latest N messages from a peer
let messages = client.get_messages(peer.clone(), 20).await?;

๐Ÿ“– Dialogs guide โ†’


๐Ÿ”— Peer Resolution

// Resolve any string (username, phone number, "me") to a TL Peer
let peer = client.resolve_peer("@telegram").await?;
let peer = client.resolve_peer("+1234567890").await?;
let peer = client.resolve_peer("me").await?;

// Resolve just the username part (without @)
let peer = client.resolve_username("telegram").await?;

Access hash caching is handled automatically. Once a peer is resolved its access hash is stored in the session and reused on all subsequent calls โ€” no need to manage it yourself.


๐Ÿ’พ Session Backends

layer ships with multiple session backends. They all implement the SessionBackend trait and are hot-swappable.

Backend Feature flag Best for
BinaryFileBackend (default) Single-process bots, scripts
InMemoryBackend (default) Tests, ephemeral tasks
StringSessionBackend (default) Serverless, env-var storage, CI bots
SqliteBackend sqlite-session Multi-session local apps
LibSqlBackend libsql-session Distributed / Turso-backed storage
Custom โ€” Implement SessionBackend for anything
use layer_client::session_backend::{SqliteBackend, SessionBackend};

// SQLite backend
let backend = SqliteBackend::new("sessions.db").await?;

let (client, _shutdown) = Client::connect(Config {
    session_backend: Box::new(backend),
    api_id:  12345,
    api_hash: "your_api_hash".into(),
    ..Default::default()
}).await?;
// Implement your own โ€” Redis, Postgres, S3, anything
use layer_client::session_backend::SessionBackend;

struct RedisBackend { /* ... */ }

#[async_trait::async_trait]
impl SessionBackend for RedisBackend {
    async fn load(&self) -> anyhow::Result<Option<Vec<u8>>> { /* ... */ }
    async fn save(&self, data: &[u8]) -> anyhow::Result<()> { /* ... */ }
}

๐Ÿ“– Session backends guide โ†’


๐Ÿ”ง Feature Flags

layer-tl-types

Flag Default Description
tl-api โœ… High-level Telegram API schema (api.tl)
tl-mtproto โŒ Low-level MTProto schema (mtproto.tl)
impl-debug โœ… #[derive(Debug)] on all generated types
impl-from-type โœ… From<types::T> for enums::E on all constructors
impl-from-enum โœ… TryFrom<enums::E> for types::T on all constructors
name-for-id โŒ name_for_id(u32) -> Option<&'static str> lookup table
impl-serde โŒ serde::Serialize + Deserialize on all types

layer-client

Flag Default Description
html โŒ Hand-rolled HTML parser (parse_html, generate_html)
html5ever โŒ Spec-compliant html5ever tokenizer, replaces the built-in parser
sqlite-session โŒ SQLite session backend (SqliteBackend)
libsql-session โŒ libsql / Turso session backend (LibSqlBackend)

๐Ÿ”ฉ Raw API Escape Hatch

Every Telegram method in Layer 224 is available via the raw invoke API, even if it has no high-level wrapper yet. The full type-safe schema is available as layer_client::tl (re-exported from layer-tl-types).

use layer_client::tl;

// Set the bot's command list โ€” no wrapper yet, use raw invoke
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?;
// Update profile info
let req = tl::functions::account::UpdateProfile {
    first_name: Some("Alice".into()),
    last_name:  None,
    about:      Some("layer user ๐Ÿฆ€".into()),
};
client.invoke(&req).await?;
// Send to a specific DC (useful for cross-DC file downloads)
client.invoke_on_dc(&req, 2).await?;

Any method listed in the Telegram API documentation can be invoked this way. Layer 224 includes 2,329 TL constructors and all RPC functions.

๐Ÿ“– Raw API guide โ†’


๐Ÿš‚ Transports

Three MTProto transport encodings are supported:

Transport Description When to use
Abridged Single-byte length prefix, lowest overhead Default โ€” best for most setups
Intermediate 4-byte LE length prefix Better compatibility with some proxies
Obfuscated2 XOR stream cipher over Abridged DPI bypass, MTProxy, restricted networks
use layer_client::{Client, TransportKind};

// Switch to Obfuscated2 (DPI bypass)
let (client, _) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .session("obfuscated.session")
    .transport(TransportKind::Obfuscated)
    .connect()
    .await?;

๐Ÿ“– Transport reference โ†’


๐ŸŒ Networking โ€” SOCKS5 and DC Pool

SOCKS5 proxy

use layer_client::{Client, Socks5Config};

// Without auth
let (client, _) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .session("proxy.session")
    .socks5("127.0.0.1", 1080)
    .connect()
    .await?;

// With username/password
let proxy = Socks5Config::with_auth("proxy.host", 1080, "user", "pass");
let (client, _) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .socks5_config(proxy)
    .connect()
    .await?;

DC pool and multi-DC

Auth keys are stored per datacenter and connections are created on demand. When Telegram responds with PHONE_MIGRATE_*, USER_MIGRATE_*, or NETWORK_MIGRATE_*, the client migrates automatically. You can also target a specific DC directly:

// Force a request to DC 2
client.invoke_on_dc(&req, 2).await?;

Reconnect and keepalive

The client reconnects automatically after network failures using exponential backoff with 20% jitter, capped at 5 seconds (tuned for mobile / Android conditions). Pings are sent every 60 seconds. To skip the backoff after a known-good network event:

// Call this when your app detects the network is back
client.signal_network_restored();

โš ๏ธ Error Handling

use layer_client::{InvocationError, RpcError};

match client.send_message("@badpeer", "Hello").await {
    Ok(()) => {}

    // Telegram RPC error โ€” has a numeric code and a string message
    Err(InvocationError::Rpc(RpcError { code, message, .. })) => {
        eprintln!("Telegram error {code}: {message}");
    }

    // Network / I/O error
    Err(InvocationError::Io(e)) => {
        eprintln!("I/O error: {e}");
    }

    // Other
    Err(e) => eprintln!("Error: {e}"),
}

FLOOD_WAIT errors are handled automatically by the default AutoSleep retry policy. You can replace this with your own policy:

use layer_client::retry::NoRetries;

// Disable all automatic retries
let (client, _) = Client::builder()
    .api_id(12345)
    .api_hash("your_api_hash")
    .retry_policy(NoRetries)
    .connect()
    .await?;

๐Ÿ“– Error handling guide โ†’


๐Ÿ›‘ Shutdown

// Client::connect returns (Client, ShutdownToken)
let (client, shutdown) = Client::connect(config).await?;

// Graceful shutdown from any task
shutdown.cancel();

// Immediate disconnect (no drain)
client.disconnect();

The ShutdownToken is a CancellationToken wrapper. You can clone it and pass it to multiple tasks.


๐Ÿ“ Updating the TL Layer

When Telegram publishes a new TL schema, updating layer is a two-step process:

# 1. Replace the schema file
cp new-api.tl layer-tl-types/tl/api.tl

# 2. Build โ€” layer-tl-gen regenerates all types at compile time
cargo build

The codegen (layer-tl-gen) runs as a build script. No manual code changes are required for pure schema updates โ€” the 2,329 type definitions are entirely auto-generated.

๐Ÿ“– Layer upgrade guide โ†’


๐Ÿงช Running Tests

# Run all tests in the workspace
cargo test --workspace

# Run only layer-client tests
cargo test -p layer-client

# Run with all features enabled
cargo test --workspace --all-features

Integration tests live in layer-client/tests/integration.rs. They use InMemoryBackend and do not require real Telegram credentials.


โŒ Unsupported Features

The following are gaps in the current high-level API. Every single one can be accessed today via client.invoke::<R>() with the raw TL types โ€” see the Raw API Escape Hatch section.

Feature Workaround
Secret chats (E2E) Not implemented at the MTProto layer-2 level
Voice and video calls No call signalling or media transport
Payments SentCode::PaymentRequired returns an error
Channel creation Use invoke with channels::CreateChannel
Sticker set management Use invoke with messages::GetStickerSet etc.
Account settings Use invoke with account::UpdateProfile etc.
Contact management Use invoke with contacts::ImportContacts etc.
Poll / quiz creation Use invoke with InputMediaPoll
Live location Not wrapped
Bot command registration Use invoke with bots::SetBotCommands
IPv6 Config flag exists but address formatting for IPv6 DCs is untested

๐Ÿ’ฌ Community

Questions, ideas, bug reports โ€” come talk to us:

Link
๐Ÿ“ข Channel โ€” releases and announcements t.me/layer_rs
๐Ÿ’ฌ Chat โ€” questions and discussion t.me/layer_chat
๐Ÿ“– Online Book โ€” narrative guide github.ankitchaubey.in/layer
๐Ÿ“ฆ Crates.io crates.io/crates/layer-client
๐Ÿ“„ API Docs docs.rs/layer-client
๐Ÿ› Issue Tracker github.com/ankit-chaubey/layer/issues

๐Ÿค Contributing

Contributions are welcome โ€” bug fixes, new wrappers, better docs, more tests. All pull requests are appreciated.

Please read CONTRIBUTING.md before opening a PR. In brief:

  • Run cargo test --workspace and cargo clippy --workspace locally before pushing.
  • For new wrappers, add a doc-test in the /// comment block.
  • For security issues, follow the responsible disclosure process in SECURITY.md โ€” do not open a public issue.

PRs Welcome Good First Issues


๐Ÿ”’ Security

Found a vulnerability? Please report it privately. See SECURITY.md for the responsible disclosure process. Do not open a public GitHub issue for security bugs.


๐Ÿ‘ค Author

Ankit Chaubey

Built with curiosity, caffeine, and a lot of Rust compiler errors ๐Ÿฆ€

GitHub Website Email Telegram


๐Ÿ™ Acknowledgements

  • Lonami for grammers โ€” the architecture, DH session design, SRP 2FA math, and session handling in layer are deeply inspired by this excellent library. Portions of this project include code derived from grammers, which is dual-licensed MIT or Apache-2.0.

  • Telegram for the detailed MTProto specification and the publicly available TL schema.

  • The Rust async ecosystem โ€” tokio, flate2, getrandom, sha2, socket2, and friends.


๐Ÿ“„ License

Licensed under either of, at your option:

Unless you explicitly state otherwise, any contribution you submit for inclusion shall be dual-licensed as above, without any additional terms or conditions.


โš ๏ธ Telegram Terms of Service

As with any third-party Telegram library, ensure your usage complies with Telegram's Terms of Service and API Terms of Service. Misuse of the Telegram API โ€” including but not limited to spam, mass scraping, or automation of normal user accounts โ€” may result in account limitations or permanent bans.


layer โ€” because sometimes you have to build it yourself to truly understand it.

Star on GitHub