โก 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 ๐ฆ
Pre-production (
0.x.x) โ APIs may change between minor versions. Review the CHANGELOG before upgrading.
Table of Contents
- What is layer?
- What makes layer unique?
- Crate Overview
- Installation
- The Minimal Bot โ 15 Lines
- Quick Start โ User Account
- Quick Start โ Bot
- ClientBuilder
- String Sessions โ Portable Auth
- Update Stream
- Messaging
- Media
- Keyboards and Reply Markup
- Text Formatting
- Reactions
- Typing Guard (RAII)
- Participants and Chat Management
- Search
- Dialogs and Iterators
- Peer Resolution
- Session Backends
- Feature Flags
- Raw API Escape Hatch
- Transports
- Networking โ SOCKS5 and DC Pool
- Error Handling
- Shutdown
- Updating the TL Layer
- Running Tests
- Unsupported Features
- Community
- Contributing
- Security
- Author
- Acknowledgements
- License
- Telegram Terms of Service
๐งฉ 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 |
High-level async client: auth, send, receive, media, bots | |
layer-tl-types |
All Layer 224 constructors, functions, and enums (2,329 definitions) | |
layer-mtproto |
MTProto session, DH exchange, message framing, transports | |
layer-crypto |
AES-IGE, RSA, SHA, Diffie-Hellman, auth key derivation | |
layer-tl-gen |
Build-time Rust code generator from the TL AST | |
layer-tl-parser |
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:
[]
= "0.4.5"
= { = "1", = ["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)
= { = "0.4.5", = ["sqlite-session"] }
# libsql / Turso remote or embedded database session
= { = "0.4.5", = ["libsql-session"] }
# Hand-rolled HTML entity parser (parse_html / generate_html)
= { = "0.4.5", = ["html"] }
# Spec-compliant html5ever tokenizer โ replaces the built-in html parser
= { = "0.4.5", = ["html5ever"] }
Note:
layer-clientre-exportslayer_tl_typesaslayer_client::tl, so you usually do not need to addlayer-tl-typesas 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 ;
async
No trait objects, no callbacks, no dyn Handler. Just an async loop and pattern matching. That's the whole bot.
๐ค Quick Start โ User Account
use ;
use ;
async
After the first successful login the session is persisted to
my.session. Subsequent runs skip the phone/code flow entirely.
๐ค Quick Start โ Bot
use ;
async
Spawning per-update tasks
For production bots the update loop should never block. Spawn each update into its own task:
use ;
use Arc;
// Wrap in Arc so it can be moved into spawned tasks
let client = new;
let mut stream = client.stream_updates;
while let Some = stream.next.await
async
๐จ ClientBuilder
The fluent ClientBuilder is the cleanest way to configure a connection when you need more than defaults:
use Client;
let = builder
.api_id
.api_hash
.session // BinaryFileBackend at this path
.catch_up // replay missed updates on reconnect
.connect
.await?;
Use .session_string(s) for portable base64 sessions (no file on disk):
let session = var.unwrap_or_default;
let = builder
.api_id
.api_hash
.session_string
.connect
.await?;
Use .socks5(host, port) for a proxy:
let = builder
.api_id
.api_hash
.session
.socks5
.connect
.await?;
๐ 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!; // save this somewhere safe
// โโ Restore later โ no phone/code needed โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let = with_string_session.await?;
// Or via builder
let = builder
.api_id
.api_hash
.session_string
.connect
.await?;
String sessions are ideal for serverless deployments, CI/CD bots, and any environment where writing files is inconvenient.
๐ก Update Stream
client.stream_updates() returns an UpdateStream that yields typed updates:
let mut stream = client.stream_updates;
while let Some = 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 Update;
match update
Important:
Updateis#[non_exhaustive]. Always include_ => {}to stay forward-compatible as new variants are added.
IncomingMessage API
IncomingMessage is the type of NewMessage and MessageEdited:
NewMessage =>
๐ฌ 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.await?;
// To Saved Messages
client.send_message.await?;
// By TL Peer (from an incoming message)
if let Some = msg.peer_id
// To self โ shorthand for "me"
client.send_to_self.await?;
InputMessage builder
InputMessage gives you full control over every send option:
use ;
use InlineKeyboard;
let = parse_markdown;
let kb = new
.row
.callback
.url
.build;
client
.send_message_to_peer_ex
.await?;
Edit, forward, delete
// Edit
client.edit_message.await?;
// Forward messages between peers
client.forward_messages.await?;
// Delete (also removes from the other side if you have permission)
client.delete_messages.await?;
Pin and unpin
// Pin a message (notify: true sends a "pinned message" service message)
client.pin_message.await?;
// Get the current pinned message
let pinned = client.get_pinned_message.await?;
// Unpin a specific message
client.unpin_message.await?;
// Unpin all at once
client.unpin_all_messages.await?;
Scheduled messages
use ;
// Schedule for 1 hour from now
let schedule_ts = as i32;
client
.send_message_to_peer_ex
.await?;
// List all scheduled messages in a chat
let scheduled = client.get_scheduled_messages.await?;
// Cancel a scheduled message
client.delete_scheduled_messages.await?;
Chat actions and typing
use layer_tl_types as tl;
// Start a "typing..." indicator
client.send_chat_action.await?;
// Mark all messages as read
client.mark_as_read.await?;
// Clear all @mention badges
client.clear_mentions.await?;
๐ Media
Upload
use UploadedFile;
// Upload from bytes โ small files sequentially
let uploaded: UploadedFile = client
.upload_file
.await?;
// Upload from bytes โ parallel chunks (faster for large files)
let uploaded = client
.upload_file_concurrent
.await?;
// Upload from an async reader (e.g. a file on disk)
use File;
let f = open.await?;
let uploaded = client
.upload_stream
.await?;
// Send the uploaded file to a peer
client.send_file.await?;
// Send multiple files as an album in one call
client.send_album.await?;
Download
// Download directly to a file path (streaming, no full memory buffer)
client
.download_media_to_file
.await?;
// Download to Vec<u8> โ sequential
let bytes: = client.download_media.await?;
// Download to Vec<u8> โ parallel chunks
let bytes: = client.download_media_concurrent.await?;
// Use the Downloadable trait for Photos, Documents, Stickers
use ;
let photo = from_message?;
let bytes = client.download.await?;
โจ๏ธ Keyboards and Reply Markup
Inline keyboards
use InlineKeyboard;
let kb = new
.row
.callback
.callback
.row
.url
.switch_inline
.build;
client
.send_message_to_peer_ex
.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 ReplyKeyboard;
let kb = new
.row
.text
.text
.row
.text
.resize
.single_use
.build;
client
.send_message_to_peer_ex
.await?;
Answer callback queries
CallbackQuery =>
Pass alert: true as the third argument to show a popup alert instead of a toast.
Inline mode
use layer_tl_types as tl;
InlineQuery =>
๐๏ธ Text Formatting
Markdown
use ;
// Parse markdown โ plain text + message entities
let = parse_markdown;
// Send with formatting
client
.send_message_to_peer_ex
.await?;
// Go the other way: entities + plain text โ markdown string
let md = generate_markdown;
HTML
Enable the html or html5ever feature flag:
= { = "0.4.5", = ["html"] }
use ;
let = parse_html;
client
.send_message_to_peer_ex
.await?;
// Always available, no feature flag needed
let html_str = generate_html;
๐ฅ Reactions
InputReactions is the typed builder for reactions:
use InputReactions;
// Single emoji reaction
client.send_reaction.await?;
// Custom premium emoji
client.send_reaction.await?;
// Big animated reaction
client.send_reaction.await?;
// Remove all reactions
client.send_reaction.await?;
โ Typing Guard (RAII)
TypingGuard is a RAII wrapper that automatically starts and stops typing/uploading indicators:
use TypingGuard;
use layer_tl_types as tl;
async
Convenience constructors for common actions:
// Typing
let _t = client.typing.await?;
// Uploading document
let _t = client.uploading_document.await?;
// Recording video
let _t = client.recording_video.await?;
// Typing in a specific forum topic
let _t = client.typing_in_topic.await?;
๐ฅ Participants and Chat Management
Fetch participants
use Participant;
// Fetch up to N participants at once
let participants: = client.get_participants.await?;
// Paginated lazy iterator โ works for very large groups
let mut iter = client.iter_participants;
while let Some = iter.next.await?
// Search within a group
let results = client.search_peer.await?;
Ban, kick, promote
use ;
// Kick (ban + immediate unban)
client.kick_participant.await?;
// Ban with custom rights and optional expiry
client
.ban_participant
.await?;
// Promote to admin with specific rights
client
.promote_participant
.await?;
// Get a user's current permissions in a channel
let perms = client.get_permissions.await?;
Profile photos
// Fetch the first page of profile photos
let photos = client.get_profile_photos.await?;
// Lazy iterator across all pages
let mut iter = client.iter_profile_photos;
while let Some = iter.next.await?
Join and leave
// Join a public group or channel by username
client.join_chat.await?;
// Accept a private invite link
client.accept_invite_link.await?;
// Leave and delete a dialog from the dialog list
client.delete_dialog.await?;
๐ Search
In-chat search
SearchBuilder is a chainable builder for messages.search:
use MessagesFilter;
let results = client
.search
.min_date
.max_date
.filter
.limit
.fetch
.await?;
for msg in results
Global search
GlobalSearchBuilder searches across all chats:
let results = client
.search_global_builder
.broadcasts_only // channels only
.min_date
.limit
.fetch
.await?;
๐ Dialogs and Iterators
// Fetch the first N dialogs
let dialogs = client.get_dialogs.await?;
for d in &dialogs
// Lazy dialog iterator (all dialogs, paginated)
let mut iter = client.iter_dialogs;
while let Some = iter.next.await?
// Lazy message iterator for a specific peer
let mut iter = client.iter_messages;
while let Some = iter.next.await?
// Fetch messages by ID
let messages = client.get_messages_by_id.await?;
// Fetch the latest N messages from a peer
let messages = client.get_messages.await?;
๐ Peer Resolution
// Resolve any string (username, phone number, "me") to a TL Peer
let peer = client.resolve_peer.await?;
let peer = client.resolve_peer.await?;
let peer = client.resolve_peer.await?;
// Resolve just the username part (without @)
let peer = client.resolve_username.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 ;
// SQLite backend
let backend = new.await?;
let = connect.await?;
// Implement your own โ Redis, Postgres, S3, anything
use SessionBackend;
๐ง 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 tl;
// Set the bot's command list โ no wrapper yet, use raw invoke
let req = SetBotCommands ;
client.invoke.await?;
// Update profile info
let req = UpdateProfile ;
client.invoke.await?;
// Send to a specific DC (useful for cross-DC file downloads)
client.invoke_on_dc.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.
๐ 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 ;
// Switch to Obfuscated2 (DPI bypass)
let = builder
.api_id
.api_hash
.session
.transport
.connect
.await?;
๐ Networking โ SOCKS5 and DC Pool
SOCKS5 proxy
use ;
// Without auth
let = builder
.api_id
.api_hash
.session
.socks5
.connect
.await?;
// With username/password
let proxy = with_auth;
let = builder
.api_id
.api_hash
.socks5_config
.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.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 ;
match client.send_message.await
FLOOD_WAIT errors are handled automatically by the default AutoSleep retry policy. You can replace this with your own policy:
use NoRetries;
// Disable all automatic retries
let = builder
.api_id
.api_hash
.retry_policy
.connect
.await?;
๐ Shutdown
// Client::connect returns (Client, ShutdownToken)
let = connect.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
# 2. Build โ layer-tl-gen regenerates all types at compile time
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.
๐งช Running Tests
# Run all tests in the workspace
# Run only layer-client tests
# Run with all features enabled
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 --workspaceandcargo clippy --workspacelocally 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.
๐ 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 ๐ฆ
๐ 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:
- MIT License โ see LICENSE-MIT
- Apache License, Version 2.0 โ see LICENSE-APACHE
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.