Skip to content

Tutorial

SnejUgal edited this page Jun 13, 2019 · 44 revisions

We're going to write a bot that can evaluate math expressions and send the result in chats and inlinely. You'll need to:

Installing tbot

Create a new crate:

cargo new tbot_example
cd tbot_example

In your Cargo.toml, add this:

[dependencies]
tbot = "0.1"
meval = "0.2"

We'll use meval to evaluate math expressions. To work with Telegram Bots API, you'll only need tbot. This is our design philosophy.

Getting updates

First of all, you'll need your bot's token. If you don't have one already, create one with BotFather and it will give you the token. We'll assume you set it in the environment as BOT_TOKEN.

Once you're done with Cargo.toml and the token, open src/main.rs. First, in main, we will create our bot with the token in the BOT_TOKEN environment variable:

let mut bot = tbot::bot!("BOT_TOKEN").event_loop();

The bot! macro allows extracting the variable at compile time. You can replace it with tbot::Bot::from_env if you want to extract it at runtime.

Using the bot! macro, we create a [Bot]. However, to configure handlers and run them, we need to create an [EventLoop] using [Bot::event_loop].

Our bot is going to listen to text messages. So, let's add a listener:

bot.text(|_context| {
    println!("Someone sent me a message!");
});

That's why we need mut on bot: under the hood, text will push the handler in bot's internal Vec. That's a trade-off for a really convenient update subscription mechanism. We'll learn about context a bit later.

Though we added a listener, one thing is missing: we need to actually listen to updates (messages are a subset of updates). This can be done with webhooks or polling. Polling is simpler, so let's use it:

bot.polling().start();

Now, run cargo run and try to send your bot a message. It won't reply you yet, but you'll see a message in your terminal that it received a message!

Processing updates and sending a reply

We can already receive updates, but how do we process them? It's easy if you try: remember context? That's where updates are coming to. To get the text of the message, we need to use context.text.value. Let's print it:

bot.text(|context| {
    println!("What is {}?", context.text.value);
});

Note: context.text is of the types::Text type which also has the entities field for entities in the message.

Now try it out. You will see your messages in the terminal.

Let's send a reply. First, we'll construct it with context.send_message_in_reply:

use tbot::prelude::*;
// ...
let reply = context
    .send_message_in_reply(&format!("You sent me {}", context.text.value))
    .into_future()
    .map_err(|error| {
        dbg!(error);
    });

Note that we brought the prelude into the scope — the into_future and map_err methods come from the IntoFuture and Future traits respectively.

context.send_message_in_reply will construct a message with the given text with a reply to that message. Because methods have optional fields, they are set with chained methods. Once you finish building a message, you must call into_future on it. Then you must handle errors that may happen during sending the method. After that, we need to run the future to send the reply. We're going to call tbot::spawn:

// ...
tbot::spawn(reply);

Though tbot::spawn calls tokio::spawn, tbot's function lets a future's Item be anything: it will map it to () under the hood. This behavior is pretty convinient because you won't need to process the response of many methods, like in our case — we might map the response and do something with the returned message, but we don't care about it in this case.

Run your bot again. Now it will reply you.

But we actually want a math bot, not an echo one! Let's do it now.

Doing math

We're going to use meval so we won't need to do math ourselves.

use tbot::types::ParseMode::Markdown;

bot.text(|context| {
    let message = match meval::eval_str(&context.text.value) {
        Ok(result) => format!("= `{}`", result),
        Err(_) => "Whops, I couldn't evaluate your expression :(".into(),
    };

    let reply = context
        .send_message_in_reply(&message)
        .parse_mode(Markdown)
        .into_future()
        .map_err(|error| {
            dbg!(error);
        });

    tbot::spawn(reply);
});

We wrap the result in Markdown's backticks, so it may be easier to copy the result in some clients (e.g. on Android). So we're calling .parse_mode(Markdown) before turning the method into a Future.

Now the bot will evaluate experessions it receives. Try it out!

Inline mode

Now we're going to implement the inline mode. It isn't hard to do.

First, ensure that your bot can accept inline updates. Go to BotFather, choose your bot, click Bot SettingsInline Mode. It's off by default, so turn it on if you haven't done it yet.

Next, we'll add another handler:

bot.inline(|_context| ());

Note that for inline handlers, context is completely different, but we'll get through this. Instead of context.text.value, we need to use context.query. Instead of context.send_message_in_reply, we need to use context.answer_inline_query. Through replacing the first one is easy, the second isn't.

use tbot::types::{
    InlineQueryResult::Article,
    InputMessageContent::Text,
};
// ...
let mut id: u32 = 0;

bot.inline(move |context| {
    let (title, message) = match meval::eval_str(&context.query) {
        Ok(result) => (
            result.into(),
            format!("`{} = {}`", context.query, result),
        ),
        Err(_) => (
            "Whops...".into(),
            "I couldn't evaluate your expression :(".into(),
        ),
    };

    let reply = context
        .answer_inline_query([
            Article::new(
                &id.to_string(),
                &title,
                Text::new(&message).parse_mode(Markdown),
            ),
        ])
        .into_future()
        .map_err(|error| {
            dbg!(error);
        });

    id += 1;

    tbot::spawn(reply);
});

We pass a slice to context.answer_inline_query of InineQueryResults. It splits into 20 (at the time of writing this tutorial) variants, but we only need one — Article. We need to pass an ID, so we created the id variable with the initial value of 0: u32 and move it to our closure. When we answer, we stringify id to pass it as an ID (as Telegram requires), and later we increment it. If you simply send a constant ID, shown results may fail to update as the user types.

It also needs InputMessageContent which is divided into four types, and of all of them we only need Text.

Now we calculate the result, then send it. And voila! Your bot now can work inlinely. If you need it, here's the complete code.

What's next?

Once you've read this tutorial, you're familiar with tbot's design, and you can start writing your own bots. You may want to check our How-to guides if you need or refer to our documentation to look up how to use several methods or construct some types. If you get stuck, feel free to ask your question in our group on Telegram.

Clone this wiki locally