Пишем Telegram-бота на Rust, который будет запускать код на…

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,752
Реакции
1,448
Credits
25,205

Пишем Telegram-бота на Rust, который будет запускать код на… Rust?​

e1da900003684042a932bb58d714ea01.png
Сегодня хотелось бы кратко рассказать о том, как написать Telegram-бота на Rust, который будет запускать код на Rust. У статьи нет цели произвести полное погружение в API telegram_bot, Serde, Telegram или в нюансы разработки на Rust. Она скорее носит ознакомительный характер. Числа Пеано с помощью системы типов складывать не будем.

Создание бота в Telegram​

Для начала создадим бота и получим HTTP API токен.
Заходим к Для просмотра ссылки Войди или Зарегистрируйся и пишем следующее:
Инициируем создание нового бота: /newbot.
Ответ крёстного отца:
Alright, a new bot. How are we going to call it? Please choose a name for your bot.
В ответе пишем имя бота, которого хотим создать: rust.
Ответ крёстного отца:
Good. Now let's choose a username for your bot. It must end in bot. Like this, for example: TetrisBot or tetris_bot.
Следуя указаниям, вводим ещё одно имя: rustlanguage_bot.
Ответ крёстного отца:
Done! Congratulations on your new bot. You will find it at t.me/rustlanguage_bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this. Use this token to access the HTTP. API: %TOKEN% For a description of the Bot API, see this page: Для просмотра ссылки Войди или Зарегистрируйся
Отлично. Бот создан. %TOKEN% — это, собственно, и есть токен.

Rust Playground​

Теперь немного о том, как и где запускать код, который пользователь будет передавать боту в виде сообщения.
Есть такой сервис Rust Playground, который позволяет запускать простейший Rust код онлайн. Им и воспользуемся. Располагается он по данному адресу: Для просмотра ссылки Войди или Зарегистрируйся
Перейдя по ссылке, введём простую hello-world программу:
Код:
fn main() {
println!("Hello world!");
}
Откроем вкладку Network из DevTools, чтобы посмотреть, что и в каком формате он шлёт для получения результатов компиляции:
Rust Playground Request

Вроде бы всё прозрачно и понятно. Попробуем воспроизвести из консоли:

Код:
[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"}

Отлично, поехали дальше.

Пишем бота​

Создаём проект:
Код:
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"

Кратко опишу, зачем они нужны:
  • Serde предназначена для сериализации/десериализации данных в различных форматах. В данном случае нам необходима работа с JSON (serde_json) и щепотка кодогенерации (serde_derive);
  • Hyper для работы с сетью будем использовать HTTP-клиент, который она предоставляет для взаимодействия с Rust Playground. Так как взаимодействие производится по протоколу HTTPS, ещё необходима батарейка в виде hyper-rustls;
  • ну и самое главное, для взаимодействия с Telegram API будем использовать готовую библиотеку telegram-bot, но не конкретно её, а форк товарища @white_oak, который подогнал её для работы с актуальной версией Hyper.

В src/main.rs подключим все необходимые библиотеки и модули:​

Код:
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;

Примечание: #[macro_use] используется для включения в область видимости текущей программы макросов из библиотеки, к которой был применён данный атрибут.
В данной строке импортируем модули из корня библиотеки для определения типа сообщения, метода "прослушки", структуры представляющей API Telegram:
Код:
use telegram_bot::{Api, MessageType, ListeningMethod, ListeningAction};

Опишем с помощью enum возможные виды ответов от сервера, а их в данном случае 2, когда программа была скомпилирована успешно, и когда произошла ошибка компиляции:
Код:
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ResponseType {
ProgramCompiled { program: String, rustc: String },
ProgramCompileError { rustc: String }
}

Заметили атрибут #[serde(untagged)], который был применён к перечислению? Он говорит о том, что при (де)сериализации для вариантов перечисления не будет искаться какой-либо тэг, явно указывающий на то, каким из вариантов он является. Так как же Serde определит, какой из вариантов ответа на запрос от сервера мы получили? На самом деле, она будет пытаться десериализовывать в каждый из вариантов, пока не дойдёт до первого успешного результата. Более подробно об этом можно почитать в официальной документации: Для просмотра ссылки Войди или Зарегистрируйся.
Определим структуру для нашего запроса в Rust Playground:
Код:
#[derive(Serialize)]
pub struct PlaygroundRequest {
code: String,
version: String,
optimize: String,
test: bool,
separate_output: bool,
color: bool,
backtrace: String
}

Из пользовательского ввода в эту структуру пойдёт только поле code. Остальное захардкодим, ибо всегда так делаем :) (нет)
В главной функции программы main создадим инстанс Telegram API и заставим его печатать всё, что пришло боту в сообщении:
Код:
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);
}
_ => {}
}
});
}

