-
Notifications
You must be signed in to change notification settings - Fork 2
Tutorial
We're going to write a bot that can evaluate math expressions and send the result in chats and inlinely. You'll need to:
- Be friends with Rust;
- Have basic knowledge of asynchromous programming in Rust.
Create a new crate:
cargo new tbot_example
cd tbot_example
In your Cargo.toml
, add this:
[dependencies]
tbot = "0.5"
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).
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]
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 withBot::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 anasync
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 byPolling::start
resolves to aResult
, 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!
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 thetypes::Text
type which also has theentities
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.
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.query);
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::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:
- 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. - The second item of our tuple is
inline_code([answer.to_string()])
. Though formatters likebold
support nesting and so they accept the same arguments asmarkup::markdown_v2
, you cannot nest inside code, and soinline_code
takes only strings. But to reduce allocations it takes something that can iterate over strings — that's why we convertanswer
to a string and then put it in an array. - Then we call
to_string
onmarkup::markdown_v2
to actually turn it into a string. - 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?). - Finally, we have the message that we want to send in the
message
variable. we need to wrap it inText::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!
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 Settings
→ Inline 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 id = id.lock().await;
id += 1;
id.to_string()
};
let content = Text::new(ParseMode::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:
-
content
— this is what the user will send when they choose a result.InputMessageContent
is divided into four variants, and we construct theText
variant. -
article
— this is one kind ofinline_query::Result
. It requires a title and any kind ofInputMessageContent
. In addition, we set itsdescription
to what we're going to send. -
result
— the result itself. Construction requires an ID and any kind ofinline_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).
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.