use crate::app_config::AppConfig; use crate::matrix_connection::matrix_manager::MatrixManagerMsg; use crate::users::UserEmail; use crate::utils::rand_utils::rand_string; use anyhow::Context; use futures_util::Stream; use matrix_sdk::authentication::oauth::error::OAuthDiscoveryError; use matrix_sdk::authentication::oauth::{ ClientId, OAuthError, OAuthSession, UrlOrQuery, UserSession, }; use matrix_sdk::config::SyncSettings; use matrix_sdk::encryption::recovery::RecoveryState; use matrix_sdk::event_handler::{EventHandler, EventHandlerHandle, SyncEvent}; use matrix_sdk::ruma::presence::PresenceState; use matrix_sdk::ruma::serde::Raw; use matrix_sdk::ruma::{DeviceId, UserId}; use matrix_sdk::sync::SyncResponse; use matrix_sdk::{Client, ClientBuildError, SendOutsideWasm}; use ractor::ActorRef; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use std::pin::Pin; use url::Url; /// The full session to persist. #[derive(Debug, Serialize, Deserialize, Clone)] struct StoredSession { /// The OAuth 2.0 user session. user_session: UserSession, /// The OAuth 2.0 client ID. client_id: ClientId, } #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum EncryptionRecoveryState { Unknown, Enabled, Disabled, Incomplete, } /// Matrix Gateway session errors #[derive(thiserror::Error, Debug)] enum MatrixClientError { #[error("Failed to destroy previous client data! {0}")] DestroyPreviousData(Box), #[error("Failed to create Matrix database storage directory! {0}")] CreateMatrixDbDir(std::io::Error), #[error("Failed to create database passphrase! {0}")] CreateDbPassphrase(std::io::Error), #[error("Failed to read database passphrase! {0}")] ReadDbPassphrase(std::io::Error), #[error("Failed to build Matrix client! {0}")] BuildMatrixClient(ClientBuildError), #[error("Failed to clear Matrix session file! {0}")] ClearMatrixSessionFile(std::io::Error), #[error("Failed to clear Matrix database storage directory! {0}")] ClearMatrixDbDir(std::io::Error), #[error("Failed to remove database passphrase! {0}")] ClearDbPassphrase(std::io::Error), #[error("Failed to fetch server metadata! {0}")] FetchServerMetadata(OAuthDiscoveryError), #[error("Failed to load stored session! {0}")] LoadStoredSession(std::io::Error), #[error("Failed to decode stored session! {0}")] DecodeStoredSession(serde_json::Error), #[error("Failed to restore stored session! {0}")] RestoreSession(matrix_sdk::Error), #[error("Failed to parse auth redirect URL! {0}")] ParseAuthRedirectURL(url::ParseError), #[error("Failed to build auth request! {0}")] BuildAuthRequest(OAuthError), #[error("Failed to finalize authentication! {0}")] FinishLogin(matrix_sdk::Error), #[error("Failed to write session file! {0}")] WriteSessionFile(std::io::Error), #[error("Failed to rename device! {0}")] RenameDevice(matrix_sdk::HttpError), #[error("Failed to set recovery key! {0}")] SetRecoveryKey(matrix_sdk::encryption::recovery::RecoveryError), } #[derive(serde::Deserialize)] pub struct FinishMatrixAuth { code: String, state: String, } #[derive(Clone)] pub struct MatrixClient { manager: ActorRef, pub email: UserEmail, client: Client, } impl MatrixClient { /// Start to build Matrix client to initiate user authentication pub async fn build_client( manager: ActorRef, email: &UserEmail, ) -> anyhow::Result { // Check if we are restoring a previous state let session_file_path = AppConfig::get().user_matrix_session_file_path(email); let is_restoring = session_file_path.is_file(); if !is_restoring { Self::destroy_data(email).map_err(MatrixClientError::DestroyPreviousData)?; } // Determine Matrix database path let db_path = AppConfig::get().user_matrix_db_path(email); std::fs::create_dir_all(&db_path).map_err(MatrixClientError::CreateMatrixDbDir)?; // Generate or load passphrase let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email); if !passphrase_path.exists() { std::fs::write(&passphrase_path, rand_string(32)) .map_err(MatrixClientError::CreateDbPassphrase)?; } let passphrase = std::fs::read_to_string(passphrase_path) .map_err(MatrixClientError::ReadDbPassphrase)?; let client = Client::builder() .server_name_or_homeserver_url(&AppConfig::get().matrix_homeserver) // Automatically refresh tokens if needed .handle_refresh_tokens() .sqlite_store(&db_path, Some(&passphrase)) .build() .await .map_err(MatrixClientError::BuildMatrixClient)?; let client = Self { manager, email: email.clone(), client, }; // Check metadata if !is_restoring { let oauth = client.client.oauth(); let server_metadata = oauth .server_metadata() .await .map_err(MatrixClientError::FetchServerMetadata)?; log::info!("OAuth2 server issuer: {:?}", server_metadata.issuer); } else { let session: StoredSession = serde_json::from_str( std::fs::read_to_string(session_file_path) .map_err(MatrixClientError::LoadStoredSession)? .as_str(), ) .map_err(MatrixClientError::DecodeStoredSession)?; // Restore session client .client .restore_session(OAuthSession { client_id: session.client_id, user: session.user_session, }) .await .map_err(MatrixClientError::RestoreSession)?; // Wait for encryption tasks to complete client .client .encryption() .wait_for_e2ee_initialization_tasks() .await; } // Automatically save session when token gets refreshed client.setup_background_session_save().await; Ok(client) } /// Destroy Matrix client related data fn destroy_data(email: &UserEmail) -> anyhow::Result<(), Box> { let session_path = AppConfig::get().user_matrix_session_file_path(email); if session_path.is_file() { std::fs::remove_file(&session_path) .map_err(MatrixClientError::ClearMatrixSessionFile)?; } let db_path = AppConfig::get().user_matrix_db_path(email); if db_path.is_dir() { std::fs::remove_dir_all(&db_path).map_err(MatrixClientError::ClearMatrixDbDir)?; } let passphrase_path = AppConfig::get().user_matrix_passphrase_path(email); if passphrase_path.is_file() { std::fs::remove_file(passphrase_path).map_err(MatrixClientError::ClearDbPassphrase)?; } Ok(()) } /// Initiate OAuth authentication pub async fn initiate_login(&self) -> anyhow::Result { let oauth = self.client.oauth(); let metadata = AppConfig::get().matrix_client_metadata(); let client_metadata = Raw::new(&metadata).expect("Couldn't serialize client metadata"); let auth = oauth .login( Url::parse(&AppConfig::get().matrix_oauth_redirect_url()) .map_err(MatrixClientError::ParseAuthRedirectURL)?, None, Some(client_metadata.into()), None, ) .build() .await .map_err(MatrixClientError::BuildAuthRequest)?; Ok(auth.url) } /// Finish OAuth authentication pub async fn finish_login(&self, info: FinishMatrixAuth) -> anyhow::Result<()> { let oauth = self.client.oauth(); oauth .finish_login(UrlOrQuery::Query(format!( "state={}&code={}", info.state, info.code ))) .await .map_err(MatrixClientError::FinishLogin)?; log::info!( "User successfully authenticated as {}!", self.client.user_id().unwrap() ); // Persist session tokens self.save_stored_session().await?; // Rename created session to give it a more explicit name self.client .rename_device( self.client .session_meta() .context("Missing device ID!")? .device_id .as_ref(), &AppConfig::get().website_origin, ) .await .map_err(MatrixClientError::RenameDevice)?; Ok(()) } /// Automatically persist session onto disk pub async fn setup_background_session_save(&self) { let this = self.clone(); tokio::spawn(async move { loop { match this.client.subscribe_to_session_changes().recv().await { Ok(update) => match update { matrix_sdk::SessionChange::UnknownToken { soft_logout } => { log::warn!( "Received an unknown token error; soft logout? {soft_logout:?}" ); if let Err(e) = this .manager .cast(MatrixManagerMsg::DisconnectClient(this.email)) { log::warn!("Failed to propagate invalid token error: {e}"); } break; } matrix_sdk::SessionChange::TokensRefreshed => { // The tokens have been refreshed, persist them to disk. if let Err(err) = this.save_stored_session().await { log::error!("Unable to store a session in the background: {err}"); } } }, Err(e) => { log::error!("[!] Session change error: {e}"); log::error!("Session change background service INTERRUPTED!"); return; } } } }); } /// Update the session stored on the filesystem. async fn save_stored_session(&self) -> anyhow::Result<()> { log::debug!("Save the stored session for {:?}...", self.email); let full_session = self .client .oauth() .full_session() .context("A logged in client must have a session")?; let stored_session = StoredSession { user_session: full_session.user, client_id: full_session.client_id, }; let serialized_session = serde_json::to_string(&stored_session)?; std::fs::write( AppConfig::get().user_matrix_session_file_path(&self.email), serialized_session, ) .map_err(MatrixClientError::WriteSessionFile)?; log::debug!("Updating the stored session: done!"); Ok(()) } /// Check whether a user is currently connected to this client or not pub fn is_client_connected(&self) -> bool { self.client.is_active() } /// Disconnect user from client pub async fn disconnect(self) -> anyhow::Result<()> { if let Err(e) = self.client.logout().await { log::warn!("Failed to send logout request: {e}"); } // Destroy user associated data Self::destroy_data(&self.email)?; Ok(()) } /// Get client Matrix device id pub fn device_id(&self) -> Option<&DeviceId> { self.client.device_id() } /// Get client Matrix user id pub fn user_id(&self) -> Option<&UserId> { self.client.user_id() } /// Get current encryption keys recovery state pub fn recovery_state(&self) -> EncryptionRecoveryState { match self.client.encryption().recovery().state() { RecoveryState::Unknown => EncryptionRecoveryState::Unknown, RecoveryState::Enabled => EncryptionRecoveryState::Enabled, RecoveryState::Disabled => EncryptionRecoveryState::Disabled, RecoveryState::Incomplete => EncryptionRecoveryState::Incomplete, } } /// Set new encryption key recovery key pub async fn set_recovery_key(&self, key: &str) -> anyhow::Result<()> { Ok(self .client .encryption() .recovery() .recover(key) .await .map_err(MatrixClientError::SetRecoveryKey)?) } /// Get matrix synchronization settings to use fn sync_settings() -> SyncSettings { SyncSettings::default().set_presence(PresenceState::Offline) } /// Perform initial synchronization pub async fn perform_initial_sync(&self) -> anyhow::Result<()> { self.client.sync_once(Self::sync_settings()).await?; Ok(()) } /// Perform routine synchronization pub async fn sync_stream( &self, ) -> Pin>>> { Box::pin(self.client.sync_stream(Self::sync_settings()).await) } /// Add new Matrix event handler #[must_use] pub fn add_event_handler(&self, handler: H) -> EventHandlerHandle where Ev: SyncEvent + DeserializeOwned + SendOutsideWasm + 'static, H: EventHandler, { self.client.add_event_handler(handler) } /// Remove Matrix event handler pub fn remove_event_handler(&self, handle: EventHandlerHandle) { self.client.remove_event_handler(handle) } }