diff --git a/Cargo.lock b/Cargo.lock index 12cbe08..a32f8de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,6 +215,7 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-targets 0.52.6", ] diff --git a/kordophone-db/src/database.rs b/kordophone-db/src/database.rs index ca53285..55f99c1 100644 --- a/kordophone-db/src/database.rs +++ b/kordophone-db/src/database.rs @@ -4,11 +4,14 @@ use diesel::prelude::*; use crate::repository::Repository; use crate::settings::Settings; +pub use kordophone::api::TokenManagement; +use kordophone::model::JwtToken; + use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); pub struct Database { - connection: SqliteConnection, + pub connection: SqliteConnection, } impl Database { @@ -39,4 +42,22 @@ impl Database { let mut settings = Settings::new(&mut self.connection); f(&mut settings) } -} \ No newline at end of file +} + +static TOKEN_KEY: &str = "token"; + +impl TokenManagement for Database { + fn get_token(&mut self) -> Option { + self.with_settings(|settings| { + let token: Result> = settings.get(TOKEN_KEY); + match token { + Ok(data) => data, + Err(_) => None, + } + }) + } + + fn set_token(&mut self, token: JwtToken) { + self.with_settings(|settings| settings.put(TOKEN_KEY, &token).unwrap()); + } +} diff --git a/kordophone/Cargo.toml b/kordophone/Cargo.toml index 68fbf2b..4d0d2aa 100644 --- a/kordophone/Cargo.toml +++ b/kordophone/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] async-trait = "0.1.80" base64 = "0.22.1" -chrono = "0.4.38" +chrono = { version = "0.4.38", features = ["serde"] } ctor = "0.2.8" env_logger = "0.11.5" hyper = { version = "0.14", features = ["full"] } diff --git a/kordophone/src/api/mod.rs b/kordophone/src/api/mod.rs index 581f35a..6a337b2 100644 --- a/kordophone/src/api/mod.rs +++ b/kordophone/src/api/mod.rs @@ -26,3 +26,27 @@ pub trait APIInterface { async fn authenticate(&mut self, credentials: Credentials) -> Result; } +pub trait TokenManagement { + fn get_token(&mut self) -> Option; + fn set_token(&mut self, token: JwtToken); +} + +pub struct InMemoryTokenManagement { + token: Option, +} + +impl InMemoryTokenManagement { + pub fn new() -> Self { + Self { token: None } + } +} + +impl TokenManagement for InMemoryTokenManagement { + fn get_token(&mut self) -> Option { + self.token.clone() + } + + fn set_token(&mut self, token: JwtToken) { + self.token = Some(token); + } +} diff --git a/kordophone/src/model/jwt.rs b/kordophone/src/model/jwt.rs index 70c86e6..8ef5683 100644 --- a/kordophone/src/model/jwt.rs +++ b/kordophone/src/model/jwt.rs @@ -7,23 +7,23 @@ use base64::{ use chrono::{DateTime, Utc}; use hyper::http::HeaderValue; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[allow(dead_code)] struct JwtHeader { alg: String, typ: String, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[allow(dead_code)] enum ExpValue { Integer(i64), String(String), } -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[allow(dead_code)] struct JwtPayload { exp: serde_json::Value, @@ -31,7 +31,7 @@ struct JwtPayload { user: Option, } -#[derive(Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] #[allow(dead_code)] pub struct JwtToken { header: JwtHeader, diff --git a/kordophoned/src/daemon/mod.rs b/kordophoned/src/daemon/mod.rs index 751d639..17282f1 100644 --- a/kordophoned/src/daemon/mod.rs +++ b/kordophoned/src/daemon/mod.rs @@ -1,13 +1,23 @@ +mod settings; +use settings::Settings; + use directories::ProjectDirs; use std::path::PathBuf; use anyhow::Result; use thiserror::Error; + use kordophone_db::{ database::Database, models::Conversation, + repository::Repository, }; -use kordophone::api::http_client::HTTPAPIClient; +use kordophone::model::JwtToken; +use kordophone::api::{ + http_client::{Credentials, HTTPAPIClient}, + APIInterface, + TokenManagement, +}; #[derive(Debug, Error)] pub enum DaemonError { @@ -18,7 +28,6 @@ pub enum DaemonError { pub struct Daemon { pub version: String, database: Database, - client: Option, } impl Daemon { @@ -31,27 +40,80 @@ impl Daemon { std::fs::create_dir_all(database_dir)?; let database = Database::new(&database_path.to_string_lossy())?; - - // TODO: Check to see if we have client settings in the database - - - Ok(Self { version: "0.1.0".to_string(), database, client: None }) + Ok(Self { version: "0.1.0".to_string(), database }) } pub fn get_conversations(&mut self) -> Vec { self.database.with_repository(|r| r.all_conversations().unwrap()) } - pub fn sync_all_conversations(&mut self) -> Result<()> { - let client = self.client - .as_mut() - .ok_or(DaemonError::ClientNotConfigured)?; + pub async fn sync_all_conversations(&mut self) -> Result<()> { + let mut client = self.get_client() + .map_err(|_| DaemonError::ClientNotConfigured)?; + + let fetched_conversations = client.get_conversations().await?; + let db_conversations: Vec = fetched_conversations.into_iter() + .map(|c| kordophone_db::models::Conversation::from(c)) + .collect(); + + // Process each conversation + let mut repository = Repository::new(&mut self.database.connection); + for conversation in db_conversations { + let conversation_id = conversation.guid.clone(); + + // Insert the conversation + repository.insert_conversation(conversation)?; + + // Fetch and sync messages for this conversation + let messages = client.get_messages(&conversation_id).await?; + let db_messages: Vec = messages.into_iter() + .map(|m| kordophone_db::models::Message::from(m)) + .collect(); + + // Insert each message + for message in db_messages { + repository.insert_message(&conversation_id, message)?; + } + } Ok(()) } + pub fn get_settings(&mut self) -> Result { + let settings = self.database.with_settings(|s| + Settings::from_db(s) + )?; + + Ok(settings) + } + + fn get_client(&mut self) -> Result { + let settings = self.database.with_settings(|s| + Settings::from_db(s) + )?; + + let server_url = settings.server_url + .ok_or(DaemonError::ClientNotConfigured)?; + + let client = HTTPAPIClient::new( + server_url.parse().unwrap(), + + match (settings.username, settings.credential_item) { + (Some(username), Some(password)) => Some( + Credentials { + username, + password, + } + ), + _ => None, + } + ); + + Ok(client) + } + fn get_database_path() -> PathBuf { - if let Some(proj_dirs) = ProjectDirs::from("com", "kordophone", "kordophone") { + if let Some(proj_dirs) = ProjectDirs::from("net", "buzzert", "kordophonecd") { let data_dir = proj_dirs.data_dir(); data_dir.join("database.db") } else { @@ -59,4 +121,15 @@ impl Daemon { PathBuf::from("database.db") } } -} \ No newline at end of file +} + +impl TokenManagement for &mut Daemon { + fn get_token(&mut self) -> Option { + self.database.get_token() + } + + fn set_token(&mut self, token: JwtToken) { + self.database.set_token(token); + } +} + diff --git a/kordophoned/src/daemon/settings.rs b/kordophoned/src/daemon/settings.rs new file mode 100644 index 0000000..4205d3b --- /dev/null +++ b/kordophoned/src/daemon/settings.rs @@ -0,0 +1,35 @@ +use kordophone_db::settings::Settings as DbSettings; +use anyhow::Result; + +mod keys { + pub static SERVER_URL: &str = "ServerURL"; + pub static USERNAME: &str = "Username"; + pub static CREDENTIAL_ITEM: &str = "CredentialItem"; +} + +pub struct Settings { + pub server_url: Option, + pub username: Option, + pub credential_item: Option, +} + +impl Settings { + pub fn from_db(db_settings: &mut DbSettings) -> Result { + let server_url = db_settings.get(keys::SERVER_URL)?; + let username = db_settings.get(keys::USERNAME)?; + let credential_item = db_settings.get(keys::CREDENTIAL_ITEM)?; + + Ok(Self { + server_url, + username, + credential_item, + }) + } + + pub fn save(&self, db_settings: &mut DbSettings) -> Result<()> { + db_settings.put(keys::SERVER_URL, &self.server_url)?; + db_settings.put(keys::USERNAME, &self.username)?; + db_settings.put(keys::CREDENTIAL_ITEM, &self.credential_item)?; + Ok(()) + } +} \ No newline at end of file diff --git a/kordophoned/src/dbus/server_impl.rs b/kordophoned/src/dbus/server_impl.rs index f107bd3..c957c3a 100644 --- a/kordophoned/src/dbus/server_impl.rs +++ b/kordophoned/src/dbus/server_impl.rs @@ -1,7 +1,8 @@ use dbus::arg; use dbus_tree::MethodErr; use std::sync::{Arc, Mutex, MutexGuard}; -use log::info; +use std::future::Future; +use std::thread; use crate::daemon::Daemon; use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository; @@ -47,10 +48,14 @@ impl DbusRepository for ServerImpl { fn sync_all_conversations(&mut self) -> Result { let mut daemon = self.get_daemon()?; - daemon.sync_all_conversations().map_err(|e| { - log::error!("Failed to sync conversations: {}", e); - MethodErr::failed(&format!("Failed to sync conversations: {}", e)) - })?; + + // TODO: We don't actually probably want to block here. + run_sync_future(daemon.sync_all_conversations()) + .unwrap() + .map_err(|e| { + log::error!("Failed to sync conversations: {}", e); + MethodErr::failed(&format!("Failed to sync conversations: {}", e)) + })?; Ok(true) } @@ -90,3 +95,26 @@ impl DbusSettings for ServerImpl { } } + +fn run_sync_future(f: F) -> Result +where + T: Send, + F: Future + Send, +{ + // We use `scope` here to ensure that the thread is joined before the + // function returns. This allows us to capture references of values that + // have lifetimes shorter than 'static, which is what thread::spawn requires. + thread::scope(move |s| { + s.spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|_| MethodErr::failed("Unable to create tokio runtime"))?; + + let result = rt.block_on(f); + Ok(result) + }) + .join() + }) + .expect("Error joining runtime thread") +} \ No newline at end of file diff --git a/kpcli/src/client/mod.rs b/kpcli/src/client/mod.rs index 671db2f..d5c441e 100644 --- a/kpcli/src/client/mod.rs +++ b/kpcli/src/client/mod.rs @@ -1,6 +1,7 @@ use kordophone::APIInterface; use kordophone::api::http_client::HTTPAPIClient; use kordophone::api::http_client::Credentials; +use kordophone::api::InMemoryTokenManagement; use dotenv; use anyhow::Result; diff --git a/kpcli/src/daemon/mod.rs b/kpcli/src/daemon/mod.rs index e7de320..8c89165 100644 --- a/kpcli/src/daemon/mod.rs +++ b/kpcli/src/daemon/mod.rs @@ -3,7 +3,7 @@ use clap::Subcommand; use dbus::blocking::{Connection, Proxy}; const DBUS_NAME: &str = "net.buzzert.kordophonecd"; -const DBUS_PATH: &str = "/net/buzzert/kordophonecd"; +const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon"; mod dbus_interface { #![allow(unused)]