← На главную

Изучаем возможности 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().

А так это выглядит на деле:

Демонстрация работы ботов для Matrix

Здесь демонстрируется работа сразу двух ботов. Первый бот пишет нам ЛС. Комнаты не существует, поэтому бот ее создает, а мы принимаем приглашение. Затем тот же бот шлет второе сообщение. В этот раз он находит существующую комнату и отправляет сообщение в нее. Далее второй бот заходит под тем же аккаунтом. Мы отправляем ему !bot сообщение и получаем ответ, подтверждающий, что бот увидел сообщение. Архив с исходниками обоих ботов лежит здесь.

Matrix SDK найдется масса применений:

  • Трансляция тематических RSS-лент для поддержания активных обсуждений в комнате;
  • Отправка уведомлений «3D-принтер закончил печать» или «замечена DX радиостанция»;
  • Удаленное управление неким устройством (тем же 3D-принтером), находящимся за NAT;
  • Двухфакторная аутентификация;
  • Защита комнат от спама;

Также Matrix SDK поддерживает E2E-шифрование с хранением ключей в SQLite. В контексте ботов оно неудобно, да и не требуется. Читателям, заинтересованным в E2E-шифровании, стоит обратиться к документации.