Fluent API

Every bot.method(required_args) returns a builder that implements IntoFuture. Chain optional params before .await — no None, no Some(...).

Sending messages

// just required args
bot.send_message(chat_id, "Hello!").await?;

// with options chained
bot.send_message(chat_id, "<b>Bold text</b>")
    .html()
    .silent()
    .reply_to(msg.message_id)
    .await?;

// all options available by field name
bot.send_message(chat_id, "hi")
    .parse_mode("MarkdownV2")
    .disable_notification(true)
    .await?;

Sending media

use ferobot::InputFile;

// File ID already on Telegram
bot.send_photo(chat_id, "AgACAgIAAxkBAAI...").await?;

// URL: Telegram fetches it
bot.send_photo(chat_id, "https://example.com/img.jpg").await?;

// Raw bytes from disk
let data = tokio::fs::read("photo.jpg").await?;
bot.send_photo(chat_id, InputFile::memory("photo.jpg", data))
    .caption("Look at this!")
    .await?;

Inline keyboard

use ferobot::ReplyMarkup;
use ferobot::types::{InlineKeyboardButton, InlineKeyboardMarkup};

let kb = ReplyMarkup::InlineKeyboard(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()
        },
    ]],
});

bot.send_message(chat_id, "Confirm?")
    .reply_markup(kb)
    .await?;

Answering callbacks

bot.answer_callback_query(&cq.id)
    .text("Done!")
    .show_alert(false)
    .await?;

Editing messages

use ferobot::types::MaybeInaccessibleMessage;

if let Some(MaybeInaccessibleMessage::Message(m)) = cq.message.as_deref() {
    bot.edit_message_text("Updated text")
        .chat_id(m.chat.id)
        .message_id(m.message_id)
        .await?;
}

Message helpers

// Reply to a message
msg.reply(&bot, "Thanks!").await?;

// Get text (also reads caption for media)
if let Some(text) = msg.get_text() { /* ... */ }

// Get a t.me deep link to the message
let link = msg.get_link(); // "https://t.me/c/1234/56"

// Get sender
let user = msg.get_sender(); // Option<&User>

// File URL from a File struct
let url = file.url(&bot); // Option<String>

InputFile

// File already on Telegram: pass file_id string directly
bot.send_photo(chat_id, "AgACAgIAAxkBAAI...").await?;

// URL: pass URL string directly
bot.send_photo(chat_id, "https://example.com/img.jpg").await?;

// Raw bytes
InputFile::memory("photo.jpg", bytes)

Raw API access

Call any Bot API method by name with arbitrary params:

let result: serde_json::Value = bot
    .raw("sendMessage")
    .param("chat_id",    123456789_i64)
    .param("text",       "Hello from raw!")
    .param("parse_mode", "HTML")
    .call()
    .await?;

Sending files (full reference)

Input type

How to pass it

Existing file_id

Pass the ID string directly

HTTP URL

Pass the URL string directly (Telegram fetches)

Bytes / disk file

InputFile::memory("name.ext", bytes)

let bytes = tokio::fs::read("document.pdf").await?;
bot.send_document(chat_id, InputFile::memory("doc.pdf", bytes))
    .await?;

Webhook (built-in vs manual)

Feature

Built-in WebhookServer

Manual (bring your own)

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

none

Manual webhook with Axum:

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").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").await;
        }
    });
    StatusCode::OK
}