Provide new url for tg-vimhelpbot
16 KiB
FYI: Updating from v0.3.4 to v0.4.0
answer_str
->answer
msg.text_owned()
->msg.map(ToOwned::to_owned)
- Use
.auto_send()
to construct your bot:let bot = Bot::from_env().auto_send();
. This allows not to write.send()
after each request; now it is done automatically. Also, rewriteUpdateWithCx<Message>
->UpdateWithCx<AutoSend<Bot>, Message>
. ResponseResult<()>
->Result<(), Box<dyn Error + Send + Sync>>
(or importResponseResult
beforehand:use teloxide::requests::ResponseResult;
)- Tokio updated to v1.2.
Note: this list is non-exhaustive; for the full list of changes, see the teloxide-core changelog and teloxide changelog.
teloxide
A full-featured framework that empowers you to easily build Telegram bots using the async
/.await
syntax in Rust. It handles all the difficult stuff so you can focus only on your business logic.
Highlights
- Functional reactive design. teloxide follows functional reactive design, allowing you to declaratively manipulate streams of updates from Telegram using filters, maps, folds, zips, and a lot of other adaptors.
- Dialogues management subsystem. We have designed our dialogues management subsystem to be easy-to-use, and, furthermore, to be agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve persistence. Out-of-the-box storages include Redis and Sqlite.
- Strongly typed bot commands. You can describe bot commands as enumerations, and then they'll be automatically constructed from strings — just like JSON structures in serde-json and command-line arguments in structopt.
Setting up your environment
- Download Rust.
- Create a new bot using @Botfather to get a token in the format
123456789:blablabla
. - Initialise the
TELOXIDE_TOKEN
environmental variable to your token:
# Unix-like
$ export TELOXIDE_TOKEN=<Your token here>
# Windows
$ set TELOXIDE_TOKEN=<Your token here>
- Make sure that your Rust compiler is up to date:
# If you're using stable
$ rustup update stable
$ rustup override set stable
# If you're using nightly
$ rustup update nightly
$ rustup override set nightly
- Run
cargo new my_bot
, enter the directory and put these lines into yourCargo.toml
:
[dependencies]
teloxide = "0.4"
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version = "1.3", features = ["rt-threaded", "macros"] }
API overview
The dices bot
This bot replies with a dice throw to each received message:
(Full)
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting dices_bot...");
let bot = Bot::from_env().auto_send();
teloxide::repl(bot, |message| async move {
message.answer_dice().await?;
respond(())
})
.await;
}
Commands
Commands are strongly typed and defined declaratively, similar to how we define CLI using structopt and JSON structures in serde-json. The following bot accepts these commands:
/username <your username>
/usernameandage <your username> <your age>
/help
(Full)
use teloxide::{prelude::*, utils::command::BotCommand};
use std::error::Error;
#[derive(BotCommand)]
#[command(rename = "lowercase", description = "These commands are supported:")]
enum Command {
#[command(description = "display this text.")]
Help,
#[command(description = "handle a username.")]
Username(String),
#[command(description = "handle a username and an age.", parse_with = "split")]
UsernameAndAge { username: String, age: u8 },
}
async fn answer(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
command: Command,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match command {
Command::Help => cx.answer(Command::descriptions()).send().await?,
Command::Username(username) => {
cx.answer(format!("Your username is @{}.", username)).await?
}
Command::UsernameAndAge { username, age } => {
cx.answer(format!("Your username is @{} and age is {}.", username, age)).await?
}
};
Ok(())
}
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting simple_commands_bot...");
let bot = Bot::from_env().auto_send();
let bot_name: String = panic!("Your bot's name here");
teloxide::commands_repl(bot, bot_name, answer).await;
}
Dialogues management
A dialogue is described by an enumeration where each variant is one of possible dialogue's states. There are also subtransition functions, which turn a dialogue from one state to another, thereby forming an FSM.
Below is a bot that asks you three questions and then sends the answers back to you. First, let's start with an enumeration (a collection of our dialogue's states):
(dialogue_bot/src/dialogue/mod.rs)
// Imports are omitted...
#[derive(Transition, From)]
pub enum Dialogue {
Start(StartState),
ReceiveFullName(ReceiveFullNameState),
ReceiveAge(ReceiveAgeState),
ReceiveLocation(ReceiveLocationState),
}
impl Default for Dialogue {
fn default() -> Self {
Self::Start(StartState)
}
}
When a user sends a message to our bot and such a dialogue does not exist yet, a Dialogue::default()
is invoked, which is a Dialogue::Start
in this case. Every time a message is received, an associated dialogue is extracted and then passed to a corresponding subtransition function:
Dialogue::Start
(dialogue_bot/src/dialogue/states/start.rs)
// Imports are omitted...
pub struct StartState;
#[teloxide(subtransition)]
async fn start(
_state: StartState,
cx: TransitionIn<AutoSend<Bot>>,
_ans: String,
) -> TransitionOut<Dialogue> {
cx.answer("Let's start! What's your full name?").await?;
next(ReceiveFullNameState)
}
Dialogue::ReceiveFullName
(dialogue_bot/src/dialogue/states/receive_full_name.rs)
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveFullNameState;
#[teloxide(subtransition)]
async fn receive_full_name(
state: ReceiveFullNameState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer("How old are you?").await?;
next(ReceiveAgeState::up(state, ans))
}
Dialogue::ReceiveAge
(dialogue_bot/src/dialogue/states/receive_age.rs)
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveAgeState {
pub full_name: String,
}
#[teloxide(subtransition)]
async fn receive_age_state(
state: ReceiveAgeState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
match ans.parse::<u8>() {
Ok(ans) => {
cx.answer("What's your location?").await?;
next(ReceiveLocationState::up(state, ans))
}
_ => {
cx.answer("Send me a number.").await?;
next(state)
}
}
}
Dialogue::ReceiveLocation
(dialogue_bot/src/dialogue/states/receive_location.rs)
// Imports are omitted...
#[derive(Generic)]
pub struct ReceiveLocationState {
pub full_name: String,
pub age: u8,
}
#[teloxide(subtransition)]
async fn receive_location(
state: ReceiveLocationState,
cx: TransitionIn<AutoSend<Bot>>,
ans: String,
) -> TransitionOut<Dialogue> {
cx.answer(format!("Full name: {}\nAge: {}\nLocation: {}", state.full_name, state.age, ans))
.await?;
exit()
}
All these subtransition functions accept a corresponding state (one of the many variants of Dialogue
), a context, and a textual message. They return TransitionOut<Dialogue>
, e.g. a mapping from <your state type>
to Dialogue
.
Finally, the main
function looks like this:
// Imports are omitted...
#[tokio::main]
async fn main() {
teloxide::enable_logging!();
log::info!("Starting dialogue_bot...");
let bot = Bot::from_env().auto_send();
teloxide::dialogues_repl(bot, |message, dialogue| async move {
handle_message(message, dialogue).await.expect("Something wrong with the bot!")
})
.await;
}
async fn handle_message(
cx: UpdateWithCx<AutoSend<Bot>, Message>,
dialogue: Dialogue,
) -> TransitionOut<Dialogue> {
match cx.update.text().map(ToOwned::to_owned) {
None => {
cx.answer("Send me a text message.").await?;
next(dialogue)
}
Some(ans) => dialogue.react(cx, ans).await,
}
}
Recommendations
- Use this pattern:
#[tokio::main]
async fn main() {
run().await;
}
async fn run() {
// Your logic here...
}
Instead of this:
#[tokio::main]
async fn main() {
// Your logic here...
}
The second one produces very strange compiler messages due to the #[tokio::main]
macro. However, the examples in this README use the second variant for brevity.
Cargo features
Feature | Description |
---|---|
redis-storage |
Enables the Redis support. |
sqlite-storage |
Enables the Sqlite support. |
cbor-serializer |
Enables the CBOR serializer for dialogues. |
bincode-serializer |
Enables the Bincode serializer for dialogues. |
macros |
Re-exports macros from teloxide-macros . |
native-tls |
Enables the native-tls TLS implementation (enabled by default). |
rustls |
Enables the rustls TLS implementation. |
auto-send |
Enables the AutoSend bot adaptor. |
cache-me |
Enables the CacheMe bot adaptor. |
frunk |
Enables teloxide::utils::UpState . |
full |
Enables all the features except nightly . |
nightly |
Enables nightly-only features (see the teloxide-core features). |
FAQ
Q: Where I can ask questions?
A: Issues is a good place for well-formed questions, for example, about:
- the library design;
- enhancements;
- bug reports;
- ...
If you can't compile your bot due to compilation errors and need quick help, feel free to ask in our official Telegram group.
Q: Do you support the Telegram API for clients?
A: No, only the bots API.
Q: Why Rust?
A: Most programming languages have their own implementations of Telegram bots frameworks, so why not Rust? We think Rust provides a good enough ecosystem and the language for it to be suitable for writing bots.
UPD: The current design relies on wide and deep trait bounds, thereby increasing cognitive complexity. It can be avoided using mux-stream, but currently the stable Rust channel doesn't support necessary features to use mux-stream conveniently. Furthermore, the mux-stream could help to make a library out of teloxide, not a framework, since the design in this case could be defined by just combining streams of updates.
Q: Can I use webhooks?
A: teloxide doesn't provide special API for working with webhooks due to their nature with lots of subtle settings. Instead, you should setup your webhook by yourself, as shown in examples/ngrok_ping_pong_bot
and examples/heroku_ping_pong_bot
.
Associated links:
Q: Can I use different loggers?
A: Yes. You can setup any logger, for example, fern, e.g. teloxide has no specific requirements as it depends only on log. Remember that enable_logging!
and enable_logging_with_filter!
are just optional utilities.
Community bots
Feel free to push your own bot into our collection!
- steadylearner/subreddit_reader
- ArtHome12/vzmuinebot -- Telegram bot for food menu navigate
- Hermitter/tepe -- A CLI to command a bot to send messages and files over Telegram
- ArtHome12/cognito_bot -- The bot is designed to anonymize messages to a group
- pro-vim/tg-vimhelpbot -- Link
:help
for Vim in Telegram - sschiz/janitor-bot -- A bot that removes users trying to join to a chat that is designed for comments
- myblackbeard/basketball-betting-bot -- The bot lets you bet on NBA games against your buddies
- slondr/BeerHolderBot -- A bot that holds your beer
Contributing
See CONRIBUTING.md.