La maggior parte dei tutorial per bot Telegram sceglie Python o Node. Rust vale davvero la pena di essere considerato se ti importa della semplicita di deployment (un binario, nessun interprete), dell’impronta di memoria, o sei gia in un codebase Rust. Questo articolo illustra un bot completo usando teloxide: comandi, inline keyboard, una macchina a stati multi-step e deployment in produzione.
Setup#
Crea un nuovo progetto e aggiungi le dipendenze al Cargo.toml:
[package]
name = "telegram-bot"
version = "0.1.0"
edition = "2021"
[dependencies]
teloxide = { version = "0.12", features = ["macros", "dialogues"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
pretty_env_logger = "0.5"
serde = { version = "1", features = ["derive"] }
[profile.release]
strip = true
opt-level = "z" # Ottimizza per dimensione del binarioteloxide 0.12 richiede Rust 1.70+. La feature macros abilita il derive #[command]. La feature dialogues abilita l’API della macchina a stati. Entrambe sono necessarie per questo articolo.
Token del bot e configurazione dell’ambiente#
Ottieni un token da @BotFather inviando /newbot. Salvalo in una variabile d’ambiente. Non hardcodificarlo mai.
TELOXIDE_TOKEN=123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghiCaricalo all’avvio con std::env::var. teloxide legge anche TELOXIDE_TOKEN automaticamente se usi Bot::from_env().
Handler dei comandi con derive macro#
Il derive BotCommands genera il parsing dei comandi e il testo di /help dai doc comment:
use teloxide::{prelude::*, utils::command::BotCommands};
#[tokio::main]
async fn main() {
pretty_env_logger::init();
log::info!("Avvio del bot...");
let bot = Bot::from_env();
Command::repl(bot, answer).await;
}
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase", description = "Comandi disponibili:")]
enum Command {
#[command(description = "Mostra questo messaggio di aiuto")]
Help,
#[command(description = "Saluta un utente per nome")]
Hello(String),
#[command(description = "Mostra l'ora attuale del server")]
Time,
}
async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
match cmd {
Command::Help => {
bot.send_message(msg.chat.id, Command::descriptions().to_string())
.await?;
}
Command::Hello(name) => {
bot.send_message(msg.chat.id, format!("Ciao, {}!", name))
.await?;
}
Command::Time => {
let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
bot.send_message(msg.chat.id, format!("Ora del server: {now}"))
.await?;
}
}
Ok(())
}Risposta a messaggi di testo arbitrari#
Per i messaggi non-comando, usa un branch separato dell’handler. I dispatcher di teloxide compongono piu handler:
use teloxide::prelude::*;
pub async fn handle_text(bot: Bot, msg: Message) -> ResponseResult<()> {
if let Some(text) = msg.text() {
if !text.starts_with('/') {
bot.send_message(msg.chat.id, format!("Hai detto: {text}"))
.await?;
}
}
Ok(())
}Inline keyboard con callback handler#
Le inline keyboard allegano pulsanti a un messaggio. Ogni pulsante porta una stringa callback_data che arriva come aggiornamento CallbackQuery, separato dagli aggiornamenti Message:
use teloxide::{
prelude::*,
types::{InlineKeyboardButton, InlineKeyboardMarkup},
};
pub async fn send_menu(bot: Bot, chat_id: ChatId) -> ResponseResult<()> {
let keyboard = InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback("Opzione A", "opt_a"),
InlineKeyboardButton::callback("Opzione B", "opt_b"),
],
vec![InlineKeyboardButton::callback("Annulla", "cancel")],
]);
bot.send_message(chat_id, "Scegli un'opzione:")
.reply_markup(keyboard)
.await?;
Ok(())
}
pub async fn handle_callback(bot: Bot, q: CallbackQuery) -> ResponseResult<()> {
// Rispondi sempre alla callback -- questo chiude lo spinner di caricamento.
bot.answer_callback_query(&q.id).await?;
let data = q.data.as_deref().unwrap_or("");
let text = match data {
"opt_a" => "Hai scelto l'Opzione A",
"opt_b" => "Hai scelto l'Opzione B",
"cancel" => "Annullato",
_ => "Opzione sconosciuta",
};
if let Some(msg) = q.message {
bot.edit_message_text(msg.chat().id, msg.id(), text).await?;
}
Ok(())
}Chiama sempre bot.answer_callback_query(&q.id) quando gestisci una CallbackQuery. Se non lo fai, lo spinner nel pulsante Telegram continua a girare per 30 secondi, il che sembra un bug all’utente.
Macchina a stati con la feature dialogue#
Le conversazioni multi-step (onboarding, form) necessitano di una macchina a stati. La feature dialogue di teloxide gestisce lo stato salvato in memoria (o Redis per la produzione):
use teloxide::{
dispatching::dialogue::{self, InMemStorage},
prelude::*,
};
type MyDialogue = Dialogue<State, InMemStorage<State>>;
type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
#[derive(Clone, Default)]
pub enum State {
#[default]
Start,
AwaitingName,
AwaitingAge { name: String },
}
pub async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
bot.send_message(msg.chat.id, "Benvenuto! Come ti chiami?")
.await?;
dialogue.update(State::AwaitingName).await?;
Ok(())
}
pub async fn receive_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
let name = msg.text().unwrap_or("sconosciuto").to_owned();
bot.send_message(msg.chat.id, format!("Piacere, {name}! Quanti anni hai?"))
.await?;
dialogue.update(State::AwaitingAge { name }).await?;
Ok(())
}
pub async fn receive_age(
bot: Bot,
dialogue: MyDialogue,
msg: Message,
name: String,
) -> HandlerResult {
let age: u32 = match msg.text().and_then(|t| t.parse().ok()) {
Some(a) => a,
None => {
bot.send_message(msg.chat.id, "Per favore invia un numero.").await?;
return Ok(());
}
};
bot.send_message(
msg.chat.id,
format!("Registrato: {name}, eta {age}. Benvenuto!"),
)
.await?;
dialogue.exit().await?;
Ok(())
}Gestione degli errori#
teloxide usa LoggingErrorHandler per gli errori non gestiti per impostazione predefinita. Configura il dispatcher esplicitamente:
use teloxide::{
dispatching::dialogue::InMemStorage,
error_handlers::LoggingErrorHandler,
prelude::*,
};
Dispatcher::builder(bot, schema())
.dependencies(dptree::deps![InMemStorage::<State>::new()])
.error_handler(LoggingErrorHandler::with_custom_text(
"Si e verificato un errore nel dispatcher",
))
.enable_ctrlc_handler()
.build()
.dispatch()
.await;Deployment come singolo binario#
Compila un binario release e pacchettizzalo in un’immagine Docker minimale:
cargo build --release
# Il binario si trova in target/release/telegram-bot (~5MB stripped)FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY target/release/telegram-bot /usr/local/bin/telegram-bot
ENV RUST_LOG=info
CMD ["telegram-bot"]Per un’immagine davvero minimale usando musl (binario completamente statico):
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-muslFROM scratch
COPY target/x86_64-unknown-linux-musl/release/telegram-bot /telegram-bot
ENV RUST_LOG=info
ENTRYPOINT ["/telegram-bot"]L’immagine scratch finisce a circa 7MB totali.
Il long polling e il default. Il bot apre una connessione HTTP di lunga durata ai server Telegram e riceve gli aggiornamenti man mano che arrivano. Funziona attraverso firewall e NAT, non richiede un IP pubblico ed e la scelta corretta per lo sviluppo e la produzione a basso traffico.
// Il long polling e il default con Bot::from_env() + Command::repl()
// Non e necessaria nessuna configurazione aggiuntiva.
let bot = Bot::from_env();
Command::repl(bot, answer).await;I webhook richiedono un endpoint HTTPS pubblico. Telegram invia gli aggiornamenti al tuo URL invece che tu li faccia polling. Questo elimina la latenza del polling ed e meglio ad alta scala, ma richiede un certificato TLS e un IP raggiungibile.
use teloxide::dispatching::update_listeners::webhooks;
let addr = ([0, 0, 0, 0], 8443).into();
let url = "https://your-domain.com/webhook".parse().unwrap();
let listener = webhooks::axum(bot.clone(), webhooks::Options::new(addr, url))
.await
.expect("webhook setup fallito");
Dispatcher::builder(bot, schema())
.build()
.dispatch_with_listener(listener, LoggingErrorHandler::new())
.await;Se vuoi approfondire questi argomenti, offro sessioni di coaching 1:1 per ingegneri che lavorano su integrazione AI, architettura cloud e platform engineering. Prenota una sessione (50 EUR / 60 min) o scrivimi a manuel.fedele+website@gmail.com.