{"id":47306308,"url":"https://github.com/ankit-chaubey/layer","last_synced_at":"2026-04-07T08:00:34.505Z","repository":{"id":339788243,"uuid":"1163349138","full_name":"ankit-chaubey/layer","owner":"ankit-chaubey","description":"Experimental Rust primitives for working with Telegram MTProto layers and TL schemas.  This crate is in very early development. APIs are unstable and subject to change.","archived":false,"fork":false,"pushed_at":"2026-04-02T11:16:47.000Z","size":3606,"stargazers_count":11,"open_issues_count":0,"forks_count":12,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-03T06:32:41.707Z","etag":null,"topics":["layer","mtproto","mtproto-api","telegram","telegrambot"],"latest_commit_sha":null,"homepage":"https://layer.ankitchaubey.in/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ankit-chaubey.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE-APACHE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-21T13:50:34.000Z","updated_at":"2026-04-02T11:17:45.000Z","dependencies_parsed_at":"2026-02-21T21:02:46.388Z","dependency_job_id":null,"html_url":"https://github.com/ankit-chaubey/layer","commit_stats":null,"previous_names":["ankit-chaubey/layer"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/ankit-chaubey/layer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankit-chaubey%2Flayer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankit-chaubey%2Flayer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankit-chaubey%2Flayer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankit-chaubey%2Flayer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ankit-chaubey","download_url":"https://codeload.github.com/ankit-chaubey/layer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ankit-chaubey%2Flayer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31499217,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["layer","mtproto","mtproto-api","telegram","telegrambot"],"created_at":"2026-03-17T08:18:49.062Z","updated_at":"2026-04-07T08:00:34.456Z","avatar_url":"https://github.com/ankit-chaubey.png","language":"Rust","readme":"\u003cdiv align=\"center\"\u003e\n\n\u003cimg src=\"https://raw.githubusercontent.com/ankit-chaubey/layer/main/docs/images/layer-banner-dark.png\" alt=\"layer — Async Rust MTProto\" width=\"100%\" /\u003e\n\n\u003cbr/\u003e\n\n# ⚡ layer\n\n**A modular, production-grade async Rust library for the Telegram MTProto protocol.**\n\n*Developed By* **[Ankit Chaubey](https://github.com/ankit-chaubey)**\n\n*Built with curiosity, caffeine, and a lot of Rust compiler errors 🦀*\n\n\u003cbr/\u003e\n\n[![GitHub](https://img.shields.io/badge/GitHub-ankit--chaubey-181717?style=for-the-badge\u0026logo=github)](https://github.com/ankit-chaubey)\n[![Website](https://img.shields.io/badge/Website-ankitchaubey.in-10b981?style=for-the-badge\u0026logo=safari)](https://ankitchaubey.in)\n\n\u003cbr/\u003e\n\n[![Crates.io](https://img.shields.io/crates/v/layer-client?style=for-the-badge\u0026color=fc8d62\u0026label=layer-client\u0026logo=rust)](https://crates.io/crates/layer-client)\n[![Downloads](https://img.shields.io/crates/d/layer-client?style=for-the-badge\u0026color=f59e0b\u0026logo=rust\u0026label=downloads)](https://crates.io/crates/layer-client)\n[![docs.rs](https://img.shields.io/badge/docs.rs-layer--client-5865F2?style=for-the-badge\u0026logo=docs.rs)](https://docs.rs/layer-client)\n[![Guide](https://img.shields.io/badge/book-online%20guide-10b981?style=for-the-badge\u0026logo=mdbook)](https://layer.ankitchaubey.in/)\n\n\u003cbr/\u003e\n\n[![License](https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-blue?style=flat-square)](LICENSE-MIT)\n[![Rust 2024](https://img.shields.io/badge/rust-2024%20edition-f74c00?style=flat-square\u0026logo=rust)](https://www.rust-lang.org/)\n[![TL Layer](https://img.shields.io/badge/TL%20Layer-224-8b5cf6?style=flat-square)](https://core.telegram.org/schema)\n[![Tokio](https://img.shields.io/badge/async-tokio-6366f1?style=flat-square)](https://tokio.rs)\n[![Build](https://img.shields.io/badge/build-passing-22c55e?style=flat-square)](https://github.com/ankit-chaubey/layer/actions)\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](CONTRIBUTING.md)\n\n\u003cbr/\u003e\n\n[![Telegram Channel](https://img.shields.io/badge/channel-%40layer__rs-2CA5E0?style=for-the-badge\u0026logo=telegram)](https://t.me/layer_rs)\n[![Telegram Chat](https://img.shields.io/badge/chat-%40layer__chat-2CA5E0?style=for-the-badge\u0026logo=telegram)](https://t.me/layer_chat)\n\n\u003c/div\u003e\n\n\u003cbr/\u003e\n\n\u003e **Pre-production (`0.x.x`)** — APIs may change between minor versions. Review the [CHANGELOG](CHANGELOG.md) before upgrading.\n\n\u003cbr/\u003e\n\n---\n\n## Table of Contents\n\n- [What is layer?](#-what-is-layer)\n- [What makes layer unique?](#-what-makes-layer-unique)\n- [Crate Overview](#-crate-overview)\n- [Installation](#-installation)\n- [The Minimal Bot — 15 Lines](#-the-minimal-bot--15-lines)\n- [Quick Start — User Account](#-quick-start--user-account)\n- [Quick Start — Bot](#-quick-start--bot)\n  - [Spawning per-update tasks](#spawning-per-update-tasks)\n- [ClientBuilder](#-clientbuilder)\n- [String Sessions — Portable Auth](#-string-sessions--portable-auth)\n- [Update Stream](#-update-stream)\n  - [Update variants](#update-variants)\n  - [IncomingMessage API](#incomingmessage-api)\n- [Messaging](#-messaging)\n  - [Send text](#send-text)\n  - [InputMessage builder](#inputmessage-builder)\n  - [Edit, forward, delete](#edit-forward-delete)\n  - [Pin and unpin](#pin-and-unpin)\n  - [Scheduled messages](#scheduled-messages)\n  - [Chat actions and typing](#chat-actions-and-typing)\n- [Media](#-media)\n  - [Upload](#upload)\n  - [Download](#download)\n- [Keyboards and Reply Markup](#-keyboards-and-reply-markup)\n  - [Inline keyboards](#inline-keyboards)\n  - [Reply keyboards](#reply-keyboards)\n  - [Answer callback queries](#answer-callback-queries)\n  - [Inline mode](#inline-mode)\n- [Text Formatting](#-text-formatting)\n  - [Markdown](#markdown)\n  - [HTML](#html)\n- [Reactions](#-reactions)\n- [Typing Guard (RAII)](#-typing-guard-raii)\n- [Participants and Chat Management](#-participants-and-chat-management)\n  - [Fetch participants](#fetch-participants)\n  - [Ban, kick, promote](#ban-kick-promote)\n  - [Profile photos](#profile-photos)\n- [Search](#-search)\n  - [In-chat search](#in-chat-search)\n  - [Global search](#global-search)\n- [Dialogs and Iterators](#-dialogs-and-iterators)\n- [Peer Resolution](#-peer-resolution)\n- [Session Backends](#-session-backends)\n- [Feature Flags](#-feature-flags)\n- [Raw API Escape Hatch](#-raw-api-escape-hatch)\n- [Transports](#-transports)\n- [Networking — SOCKS5 and DC Pool](#-networking--socks5-and-dc-pool)\n- [Error Handling](#-error-handling)\n- [Shutdown](#-shutdown)\n- [Updating the TL Layer](#-updating-the-tl-layer)\n- [Running Tests](#-running-tests)\n- [Unsupported Features](#-unsupported-features)\n- [Community](#-community)\n- [Contributing](#-contributing)\n- [Security](#-security)\n- [Author](#-author)\n- [Acknowledgements](#-acknowledgements)\n- [License](#-license)\n- [Telegram Terms of Service](#%EF%B8%8F-telegram-terms-of-service)\n\n\u003cbr/\u003e\n\n---\n\n## 🧩 What is layer?\n\n**layer** is a hand-crafted, bottom-up async Rust implementation of the [Telegram MTProto](https://core.telegram.org/mtproto) protocol.\n\nEvery 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.\n\nThe 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?**\n\n\u003cbr/\u003e\n\n---\n\n## 💡 What makes layer unique?\n\nMost Telegram libraries are thin wrappers around generated code or ports from other languages. layer is different.\n\n**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.\n\n**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.\n\n**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.\n\n**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.).\n\n**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.\n\n**No `unsafe`, pure async Rust.** The entire stack from cryptographic primitives to the high-level client is safe Rust, running on Tokio.\n\n\u003cbr/\u003e\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://raw.githubusercontent.com/ankit-chaubey/layer/main/docs/images/arch-stack.svg\" alt=\"layer crate architecture\" width=\"100%\"/\u003e\n\u003c/div\u003e\n\n\u003cbr/\u003e\n\n---\n\n## 🏗️ Crate Overview\n\n\u003cdiv align=\"center\"\u003e\n\u003cimg src=\"https://raw.githubusercontent.com/ankit-chaubey/layer/main/docs/images/feature-flags.svg\" alt=\"Feature flags\" width=\"100%\"/\u003e\n\u003c/div\u003e\n\n\u003cbr/\u003e\n\nlayer is a workspace of focused crates. Most users only ever need **`layer-client`**.\n\n| Crate | Version | Description |\n|---|:---:|---|\n| [`layer-client`](./layer-client) | [![crates.io](https://img.shields.io/crates/v/layer-client?style=flat-square\u0026color=fc8d62)](https://crates.io/crates/layer-client) | High-level async client: auth, send, receive, media, bots |\n| [`layer-tl-types`](./layer-tl-types) | [![crates.io](https://img.shields.io/crates/v/layer-tl-types?style=flat-square\u0026color=f59e0b)](https://crates.io/crates/layer-tl-types) | All Layer **224** constructors, functions, and enums (2,329 definitions) |\n| [`layer-mtproto`](./layer-mtproto) | [![crates.io](https://img.shields.io/crates/v/layer-mtproto?style=flat-square\u0026color=6366f1)](https://crates.io/crates/layer-mtproto) | MTProto session, DH exchange, message framing, transports |\n| [`layer-crypto`](./layer-crypto) | [![crates.io](https://img.shields.io/crates/v/layer-crypto?style=flat-square\u0026color=8b5cf6)](https://crates.io/crates/layer-crypto) | AES-IGE, RSA, SHA, Diffie-Hellman, auth key derivation |\n| [`layer-tl-gen`](./layer-tl-gen) | [![crates.io](https://img.shields.io/crates/v/layer-tl-gen?style=flat-square\u0026color=10b981)](https://crates.io/crates/layer-tl-gen) | Build-time Rust code generator from the TL AST |\n| [`layer-tl-parser`](./layer-tl-parser) | [![crates.io](https://img.shields.io/crates/v/layer-tl-parser?style=flat-square\u0026color=22c55e)](https://crates.io/crates/layer-tl-parser) | Parses `.tl` schema text into an AST |\n| `layer-app` | ❌ | Interactive demo binary (not published) |\n| `layer-connect` | ❌ | Raw DH connection demo (not published) |\n\n```\nlayer/\n├── layer-tl-parser/      .tl schema text → AST\n├── layer-tl-gen/         AST → Rust source (build-time codegen)\n├── layer-tl-types/       Auto-generated types, functions \u0026 enums (Layer 224)\n├── layer-crypto/         AES-IGE, RSA, SHA, auth key derivation, PQ factorization\n├── layer-mtproto/        MTProto session, DH handshake, framing, transport\n├── layer-client/         High-level async Client API  ← you are here\n├── layer-connect/        Demo: raw DH + getConfig\n└── layer-app/            Demo: interactive login + update stream\n```\n\n\u003e The full API reference lives at **[docs.rs/layer-client](https://docs.rs/layer-client)**.\n\u003e The narrative guide lives at **[layer.ankitchaubey.in](https://layer.ankitchaubey.in/)**.\n\n\u003cbr/\u003e\n\n---\n\n## 📦 Installation\n\nAdd to your `Cargo.toml`:\n\n```toml\n[dependencies]\nlayer-client = \"0.4.5\"\ntokio        = { version = \"1\", features = [\"full\"] }\n```\n\nGet your `api_id` and `api_hash` from **[my.telegram.org](https://my.telegram.org)** — every Telegram client needs them.\n\n**Optional feature flags:**\n\n```toml\n# SQLite session persistence (stores auth key in a local .db file)\nlayer-client = { version = \"0.4.5\", features = [\"sqlite-session\"] }\n\n# libsql / Turso remote or embedded database session\nlayer-client = { version = \"0.4.5\", features = [\"libsql-session\"] }\n\n# Hand-rolled HTML entity parser (parse_html / generate_html)\nlayer-client = { version = \"0.4.5\", features = [\"html\"] }\n\n# Spec-compliant html5ever tokenizer — replaces the built-in html parser\nlayer-client = { version = \"0.4.5\", features = [\"html5ever\"] }\n```\n\n\u003e **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.\n\n\u003cbr/\u003e\n\n---\n\n## ⚡ The Minimal Bot — 15 Lines\n\nThis is the least code you need to have a working, update-receiving Telegram bot running with layer.\n\n```rust\nuse layer_client::{Client, Config, update::Update};\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let (client, _shutdown) = Client::connect(Config {\n        session_path: \"bot.session\".into(),\n        api_id:   std::env::var(\"API_ID\")?.parse()?,\n        api_hash: std::env::var(\"API_HASH\")?,\n        ..Default::default()\n    }).await?;\n\n    client.bot_sign_in(\u0026std::env::var(\"BOT_TOKEN\")?).await?;\n    client.save_session().await?;\n\n    let mut stream = client.stream_updates();\n    while let Some(Update::NewMessage(msg)) = stream.next().await {\n        if let (false, Some(text), Some(peer)) = (msg.outgoing(), msg.text(), msg.peer_id()) {\n            client.send_message_to_peer(peer.clone(), \u0026format!(\"Echo: {text}\")).await?;\n        }\n    }\n    Ok(())\n}\n```\n\nNo trait objects, no callbacks, no `dyn Handler`. Just an async loop and pattern matching. That's the whole bot.\n\n\u003e [📖 Read more in the Bot Quick Start guide →](https://layer.ankitchaubey.in/quickstart-bot.html)\n\n\u003cbr/\u003e\n\n---\n\n## 👤 Quick Start — User Account\n\n```rust\nuse layer_client::{Client, Config, SignInError};\nuse std::io::{self, BufRead};\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let (client, _shutdown) = Client::connect(Config {\n        session_path: \"my.session\".into(),\n        api_id:       12345,\n        api_hash:     \"your_api_hash\".into(),\n        ..Default::default()\n    })\n    .await?;\n\n    if !client.is_authorized().await? {\n        let phone = \"+1234567890\";\n        let token = client.request_login_code(phone).await?;\n\n        print!(\"Enter code: \");\n        let stdin = io::stdin();\n        let code  = stdin.lock().lines().next().unwrap()?;\n\n        match client.sign_in(\u0026token, \u0026code).await {\n            Ok(name) =\u003e println!(\"Welcome, {name}!\"),\n            Err(SignInError::PasswordRequired(t)) =\u003e {\n                // 2FA — read password and call check_password\n                client.check_password(*t, \"my_2fa_password\").await?;\n            }\n            Err(e) =\u003e return Err(e.into()),\n        }\n        client.save_session().await?;\n    }\n\n    let me = client.get_me().await?;\n    println!(\"Logged in as: {}\", me.first_name.unwrap_or_default());\n\n    // Send a message to Saved Messages\n    client.send_message(\"me\", \"Hello from layer! 👋\").await?;\n\n    // Or send to any peer\n    client.send_message_to_peer(\"@username\", \"Hello!\").await?;\n\n    Ok(())\n}\n```\n\n\u003e After the first successful login the session is persisted to `my.session`. Subsequent runs skip the phone/code flow entirely.\n\n\u003e [📖 Full user account guide →](https://layer.ankitchaubey.in/quickstart-user.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🤖 Quick Start — Bot\n\n```rust\nuse layer_client::{Client, Config, update::Update};\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let (client, _shutdown) = Client::connect(Config {\n        session_path: \"bot.session\".into(),\n        api_id:       12345,\n        api_hash:     \"your_api_hash\".into(),\n        ..Default::default()\n    })\n    .await?;\n\n    if !client.is_authorized().await? {\n        client.bot_sign_in(\"1234567890:ABCdef...\").await?;\n        client.save_session().await?;\n    }\n\n    let me = client.get_me().await?;\n    println!(\"@{} is online\", me.username.as_deref().unwrap_or(\"bot\"));\n\n    let mut stream = client.stream_updates();\n    while let Some(update) = stream.next().await {\n        match update {\n            Update::NewMessage(msg) if !msg.outgoing() =\u003e {\n                if let Some(peer) = msg.peer_id() {\n                    client\n                        .send_message_to_peer(\n                            peer.clone(),\n                            \u0026format!(\"You said: {}\", msg.text().unwrap_or(\"\")),\n                        )\n                        .await?;\n                }\n            }\n            Update::CallbackQuery(cb) =\u003e {\n                client\n                    .answer_callback_query(cb.query_id, Some(\"✅ Done!\"), false)\n                    .await?;\n            }\n            _ =\u003e {}\n        }\n    }\n    Ok(())\n}\n```\n\n### Spawning per-update tasks\n\nFor production bots the update loop should never block. Spawn each update into its own task:\n\n```rust\nuse layer_client::{Client, update::Update};\nuse std::sync::Arc;\n\n// Wrap in Arc so it can be moved into spawned tasks\nlet client = Arc::new(client);\nlet mut stream = client.stream_updates();\n\nwhile let Some(update) = stream.next().await {\n    let c = client.clone();\n    tokio::spawn(async move {\n        if let Err(e) = handle_update(update, \u0026c).await {\n            eprintln!(\"handler error: {e}\");\n        }\n    });\n}\n\nasync fn handle_update(\n    update: Update,\n    client: \u0026Client,\n) -\u003e Result\u003c(), Box\u003cdyn std::error::Error + Send + Sync\u003e\u003e {\n    match update {\n        Update::NewMessage(msg) if !msg.outgoing() =\u003e {\n            if let Some(peer) = msg.peer_id() {\n                client.send_message_to_peer(peer.clone(), \"👋\").await?;\n            }\n        }\n        _ =\u003e {}\n    }\n    Ok(())\n}\n```\n\n\u003e [📖 Full production bot guide →](https://layer.ankitchaubey.in/quickstart-bot.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🔨 ClientBuilder\n\nThe fluent [`ClientBuilder`](./layer-client/src/builder.rs) is the cleanest way to configure a connection when you need more than defaults:\n\n```rust\nuse layer_client::Client;\n\nlet (client, _shutdown) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .session(\"my.session\")          // BinaryFileBackend at this path\n    .catch_up(true)                 // replay missed updates on reconnect\n    .connect()\n    .await?;\n```\n\nUse `.session_string(s)` for portable base64 sessions (no file on disk):\n\n```rust\nlet session = std::env::var(\"SESSION\").unwrap_or_default();\n\nlet (client, _shutdown) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .session_string(session)\n    .connect()\n    .await?;\n```\n\nUse `.socks5(host, port)` for a proxy:\n\n```rust\nlet (client, _shutdown) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .session(\"proxy.session\")\n    .socks5(\"127.0.0.1\", 1080)\n    .connect()\n    .await?;\n```\n\n\u003e [📖 ClientBuilder reference →](https://docs.rs/layer-client/latest/layer_client/builder/struct.ClientBuilder.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🔑 String Sessions — Portable Auth\n\nA 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.\n\n```rust\n// ── Export from any running client ────────────────────────────────────────────\nlet session_string = client.export_session_string().await?;\nprintln!(\"{session_string}\");  // save this somewhere safe\n\n// ── Restore later — no phone/code needed ─────────────────────────────────────\nlet (client, _shutdown) = Client::with_string_session(\u0026session_string).await?;\n\n// Or via builder\nlet (client, _shutdown) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .session_string(session_string)\n    .connect()\n    .await?;\n```\n\nString sessions are ideal for serverless deployments, CI/CD bots, and any environment where writing files is inconvenient.\n\n\u003e [📖 Session backends guide →](https://layer.ankitchaubey.in/authentication/session-backends.html)\n\n\u003cbr/\u003e\n\n---\n\n## 📡 Update Stream\n\n[`client.stream_updates()`](https://docs.rs/layer-client/latest/layer_client/struct.Client.html#method.stream_updates) returns an [`UpdateStream`](https://docs.rs/layer-client/latest/layer_client/struct.UpdateStream.html) that yields typed updates:\n\n```rust\nlet mut stream = client.stream_updates();\nwhile let Some(update) = stream.next().await {\n    // ...\n}\n```\n\n`stream_updates()` is cheap and can be called multiple times. Each call returns an independent receiver. Use `Arc\u003cClient\u003e` and clone it into spawned tasks.\n\n### Update variants\n\n```rust\nuse layer_client::update::Update;\n\nmatch update {\n    // ── Messages ──────────────────────────────────────────────────────────\n    Update::NewMessage(msg)     =\u003e { /* new incoming message */ }\n    Update::MessageEdited(msg)  =\u003e { /* existing message was edited */ }\n    Update::MessageDeleted(del) =\u003e { /* one or more messages were deleted */ }\n\n    // ── Bot interactions ──────────────────────────────────────────────────\n    Update::CallbackQuery(cb)   =\u003e { /* inline button was pressed */ }\n    Update::InlineQuery(iq)     =\u003e { /* @bot query in inline mode */ }\n    Update::InlineSend(is)      =\u003e { /* user selected an inline result */ }\n\n    // ── Presence ──────────────────────────────────────────────────────────\n    Update::UserTyping(action)  =\u003e { /* typing / uploading / recording */ }\n    Update::UserStatus(status)  =\u003e { /* contact went online / offline */ }\n\n    // ── Raw passthrough ───────────────────────────────────────────────────\n    Update::Raw(raw)            =\u003e { /* any unmapped TL update */ }\n\n    _ =\u003e {}  // Update is #[non_exhaustive] — always add a fallback\n}\n```\n\n\u003e **Important:** `Update` is `#[non_exhaustive]`. Always include `_ =\u003e {}` to stay forward-compatible as new variants are added.\n\n### IncomingMessage API\n\n[`IncomingMessage`](https://docs.rs/layer-client/latest/layer_client/update/struct.IncomingMessage.html) is the type of `NewMessage` and `MessageEdited`:\n\n```rust\nUpdate::NewMessage(msg) =\u003e {\n    msg.id()          // i32 — unique message ID in the chat\n    msg.text()        // Option\u003c\u0026str\u003e — text or caption\n    msg.peer_id()     // Option\u003c\u0026tl::enums::Peer\u003e — the chat this message is in\n    msg.sender_id()   // Option\u003c\u0026tl::enums::Peer\u003e — who sent it\n    msg.outgoing()    // bool — was this sent by us?\n    msg.date()        // i32 — Unix timestamp\n    msg.edit_date()   // Option\u003ci32\u003e — last edit timestamp\n    msg.mentioned()   // bool — are we mentioned?\n    msg.silent()      // bool — no notification?\n    msg.pinned()      // bool — is the message currently pinned?\n    msg.post()        // bool — is this a channel post (no sender)?\n    msg.raw           // tl::enums::Message — full TL object for everything else\n}\n```\n\n\u003e [📖 Incoming message reference →](https://layer.ankitchaubey.in/updates/incoming-message.html)\n\n\u003cbr/\u003e\n\n---\n\n## 💬 Messaging\n\n### Send text\n\nThe simplest send methods accept any `impl Into\u003cPeerRef\u003e` — a `\u0026str` username, `\"me\"` for Saved Messages, a `tl::enums::Peer` clone, or a numeric ID:\n\n```rust\n// By username\nclient.send_message(\"@username\", \"Hello!\").await?;\n\n// To Saved Messages\nclient.send_message(\"me\", \"Note to self\").await?;\n\n// By TL Peer (from an incoming message)\nif let Some(peer) = msg.peer_id() {\n    client.send_message_to_peer(peer.clone(), \"Reply!\").await?;\n}\n\n// To self — shorthand for \"me\"\nclient.send_to_self(\"Reminder: buy milk 🥛\").await?;\n```\n\n### InputMessage builder\n\n[`InputMessage`](https://docs.rs/layer-client/latest/layer_client/struct.InputMessage.html) gives you full control over every send option:\n\n```rust\nuse layer_client::{InputMessage, parsers::parse_markdown};\nuse layer_client::keyboard::InlineKeyboard;\n\nlet (text, entities) = parse_markdown(\"**Bold** and `code`\");\n\nlet kb = InlineKeyboard::new()\n    .row()\n    .callback(\"✅ Confirm\", b\"confirm\")\n    .url(\"🔗 Docs\", \"https://docs.rs/layer-client\")\n    .build();\n\nclient\n    .send_message_to_peer_ex(\n        peer.clone(),\n        \u0026InputMessage::text(text)\n            .entities(entities)         // formatted text\n            .reply_to(Some(msg_id))     // reply to a specific message\n            .silent(true)               // no notification\n            .no_webpage(true)           // suppress link preview\n            .keyboard(kb),              // attach inline keyboard\n    )\n    .await?;\n```\n\n### Edit, forward, delete\n\n```rust\n// Edit\nclient.edit_message(peer.clone(), message_id, \"Updated text\").await?;\n\n// Forward messages between peers\nclient.forward_messages(\n    from_peer.clone(),\n    to_peer.clone(),\n    \u0026[message_id_1, message_id_2],\n).await?;\n\n// Delete (also removes from the other side if you have permission)\nclient.delete_messages(peer.clone(), \u0026[message_id]).await?;\n```\n\n### Pin and unpin\n\n```rust\n// Pin a message (notify: true sends a \"pinned message\" service message)\nclient.pin_message(peer.clone(), message_id, true).await?;\n\n// Get the current pinned message\nlet pinned = client.get_pinned_message(peer.clone()).await?;\n\n// Unpin a specific message\nclient.unpin_message(peer.clone(), message_id).await?;\n\n// Unpin all at once\nclient.unpin_all_messages(peer.clone()).await?;\n```\n\n### Scheduled messages\n\n```rust\nuse std::time::{SystemTime, UNIX_EPOCH};\n\n// Schedule for 1 hour from now\nlet schedule_ts = (SystemTime::now()\n    .duration_since(UNIX_EPOCH)\n    .unwrap()\n    .as_secs() + 3600) as i32;\n\nclient\n    .send_message_to_peer_ex(\n        peer.clone(),\n        \u0026InputMessage::text(\"Reminder! ⏰\").schedule_date(Some(schedule_ts)),\n    )\n    .await?;\n\n// List all scheduled messages in a chat\nlet scheduled = client.get_scheduled_messages(peer.clone()).await?;\n\n// Cancel a scheduled message\nclient.delete_scheduled_messages(peer.clone(), \u0026[scheduled_msg_id]).await?;\n```\n\n### Chat actions and typing\n\n```rust\nuse layer_tl_types as tl;\n\n// Start a \"typing...\" indicator\nclient.send_chat_action(\n    peer.clone(),\n    tl::enums::SendMessageAction::SendMessageTypingAction,\n    None,  // top_msg_id — None for normal chats, Some(id) for forum topics\n).await?;\n\n// Mark all messages as read\nclient.mark_as_read(peer.clone()).await?;\n\n// Clear all @mention badges\nclient.clear_mentions(peer.clone()).await?;\n```\n\n\u003e [📖 Full messaging reference →](https://layer.ankitchaubey.in/messaging/sending.html)\n\n\u003cbr/\u003e\n\n---\n\n## 📎 Media\n\n### Upload\n\n```rust\nuse layer_client::media::UploadedFile;\n\n// Upload from bytes — small files sequentially\nlet uploaded: UploadedFile = client\n    .upload_file(\"photo.jpg\", file_bytes.as_ref())\n    .await?;\n\n// Upload from bytes — parallel chunks (faster for large files)\nlet uploaded = client\n    .upload_file_concurrent(\"video.mp4\", video_bytes.as_ref())\n    .await?;\n\n// Upload from an async reader (e.g. a file on disk)\nuse tokio::fs::File;\nlet f = File::open(\"document.pdf\").await?;\nlet uploaded = client\n    .upload_stream(\"document.pdf\", f)\n    .await?;\n\n// Send the uploaded file to a peer\nclient.send_file(peer.clone(), uploaded, /* as_photo */ false).await?;\n\n// Send multiple files as an album in one call\nclient.send_album(peer.clone(), vec![uploaded_a, uploaded_b]).await?;\n```\n\n### Download\n\n```rust\n// Download directly to a file path (streaming, no full memory buffer)\nclient\n    .download_media_to_file(\u0026message_media, \"output.jpg\")\n    .await?;\n\n// Download to Vec\u003cu8\u003e — sequential\nlet bytes: Vec\u003cu8\u003e = client.download_media(\u0026message_media).await?;\n\n// Download to Vec\u003cu8\u003e — parallel chunks\nlet bytes: Vec\u003cu8\u003e = client.download_media_concurrent(\u0026message_media).await?;\n\n// Use the Downloadable trait for Photos, Documents, Stickers\nuse layer_client::media::{Photo, Downloadable};\nlet photo = Photo::from_message(\u0026msg.raw)?;\nlet bytes = client.download(\u0026photo).await?;\n```\n\n\u003e [📖 Media guide →](https://layer.ankitchaubey.in/messaging/media.html)\n\n\u003cbr/\u003e\n\n---\n\n## ⌨️ Keyboards and Reply Markup\n\n### Inline keyboards\n\n```rust\nuse layer_client::keyboard::InlineKeyboard;\n\nlet kb = InlineKeyboard::new()\n    .row()\n        .callback(\"👍 Like\",    b\"like\")\n        .callback(\"👎 Dislike\", b\"dislike\")\n    .row()\n        .url(\"🔗 Open docs\", \"https://docs.rs/layer-client\")\n        .switch_inline(\"🔍 Search\", \"query\")\n    .build();\n\nclient\n    .send_message_to_peer_ex(peer.clone(), \u0026InputMessage::text(\"Vote!\").keyboard(kb))\n    .await?;\n```\n\nAvailable 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`.\n\n### Reply keyboards\n\n```rust\nuse layer_client::keyboard::ReplyKeyboard;\n\nlet kb = ReplyKeyboard::new()\n    .row()\n        .text(\"📸 Photo\")\n        .text(\"📄 Document\")\n    .row()\n        .text(\"❌ Cancel\")\n    .resize(true)\n    .single_use(true)\n    .build();\n\nclient\n    .send_message_to_peer_ex(peer.clone(), \u0026InputMessage::text(\"Choose:\").keyboard(kb))\n    .await?;\n```\n\n### Answer callback queries\n\n```rust\nUpdate::CallbackQuery(cb) =\u003e {\n    let data = cb.data().unwrap_or(\"\");\n    match data {\n        b\"like\"    =\u003e client.answer_callback_query(cb.query_id, Some(\"❤️ Liked!\"), false).await?,\n        b\"dislike\" =\u003e client.answer_callback_query(cb.query_id, Some(\"👎 Noted\"),   false).await?,\n        _          =\u003e client.answer_callback_query(cb.query_id, None,              false).await?,\n    }\n}\n```\n\nPass `alert: true` as the third argument to show a popup alert instead of a toast.\n\n### Inline mode\n\n```rust\nuse layer_tl_types as tl;\n\nUpdate::InlineQuery(iq) =\u003e {\n    let q   = iq.query().to_string();\n    let qid = iq.query_id;\n\n    let results = vec![\n        tl::enums::InputBotInlineResult::InputBotInlineResult(\n            tl::types::InputBotInlineResult {\n                id: \"1\".into(), r#type: \"article\".into(),\n                title: Some(\"Result title\".into()),\n                description: Some(q.clone()),\n                url: None, thumb: None, content: None,\n                send_message: tl::enums::InputBotInlineMessage::Text(\n                    tl::types::InputBotInlineMessageText {\n                        no_webpage: false, invert_media: false,\n                        message: q, entities: None, reply_markup: None,\n                    },\n                ),\n            },\n        ),\n    ];\n\n    // cache_time: 30s, is_personal: false, next_offset: None\n    client.answer_inline_query(qid, results, 30, false, None).await?;\n}\n```\n\n\u003e [📖 Keyboards guide →](https://layer.ankitchaubey.in/messaging/keyboards.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🖊️ Text Formatting\n\n### Markdown\n\n```rust\nuse layer_client::parsers::{parse_markdown, generate_markdown};\n\n// Parse markdown → plain text + message entities\nlet (text, entities) = parse_markdown(\"**Bold**, `code`, _italic_, [link](https://example.com)\");\n\n// Send with formatting\nclient\n    .send_message_to_peer_ex(\n        peer.clone(),\n        \u0026InputMessage::text(text).entities(entities),\n    )\n    .await?;\n\n// Go the other way: entities + plain text → markdown string\nlet md = generate_markdown(\u0026plain_text, \u0026entities);\n```\n\n### HTML\n\nEnable the `html` or `html5ever` feature flag:\n\n```toml\nlayer-client = { version = \"0.4.5\", features = [\"html\"] }\n```\n\n```rust\nuse layer_client::parsers::{parse_html, generate_html};\n\nlet (text, entities) = parse_html(\"\u003cb\u003eBold\u003c/b\u003e and \u003ccode\u003emonospace\u003c/code\u003e\");\n\nclient\n    .send_message_to_peer_ex(peer.clone(), \u0026InputMessage::text(text).entities(entities))\n    .await?;\n\n// Always available, no feature flag needed\nlet html_str = generate_html(\u0026plain_text, \u0026entities);\n```\n\n\u003e [📖 Formatting reference →](https://layer.ankitchaubey.in/messaging/formatting.html)\n\n\u003cbr/\u003e\n\n---\n\n## 💥 Reactions\n\n[`InputReactions`](https://docs.rs/layer-client/latest/layer_client/reactions/struct.InputReactions.html) is the typed builder for reactions:\n\n```rust\nuse layer_client::reactions::InputReactions;\n\n// Single emoji reaction\nclient.send_reaction(peer.clone(), message_id, InputReactions::emoticon(\"👍\")).await?;\n\n// Custom premium emoji\nclient.send_reaction(peer.clone(), message_id, InputReactions::custom_emoji(1234567890)).await?;\n\n// Big animated reaction\nclient.send_reaction(peer.clone(), message_id, InputReactions::emoticon(\"🔥\").big()).await?;\n\n// Remove all reactions\nclient.send_reaction(peer.clone(), message_id, InputReactions::remove()).await?;\n```\n\n\u003e [📖 Reactions guide →](https://layer.ankitchaubey.in/messaging/reactions.html)\n\n\u003cbr/\u003e\n\n---\n\n## ⌛ Typing Guard (RAII)\n\n[`TypingGuard`](https://docs.rs/layer-client/latest/layer_client/struct.TypingGuard.html) is a RAII wrapper that automatically starts and stops typing/uploading indicators:\n\n```rust\nuse layer_client::TypingGuard;\nuse layer_tl_types as tl;\n\nasync fn handle_long_task(client: \u0026Client, peer: tl::enums::Peer) -\u003e anyhow::Result\u003c()\u003e {\n    // Typing indicator starts immediately and is renewed every ~4 seconds\n    let _typing = TypingGuard::start(\n        client,\n        peer.clone(),\n        tl::enums::SendMessageAction::SendMessageTypingAction,\n    )\n    .await?;\n\n    // Do expensive work — user sees \"typing...\"\n    do_expensive_work().await;\n\n    // _typing is dropped here → Telegram sees the indicator stop\n    Ok(())\n}\n```\n\nConvenience constructors for common actions:\n\n```rust\n// Typing\nlet _t = client.typing(peer.clone()).await?;\n\n// Uploading document\nlet _t = client.uploading_document(peer.clone()).await?;\n\n// Recording video\nlet _t = client.recording_video(peer.clone()).await?;\n\n// Typing in a specific forum topic\nlet _t = client.typing_in_topic(peer.clone(), topic_id).await?;\n```\n\n\u003e [📖 Typing guard reference →](https://layer.ankitchaubey.in/api/typing-guard.html)\n\n\u003cbr/\u003e\n\n---\n\n## 👥 Participants and Chat Management\n\n### Fetch participants\n\n```rust\nuse layer_client::participants::Participant;\n\n// Fetch up to N participants at once\nlet participants: Vec\u003cParticipant\u003e = client.get_participants(peer.clone(), 100).await?;\n\n// Paginated lazy iterator — works for very large groups\nlet mut iter = client.iter_participants(peer.clone());\nwhile let Some(p) = iter.next(\u0026client).await? {\n    println!(\"{}\", p.user.first_name.as_deref().unwrap_or(\"\"));\n}\n\n// Search within a group\nlet results = client.search_peer(peer.clone(), \"John\").await?;\n```\n\n### Ban, kick, promote\n\n```rust\nuse layer_client::participants::{BanRights, AdminRightsBuilder};\n\n// Kick (ban + immediate unban)\nclient.kick_participant(peer.clone(), user_id).await?;\n\n// Ban with custom rights and optional expiry\nclient\n    .ban_participant(\n        peer.clone(),\n        user_id,\n        BanRights::new()\n            .no_messages(true)\n            .no_media(true)\n            .until(expiry_unix_timestamp),\n    )\n    .await?;\n\n// Promote to admin with specific rights\nclient\n    .promote_participant(\n        peer.clone(),\n        user_id,\n        AdminRightsBuilder::new()\n            .post_messages(true)\n            .delete_messages(true)\n            .ban_users(true)\n            .title(\"Moderator\"),\n    )\n    .await?;\n\n// Get a user's current permissions in a channel\nlet perms = client.get_permissions(peer.clone(), user_id).await?;\n```\n\n### Profile photos\n\n```rust\n// Fetch the first page of profile photos\nlet photos = client.get_profile_photos(user_id, 0, 10).await?;\n\n// Lazy iterator across all pages\nlet mut iter = client.iter_profile_photos(user_id);\nwhile let Some(photo) = iter.next(\u0026client).await? {\n    let bytes = client.download(\u0026photo).await?;\n}\n```\n\n### Join and leave\n\n```rust\n// Join a public group or channel by username\nclient.join_chat(\"@somegroup\").await?;\n\n// Accept a private invite link\nclient.accept_invite_link(\"https://t.me/joinchat/AbCdEfG...\").await?;\n\n// Leave and delete a dialog from the dialog list\nclient.delete_dialog(peer.clone()).await?;\n```\n\n\u003e [📖 Participants guide →](https://layer.ankitchaubey.in/api/participants.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🔍 Search\n\n### In-chat search\n\n[`SearchBuilder`](https://docs.rs/layer-client/latest/layer_client/search/struct.SearchBuilder.html) is a chainable builder for `messages.search`:\n\n```rust\nuse layer_tl_types::enums::MessagesFilter;\n\nlet results = client\n    .search(peer.clone(), \"hello world\")\n    .min_date(1_700_000_000)\n    .max_date(1_720_000_000)\n    .filter(MessagesFilter::InputMessagesFilterPhotos)\n    .limit(50)\n    .fetch(\u0026client)\n    .await?;\n\nfor msg in results {\n    println!(\"[{}] {}\", msg.id, msg.message);\n}\n```\n\n### Global search\n\n[`GlobalSearchBuilder`](https://docs.rs/layer-client/latest/layer_client/search/struct.GlobalSearchBuilder.html) searches across all chats:\n\n```rust\nlet results = client\n    .search_global_builder(\"rust async\")\n    .broadcasts_only(true)       // channels only\n    .min_date(1_700_000_000)\n    .limit(30)\n    .fetch(\u0026client)\n    .await?;\n```\n\n\u003e [📖 Search guide →](https://layer.ankitchaubey.in/api/search.html)\n\n\u003cbr/\u003e\n\n---\n\n## 📜 Dialogs and Iterators\n\n```rust\n// Fetch the first N dialogs\nlet dialogs = client.get_dialogs(50).await?;\nfor d in \u0026dialogs {\n    println!(\"{} — {} unread\", d.title(), d.unread_count());\n}\n\n// Lazy dialog iterator (all dialogs, paginated)\nlet mut iter = client.iter_dialogs();\nwhile let Some(dialog) = iter.next(\u0026client).await? {\n    println!(\"{}\", dialog.title());\n}\n\n// Lazy message iterator for a specific peer\nlet mut iter = client.iter_messages(peer.clone());\nwhile let Some(msg) = iter.next(\u0026client).await? {\n    println!(\"{}\", msg.message);\n}\n\n// Fetch messages by ID\nlet messages = client.get_messages_by_id(peer.clone(), \u0026[100, 101, 102]).await?;\n\n// Fetch the latest N messages from a peer\nlet messages = client.get_messages(peer.clone(), 20).await?;\n```\n\n\u003e [📖 Dialogs guide →](https://layer.ankitchaubey.in/api/dialogs.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🔗 Peer Resolution\n\n```rust\n// Resolve any string (username, phone number, \"me\") to a TL Peer\nlet peer = client.resolve_peer(\"@telegram\").await?;\nlet peer = client.resolve_peer(\"+1234567890\").await?;\nlet peer = client.resolve_peer(\"me\").await?;\n\n// Resolve just the username part (without @)\nlet peer = client.resolve_username(\"telegram\").await?;\n```\n\nAccess 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.\n\n\u003cbr/\u003e\n\n---\n\n## 💾 Session Backends\n\nlayer ships with multiple session backends. They all implement the [`SessionBackend`](https://docs.rs/layer-client/latest/layer_client/session_backend/trait.SessionBackend.html) trait and are hot-swappable.\n\n| Backend | Feature flag | Best for |\n|---|---|---|\n| [`BinaryFileBackend`](https://docs.rs/layer-client/latest/layer_client/session_backend/struct.BinaryFileBackend.html) | *(default)* | Single-process bots, scripts |\n| [`InMemoryBackend`](https://docs.rs/layer-client/latest/layer_client/session_backend/struct.InMemoryBackend.html) | *(default)* | Tests, ephemeral tasks |\n| [`StringSessionBackend`](https://docs.rs/layer-client/latest/layer_client/session_backend/struct.StringSessionBackend.html) | *(default)* | Serverless, env-var storage, CI bots |\n| [`SqliteBackend`](https://docs.rs/layer-client/latest/layer_client/session_backend/struct.SqliteBackend.html) | `sqlite-session` | Multi-session local apps |\n| [`LibSqlBackend`](https://docs.rs/layer-client/latest/layer_client/session_backend/struct.LibSqlBackend.html) | `libsql-session` | Distributed / Turso-backed storage |\n| Custom | — | Implement `SessionBackend` for anything |\n\n```rust\nuse layer_client::session_backend::{SqliteBackend, SessionBackend};\n\n// SQLite backend\nlet backend = SqliteBackend::new(\"sessions.db\").await?;\n\nlet (client, _shutdown) = Client::connect(Config {\n    session_backend: Box::new(backend),\n    api_id:  12345,\n    api_hash: \"your_api_hash\".into(),\n    ..Default::default()\n}).await?;\n```\n\n```rust\n// Implement your own — Redis, Postgres, S3, anything\nuse layer_client::session_backend::SessionBackend;\n\nstruct RedisBackend { /* ... */ }\n\n#[async_trait::async_trait]\nimpl SessionBackend for RedisBackend {\n    async fn load(\u0026self) -\u003e anyhow::Result\u003cOption\u003cVec\u003cu8\u003e\u003e\u003e { /* ... */ }\n    async fn save(\u0026self, data: \u0026[u8]) -\u003e anyhow::Result\u003c()\u003e { /* ... */ }\n}\n```\n\n\u003e [📖 Session backends guide →](https://layer.ankitchaubey.in/authentication/session-backends.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🔧 Feature Flags\n\n### `layer-tl-types`\n\n| Flag | Default | Description |\n|---|:---:|---|\n| `tl-api` | ✅ | High-level Telegram API schema (`api.tl`) |\n| `tl-mtproto` | ❌ | Low-level MTProto schema (`mtproto.tl`) |\n| `impl-debug` | ✅ | `#[derive(Debug)]` on all generated types |\n| `impl-from-type` | ✅ | `From\u003ctypes::T\u003e for enums::E` on all constructors |\n| `impl-from-enum` | ✅ | `TryFrom\u003cenums::E\u003e for types::T` on all constructors |\n| `name-for-id` | ❌ | `name_for_id(u32) -\u003e Option\u003c\u0026'static str\u003e` lookup table |\n| `impl-serde` | ❌ | `serde::Serialize` + `Deserialize` on all types |\n\n### `layer-client`\n\n| Flag | Default | Description |\n|---|:---:|---|\n| `html` | ❌ | Hand-rolled HTML parser (`parse_html`, `generate_html`) |\n| `html5ever` | ❌ | Spec-compliant `html5ever` tokenizer, replaces the built-in parser |\n| `sqlite-session` | ❌ | SQLite session backend (`SqliteBackend`) |\n| `libsql-session` | ❌ | libsql / Turso session backend (`LibSqlBackend`) |\n\n\u003cbr/\u003e\n\n---\n\n## 🔩 Raw API Escape Hatch\n\nEvery Telegram method in **Layer 224** is available via the raw [`invoke`](https://docs.rs/layer-client/latest/layer_client/struct.Client.html#method.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`).\n\n```rust\nuse layer_client::tl;\n\n// Set the bot's command list — no wrapper yet, use raw invoke\nlet req = tl::functions::bots::SetBotCommands {\n    scope: tl::enums::BotCommandScope::Default(tl::types::BotCommandScopeDefault {}),\n    lang_code: \"en\".into(),\n    commands: vec![\n        tl::enums::BotCommand::BotCommand(tl::types::BotCommand {\n            command:     \"start\".into(),\n            description: \"Start the bot\".into(),\n        }),\n    ],\n};\nclient.invoke(\u0026req).await?;\n```\n\n```rust\n// Update profile info\nlet req = tl::functions::account::UpdateProfile {\n    first_name: Some(\"Alice\".into()),\n    last_name:  None,\n    about:      Some(\"layer user 🦀\".into()),\n};\nclient.invoke(\u0026req).await?;\n```\n\n```rust\n// Send to a specific DC (useful for cross-DC file downloads)\nclient.invoke_on_dc(\u0026req, 2).await?;\n```\n\nAny method listed in the [Telegram API documentation](https://core.telegram.org/method) can be invoked this way. Layer 224 includes **2,329** TL constructors and all RPC functions.\n\n\u003e [📖 Raw API guide →](https://layer.ankitchaubey.in/advanced/raw-api.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🚂 Transports\n\nThree MTProto transport encodings are supported:\n\n| Transport | Description | When to use |\n|---|---|---|\n| **Abridged** | Single-byte length prefix, lowest overhead | Default — best for most setups |\n| **Intermediate** | 4-byte LE length prefix | Better compatibility with some proxies |\n| **Obfuscated2** | XOR stream cipher over Abridged | DPI bypass, MTProxy, restricted networks |\n\n```rust\nuse layer_client::{Client, TransportKind};\n\n// Switch to Obfuscated2 (DPI bypass)\nlet (client, _) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .session(\"obfuscated.session\")\n    .transport(TransportKind::Obfuscated)\n    .connect()\n    .await?;\n```\n\n\u003e [📖 Transport reference →](https://layer.ankitchaubey.in/advanced/proxy.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🌐 Networking — SOCKS5 and DC Pool\n\n### SOCKS5 proxy\n\n```rust\nuse layer_client::{Client, Socks5Config};\n\n// Without auth\nlet (client, _) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .session(\"proxy.session\")\n    .socks5(\"127.0.0.1\", 1080)\n    .connect()\n    .await?;\n\n// With username/password\nlet proxy = Socks5Config::with_auth(\"proxy.host\", 1080, \"user\", \"pass\");\nlet (client, _) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .socks5_config(proxy)\n    .connect()\n    .await?;\n```\n\n### DC pool and multi-DC\n\nAuth 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:\n\n```rust\n// Force a request to DC 2\nclient.invoke_on_dc(\u0026req, 2).await?;\n```\n\n### Reconnect and keepalive\n\nThe 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:\n\n```rust\n// Call this when your app detects the network is back\nclient.signal_network_restored();\n```\n\n\u003cbr/\u003e\n\n---\n\n## ⚠️ Error Handling\n\n```rust\nuse layer_client::{InvocationError, RpcError};\n\nmatch client.send_message(\"@badpeer\", \"Hello\").await {\n    Ok(()) =\u003e {}\n\n    // Telegram RPC error — has a numeric code and a string message\n    Err(InvocationError::Rpc(RpcError { code, message, .. })) =\u003e {\n        eprintln!(\"Telegram error {code}: {message}\");\n    }\n\n    // Network / I/O error\n    Err(InvocationError::Io(e)) =\u003e {\n        eprintln!(\"I/O error: {e}\");\n    }\n\n    // Other\n    Err(e) =\u003e eprintln!(\"Error: {e}\"),\n}\n```\n\n`FLOOD_WAIT` errors are handled automatically by the default [`AutoSleep`](https://docs.rs/layer-client/latest/layer_client/retry/struct.AutoSleep.html) retry policy. You can replace this with your own policy:\n\n```rust\nuse layer_client::retry::NoRetries;\n\n// Disable all automatic retries\nlet (client, _) = Client::builder()\n    .api_id(12345)\n    .api_hash(\"your_api_hash\")\n    .retry_policy(NoRetries)\n    .connect()\n    .await?;\n```\n\n\u003e [📖 Error handling guide →](https://layer.ankitchaubey.in/errors.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🛑 Shutdown\n\n```rust\n// Client::connect returns (Client, ShutdownToken)\nlet (client, shutdown) = Client::connect(config).await?;\n\n// Graceful shutdown from any task\nshutdown.cancel();\n\n// Immediate disconnect (no drain)\nclient.disconnect();\n```\n\nThe [`ShutdownToken`](https://docs.rs/layer-client/latest/layer_client/struct.ShutdownToken.html) is a `CancellationToken` wrapper. You can clone it and pass it to multiple tasks.\n\n\u003cbr/\u003e\n\n---\n\n## 📐 Updating the TL Layer\n\nWhen Telegram publishes a new TL schema, updating layer is a two-step process:\n\n```bash\n# 1. Replace the schema file\ncp new-api.tl layer-tl-types/tl/api.tl\n\n# 2. Build — layer-tl-gen regenerates all types at compile time\ncargo build\n```\n\nThe 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.\n\n\u003e [📖 Layer upgrade guide →](https://layer.ankitchaubey.in/advanced/layer-upgrade.html)\n\n\u003cbr/\u003e\n\n---\n\n## 🧪 Running Tests\n\n```bash\n# Run all tests in the workspace\ncargo test --workspace\n\n# Run only layer-client tests\ncargo test -p layer-client\n\n# Run with all features enabled\ncargo test --workspace --all-features\n```\n\nIntegration tests live in [`layer-client/tests/integration.rs`](./layer-client/tests/integration.rs). They use `InMemoryBackend` and do not require real Telegram credentials.\n\n\u003cbr/\u003e\n\n---\n\n## ❌ Unsupported Features\n\nThe following are gaps in the current high-level API. Every single one can be accessed today via `client.invoke::\u003cR\u003e()` with the raw TL types — see the [Raw API Escape Hatch](#-raw-api-escape-hatch) section.\n\n| Feature | Workaround |\n|---|---|\n| **Secret chats (E2E)** | Not implemented at the MTProto layer-2 level |\n| **Voice and video calls** | No call signalling or media transport |\n| **Payments** | `SentCode::PaymentRequired` returns an error |\n| **Channel creation** | Use `invoke` with `channels::CreateChannel` |\n| **Sticker set management** | Use `invoke` with `messages::GetStickerSet` etc. |\n| **Account settings** | Use `invoke` with `account::UpdateProfile` etc. |\n| **Contact management** | Use `invoke` with `contacts::ImportContacts` etc. |\n| **Poll / quiz creation** | Use `invoke` with `InputMediaPoll` |\n| **Live location** | Not wrapped |\n| **Bot command registration** | Use `invoke` with `bots::SetBotCommands` |\n| **IPv6** | Config flag exists but address formatting for IPv6 DCs is untested |\n\n\u003cbr/\u003e\n\n---\n\n## 💬 Community\n\nQuestions, ideas, bug reports — come talk to us:\n\n| | Link |\n|---|---|\n| 📢 **Channel** — releases and announcements | [t.me/layer_rs](https://t.me/layer_rs) |\n| 💬 **Chat** — questions and discussion | [t.me/layer_chat](https://t.me/layer_chat) |\n| 📖 **Online Book** — narrative guide | [layer.ankitchaubey.in](https://layer.ankitchaubey.in/) |\n| 📦 **Crates.io** | [crates.io/crates/layer-client](https://crates.io/crates/layer-client) |\n| 📄 **API Docs** | [docs.rs/layer-client](https://docs.rs/layer-client) |\n| 🐛 **Issue Tracker** | [github.com/ankit-chaubey/layer/issues](https://github.com/ankit-chaubey/layer/issues) |\n\n\u003cbr/\u003e\n\n---\n\n## 🤝 Contributing\n\nContributions are welcome — bug fixes, new wrappers, better docs, more tests. All pull requests are appreciated.\n\nPlease read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a PR. In brief:\n\n- Run `cargo test --workspace` and `cargo clippy --workspace` locally before pushing.\n- For new wrappers, add a doc-test in the `///` comment block.\n- For security issues, follow the responsible disclosure process in [SECURITY.md](SECURITY.md) — **do not** open a public issue.\n\n[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square)](CONTRIBUTING.md)\n[![Good First Issues](https://img.shields.io/github/issues/ankit-chaubey/layer/good%20first%20issue?style=flat-square\u0026color=5865F2\u0026label=good%20first%20issues)](https://github.com/ankit-chaubey/layer/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)\n\n\u003cbr/\u003e\n\n---\n\n## 🔒 Security\n\nFound a vulnerability? Please report it **privately**. See [SECURITY.md](SECURITY.md) for the responsible disclosure process. Do not open a public GitHub issue for security bugs.\n\n\u003cbr/\u003e\n\n---\n\n## 👤 Author\n\n\u003cdiv align=\"center\"\u003e\n\n\u003cbr/\u003e\n\n\u003ca href=\"https://github.com/ankit-chaubey\"\u003e\n  \u003cimg src=\"https://github.com/ankit-chaubey.png\" width=\"96\" style=\"border-radius:50%\" alt=\"Ankit Chaubey\" /\u003e\n\u003c/a\u003e\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n**Ankit Chaubey**\n\n*Built with curiosity, caffeine, and a lot of Rust compiler errors 🦀*\n\n\u003cbr/\u003e\n\n[![GitHub](https://img.shields.io/badge/GitHub-ankit--chaubey-181717?style=for-the-badge\u0026logo=github)](https://github.com/ankit-chaubey)\n[![Website](https://img.shields.io/badge/Website-ankitchaubey.in-10b981?style=for-the-badge\u0026logo=safari)](https://ankitchaubey.in)\n[![Email](https://img.shields.io/badge/Email-ankitchaubey.dev%40gmail.com-ea4335?style=for-the-badge\u0026logo=gmail)](mailto:ankitchaubey.dev@gmail.com)\n[![Telegram](https://img.shields.io/badge/Telegram-%40layer__rs-2CA5E0?style=for-the-badge\u0026logo=telegram)](https://t.me/layer_rs)\n\n\u003cbr/\u003e\n\n\u003c/div\u003e\n\n---\n\n## 🙏 Acknowledgements\n\n- [**Lonami**](https://codeberg.org/Lonami) for [**grammers**](https://codeberg.org/Lonami/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.\n\n- [**Telegram**](https://core.telegram.org/mtproto) for the detailed MTProto specification and the publicly available TL schema.\n\n- The Rust async ecosystem — [`tokio`](https://tokio.rs), [`flate2`](https://crates.io/crates/flate2), [`getrandom`](https://crates.io/crates/getrandom), [`sha2`](https://crates.io/crates/sha2), [`socket2`](https://crates.io/crates/socket2), and friends.\n\n\u003cbr/\u003e\n\n---\n\n## 📄 License\n\nLicensed under either of, at your option:\n\n- **MIT License** — see [LICENSE-MIT](LICENSE-MIT)\n- **Apache License, Version 2.0** — see [LICENSE-APACHE](LICENSE-APACHE)\n\nUnless you explicitly state otherwise, any contribution you submit for inclusion shall be dual-licensed as above, without any additional terms or conditions.\n\n\u003cbr/\u003e\n\n---\n\n## ⚠️ Telegram Terms of Service\n\nAs with any third-party Telegram library, ensure your usage complies with [Telegram's Terms of Service](https://core.telegram.org/api/terms) and [API Terms of Service](https://core.telegram.org/api/terms). 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.\n\n\u003cbr/\u003e\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n*layer — because sometimes you have to build it yourself to truly understand it.*\n\n[![Star on GitHub](https://img.shields.io/github/stars/ankit-chaubey/layer?style=social)](https://github.com/ankit-chaubey/layer)\n\n\u003c/div\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fankit-chaubey%2Flayer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fankit-chaubey%2Flayer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fankit-chaubey%2Flayer/lists"}