Goodnight! Today I would like to briefly describe how to write a Telegram bot on Rust, which will run the code on Rust. The article does not have a goal to make a full immersion in the API telegram_bot, Serde, Telegram or in the nuances of development on Rust. Rather, it is exploratory in nature. Peano numbers using the type system will not add.
A temporary link to the bot, so that you can see the result
First, create a bot and get an HTTP API token.
We go to this guy and write the following:
We initiate the creation of a new bot: /newbot
.
The answer of the godfather:
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
In the answer we write the name of the bot we want to create: rust
.
The answer of the godfather:
Good Now let's choose a username for your bot. It must end in bot
. Like this, for example: TetrisBot or tetris_bot.
Following the directions, enter another name: rustlanguage_bot
.
The answer of the godfather:
Done! Congratulations on your new bot. You will find it at t.me/rustlanguage_bot. You can now add a list of commands. By the way, you’ll be better than the username for it. Just make sure the bot is fully functional before you do this. Use this token to access the HTTP. API:% TOKEN% For a description of the Bot API, see this page: https://core.telegram.org/bots/api
Fine. Bot created. %TOKEN%
- this is actually the token.
Now a little about how and where to run the code that the user will send to the bot as a message.
There is a Rust Playground
service that allows you to run the simplest Rust code online. We will use it. It is located at this address: https://play.rust-lang.org/
Following the link, we introduce a simple hello-world program:
fn main() { println!("Hello world!"); }
Open the Network
tab from DevTools
to see what and in what format it sends to get the results of the compilation:
It seems to be all transparent and clear. Let's try to reproduce from the console:
[loomaclin@localhost ~]$ curl -X POST -d '{"code":"fn main() {\n println!(\"Hello world!\");\n}","version":"stable","optimize":"0","test":false,"separate_output":true,"color":true,"backtrace":"0"}' https://play.rust-lang.org/evaluate.json {"program":"Hello world!\n","rustc":"rustc 1.16.0 (30cf806ef 2017-03-10)\n"}
Great, let's go further.
Create a project:
cargo new rust_telegram_bot --bin
Cargo.toml
: [dependencies] telegram-bot = { git = "https://github.com/White-Oak/telegram-bot.git" } hyper = "0.10.8" hyper-rustls = "0.3.2" serde_json = "0.9.10" serde = "0.9.14" serde_derive = "0.9.14"
I will briefly describe why they are needed:
Serde
designed to serialize / deserialize data in various formats. In this case, we need to work with JSON (serde_json) and pinch code generation (serde_derive);
Hyper
to work with the network will use the HTTP client, which it provides to interact with the Rust Playground. Since the interaction is carried out via the HTTPS protocol, a battery in the form of hyper-rustls
still needed;
telegram-bot
library, but not specifically it, but fork of comrade @white_oak, which fitted it to work with the current version of Hyper
.src/main.rs
include all the necessary libraries and modules: extern crate telegram_bot; extern crate hyper; extern crate hyper_rustls; extern crate serde_json; extern crate serde; #[macro_use] extern crate serde_derive; use serde_json::Value; use telegram_bot::{Api, MessageType, ListeningMethod, ListeningAction}; use std::io::Read; use hyper::client::Client; use hyper::net::HttpsConnector; use hyper_rustls::TlsClient;
Note: #[macro_use]
used to include in the scope of the current program macros from the library to which this attribute was applied.
In this line, we import modules from the library root to determine the type of message, the "wiretapping" method, and the structure representing the Telegram API:
use telegram_bot::{Api, MessageType, ListeningMethod, ListeningAction};
Using enum
, we describe the possible types of responses from the server, and there are 2 of them in this case, when the program was compiled successfully, and when a compilation error occurred:
#[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum ResponseType { ProgramCompiled { program: String, rustc: String }, ProgramCompileError { rustc: String } }
Noticed the attribute #[serde(untagged)]
, which was applied to the enumeration? He says that when (de) serializing, for the enumeration options, no tag will be searched for that clearly indicates which one it is. So how does Serde
determine which response option from the server we received? In fact, it will try to deserialize into each of the options until it reaches the first successful result. More details about this can be found in the official documentation: https://serde.rs/enum-representations.html .
Define the structure for our query in the Rust Playground:
#[derive(Serialize)] pub struct PlaygroundRequest { code: String, version: String, optimize: String, test: bool, separate_output: bool, color: bool, backtrace: String }
From the user input in this structure only the code
field will go. The rest is hard-coded, because we always do this :) (no)
In the main function of the main
program, we will create the Telegram API instance and make it print everything that came to the bot in the message:
fn main() { let api = Api::from_env("TOKEN").unwrap(); println!("getMe: {:?}", api.get_me()); let mut listener = api.listener(ListeningMethod::LongPoll(None)); let res = listener.listen(|u| if let Some(m) = u.message { let name = m.from.first_name; match m.msg { MessageType::Text(t) => { println!("<{}> {}", name, t); } _ => {} } }); }
To check the performance of this code, run the program, not forgetting to pass the real token received earlier as the environment variable:
TOKEN=%TOKEN% cargo run
Let us analyze a bit what we wrote above.
let api = Api::from_env("TOKEN").unwrap(); println!("getMe: {:?}", api.get_me());
Here we create an instance of the Api
structure imported from telegram_bot
, then create a bot listener in long-polling mode:
let mut listener = api.listener(ListeningMethod::LongPoll(None));
Finally, we create a message processing loop using the listen
function and matching by the message type pattern:
let res = listener.listen(|u| if let Some(m) = u.message { let name = m.from.first_name; match m.msg { MessageType::Text(t) => { println!("<{}> {}", name, t); } _ => {} } });
We agree that we will transmit the code only in text form. Files, etc. exclude. For this, as you can see, all the other enumeration options for MessageType
simply ignored.
We process the /rust
command by sending a request to the Rust Playground, and read the answer:
if t.starts_with("/rust ") { let program = t.split("/rust ").collect(); let mut result = String::new(); let tls = hyper_rustls::TlsClient::new(); let connector = HttpsConnector::new(tls); let client = Client::with_connector(connector); let playground_request = serde_json::to_string(&PlaygroundRequest { code: program, version: String::from("stable"), optimize: String::from("0"), test: false, separate_output: true, color: false, backtrace: String::from("0"), }) .unwrap(); let mut response = client .post("https://play.rust-lang.org/evaluate.json") .body(&playground_request) .send() .unwrap(); response.read_to_string(&mut result); println!("Result : {:?}", result); }
We process the request only if the message starts with a certain command ( /rust
):
if t.starts_with("/rust ") {
And also pull out the program code that needs to be compiled:
let program = t.split("/rust ").collect();
The serde_json::to_string(&PlaygroundReques { ... })
function serializes our query structure to a string. The rest of the code refers to the initialization of the HTTPS client, sending and reading the request, more about this can be found here: https://hyper.rs/hyper/v0.10.7/hyper/index.html .
We process the come answer:
let result : ResponseType = serde_json::from_str(&result) .unwrap_or(ResponseType::ProgramCompileError { rustc: String::from(" ") }); let mut result = match result { ResponseType::ProgramCompiled { program, .. } => { format!(" : {}", program) } ResponseType::ProgramCompileError { rustc, .. } => { format!(" : {}", rustc) } };
The serde::from_str
deserializes the reply to one of our enum
variants. In case the answer could not be deserialized, for simplicity we wrap it in a compilation error variant with the appropriate text. Next, we form our resulting message, which will be sent to the user, based on which of the enum
options was presented. Perhaps, for the first time, you see the pattern type { program, .. }
in the pattern matching, I will explain - this ignores the structure, which we do not need during the processing of this variant, when the structure is restructured.
Sending compilation results to chat:
if result.len() > 500 { result.truncate(500); } try!(api.send_message(m.chat.id(), result, None, None, Some(m.message_id), None));
At the end, we check the length of the message to exclude the results of the compilation with a large amount of output, and cut the case. After we send the message, specifying the identifier of the chat from which the request for compilation came, and transmit the final result of the compilation. We also send the id of the message you need to reply to. The rest of the transmitted parameters are optional and are responsible for displaying the preview, the type of response, and the like.
Output to console:
Finished dev [unoptimized + debuginfo] target(s) in 2.38 secs Running `target/debug/rust_telegram_bot` getMe: Ok(User { id: 334562900, first_name: "rust", last_name: None, username: Some("rustlanguage_bot") }) <Arsen> /rust abc Result : "{\"rustc\":\"rustc 1.16.0 (30cf806ef 2017-03-10)\\nerror: expected one of `!` or `::`, found `<eof>`\\n --> <anon>:1:1\\n |\\n1 | abc\\n | ^^^\\n\\nerror: aborting due to previous error\\n\\n\"}" <Arsen> /rust fn main() { println!("Hello habrahabr!"); } Result : "{\"program\":\"Hello habrahabr!\\n\",\"rustc\":\"rustc 1.16.0 (30cf806ef 2017-03-10)\\n\" }"
I think that's all. Thanks to WhiteOak for the telegram_bot
fork.
By the way, he has a project for binding to QML from Rust: https://github.com/White-Oak/qml-rust . Perhaps someone will be interesting.
Any constructive criticism is welcome.
The repository with the full code of this bot is located here .
I almost forgot to leave a link to the chat of the Russian-speaking community Rust, where you will always be helped to cope with the language: https://gitter.im/ruRust/general
Source: https://habr.com/ru/post/326830/
All Articles