// bebot -- a Gitlab -> Matrix event publisher // Copyright (C) 2023-2025 Brian Tarricone // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use std::{fmt, process::exit, time::Duration}; use matrix_sdk::{ config::SyncSettings, room::Room, ruma::{OwnedRoomOrAliasId, OwnedUserId, RoomOrAliasId, UserId}, BaseRoom, Client, }; use serde::de; use tokio::time::sleep; use crate::config::Config; async fn build_sync_settings() -> SyncSettings { SyncSettings::default().timeout(Duration::from_secs(30)) } pub async fn connect(config: &Config) -> anyhow::Result { let client = Client::builder() .server_name(config.user_id.server_name()) .user_agent(format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))) .build() .await?; client .matrix_auth() .login_username(&config.user_id, &config.password) .initial_device_display_name("Bebot") .await?; info!("Connected to matrix as {}; waiting for first sync", config.user_id); let settings = build_sync_settings().await; client.sync_once(settings).await?; info!("First matrix sync complete"); let sync_client = client.clone(); tokio::spawn(async move { let settings = build_sync_settings().await; if let Err(err) = sync_client.sync(settings).await { error!("Matrix sync failed: {err}"); exit(1); } }); Ok(client) } pub async fn ensure_room_joined(matrix_client: &Client, room_id: &OwnedRoomOrAliasId) -> anyhow::Result { fn room_matches(a_room: &BaseRoom, our_room: &OwnedRoomOrAliasId) -> bool { let our_room_str = our_room.as_str(); a_room.room_id().as_str() == our_room_str || a_room .canonical_alias() .iter() .any(|alias| alias.as_str() == our_room_str) || a_room.alt_aliases().iter().any(|alias| alias.as_str() == our_room_str) } let mut room = matrix_client .joined_rooms() .iter() .find(|a_room| room_matches(a_room, room_id)) .cloned(); if room.is_none() { if let Some(invited) = matrix_client .invited_rooms() .iter() .find(|a_room| room_matches(a_room, room_id)) { info!("Accepting invitation to room {room_id}"); invited.join().await?; } else { info!("Joining room {room_id}"); matrix_client.join_room_by_id_or_alias(room_id, &[]).await?; } for _ in 0..4 { let settings = build_sync_settings().await; matrix_client.sync_once(settings).await?; room = matrix_client .joined_rooms() .iter() .find(|a_room| room_matches(a_room, room_id)) .cloned(); if room.is_some() { break; } sleep(Duration::from_millis(500)).await; } } room.ok_or_else(|| anyhow!("Unable to join room {}", room_id)) } pub fn deser_user_id<'de, D>(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct UserIdVisitor; impl<'de> de::Visitor<'de> for UserIdVisitor { type Value = OwnedUserId; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a matrix user ID") } fn visit_str(self, v: &str) -> Result where E: de::Error, { UserId::parse(v).map_err(E::custom) } } deserializer.deserialize_any(UserIdVisitor) } fn deser_room_or_alias_id<'de, D>(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct RoomOrAliasIdVisitor; impl<'de> de::Visitor<'de> for RoomOrAliasIdVisitor { type Value = OwnedRoomOrAliasId; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a matrix room ID") } fn visit_str(self, v: &str) -> Result where E: de::Error, { RoomOrAliasId::parse(v).map_err(E::custom) } } deserializer.deserialize_any(RoomOrAliasIdVisitor) } pub fn deser_optional_room_or_alias_id<'de, D>(deserializer: D) -> Result, D::Error> where D: de::Deserializer<'de>, { struct OptionalRoomOrAliasIdVisitor; impl<'de> de::Visitor<'de> for OptionalRoomOrAliasIdVisitor { type Value = Option; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("null or matrix room ID") } fn visit_none(self) -> Result where E: de::Error, { Ok(None) } fn visit_some(self, deserializer: D) -> Result where D: serde::Deserializer<'de>, { Ok(Some(deser_room_or_alias_id(deserializer)?)) } fn visit_str(self, v: &str) -> Result where E: de::Error, { RoomOrAliasId::parse(v).map(Some).map_err(E::custom) } } deserializer.deserialize_any(OptionalRoomOrAliasIdVisitor) }