-
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;
- Know about Rust 2018;
- 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.1"
meval = "0.2"
We'll use meval
to evaluate math expressions. To work with the Bots API,
you'll only need tbot
. This is our design philosophy.
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 withBot::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 handler:
bot.text(|_context| {
println!("Someone sent me a message!");
});
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 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!
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 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 construct it with
context.send_message_in_reply
:
use tbot::prelude::*;
// ...
let message = format!("You sent me {}", context.text.value);
let reply = context
.send_message_in_reply(&message)
.into_future()
.map_err(|err| {
dbg!(err);
});
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
callstokio::spawn
,tbot
's function allowsF::Item
to 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 mightmap
the response and do something with the returned message, but we don't care about it in this case.
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'll need to import another struct with the same name later
use tbot::types::parameters::Text as ParseMode;
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(ParseMode::markdown(&message))
.into_future()
.map_err(|err| {
dbg!(err);
});
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). We need to tell Telegram to parse
this message as Markdown, so, instead of passing a bare string, we wrap the
message in Text
, calling the Text::markdown
method.
Now the bot will evaluate experessions it receives. 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.
Next, we'll add another handler:
bot.inline(|_context| ());
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
. Through replacing the first one is easy, the second isn't.
use tbot::types::{
inline_query::{self, result::Article},
input_message_content::Text,
};
// ...
let mut id: u32 = 0;
bot.inline(move |context| {
let (title, message) = match meval::eval_str(&context.query) {
Ok(result) => (
result.to_string(),
format!("`{} = {}`", context.query, result),
),
Err(_) => (
"Whops...".into(),
"I couldn't evaluate your expression :(".into(),
),
};
id += 1;
let id = id.to_string();
let content = Text::new(ParseMode::markdown(&message));
let article = Article::new(&title, content).description(&message);
let result = inline_query::Result::new(&id, article);
let answer = context.answer(&[result]).into_future().map_err(|err| {
dbg!(err);
});
tbot::spawn(reply);
});
First of all, we evaluate the requested expression. Then, we need to generate
an ID: we simply declared id: u32
, move it to our handler, increment it each
time and convert it to a string (as the Telegram's API requies).
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
,
convert it into a future and spawn it.
Voila! Your bot now can work inlinely. If you need it, here's the complete code.
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.
If you get stuck, feel free to ask your question in
our group on Telegram.