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

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



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

Временная ссылка на бота, чтобы можно было посмотреть результат


Для начала создадим бота и получим HTTP API токен:


Заходим к этому парню Bot Father и пишем следующее:


Инициируем создание нового бота: /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: https://core.telegram.org/bots/api

Отлично. Бот создан. %TOKEN% — это собственно и есть токен.


Rust Playground

Теперь немного, о том, как, и где запускать код, который пользователь будет передавать боту в виде сообщения.


Есть такой сервис Rust Playground, который позволяет запускать простейший Rust код онлайн. Им и воспользуемся. Располагается он по данному адресу: https://play.rust-lang.org/


Перейдя по ссылке введём простую 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::*;
use std::io::Read;
use hyper::client::Client;
use hyper::net::HttpsConnector;
use hyper_rustls::TlsClient;

Примечание: #[macro_use] используется для включения в область видимости текущей программы макросов из библиотеки к которой был применён данный атрибут. use telegram_bot::* импортирует все возможные модули из корня библиотеки.


Опишем с помощью enum возможные виды ответов от сервера, а их в данном случае 2, когда программа была скомпилирована успешно и когда произошла ошибка компиляции:


#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum ResponseType {
    ProgramCompiled { program: String, rustc: String },
    ProgramCompileError { rustc: String }
}

Заметили атрибут #[serde(untagged)] который был применён к перечислению? Он говорит, о том, что при (де)сериализации для вариантов перечисления не будет искаться какой-либо тэг явно указывающий на то, каким из вариантов он является. Так как же Serde определит какой из вариантов ответа на запрос от сервера мы получили? На самом деле она будет пытаться десериализовывать в каждый из вариантов пока не дойдёт до первого успешного результата. Более подробно об этом можно почитать в официальной документации: https://serde.rs/enum-representations.html.


Определим структуру для нашего запроса в 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 клиента, отправки и чтения запроса, об этом подробней можно прочесть здесь: https://hyper.rs/hyper/v0.10.7/hyper/index.html.


Обрабатываем пришедший ответ


  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 сообщения на которое необходимо ответить. Остальные передаваемые параметры не обязательны и отвечают за вывод превью, вид ответа и тому подобное.


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

Чат с rust телеграм ботом


Вывод в консоль:


    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") })
 /rust abc
Result : "{\"rustc\":\"rustc 1.16.0 (30cf806ef 2017-03-10)\\nerror: expected one of `!` or `::`, found ``\\n --> :1:1\\n  |\\n1 | abc\\n  | ^^^\\n\\nerror: aborting due to previous error\\n\\n\"}"
 /rust fn main() { println!("Hello habrahabr!"); }
Result : "{\"program\":\"Hello habrahabr!\\n\",\"rustc\":\"rustc 1.16.0 (30cf806ef 2017-03-10)\\n\"}"

Заключение

Думаю на этом всё. Спасибо WhiteOak за рабочий форк telegram_bot.
Кстати у него есть проект биндингов к QML из Rust: https://github.com/White-Oak/qml-rust возможно кому то это будет интересным. Любая конструктивная критика приветствуется.


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


Чуть не забыл оставить ссылку на чат русскоговорящего сообщества Rust где вам всегда помогут совладать с языком: https://gitter.im/ruRust/general

Комментарии (1)

  • 19 апреля 2017 в 01:43

    +1

    Отличная статья! Огромное спасибо!

© Habrahabr.ru