Изучаем возможности Matrix SDK для Rust
Некоторое время назад мы открыли для себя Matrix, а также настроили собственный сервер. Чем еще интересен Matrix, так это наличием официального SDK для языка Rust. Это библиотека / крейт, позволяющая разрабатывать как простых ботов, так и полноценные десктоп-клиенты. Предлагаю разобраться, как ею пользоваться.
SDK работает поверх Tokio, в связи с чем понимание данного асинхронного рантайма является обязательным. См. статью Знакомство с футурами в Rust и рантаймом Tokio, если вдруг вы ее пропустили.
Для работы с Matrix-сервером первым делом необходимо создать клиента:
let client = Client::builder()
.homeserver_url("https://matrix.eax.me")
.build()
.await?;
Здесь build() является async-методом и возвращает Result<Client, ClientBuildError>. Любой современный редактор отображает типы переменных и функций, поэтому далее я не буду заострять внимание на типах. Заинтересованным читателям предлагается изучить их самостоятельно, открыв полную версию кода в условном Zed.
Имея клиента, проходим аутентификацию по логину и паролю:
if let Err(e) = client
.matrix_auth()
.login_username("@bot:matrix.eax.me", "s3cr3tp4$$w0rd")
.initial_device_display_name("simple-matrix-bot")
.await
{
eprintln!("Login failed: {e}");
std::process::exit(1);
}
println!("Logged in as {}", client.user_id().unwrap());
Следующим шагом получаем текущее состояние с сервера:
if let Err(e) = client.sync_once(SyncSettings::default()).await {
eprintln!("Initial sync failed: {e}");
std::process::exit(1);
}
Без этого вызова клиент не знает, в каких комнатах он находится и какие происходили события, пока он был оффлайн.
Далее все определяется тем, что мы хотим от клиента. Если требуется отправить текстовое сообщение в публичную комнату, то код будет таким:
let message = RoomMessageEventContent::text_plain("Текст сообщения");
let alias = "#some-public-room:matrix.eax.me";
let alias_or_id = match <&RoomOrAliasId>::try_from(alias) {
Ok(id) => id,
Err(e) => {
eprintln!("Invalid room identifier '{alias}': {e}");
std::process::exit(1);
}
};
let room = match client.join_room_by_id_or_alias(alias_or_id, &[]).await {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to join room '{alias}': {e}");
std::process::exit(1);
}
};
if let Err(e) = room.send(message).await {
eprintln!("Failed to send message: {e}");
std::process::exit(1);
}
Вызов client.join_room_by_id_or_alias() идемпотентен. То есть, если клиент уже находится в комнате, то во второй раз он не добавится. Затем просто делаем room.send().
Отправка ЛС происходит иначе. Протокол позволяет иметь много комнат 1:1 с одними и теми же участниками. Поэтому перед отправкой ЛС необходимо проверить, находится ли клиент в нужной комнате, и если нет, то создать ее. Это не сложно, просто требует больше кода. Пример вы найдете в полной версии исходников к посту.
А что, если клиент должен реагировать на сообщения? Тогда понадобится обработчик событий:
client.add_event_handler(on_message);
if let Err(e) = client.sync(SyncSettings::default()).await {
eprintln!("Sync error: {e}");
std::process::exit(1);
}
Метод client.sync() фактически представляет собой client.sync_once(), обернутый в цикл. Клиент получает события от сервера и как-то реагирует на них при помощи зарегистрированных обработчиков.
Пример обработчика:
async fn on_message(event: OriginalSyncRoomMessageEvent, room: Room,
client: Client) {
// Никогда не реагируем на собственные сообщения
if Some(event.sender.as_ref()) == client.user_id() {
return;
}
// Обрабатываем только текстовые сообщения
let MessageType::Text(text) = &event.content.msgtype else {
return;
};
let body = text.body.trim();
let Some(rest) = body.strip_prefix("!bot ") else {
return;
};
let reply = format!("Bot sees message: {rest}");
let msg_event = RoomMessageEventContent::text_plain(reply);
if let Err(e) = room.send(msg_event).await {
eprintln!("Failed to send reply to {}: {e}", room.room_id());
}
}
В полной версии исходников вы найдете еще один обработчик. Он принимает все приглашения присоединиться к какой-нибудь комнате.
Если зарегистрировано несколько обработчиков, то они выполняются конкурентно. Это не отражено в документации на SDK, но следует из использования стрима FuturesUnordered (документация) в реализации call_event_handlers() (исходник). Кроме того, пока выполняется обработчик события, не начнется обработка новых событий. По этой причине тяжелые вызовы вроде room.send() и room.join() может иметь смысл оборачивать в tokio::spawn().
А так это выглядит на деле:
Здесь демонстрируется работа сразу двух ботов. Первый бот пишет нам ЛС. Комнаты не существует, поэтому бот ее создает, а мы принимаем приглашение. Затем тот же бот шлет второе сообщение. В этот раз он находит существующую комнату и отправляет сообщение в нее. Далее второй бот заходит под тем же аккаунтом. Мы отправляем ему !bot сообщение и получаем ответ, подтверждающий, что бот увидел сообщение. Архив с исходниками обоих ботов лежит здесь.
Matrix SDK найдется масса применений:
- Трансляция тематических RSS-лент для поддержания активных обсуждений в комнате;
- Отправка уведомлений «3D-принтер закончил печать» или «замечена DX радиостанция»;
- Удаленное управление неким устройством (тем же 3D-принтером), находящимся за NAT;
- Двухфакторная аутентификация;
- Защита комнат от спама;
Также Matrix SDK поддерживает E2E-шифрование с хранением ключей в SQLite. В контексте ботов оно неудобно, да и не требуется. Читателям, заинтересованным в E2E-шифровании, стоит обратиться к документации.