use s3::error::S3Error; use s3::request::ResponseData; use s3::{Bucket, BucketConfiguration}; use std::str::FromStr; use thiserror::Error; use crate::app_config::AppConfig; use crate::constants::TOKEN_LEN; use crate::utils::base_utils::{curr_time, format_time, rand_str}; type HttpClient = ruma::client::http_client::HyperNativeTls; pub type RumaClient = ruma::Client; #[derive(Error, Debug)] pub enum UserError { #[error("failed to fetch user configuration: {0}")] FetchUserConfig(S3Error), #[error("missing matrix token")] MissingMatrixToken, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, Hash)] pub struct UserID(pub String); impl UserID { fn conf_path_in_bucket(&self) -> String { format!("confs/{}.json", urlencoding::encode(&self.0)) } } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct User { pub id: UserID, pub name: String, pub email: String, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Eq, PartialEq)] pub struct APIClientID(pub uuid::Uuid); impl APIClientID { pub fn generate() -> Self { Self(uuid::Uuid::new_v4()) } } impl FromStr for APIClientID { type Err = uuid::Error; fn from_str(s: &str) -> Result { Ok(Self(uuid::Uuid::from_str(s)?)) } } /// Single API client information #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)] pub struct APIClient { /// Client unique ID pub id: APIClientID, /// Client description pub description: String, /// Restricted API network for token pub network: Option, /// Client secret pub secret: String, /// Client creation time pub created: u64, /// Client last usage time pub used: u64, /// Read only access pub readonly_client: bool, } impl APIClient { pub fn fmt_created(&self) -> String { format_time(self.created).unwrap_or_default() } pub fn fmt_used(&self) -> String { format_time(self.used).unwrap_or_default() } pub fn need_update_last_used(&self) -> bool { self.used + 60 * 15 < curr_time().unwrap() } } impl APIClient { /// Generate a new API client pub fn generate(description: String, network: Option) -> Self { Self { id: APIClientID::generate(), description, network, secret: rand_str(TOKEN_LEN), created: curr_time().unwrap(), used: curr_time().unwrap(), readonly_client: true, } } } #[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct UserConfig { /// Target user ID pub user_id: UserID, /// Configuration creation time pub created: u64, /// Configuration last update time pub updated: u64, /// Current user matrix token pub matrix_token: String, /// API clients pub clients: Vec, } impl UserConfig { /// Create S3 bucket if required pub async fn create_bucket_if_required() -> anyhow::Result<()> { if AppConfig::get().s3_skip_auto_create_bucket { log::debug!("Skipping bucket existence check"); return Ok(()); } let bucket = AppConfig::get().s3_bucket()?; match bucket.location().await { Ok(_) => { log::debug!("The bucket already exists."); return Ok(()); } Err(S3Error::HttpFailWithBody(404, s)) if s.contains("NoSuchKey") => { log::warn!("Failed to fetch bucket location, but it seems that bucket exists."); return Ok(()); } Err(S3Error::HttpFailWithBody(404, s)) if s.contains("NoSuchBucket") => { log::warn!("The bucket does not seem to exists, trying to create it!") } Err(e) => { log::error!("Got unexpected error when querying bucket info: {}", e); return Err(e.into()); } } Bucket::create_with_path_style( &bucket.name, bucket.region, AppConfig::get().s3_credentials()?, BucketConfiguration::private(), ) .await?; Ok(()) } /// Get current user configuration pub async fn load(user_id: &UserID, allow_non_existing: bool) -> anyhow::Result { let res: Result = AppConfig::get() .s3_bucket()? .get_object(user_id.conf_path_in_bucket()) .await; match (res, allow_non_existing) { (Ok(res), _) => Ok(serde_json::from_slice(res.as_slice())?), (Err(S3Error::HttpFailWithBody(404, _)), true) => { log::warn!("User configuration does not exists, generating a new one..."); Ok(Self { user_id: user_id.clone(), created: curr_time()?, updated: curr_time()?, matrix_token: "".to_string(), clients: vec![], }) } (Err(e), _) => Err(UserError::FetchUserConfig(e).into()), } } /// Set user configuration pub async fn save(&mut self) -> anyhow::Result<()> { log::info!("Saving new configuration for user {:?}", self.user_id); self.updated = curr_time()?; // Save updated configuration AppConfig::get() .s3_bucket()? .put_object( self.user_id.conf_path_in_bucket(), &serde_json::to_vec(self)?, ) .await?; Ok(()) } /// Get current user matrix token, in an obfuscated form pub fn obfuscated_matrix_token(&self) -> String { self.matrix_token .chars() .enumerate() .map(|(num, c)| match num { 0 | 1 => c, _ => 'X', }) .collect() } /// Find a client by its id pub fn find_client_by_id(&self, id: &APIClientID) -> Option<&APIClient> { self.clients.iter().find(|c| &c.id == id) } /// Find a client by its id and get a mutable reference pub fn find_client_by_id_mut(&mut self, id: &APIClientID) -> Option<&mut APIClient> { self.clients.iter_mut().find(|c| &c.id == id) } /// Get a matrix client instance for the current user pub async fn matrix_client(&self) -> anyhow::Result { if self.matrix_token.is_empty() { return Err(UserError::MissingMatrixToken.into()); } Ok(ruma::Client::builder() .homeserver_url(AppConfig::get().matrix_homeserver.to_string()) .access_token(Some(self.matrix_token.clone())) .build() .await?) } }