use std::{fmt, path::PathBuf};
use gtk::glib;
use matrix_sdk::{
matrix_auth::{MatrixSession, MatrixSessionTokens},
SessionMeta,
};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use ruma::{OwnedDeviceId, OwnedUserId};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::fs;
use tracing::{debug, error};
use url::Url;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(not(target_os = "linux"))]
mod unimplemented;
#[cfg(target_os = "linux")]
use self::linux::delete_session;
#[cfg(target_os = "linux")]
pub use self::linux::{restore_sessions, store_session};
#[cfg(not(target_os = "linux"))]
use self::unimplemented::delete_session;
#[cfg(not(target_os = "linux"))]
pub use self::unimplemented::{restore_sessions, store_session};
use crate::{
prelude::*,
spawn_tokio,
utils::{data_dir_path, DataType},
};
pub const SESSION_ID_LENGTH: usize = 8;
pub const PASSPHRASE_LENGTH: usize = 30;
#[derive(Debug, Error)]
pub enum SecretError {
#[error("Service error: {0}")]
Service(String),
}
impl UserFacingError for SecretError {
fn to_user_facing(&self) -> String {
match self {
SecretError::Service(error) => error.clone(),
}
}
}
#[derive(Clone, glib::Boxed)]
#[boxed_type(name = "StoredSession")]
pub struct StoredSession {
pub homeserver: Url,
pub user_id: OwnedUserId,
pub device_id: OwnedDeviceId,
pub id: String,
pub secret: Secret,
}
impl fmt::Debug for StoredSession {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StoredSession")
.field("homeserver", &self.homeserver)
.field("user_id", &self.user_id)
.field("device_id", &self.device_id)
.field("id", &self.id)
.finish_non_exhaustive()
}
}
impl StoredSession {
pub fn with_login_data(homeserver: Url, data: MatrixSession) -> Result<Self, ()> {
let MatrixSession {
meta: SessionMeta { user_id, device_id },
tokens: MatrixSessionTokens { access_token, .. },
} = data;
let mut id = None;
let data_path = data_dir_path(DataType::Persistent);
for _ in 0..10 {
let generated = thread_rng()
.sample_iter(Alphanumeric)
.take(SESSION_ID_LENGTH)
.map(char::from)
.collect::<String>();
let path = data_path.join(&generated);
if !path.exists() {
id = Some(generated);
break;
}
}
let Some(id) = id else {
return Err(());
};
let passphrase = thread_rng()
.sample_iter(Alphanumeric)
.take(PASSPHRASE_LENGTH)
.map(char::from)
.collect();
let secret = Secret {
access_token,
passphrase,
};
Ok(Self {
homeserver,
user_id,
device_id,
id,
secret,
})
}
pub fn data_path(&self) -> PathBuf {
let mut path = data_dir_path(DataType::Persistent);
path.push(&self.id);
path
}
pub fn cache_path(&self) -> PathBuf {
let mut path = data_dir_path(DataType::Cache);
path.push(&self.id);
path
}
pub async fn delete(self) {
debug!(
"Removing stored session {} for Matrix user {}…",
self.id, self.user_id,
);
delete_session(&self).await;
spawn_tokio!(async move {
if let Err(error) = fs::remove_dir_all(self.data_path()).await {
error!("Could not remove session database: {error}");
}
if let Err(error) = fs::remove_dir_all(self.cache_path()).await {
error!("Could not remove session cache: {error}");
}
})
.await
.unwrap();
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct Secret {
pub access_token: String,
pub passphrase: String,
}