Skip to content

Tutorial

SnejUgal edited this page Oct 24, 2020 · 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.6.6"
tokio = { version = "0.2", features = ["macros", "sync"] }
meval = "0.2"

You'll use meval to evaluate math expressions, tbot to make the bot and tokio to start the runtime (you'll need the sync feature to use the asynchronous version of Mutex later).

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 we will create our bot with the token in the BOT_TOKEN environment variable:

use tbot::prelude::*;

#[tokio::main]
async fn main() {
    let mut bot = tbot::from_env!("BOT_TOKEN").event_loop();
}

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

Using the from_env! 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 handler:

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

Handlers are required to return a Future so that you don't have to spawn futures yourself — tbot does that for you. Here, our handler is a closure whose body is wrapped in an async block, and we're going to stick to this style for the rest of the tutorial.

That's why we need mut on bot: EventLoop::text pushes 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 handler, 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().await.unwrap();

The Future returned by Polling::start resolves to a Result, but it can only return an error if the error occured during initialization — after that, the event loop runs for ever.

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| async move {
    println!("Someone sent me this: {}", context.text.value);
});

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 use context.send_message_in_reply:

// ...
let message = format!("You sent me {}", context.text.value);
context.send_message_in_reply(&message).call().await.unwrap();

context.send_message_in_reply will construct a method with the given text and a reply to that message. Because methods have optional fields, they are set with chained methods. Once you finish building the method, you must call and .await it. The returned Future resovles to a Result — whether the call sucessed or failed. Here, we simply unwrap the Result, but in production, you should handle errors properly.

Now you can run your bot again, and 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.

// We're going to import another struct with the name `Text` later
use tbot::{
    markup::{markdown_v2, inline_code},
    types::parameters::Text as ParseMode,
};

bot.text(|context| async move {
    let calc_result = meval::eval_str(&context.text.value);
    let message = if let Ok(answer) = calc_result {
        markdown_v2(("= ", inline_code([answer.to_string()]))).to_string()
    } else {
        markdown_v2("Whops, I couldn't evaluate your expression :(")
            .to_string()
    };

    context
        .send_message_in_reply(ParseMode::with_markdown_v2(&message))
        .call()
        .await
        .unwrap();
});

We want to display the result as code, which can be easily copied in some clients. We need to work with markup, and that's where tbot::markup comes in: it makes it easy and painless to work with markup. It has formatters like bold and inline_code, which are then passed to a markup formatter — either markup::markdown_v2 or markup::html — which does the actual formatting and can be turned into a string. Let's break it up into parts:

  1. We decide that we want to use MarkdownV2 for formatting, so we import markup::markdown_v2 into scope. It can take strings and basic formatters, but, most importantly, it can take tuples of the basic formatters or strings. So we pass a tuple with the string "= " as the first item and some inline code as the next item. All strings are properly escaped, so you don't need to care that an unexpected underscore form user input breaks your markup.
  2. The second item of our tuple is inline_code([answer.to_string()]). Though formatters like bold support nesting and so they accept the same arguments as markup::markdown_v2, you cannot nest inside code, and so inline_code takes only strings. But to reduce allocations it takes something that can iterate over strings — that's why we convert answer to a string and then put it in an array.
  3. Then we call to_string on markup::markdown_v2 to actually turn it into a string.
  4. Since evalution can fail, we want to send an error in this case. It is easier to return MarkdownV2-formatted strings from all the arms, so we wrap the error's text in markup::markdown_v2 so that it can escape it (MarkdownV2 is very strict about reserved characters, and that list even includes '.' — so why not automatically escape all strings instead of manually escaping each point and bracket for a dozen minutes until Telegram stops sending errors back?).
  5. Finally, we have the message that we want to send in the message variable. we need to wrap it in Text::with_markdown_v2 so that Telegram knows that our message has markup in MarkdownV2.

Now the bot will evaluate experessions it receives and sends the result back. 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.

Second, we're going to need some state for this. To send inline results, we need to add an ID to each result. Even though we're going to send only one result at a time, if we use the same ID for all results and the user calculates something through inline mode twice or more, they may see old results even though they're calculating another problem. So we need to have a counter that we increase and use each time we need an ID. So bring tokio::sync::Mutex into scope and change bot's declaration to this:

let mut bot = tbot::from_env!("BOT_TOKEN").stateful_event_loop(Mutex::new(0));

Once we opt in to the stateful event loop, our handlers need to take two arguments: the first argument is still the context and the second one is the state. So you need to change the first handler's a little bit:

- bot.text(|context| async move {
+ bot.text(|context, _| async move {

Third, we'll add another handler:

bot.inline(|_context, _state| async { });

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

use tbot::types::{
    inline_query::{self, result::Article},
    input_message_content::Text,
};

bot.inline(move |context, id| async move {
    let calc_result = meval::eval_str(&context.query);
    let (title, message) = if let Ok(answer) = calc_result {
        let answer = answer.to_string();
        let message = markdown_v2(inline_code([
            context.query.as_str(),
            " = ",
            answer.as_str(),
        ]))
        .to_string();
        (answer, message)
    } else {
        let title = "Whops...".into();
        let message = markdown_v2("I couldn't evaluate your expression :(")
            .to_string();
        (title, message)
    };

    let id = {
        let mut id = id.lock().await;
        *id += 1;
        id.to_string()
    };

    let content = Text::new(ParseMode::with_markdown_v2(&message));
    let article = Article::new(&title, content).description(&message);
    let result = inline_query::Result::new(&id, article);
    context.answer(&[result]).call().await.unwrap();
});

First of all, we evaluate the requested expression and generate the message that we want to send. Then, we need to generate an ID, so we lock our counter (which is passed as the second argument to our handler), increment and stringify it.

Then we need to generate the result that we're going to send to Telegram. It's possible to show several results, but we only need only one — with the calculation result. We construct it step-by-step:

  1. content — this is what the user will send when they choose a result. InputMessageContent is divided into four variants, and we construct the Text variant.
  2. article — this is one kind of inline_query::Result. It requires a title and any kind of InputMessageContent. In addition, we set its description to what we're going to send.
  3. result — the result itself. Construction requires an ID and any kind of inline_query::Result.

Finally, we call context.answer with a slice of one item, the result. And voila! Your bot now can work inlinely. If you need it, here's the complete code (and with better error handling, too).

What's next?

Now you're familiar with tbot, 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. You can also take a look at our examples to see how to write bots with tbot. If you get stuck, feel free to ask your question in our Telegram group.