🤖 An elegant Telegram bots framework for Rust https://docs.rs/teloxide
Find a file
2021-07-08 01:56:45 -07:00
.github Merge pull request #376 from teloxide/WaffleLapkin-patch-1 2021-04-28 09:51:45 -07:00
examples Update dialogue_bot example 2021-07-06 23:15:12 +03:00
media Replace media/PING_PONG_BOT.gif with DICES_BOT.gif 2020-07-29 16:10:19 +06:00
src Show "This is supported on feature="..." only." in socs of reexported teloxide-core items 2021-07-07 20:27:38 +03:00
tests Ignore buggy nonstandard_macro_braces clippy lint 2021-07-04 00:22:11 +03:00
.gitignore Gardening + update docs 2020-10-24 20:05:48 +03:00
Cargo.toml Show "This is supported on feature="..." only." in socs of reexported teloxide-core items 2021-07-07 20:27:38 +03:00
CHANGELOG.md Update dependencies 2021-07-05 21:29:36 +03:00
CODE_STYLE.md Enforce writing log::<op>!(...) in CODE_STYLE.md 2021-03-28 09:00:36 +06:00
CONTRIBUTING.md Gardening + docs improvals 2020-10-25 11:50:47 +03:00
ICON.png Improve the ICON.png quality 2019-10-15 16:16:56 +06:00
LICENSE Update LICENSE 2020-02-22 02:56:02 +06:00
logo.svg Make logo svg 2019-12-30 12:14:05 +03:00
MIGRATION_GUIDE.md Add migration guide 2021-07-08 02:21:12 +03:00
netlify.toml fix netlify config 2021-07-07 20:55:55 +03:00
README.md add community bot: dracary18/grpmr-rs 2021-06-22 11:37:07 +05:30
rustfmt.toml Update the dependencies 2021-02-22 08:14:33 +06:00

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, rewrite UpdateWithCx<Message> -> UpdateWithCx<AutoSend<Bot>, Message>.
  • ResponseResult<()> -> Result<(), Box<dyn Error + Send + Sync>> (or import ResponseResult 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

  1. Download Rust.
  2. Create a new bot using @Botfather to get a token in the format 123456789:blablabla.
  3. Initialise the TELOXIDE_TOKEN environmental variable to your token:
# Unix-like
$ export TELOXIDE_TOKEN=<Your token here>

# Windows
$ set TELOXIDE_TOKEN=<Your token here>
  1. 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
  1. Run cargo new my_bot, enter the directory and put these lines into your Cargo.toml:
[dependencies]
teloxide = { version = "0.4", features = ["auto-send", "macros"] }
log = "0.4.8"
pretty_env_logger = "0.4.0"
tokio = { version =  "1.3", features = ["rt-multi-thread", "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()).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:

(dialogue_bot/src/main.rs)

// 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,
    }
}

More examples!

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.

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 propose your own bot to our collection!

Contributing

See CONRIBUTING.md.