Чтобы проверить работоспособность данного кода, запустите программу, не забыв передать в качестве переменной окружения реальный токен, полученный ранее:
Код:
TOKEN=%TOKEN% cargo run

Немного разберём, что мы написали выше.
Код:
let api = Api::from_env("TOKEN").unwrap();
println!("getMe: {:?}", api.get_me());

Здесь мы создаём инстанс структуры Api, импортированной из telegram_bot, далее создаём слушатель бота в режиме long-polling:
let mut listener = api.listener(ListeningMethod::LongPoll(None));

Под конец создаём цикл обработки сообщений при помощи функции listen и сопоставления по шаблону типа сообщения:
Код:
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);
}
_ => {}
}
});

Условимся, что код мы будем передавать только в текстовом виде. Файлы и прочее исключим. Для этого, как вы могли заметить, все остальные варианты перечисления MessageType просто игнорируются.
Обрабатываем команду /rust, отправляя запрос на Rust Playground, и считываем ответ:
Код:
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);
}

Мы обрабатываем запрос только лишь в случае, если сообщение начинается с определённой команды (/rust):
Код:
if t.starts_with("/rust ") {

А так же вытаскиваем код программы, которую необходимо скомпилировать:
Код:
let program = t.split("/rust ").collect();

Функция serde_json::to_string(&PlaygroundReques { ... }) сериализует нашу структуру запроса в строку. Остальная часть кода относится к инициализации HTTPS клиента, отправке и чтению запроса, об этом подробней можно прочесть здесь: Для просмотра ссылки Войди или Зарегистрируйся.
Обрабатываем пришедший ответ:
Код:
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)
}
};

Функция serde::from_str десериализует пришедший ответ в один из вариантов нашего enum. В случае, если ответ не удалось десериализовать, для упрощения мы заворачиваем это в вариант ошибки компиляции с соответствующим текстом. Далее мы формируем наше результирующее сообщение, которое будет отослано пользователю, основываясь на том, какой из вариантов enum был представлен. Возможно, вы в первый раз видите при сопоставлении по шаблону конструкцию вида { program, .. }, объясню — это игнорирование при деструктуризации полей структуры, которые нам не нужны в ходе обработки этого варианта.
Отправка результатов компиляции в чат:
Код:
if result.len() > 500 {
result.truncate(500);
}
try!(api.send_message(m.chat.id(), result, None, None, Some(m.message_id), None));

В конце мы проверяем длину сообщения, чтобы исключить результаты компиляции с большим количеством вывода, и обрезаем это дело. После производим отправку сообщения, указывая идентификатор чата, из которого пришел запрос на компиляцию, и передаём итоговый результат компиляции. Так же передаём id сообщения, на которое необходимо ответить. Остальные передаваемые параметры необязательны и отвечают за вывод превью, вид ответа и тому подобное.

Проверяем работоспособность​

image.png
Вывод в консоль:
Код:
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\"
}"

Репозиторий с полным кодом данного бота располагается Для просмотра ссылки Войди или Зарегистрируйся.