

Install · Quick Start · Examples · API Reference · docs.rs
Installation
[dependencies]
ferobot = "0.1"
tokio = { version = "1", features = ["full"] }
Optional features:
ferobot = { version = "0.1", features = ["webhook"] }
ferobot = { version = "0.1", features = ["per-chat"] }
ferobot = { version = "0.1", features = ["redis-storage"] }
Quick Start
use ferobot::Bot;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bot = Bot::new("YOUR_TOKEN").await?;
println!("running as @{}", bot.me.username.as_deref().unwrap_or("?"));
bot.send_message(123456789i64, "hello 🦀", None).await?;
Ok(())
}
Examples
Dispatcher + commands
The recommended way to build a bot. Handlers are async functions; the dispatcher routes updates to the first match.
use ferobot::{Bot, CommandHandler, Dispatcher, DispatcherOpts, HandlerResult, Updater};
use ferobot::framework::Context;
async fn start(bot: Bot, ctx: Context) -> HandlerResult {
if let Some(msg) = ctx.effective_message() {
msg.reply(&bot, "hello!", None).await?;
}
Ok(())
}
#[tokio::main]
async fn main() {
let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap()).await.unwrap();
let mut dp = Dispatcher::new(DispatcherOpts::default());
dp.add_handler(CommandHandler::new("start", start));
Updater::new(bot, dp).start_polling().await.unwrap();
}
Echo bot (low-level polling)
Use Poller directly when you don't need the dispatcher.
use ferobot::{Bot, Poller, UpdateHandler};
#[tokio::main]
async fn main() {
let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap()).await.unwrap();
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).start().await.unwrap();
}
Middleware
Middleware runs before and after every update. Return false from before to drop the update.
use ferobot::{Dispatcher, DispatcherOpts, Updater};
use ferobot::middleware::{LoggingMiddleware, RateLimiter};
let opts = DispatcherOpts::default()
.middleware(LoggingMiddleware) .middleware(RateLimiter::new(5));
Custom middleware:
use ferobot::middleware::Middleware;
use ferobot::{Bot, types::Update};
use async_trait::async_trait;
struct AuthGuard;
#[async_trait]
impl Middleware for AuthGuard {
async fn before(&self, _bot: &Bot, update: &Update) -> bool {
let allowed = [111111111i64, 222222222i64];
let uid = update.message.as_ref()
.and_then(|m| m.from.as_ref())
.map(|u| u.id);
uid.map(|id| allowed.contains(&id)).unwrap_or(false)
}
}
Retry
Wraps any API call and retries on flood-wait (429) and network errors.
use ferobot::RetryPolicy;
let msg = RetryPolicy::new()
.max_attempts(3)
.run(|| bot.send_message(chat_id, "hello", None))
.await?;
Inline keyboards
use ferobot::{ReplyMarkup, gen_methods::SendMessageParams};
use ferobot::types::{InlineKeyboardButton, InlineKeyboardMarkup};
let keyboard = InlineKeyboardMarkup {
inline_keyboard: vec![vec![
InlineKeyboardButton {
text: "yes".into(),
callback_data: Some("yes".into()),
..Default::default()
},
InlineKeyboardButton {
text: "no".into(),
callback_data: Some("no".into()),
..Default::default()
},
]],
};
let params = SendMessageParams::new()
.reply_markup(ReplyMarkup::InlineKeyboard(keyboard));
bot.send_message(chat_id, "pick one", Some(params)).await?;
Callback queries
use ferobot::gen_methods::{AnswerCallbackQueryParams, EditMessageTextParams};
use ferobot::types::MaybeInaccessibleMessage;
let Some(cq) = update.callback_query else { return };
let data = cq.data.as_deref().unwrap_or("");
bot.answer_callback_query(
cq.id.clone(),
Some(AnswerCallbackQueryParams::new().text(format!("got: {data}"))),
).await?;
if let Some(MaybeInaccessibleMessage::Message(m)) = cq.message.as_deref() {
let params = EditMessageTextParams::new()
.chat_id(m.chat.id)
.message_id(m.message_id);
bot.edit_message_text(format!("you chose: {data}"), Some(params)).await?;
}
Send files
use ferobot::InputFile;
bot.send_photo(chat_id, "AgACAgIAAxkBAAI...", None).await?;
bot.send_photo(chat_id, "https://example.com/img.jpg", None).await?;
let data = tokio::fs::read("photo.jpg").await?;
bot.send_photo(chat_id, InputFile::memory("photo.jpg", data), None).await?;
Webhook (built-in server)
Requires the webhook feature.
use ferobot::{Bot, Dispatcher, DispatcherOpts, Updater};
let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap()).await.unwrap();
let dp = Dispatcher::new(DispatcherOpts::default());
Updater::new(bot, dp)
.webhook_port(8443)
.webhook_secret("my_secret")
.start_webhook("https://yourdomain.com/bot")
.await
.unwrap();
For local testing: ngrok http 8443.
Webhook (manual, bring your own server)
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use std::sync::Arc;
use ferobot::{types::Update, Bot};
struct AppState { bot: Bot }
#[tokio::main]
async fn main() {
let bot = Bot::new("YOUR_TOKEN").await.unwrap();
bot.set_webhook("https://yourdomain.com/bot", None).await.unwrap();
let app = Router::new()
.route("/bot", post(handle))
.with_state(Arc::new(AppState { bot }));
axum::serve(
tokio::net::TcpListener::bind("0.0.0.0:8443").await.unwrap(),
app,
).await.unwrap();
}
async fn handle(State(s): State<Arc<AppState>>, Json(update): Json<Update>) -> StatusCode {
let bot = s.bot.clone();
tokio::spawn(async move {
if let Some(msg) = update.message {
let _ = bot.send_message(msg.chat.id, "got it", None).await;
}
});
StatusCode::OK
}
|
Built-in WebhookServer |
Manual |
| Zero boilerplate |
yes |
no |
| Secret token validation |
built-in |
manual |
| Custom routing / middleware |
no |
yes |
| Works with existing server |
no |
yes |
| Feature flag needed |
webhook |
no |
Error handling
use ferobot::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"),
Err(e) if e.is_api_error_code(429) => {
let secs = e.flood_wait_seconds().unwrap_or(5);
tokio::time::sleep(std::time::Duration::from_secs(secs as u64)).await;
}
Err(e) => eprintln!("error: {e}"),
}
Conversation (FSM)
Multi-step flows with pluggable state storage (in-memory or Redis).
use ferobot::{
Bot, CommandHandler, ConversationHandler, ConversationOpts,
Dispatcher, DispatcherOpts, HandlerResult, MessageHandler, NextState, Updater,
};
use ferobot::framework::{Context, filters::message as mf};
async fn ask_name(bot: Bot, ctx: Context) -> HandlerResult {
if let Some(msg) = ctx.effective_message() {
msg.reply(&bot, "what's your name?", None).await?;
}
Ok(NextState::new("waiting_name").into())
}
async fn got_name(bot: Bot, ctx: Context) -> HandlerResult {
if let Some(msg) = ctx.effective_message() {
let name = msg.get_text().unwrap_or("?");
msg.reply(&bot, format!("hi {name}!"), None).await?;
}
Ok(())
}
#[tokio::main]
async fn main() {
let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap()).await.unwrap();
let conv = ConversationHandler::new(
ConversationOpts::default(),
vec![CommandHandler::new("start", ask_name)],
vec![("waiting_name", vec![
Box::new(MessageHandler::new("name", mf::text(), got_name))
])],
vec![],
);
let mut dp = Dispatcher::new(DispatcherOpts::default());
dp.add_handler(conv);
Updater::new(bot, dp).start_polling().await.unwrap();
}
Redis storage:
ferobot = { version = "0.1", features = ["redis-storage"] }
use ferobot::storage::RedisStorage;
let storage = RedisStorage::new("redis://127.0.0.1/")
.await?
.with_prefix("mybot:")
.with_ttl(86400);
let opts = ConversationOpts { storage: Some(storage), ..Default::default() };
API Reference
Bot constructors
| Method |
Description |
Bot::new(token) |
Connect and verify token via getMe |
Bot::with_api_url(token, url) |
Use a custom or local Bot API server |
Bot::new_unverified(token) |
Skip getMe on startup |
bot.token() |
Get the raw token string |
bot.me |
User populated on creation |
ChatId
Numeric IDs, negative group IDs, and @username strings all work anywhere a ChatId is expected:
bot.send_message(123456789i64, "user", None).await?;
bot.send_message(-100123456789i64, "group or channel", None).await?;
bot.send_message("@username", "by username", None).await?;
InputFile
bot.send_photo(chat_id, "AgACAgIAAxkBAAI...", None).await?;
bot.send_photo(chat_id, "https://example.com/img.jpg", None).await?;
InputFile::memory("photo.jpg", bytes)
BotError
pub enum BotError {
Http(reqwest::Error),
Json(serde_json::Error),
Api {
code: i64,
description: String,
retry_after: Option<i64>, migrate_to_chat_id: Option<i64>, },
InvalidToken,
Other(String),
}
err.is_api_error_code(429) err.flood_wait_seconds()
Optional params
Every method with optional fields has a *Params builder:
use ferobot::gen_methods::SendMessageParams;
let params = SendMessageParams::new()
.parse_mode("HTML".to_string())
.disable_notification(true);
bot.send_message(chat_id, "<b>hello</b>", Some(params)).await?;
Auto-codegen
Types and methods are generated from tgapis/x, which tracks the official Telegram Bot API. CI auto-regenerates on every upstream change.
Manual regeneration:
curl -sSf https://raw.githubusercontent.com/tgapis/x/data/botapi.json -o api.json
python3 codegen/codegen.py api.json ferobot/src/
cargo build
Never edit gen_types.rs or gen_methods.rs by hand.
Contributing
cargo build --workspace
cargo test --workspace
cargo clippy --workspace -- -D warnings
cargo fmt --all
One concern per PR. Run fmt and clippy before submitting. Open a GitHub issue for bugs and features, or email ankitchaubey.dev@gmail.com for security issues.
License
MIT License - Copyright (c) 2026 Ankit Chaubey
Built by Ankit Chaubey · Telegram · docs.rs · crates.io
If ferobot saved you time, a star on GitHub means a lot.