<div align="center">
<img src="https://raw.githubusercontent.com/rust-lang/www.rust-lang.org/master/static/images/ferris/rustacean-orig-noshadow.svg" width="120" alt="Ferris the Crab"/>
<h1>tgbotrs</h1>
<p><strong>A fully-featured, auto-generated Telegram Bot API library for Rust π¦</strong></p>
[](https://crates.io/crates/tgbotrs)
[](https://docs.rs/tgbotrs)
[](https://github.com/ankit-chaubey/tgbotrs/actions/workflows/ci.yml)
[](https://github.com/ankit-chaubey/tgbotrs/actions/workflows/auto-regenerate.yml)
[](https://core.telegram.org/bots/api)
[](https://www.rust-lang.org)
[](LICENSE)
[](https://crates.io/crates/tgbotrs)
[](https://docs.rs/tgbotrs)
[](https://docs.rs/tgbotrs)
[](https://github.com/ankit-chaubey/tgbotrs/actions)
[](https://tokio.rs)
[](https://serde.rs)
<br/>
> **All 285 types and 165 methods** of the Telegram Bot API β strongly typed, fully async, automatically kept up-to-date.
<br/>
[π¦ Install](#-installation) β’ [π Quick Start](#-quick-start) β’ [π Examples](#-examples) β’ [π§ API Reference](#-api-reference) β’ [π Auto-Codegen](#-auto-codegen) β’ [π€ Contributing](#-contributing)
</div>
---
## β¨ Features
<table>
<tr>
<td>
**π€ Complete API Coverage**
- All **285 types** β structs, enums, markers
- All **165 methods** β fully async
- All **21 union types** as Rust enums
- **100 optional params structs** with builder pattern
</td>
<td>
**π Auto-Generated & Always Fresh**
- Generated from the [official spec](https://github.com/ankit-chaubey/api-spec)
- Daily automated check for API updates
- PR auto-opened on every new API version
- Zero manual work to stay up-to-date
</td>
</tr>
<tr>
<td>
**π¦ Idiomatic Rust**
- Fully `async/await` with **Tokio**
- `Into<ChatId>` β accepts `i64` or `"@username"`
- `Into<String>` on all text params
- `Option<T>` for all optional fields
- `Box<T>` to break recursive type cycles
</td>
<td>
**π‘οΈ Type Safe**
- `ChatId` β integer or username, no stringly typing
- `InputFile` β file_id / URL / raw bytes
- `ReplyMarkup` β unified enum for all 4 keyboard types
- `InputMedia` β typed enum for media groups
- Compile-time guarantees on all API calls
</td>
</tr>
<tr>
<td>
**π‘ Flexible HTTP Layer**
- Custom API server support (local Bot API)
- Multipart file uploads
- Configurable timeout
- Flood-wait aware error handling
- `reqwest` backend
</td>
<td>
**π¬ Built-in Polling**
- Long-polling dispatcher included
- Spawns a Tokio task per update
- Configurable timeout, limit, allowed_updates
- Clean concurrent update processing
</td>
</tr>
</table>
---
## π¦ Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
tgbotrs = "0.1"
tokio = { version = "1", features = ["full"] }
```
**Requirements:** Rust `1.75+` Β· Tokio async runtime
---
## π Quick Start
```rust
use tgbotrs::Bot;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bot = Bot::new("YOUR_BOT_TOKEN").await?;
println!("β
Running as @{}", bot.me.username.as_deref().unwrap_or("unknown"));
println!(" ID: {}", bot.me.id);
// Send a message β chat_id accepts i64 or "@username"
let msg = bot.send_message(123456789i64, "Hello from tgbotrs! π¦", None).await?;
println!("π¨ Sent message #{}", msg.message_id);
Ok(())
}
```
---
## π Examples
### π Echo Bot β Long Polling
```rust
use tgbotrs::{Bot, Poller, UpdateHandler};
#[tokio::main]
async fn main() {
let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap())
.await.expect("Invalid token");
println!("π€ @{} is running...", bot.me.username.as_deref().unwrap_or(""));
let handler: UpdateHandler = Box::new(|bot, update| {
Box::pin(async move {
let Some(msg) = update.message else { return };
let Some(text) = msg.text else { return };
let _ = bot.send_message(msg.chat.id, text, None).await;
})
});
Poller::new(bot, handler).timeout(30).limit(100).start().await.unwrap();
}
```
---
### π¬ Formatted Messages
```rust
use tgbotrs::gen_methods::SendMessageParams;
let params = SendMessageParams::new()
.parse_mode("HTML".to_string())
.disable_notification(true);
bot.send_message(
"@mychannel",
"<b>Bold</b>, <i>italic</i>, <code>code</code>",
Some(params),
).await?;
```
---
### πΉ Inline Keyboards
```rust
use tgbotrs::{ReplyMarkup, gen_methods::SendMessageParams};
use tgbotrs::types::{InlineKeyboardMarkup, InlineKeyboardButton};
let keyboard = InlineKeyboardMarkup {
inline_keyboard: vec![
vec![
InlineKeyboardButton { text: "β
Accept".into(), callback_data: Some("accept".into()), ..Default::default() },
InlineKeyboardButton { text: "β Decline".into(), callback_data: Some("decline".into()), ..Default::default() },
],
vec![
InlineKeyboardButton { text: "π Visit".into(), url: Some("https://example.com".into()), ..Default::default() },
],
],
};
let params = SendMessageParams::new()
.reply_markup(ReplyMarkup::InlineKeyboard(keyboard));
bot.send_message(chat_id, "Choose an option:", Some(params)).await?;
```
---
### πΈ Send Photos
```rust
use tgbotrs::{InputFile, gen_methods::SendPhotoParams};
let params = SendPhotoParams::new().caption("Nice photo! π·".to_string());
// By file_id (fastest β already on Telegram's servers)
bot.send_photo(chat_id, "AgACAgIAAxkBAAI...", Some(params.clone())).await?;
// By URL
bot.send_photo(chat_id, "https://example.com/photo.jpg", Some(params.clone())).await?;
// Upload raw bytes
let data = tokio::fs::read("photo.jpg").await?;
bot.send_photo(chat_id, InputFile::memory("photo.jpg", data), Some(params)).await?;
```
---
### π¬ Media Groups
```rust
use tgbotrs::{InputMedia};
use tgbotrs::types::{InputMediaPhoto, InputMediaVideo};
let media = vec![
InputMedia::Photo(InputMediaPhoto {
r#type: "photo".into(),
media: "AgACAgIAAxkBAAI...".into(),
caption: Some("First photo πΈ".into()),
..Default::default()
}),
InputMedia::Video(InputMediaVideo {
r#type: "video".into(),
media: "BAACAgIAAxkBAAI...".into(),
caption: Some("A video π¬".into()),
..Default::default()
}),
];
bot.send_media_group(chat_id, media, None).await?;
```
---
### β¨οΈ Reply Keyboards
```rust
use tgbotrs::{ReplyMarkup, gen_methods::SendMessageParams};
use tgbotrs::types::{ReplyKeyboardMarkup, KeyboardButton};
let keyboard = ReplyKeyboardMarkup {
keyboard: vec![
vec![
KeyboardButton { text: "π Location".into(), request_location: Some(true), ..Default::default() },
KeyboardButton { text: "π± Contact".into(), request_contact: Some(true), ..Default::default() },
],
],
resize_keyboard: Some(true),
one_time_keyboard: Some(true),
..Default::default()
};
let params = SendMessageParams::new()
.reply_markup(ReplyMarkup::ReplyKeyboard(keyboard));
bot.send_message(chat_id, "Use the keyboard below:", Some(params)).await?;
```
---
### π Polls
```rust
use tgbotrs::{gen_methods::SendPollParams};
use tgbotrs::types::InputPollOption;
let options = vec![
InputPollOption { text: "π¦ Rust".into(), ..Default::default() },
InputPollOption { text: "πΉ Go".into(), ..Default::default() },
InputPollOption { text: "π Python".into(), ..Default::default() },
];
let params = SendPollParams::new().is_anonymous(false);
bot.send_poll(chat_id, "Best language for bots?", options, Some(params)).await?;
```
---
### β‘ Callback Queries
```rust
use tgbotrs::gen_methods::AnswerCallbackQueryParams;
use tgbotrs::types::MaybeInaccessibleMessage;
let Some(cq) = update.callback_query else { return };
let data = cq.data.as_deref().unwrap_or("");
// Dismiss the loading spinner
let _ = bot.answer_callback_query(
cq.id.clone(),
Some(AnswerCallbackQueryParams::new()
.text(format!("You chose: {}", data))
.show_alert(false)),
).await;
// Edit original message
if let Some(MaybeInaccessibleMessage::Message(m)) = cq.message {
let _ = bot.edit_message_text(
m.chat.id, m.message_id,
format!("β
Selected: <b>{}</b>", data),
Some(tgbotrs::gen_methods::EditMessageTextParams::new()
.parse_mode("HTML".to_string())),
).await;
}
})
});
```
---
### πͺ Inline Queries
```rust
use tgbotrs::types::{InlineQueryResult, InlineQueryResultArticle, InputMessageContent, InputTextMessageContent};
let results = vec![
InlineQueryResult::Article(InlineQueryResultArticle {
r#type: "article".into(),
id: "1".into(),
title: "Hello World".into(),
input_message_content: InputMessageContent::Text(InputTextMessageContent {
message_text: "Hello from inline mode! π".into(),
..Default::default()
}),
description: Some("Send a greeting".into()),
..Default::default()
}),
];
bot.answer_inline_query(query.id.clone(), results, None).await?;
```
---
### π Payments
```rust
use tgbotrs::{gen_methods::SendInvoiceParams};
use tgbotrs::types::LabeledPrice;
let prices = vec![
LabeledPrice { label: "Premium Plan".into(), amount: 999 },
];
bot.send_invoice(
chat_id,
"Premium Access",
"30 days of unlimited features",
"payload_premium_30d",
"XTR", // Telegram Stars
prices,
None,
).await?;
```
---
### π Webhooks
```rust
use tgbotrs::gen_methods::SetWebhookParams;
let params = SetWebhookParams::new()
.max_connections(100i64)
.allowed_updates(vec!["message".into(), "callback_query".into()])
.secret_token("my_secret_token".to_string());
bot.set_webhook("https://mybot.example.com/webhook", Some(params)).await?;
```
---
### π Local Bot API Server
```rust
let bot = Bot::with_api_url("YOUR_TOKEN", "http://localhost:8081").await?;
```
---
### π οΈ Error Handling
```rust
use tgbotrs::BotError;
match bot.send_message(chat_id, "Hello!", None).await {
Ok(msg) => println!("β
Sent: #{}", msg.message_id),
Err(BotError::Api { code: 403, .. }) => {
eprintln!("π« Bot was blocked by the user");
}
Err(BotError::Api { code: 400, description, .. }) => {
eprintln!("β οΈ Bad request: {}", description);
}
Err(e) if e.is_api_error_code(429) => {
if let Some(secs) = e.flood_wait_seconds() {
println!("β³ Flood wait: {} seconds", secs);
tokio::time::sleep(std::time::Duration::from_secs(secs as u64)).await;
}
}
Err(e) => eprintln!("β Error: {}", e),
}
```
---
## π§ API Reference
### `Bot` β Core Struct
```rust
pub struct Bot {
pub token: String, // Bot token from @BotFather
pub me: User, // Populated via getMe on creation
pub api_url: String, // API base URL (default: api.telegram.org)
}
```
| `Bot::new(token)` | Create bot, calls getMe, verifies token |
| `Bot::with_api_url(token, url)` | Create with a custom/local API server |
| `Bot::new_unverified(token)` | Create without calling getMe |
| `bot.call_api(method, body)` | Raw JSON POST API call |
| `bot.call_api_multipart(method, form)` | Multipart POST (for file uploads) |
| `bot.endpoint(method)` | Returns full URL for a method |
---
### `ChatId` β Flexible Chat Identifier
```rust
// All of these work wherever ChatId is expected:
bot.send_message(123456789i64, "by integer id", None).await?;
bot.send_message(-100123456789i64, "group/channel", None).await?;
bot.send_message("@channelname", "by username", None).await?;
bot.send_message(ChatId::Id(123), "explicit", None).await?;
```
---
### `InputFile` β File Sending
```rust
// Reference an already-uploaded file by file_id
InputFile::file_id("AgACAgIAAxkBAAI...")
// Let Telegram download from a URL
InputFile::url("https://example.com/image.png")
// Upload raw bytes directly
let data = tokio::fs::read("photo.jpg").await?;
InputFile::memory("photo.jpg", data)
```
---
### `ReplyMarkup` β All Keyboard Types
```rust
// Inline keyboard (buttons inside messages)
ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup { .. })
// Reply keyboard (custom keyboard at bottom of screen)
ReplyMarkup::ReplyKeyboard(ReplyKeyboardMarkup { .. })
// Remove the reply keyboard
ReplyMarkup::ReplyKeyboardRemove(ReplyKeyboardRemove { remove_keyboard: true, .. })
// Force the user to reply
ReplyMarkup::ForceReply(ForceReply { force_reply: true, .. })
```
---
### `Poller` β Long Polling Dispatcher
```rust
Poller::new(bot, handler)
.timeout(30) // Seconds to long-poll (0 = short poll)
.limit(100) // Max updates per request (1-100)
.allowed_updates(vec![ // Only receive these update types
"message".into(),
"callback_query".into(),
"inline_query".into(),
"chosen_inline_result".into(),
"shipping_query".into(),
"pre_checkout_query".into(),
])
.start()
.await?;
```
---
### `BotError` β Error Handling
```rust
pub enum BotError {
Http(reqwest::Error), // Network or HTTP transport error
Json(serde_json::Error),// Serialization/deserialization error
Api {
code: i64, // Telegram error code (e.g. 400, 403, 429)
description: String, // Human-readable error message
retry_after: Option<i64>, // Seconds to wait (flood-wait, code 429)
migrate_to_chat_id: Option<i64>,// New chat ID (migration error, code 400)
},
InvalidToken, // Token does not contain ':'
Other(String), // Catch-all
}
// Helper methods:
error.is_api_error_code(429) // β bool
error.flood_wait_seconds() // β Option<i64>
```
---
### Builder Pattern for Optional Params
Every method with optional parameters has a `*Params` struct with a builder API:
```rust
// Pattern: MethodNameParams::new().field(value).field(value)
let params = SendMessageParams::new()
.parse_mode("MarkdownV2".to_string())
.disable_notification(true)
.protect_content(false)
.message_thread_id(123i64)
.reply_parameters(ReplyParameters { message_id: 42, ..Default::default() })
.reply_markup(ReplyMarkup::ForceReply(ForceReply {
force_reply: true, ..Default::default()
}));
```
---
## π Coverage Statistics
| **Total Types** | **285** | β
100% |
| β³ Struct types | 257 | β
|
| β³ Union/Enum types | 21 | β
|
| β³ Marker types | 7 | β
|
| **Total Methods** | **165** | β
100% |
| β³ `set*` methods | 30 | β
|
| β³ `get*` methods | 29 | β
|
| β³ `send*` methods | 22 | β
|
| β³ `edit*` methods | 12 | β
|
| β³ `delete*` methods | 11 | β
|
| β³ Other methods | 61 | β
|
| **Params structs** | 100 | β
|
| **Lines generated** | ~11,258 | β |
---
## π Auto-Codegen
tgbotrs is the only Rust Telegram library that **automatically stays in sync** with the official API spec via GitHub Actions.
### Architecture
```
Every Day at 08:00 UTC
β
βΌ
βββββββββββββββββββ
β Fetch latest β βββ github.com/ankit-chaubey/api-spec
β api.json spec β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Compare with βββ No change? βββΊ Stop β
β pinned version β
ββββββββββ¬βββββββββ
β Changed!
βΌ
βββββββββββββββββββ
β diff_spec.py β βββ Full semantic diff
β β β’ Added/removed types
β β β’ Added/removed methods
β β β’ Per-field changes
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β codegen.py β βββ Pure Python, zero dependencies
β β Generates:
β β β’ gen_types.rs (5,821 lines)
β β β’ gen_methods.rs (5,437 lines)
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β validate.py β βββ Verify 100% coverage
β β All types & methods present
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Open PR with β βββ Rich description:
β full report β β’ Summary table
β β β’ New/removed items
β β β’ Per-field diff (collapsible)
β β β’ Checklist
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β GitHub Issue β βββ Notification with full changelog
β notification β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β On PR merge: β
β β’ Bump semver β
β β’ Git tag β
β β’ GitHub Releaseβ
β β’ crates.io β
βββββββββββββββββββ
```
### Regenerate Manually
```sh
# 1. Download latest spec
curl -o api.json \
https://raw.githubusercontent.com/ankit-chaubey/api-spec/main/api.json
# 2. Run codegen (pure Python, no pip installs needed)
python3 codegen/codegen.py api.json tgbotrs/src/
# 3. Rebuild your project
cargo build
```
### GitHub Actions Workflows
| `auto-regenerate.yml` | β° Daily 08:00 UTC + manual | Fetch spec β diff β codegen β PR |
| `ci.yml` | Every push/PR | Build, test, lint, validate sync |
| `release.yml` | PR merged β main | Version bump + crates.io publish |
| `notify.yml` | After regen | GitHub Issue with change summary |
### Setting Up in Your Fork
Add these secrets in **Settings β Secrets β Actions**:
| `CRATES_IO_TOKEN` | API token from [crates.io/settings/tokens](https://crates.io/settings/tokens) |
Enable PR creation in **Settings β Actions β General β Workflow permissions**.
---
## ποΈ Project Structure
```
tgbotrs/
β
βββ π README.md β You are here
βββ π CHANGELOG.md β Auto-updated on each release
βββ π LICENSE β MIT β Ankit Chaubey 2024-present
βββ π api.json β Pinned Telegram Bot API spec
βββ π spec_commit β Pinned spec commit SHA
βββ π Cargo.toml β Workspace root
β
βββ ποΈ .github/
β βββ workflows/
β β βββ auto-regenerate.yml β Daily spec sync + codegen + PR opener
β β βββ ci.yml β Build/test on 3 OSes Γ 2 Rust versions
β β βββ release.yml β Semver bump + tag + publish
β β βββ notify.yml β Issue creation on API updates
β βββ scripts/
β βββ diff_spec.py β Semantic diff: added/removed/changed
β βββ validate_generated.pyβ Verifies 100% type + method coverage
β βββ build_pr_body.py β Generates rich PR descriptions
β βββ coverage_report.py β Markdown coverage table for CI
β βββ update_changelog.py β Auto-prepends entries to CHANGELOG.md
β
βββ ποΈ codegen/
β βββ Cargo.toml
β βββ codegen.py β Main codegen: Python, zero deps
β βββ src/main.rs β Rust codegen binary (alternative)
β
βββ ποΈ tgbotrs/ β The library crate
βββ Cargo.toml
βββ examples/
β βββ echo_bot.rs β Basic echo bot
β βββ advanced_bot.rs β Keyboards, photos, callbacks
βββ src/
βββ lib.rs β Crate root + public API + re-exports
βββ bot.rs β Bot struct + HTTP + JSON API layer
βββ error.rs β BotError with all error variants
βββ chat_id.rs β ChatId (i64 | @username)
βββ input_file.rs β InputFile + InputFileOrString
βββ reply_markup.rs β ReplyMarkup (4-variant enum)
βββ polling.rs β Poller (long-polling dispatcher)
βββ types.rs β Re-exports gen_types
βββ gen_types.rs β β‘ AUTO-GENERATED β 5,821 lines
βββ gen_methods.rs β β‘ AUTO-GENERATED β 5,437 lines
```
---
## π€ Contributing
Contributions are very welcome!
### Report Issues
- π **Bug?** β [Open a bug report](https://github.com/ankit-chaubey/tgbotrs/issues/new?template=bug_report.md)
- π‘ **Feature request?** β [Open a feature request](https://github.com/ankit-chaubey/tgbotrs/issues/new?template=feature_request.md)
- π **Security issue?** β Email [ankitchaubey.dev@gmail.com](mailto:ankitchaubey.dev@gmail.com) directly
### Development
```sh
# Clone the repo
git clone https://github.com/ankit-chaubey/tgbotrs
cd tgbotrs
# Build everything
cargo build --workspace
# Run tests
cargo test --workspace
# Regenerate from latest spec
python3 codegen/codegen.py api.json tgbotrs/src/
# Validate 100% coverage
python3 .github/scripts/validate_generated.py \
api.json \
tgbotrs/src/gen_types.rs \
tgbotrs/src/gen_methods.rs
# Lint
cargo clippy --workspace --all-targets -- -D warnings
# Format
cargo fmt --all
```
### PR Guidelines
- One concern per PR
- Run `cargo fmt` and `cargo clippy` before submitting
- Add examples for new helpers
- Keep generated files (`gen_*.rs`) untouched β edit `codegen.py` instead
---
## π Changelog
See [CHANGELOG.md](CHANGELOG.md) for the full release history.
---
## π Credits
| **[Telegram](https://core.telegram.org/bots/api)** | The official Bot API this library implements |
| **[PaulSonOfLars / gotgbot](https://github.com/PaulSonOfLars/gotgbot)** | Design inspiration for the auto-generation approach and code structure |
| **[ankit-chaubey / api-spec](https://github.com/ankit-chaubey/api-spec)** | Machine-readable Telegram Bot API spec used as the codegen source |
---
## π¬ Contact
<div align="center">
| π§ **Email** | [ankitchaubey.dev@gmail.com](mailto:ankitchaubey.dev@gmail.com) |
| π¬ **Telegram** | [@ankify](https://t.me/ankify) |
| π **GitHub** | [github.com/ankit-chaubey](https://github.com/ankit-chaubey) |
| π¦ **crates.io** | [crates.io/crates/tgbotrs](https://crates.io/crates/tgbotrs) |
| π **docs.rs** | [docs.rs/tgbotrs](https://docs.rs/tgbotrs) |
</div>
---
## π License
```
MIT License
Copyright (c) 2024-present Ankit Chaubey
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
```
---
<div align="center">
**Created and developed by [Ankit Chaubey](https://github.com/ankit-chaubey)**
*If tgbotrs saved you time, a β on GitHub means a lot!*
<br/>
[](https://github.com/ankit-chaubey/tgbotrs/stargazers)
[](https://github.com/ankit-chaubey/tgbotrs/network/members)
[](https://t.me/ankify)
</div>