From cecfd7cd762fc573a994d7ec0df308114622165a Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sun, 27 Apr 2025 18:07:58 -0700 Subject: [PATCH] implements settings, conversation dbus encoding --- Cargo.lock | 68 ++++++--- kordophone-db/Cargo.toml | 1 + kordophone-db/src/repository.rs | 47 ++++++ kordophone-db/src/tests/mod.rs | 144 +++++++++++++++--- .../net.buzzert.kordophonecd.Server.xml | 5 +- kordophoned/src/daemon/events.rs | 7 + kordophoned/src/daemon/mod.rs | 76 ++++----- kordophoned/src/daemon/settings.rs | 28 +++- kordophoned/src/dbus/server_impl.rs | 70 +++++++-- kpcli/src/daemon/mod.rs | 76 ++++++++- kpcli/src/printers.rs | 20 +++ 11 files changed, 446 insertions(+), 96 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24f8823..7e5ea67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,14 +509,14 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] @@ -712,12 +712,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.28" @@ -806,6 +800,30 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jiff" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.72" @@ -846,6 +864,7 @@ dependencies = [ "diesel", "diesel_migrations", "kordophone", + "log", "serde", "time", "tokio", @@ -950,9 +969,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "memchr" @@ -1141,6 +1160,21 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1167,18 +1201,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -1422,9 +1456,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", diff --git a/kordophone-db/Cargo.toml b/kordophone-db/Cargo.toml index 65e7bba..fe8fd87 100644 --- a/kordophone-db/Cargo.toml +++ b/kordophone-db/Cargo.toml @@ -11,6 +11,7 @@ chrono = "0.4.38" diesel = { version = "2.2.6", features = ["chrono", "sqlite", "time"] } diesel_migrations = { version = "2.2.0", features = ["sqlite"] } kordophone = { path = "../kordophone" } +log = "0.4.27" serde = { version = "1.0.215", features = ["derive"] } time = "0.3.37" tokio = "1.44.2" diff --git a/kordophone-db/src/repository.rs b/kordophone-db/src/repository.rs index a82457a..1371c3c 100644 --- a/kordophone-db/src/repository.rs +++ b/kordophone-db/src/repository.rs @@ -130,6 +130,53 @@ impl<'a> Repository<'a> { Ok(()) } + pub fn insert_messages(&mut self, conversation_guid: &str, in_messages: Vec) -> Result<()> { + use crate::schema::messages::dsl::*; + use crate::schema::conversation_messages::dsl::*; + + // Local insertable struct for the join table + #[derive(Insertable)] + #[diesel(table_name = crate::schema::conversation_messages)] + struct InsertableConversationMessage { + pub conversation_id: String, + pub message_id: String, + } + + if in_messages.is_empty() { + return Ok(()); + } + + // Build the collections of insertable records + let mut db_messages: Vec = Vec::with_capacity(in_messages.len()); + let mut conv_msg_records: Vec = Vec::with_capacity(in_messages.len()); + + for message in in_messages { + // Handle participant if message has a remote sender + let sender = message.sender.clone(); + let mut db_message: MessageRecord = message.into(); + db_message.sender_participant_id = self.get_or_create_participant(&sender); + + conv_msg_records.push(InsertableConversationMessage { + conversation_id: conversation_guid.to_string(), + message_id: db_message.id.clone(), + }); + + db_messages.push(db_message); + } + + // Batch insert or replace messages + diesel::replace_into(messages) + .values(&db_messages) + .execute(self.connection)?; + + // Batch insert the conversation-message links + diesel::replace_into(conversation_messages) + .values(&conv_msg_records) + .execute(self.connection)?; + + Ok(()) + } + pub fn get_messages_for_conversation(&mut self, conversation_guid: &str) -> Result> { use crate::schema::messages::dsl::*; use crate::schema::conversation_messages::dsl::*; diff --git a/kordophone-db/src/tests/mod.rs b/kordophone-db/src/tests/mod.rs index 3918875..2b21712 100644 --- a/kordophone-db/src/tests/mod.rs +++ b/kordophone-db/src/tests/mod.rs @@ -26,13 +26,13 @@ fn participants_vec_equal_ignoring_id(a: &[Participant], b: &[Participant]) -> b a.iter().zip(b.iter()).all(|(a, b)| participants_equal_ignoring_id(a, b)) } -#[test] -fn test_database_init() { +#[tokio::test] +async fn test_database_init() { let _ = Database::new_in_memory().unwrap(); } -#[test] -fn test_add_conversation() { +#[tokio::test] +async fn test_add_conversation() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { let guid = "test"; @@ -62,11 +62,11 @@ fn test_add_conversation() { // And make sure the display name was updated let conversation = repository.get_conversation_by_guid(guid).unwrap().unwrap(); assert_eq!(conversation.display_name.unwrap(), "Modified Conversation"); - }); + }).await; } -#[test] -fn test_conversation_participants() { +#[tokio::test] +async fn test_conversation_participants() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { let participants: Vec = vec!["one".into(), "two".into()]; @@ -97,11 +97,11 @@ fn test_conversation_participants() { let read_participants: Vec = read_conversation.participants; assert!(participants_vec_equal_ignoring_id(&participants, &read_participants)); - }); + }).await; } -#[test] -fn test_all_conversations_with_participants() { +#[tokio::test] +async fn test_all_conversations_with_participants() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // Create two conversations with different participants @@ -136,11 +136,11 @@ fn test_all_conversations_with_participants() { assert!(participants_vec_equal_ignoring_id(&conv1.participants, &participants1)); assert!(participants_vec_equal_ignoring_id(&conv2.participants, &participants2)); - }); + }).await; } -#[test] -fn test_messages() { +#[tokio::test] +async fn test_messages() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // First create a conversation with participants @@ -185,11 +185,11 @@ fn test_messages() { } else { panic!("Expected Remote participant. Got: {:?}", retrieved_message2.sender); } - }); + }).await; } -#[test] -fn test_message_ordering() { +#[tokio::test] +async fn test_message_ordering() { let mut db = Database::new_in_memory().unwrap(); db.with_repository(|repository| { // Create a conversation @@ -229,11 +229,93 @@ fn test_message_ordering() { for i in 1..messages.len() { assert!(messages[i].date > messages[i-1].date); } - }); + }).await; } -#[test] -fn test_settings() { +#[tokio::test] +async fn test_insert_messages_batch() { + let mut db = Database::new_in_memory().unwrap(); + db.with_repository(|repository| { + // Create a conversation with two remote participants + let participants: Vec = vec!["Alice".into(), "Bob".into()]; + let conversation = ConversationBuilder::new() + .display_name("Batch Chat") + .participants(participants.clone()) + .build(); + let conversation_id = conversation.guid.clone(); + repository.insert_conversation(conversation).unwrap(); + + // Prepare a batch of messages with increasing timestamps + let now = chrono::Utc::now().naive_utc(); + let message1 = Message::builder() + .text("Hi".to_string()) + .date(now) + .build(); + + let message2 = Message::builder() + .text("Hello".to_string()) + .sender("Alice".into()) + .date(now + chrono::Duration::seconds(1)) + .build(); + + let message3 = Message::builder() + .text("How are you?".to_string()) + .sender("Bob".into()) + .date(now + chrono::Duration::seconds(2)) + .build(); + + let message4 = Message::builder() + .text("Great!".to_string()) + .date(now + chrono::Duration::seconds(3)) + .build(); + + let original_messages = vec![ + message1.clone(), + message2.clone(), + message3.clone(), + message4.clone(), + ]; + + // Batch insert the messages + repository + .insert_messages(&conversation_id, original_messages.clone()) + .unwrap(); + + // Retrieve messages and verify + let retrieved_messages = repository.get_messages_for_conversation(&conversation_id).unwrap(); + assert_eq!(retrieved_messages.len(), original_messages.len()); + + // Ensure ordering by date + for i in 1..retrieved_messages.len() { + assert!(retrieved_messages[i].date > retrieved_messages[i - 1].date); + } + + // Verify that all messages are present with correct content and sender + for original in &original_messages { + let retrieved = retrieved_messages + .iter() + .find(|m| m.id == original.id) + .expect("Message not found"); + assert_eq!(retrieved.text, original.text); + + match (&original.sender, &retrieved.sender) { + (Participant::Me, Participant::Me) => {} + ( + Participant::Remote { display_name: o_name, .. }, + Participant::Remote { display_name: r_name, .. }, + ) => assert_eq!(o_name, r_name), + _ => panic!( + "Sender mismatch: original {:?}, retrieved {:?}", + original.sender, retrieved.sender + ), + } + } + }) + .await; +} + +#[tokio::test] +async fn test_settings() { let mut db = Database::new_in_memory().unwrap(); db.with_settings(|settings| { settings.put("test", &"test".to_string()).unwrap(); @@ -244,5 +326,27 @@ fn test_settings() { let keys = settings.list_keys().unwrap(); assert_eq!(keys.len(), 0); - }); + + // Try encoding a struct + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq)] + struct TestStruct { + name: String, + age: u32, + } + + let test_struct = TestStruct { + name: "James".to_string(), + age: 35, + }; + + settings.put("test_struct", &test_struct).unwrap(); + assert_eq!(settings.get::("test_struct").unwrap().unwrap(), test_struct); + + // Test with an option + settings.put("test_struct_option", &Option::::None).unwrap(); + assert!(settings.get::>("test_struct_option").unwrap().unwrap().is_none()); + + settings.put("test_struct_option", &Option::::Some("test".to_string())).unwrap(); + assert_eq!(settings.get::>("test_struct_option").unwrap().unwrap(), Some("test".to_string())); + }).await; } diff --git a/kordophoned/include/net.buzzert.kordophonecd.Server.xml b/kordophoned/include/net.buzzert.kordophonecd.Server.xml index 9f8bc32..e5f4474 100644 --- a/kordophoned/include/net.buzzert.kordophonecd.Server.xml +++ b/kordophoned/include/net.buzzert.kordophonecd.Server.xml @@ -13,7 +13,10 @@ 'id' (string): Unique identifier 'title' (string): Display name 'last_message' (string): Preview text - 'is_unread' (boolean): Unread status"/> + 'is_unread' (boolean): Unread status + 'date' (int64): Date of last message + 'participants' (array of strings): List of participants + 'unread_count' (int32): Number of unread messages"/> diff --git a/kordophoned/src/daemon/events.rs b/kordophoned/src/daemon/events.rs index 3d0e2ba..9b52874 100644 --- a/kordophoned/src/daemon/events.rs +++ b/kordophoned/src/daemon/events.rs @@ -1,5 +1,6 @@ use tokio::sync::oneshot; use kordophone_db::models::Conversation; +use crate::daemon::settings::Settings; pub type Reply = oneshot::Sender; @@ -13,6 +14,12 @@ pub enum Event { /// Returns all known conversations from the database. GetAllConversations(Reply>), + + /// Returns all known settings from the database. + GetAllSettings(Reply), + + /// Update settings in the database. + UpdateSettings(Settings, Reply<()>), } diff --git a/kordophoned/src/daemon/mod.rs b/kordophoned/src/daemon/mod.rs index d2d140a..e50fd7d 100644 --- a/kordophoned/src/daemon/mod.rs +++ b/kordophoned/src/daemon/mod.rs @@ -50,6 +50,10 @@ impl TokenStore for DatabaseTokenStore { } } +mod target { + pub static SYNC: &str = "sync"; +} + pub struct Daemon { pub event_sender: Sender, event_receiver: Receiver, @@ -110,6 +114,25 @@ impl Daemon { let conversations = self.get_conversations().await; reply.send(conversations).unwrap(); }, + + Event::GetAllSettings(reply) => { + let settings = self.get_settings().await + .unwrap_or_else(|e| { + log::error!("Failed to get settings: {:#?}", e); + Settings::default() + }); + + reply.send(settings).unwrap(); + }, + + Event::UpdateSettings(settings, reply) => { + self.update_settings(settings).await + .unwrap_or_else(|e| { + log::error!("Failed to update settings: {}", e); + }); + + reply.send(()).unwrap(); + }, } } @@ -118,32 +141,9 @@ impl Daemon { } async fn sync_all_conversations_impl(mut database: Arc>) -> Result<()> { - log::info!("Starting conversation sync"); - - // Get client from the database - let settings = database.with_settings(|s| Settings::from_db(s)) - .await?; + log::info!(target: target::SYNC, "Starting conversation sync"); - let server_url = settings.server_url - .ok_or(DaemonError::ClientNotConfigured)?; - - let mut client = HTTPAPIClient::new( - server_url.parse().unwrap(), - match (settings.username, settings.credential_item) { - (Some(username), Some(password)) => Some( - Credentials { - username, - password, - } - ), - _ => None, - }, - DatabaseTokenStore { database: database.clone() } - ); - - // This function needed to implement TokenManagement - // let token = database.lock().await.get_token(); - // TODO: Clent.token = token + let mut client = Self::get_client_impl(database.clone()).await?; // Fetch conversations from server let fetched_conversations = client.get_conversations().await?; @@ -152,6 +152,7 @@ impl Daemon { .collect(); // Process each conversation + let num_conversations = db_conversations.len(); for conversation in db_conversations { let conversation_id = conversation.guid.clone(); @@ -159,21 +160,18 @@ impl Daemon { database.with_repository(|r| r.insert_conversation(conversation)).await?; // Fetch and sync messages for this conversation + log::info!(target: target::SYNC, "Fetching messages for conversation {}", conversation_id); 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 - database.with_repository(|r| -> Result<()> { - for message in db_messages { - r.insert_message(&conversation_id, message)?; - } - - Ok(()) - }).await?; + log::info!(target: target::SYNC, "Inserting {} messages for conversation {}", db_messages.len(), conversation_id); + database.with_repository(|r| r.insert_messages(&conversation_id, db_messages)).await?; } - + + log::info!(target: target::SYNC, "Synchronized {} conversations", num_conversations); Ok(()) } @@ -185,8 +183,16 @@ impl Daemon { Ok(settings) } + async fn update_settings(&mut self, settings: Settings) -> Result<()> { + self.database.with_settings(|s| settings.save(s)).await + } + async fn get_client(&mut self) -> Result> { - let settings = self.database.with_settings(|s| + Self::get_client_impl(self.database.clone()).await + } + + async fn get_client_impl(mut database: Arc>) -> Result> { + let settings = database.with_settings(|s| Settings::from_db(s) ).await?; @@ -205,7 +211,7 @@ impl Daemon { ), _ => None, }, - DatabaseTokenStore { database: self.database.clone() } + DatabaseTokenStore { database: database.clone() } ); Ok(client) diff --git a/kordophoned/src/daemon/settings.rs b/kordophoned/src/daemon/settings.rs index 29f9899..8d7a8ed 100644 --- a/kordophoned/src/daemon/settings.rs +++ b/kordophoned/src/daemon/settings.rs @@ -16,9 +16,9 @@ pub struct Settings { 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)?; + let server_url: Option = db_settings.get(keys::SERVER_URL)?; + let username: Option = db_settings.get(keys::USERNAME)?; + let credential_item: Option = db_settings.get(keys::CREDENTIAL_ITEM)?; Ok(Self { server_url, @@ -28,9 +28,25 @@ impl Settings { } 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)?; + if let Some(server_url) = &self.server_url { + db_settings.put(keys::SERVER_URL, &server_url)?; + } + if let Some(username) = &self.username { + db_settings.put(keys::USERNAME, &username)?; + } + if let Some(credential_item) = &self.credential_item { + db_settings.put(keys::CREDENTIAL_ITEM, &credential_item)?; + } Ok(()) } +} + +impl Default for Settings { + fn default() -> Self { + Self { + server_url: None, + username: None, + credential_item: None, + } + } } \ No newline at end of file diff --git a/kordophoned/src/dbus/server_impl.rs b/kordophoned/src/dbus/server_impl.rs index 469507c..8ab4cbf 100644 --- a/kordophoned/src/dbus/server_impl.rs +++ b/kordophoned/src/dbus/server_impl.rs @@ -1,17 +1,14 @@ use dbus::arg; use dbus_tree::MethodErr; -use std::sync::Arc; -use tokio::sync::{Mutex, MutexGuard}; +use tokio::sync::mpsc; use std::future::Future; use std::thread; use tokio::sync::oneshot; -use tokio::sync::mpsc; -use futures_util::future::FutureExt; use crate::daemon::{ - Daemon, DaemonResult, events::{Event, Reply}, + settings::Settings, }; use crate::dbus::interface::NetBuzzertKordophoneRepository as DbusRepository; @@ -63,7 +60,10 @@ impl DbusRepository for ServerImpl { map.insert("guid".into(), arg::Variant(Box::new(conv.guid))); map.insert("display_name".into(), arg::Variant(Box::new(conv.display_name.unwrap_or_default()))); map.insert("unread_count".into(), arg::Variant(Box::new(conv.unread_count as i32))); - map + map.insert("last_message_preview".into(), arg::Variant(Box::new(conv.last_message_preview.unwrap_or_default()))); + map.insert("participants".into(), arg::Variant(Box::new(conv.participants.into_iter().map(|p| p.display_name()).collect::>()))); + map.insert("date".into(), arg::Variant(Box::new(conv.date.and_utc().timestamp()))); + map }).collect(); Ok(result) @@ -77,35 +77,77 @@ impl DbusRepository for ServerImpl { impl DbusSettings for ServerImpl { fn set_server(&mut self, url: String, user: String) -> Result<(), dbus::MethodErr> { - todo!() + self.send_event_sync(|r| + Event::UpdateSettings(Settings { + server_url: Some(url), + username: Some(user), + credential_item: None, + }, r) + ) } fn set_credential_item_(&mut self, item_path: dbus::Path<'static>) -> Result<(), dbus::MethodErr> { - todo!() + self.send_event_sync(|r| + Event::UpdateSettings(Settings { + server_url: None, + username: None, + credential_item: Some(item_path.to_string()), + }, r) + ) } fn server_url(&self) -> Result { - todo!() + self.send_event_sync(Event::GetAllSettings) + .and_then(|settings| { + Ok(settings.server_url.unwrap_or_default()) + }) } fn set_server_url(&self, value: String) -> Result<(), dbus::MethodErr> { - todo!() + self.send_event_sync(|r| + Event::UpdateSettings(Settings { + server_url: Some(value), + username: None, + credential_item: None, + }, r) + ) } fn username(&self) -> Result { - todo!() + self.send_event_sync(Event::GetAllSettings) + .and_then(|settings| { + Ok(settings.username.unwrap_or_default()) + }) } fn set_username(&self, value: String) -> Result<(), dbus::MethodErr> { - todo!() + self.send_event_sync(|r| + Event::UpdateSettings(Settings { + server_url: None, + username: Some(value), + credential_item: None, + }, r) + ) } fn credential_item(&self) -> Result, dbus::MethodErr> { - todo!() + self.send_event_sync(Event::GetAllSettings) + .and_then(|settings| { + Ok(settings.credential_item.unwrap_or_default()) + }) + .and_then(|item| { + Ok(dbus::Path::new(item).unwrap_or_default()) + }) } fn set_credential_item(&self, value: dbus::Path<'static>) -> Result<(), dbus::MethodErr> { - todo!() + self.send_event_sync(|r| + Event::UpdateSettings(Settings { + server_url: None, + username: None, + credential_item: Some(value.to_string()), + }, r) + ) } } diff --git a/kpcli/src/daemon/mod.rs b/kpcli/src/daemon/mod.rs index 7a96180..17889aa 100644 --- a/kpcli/src/daemon/mod.rs +++ b/kpcli/src/daemon/mod.rs @@ -1,6 +1,7 @@ use anyhow::Result; use clap::Subcommand; use dbus::blocking::{Connection, Proxy}; +use crate::printers::{ConversationPrinter, MessagePrinter}; const DBUS_NAME: &str = "net.buzzert.kordophonecd"; const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon"; @@ -11,6 +12,7 @@ mod dbus_interface { } use dbus_interface::NetBuzzertKordophoneRepository as KordophoneRepository; +use dbus_interface::NetBuzzertKordophoneSettings as KordophoneSettings; #[derive(Subcommand)] pub enum Commands { @@ -22,6 +24,33 @@ pub enum Commands { /// Prints the server Kordophone version. Version, + + /// Configuration options + Config { + #[command(subcommand)] + command: ConfigCommands, + }, +} + +#[derive(Subcommand)] +pub enum ConfigCommands { + /// Prints the current settings. + Print, + + /// Sets the server URL. + SetServerUrl { + url: String, + }, + + /// Sets the username. + SetUsername { + username: String, + }, + + /// Sets the credential item. + SetCredentialItem { + item: String, + }, } impl Commands { @@ -31,6 +60,7 @@ impl Commands { Commands::Version => client.print_version().await, Commands::Conversations => client.print_conversations().await, Commands::Sync => client.sync_conversations().await, + Commands::Config { command } => client.config(command).await, } } } @@ -58,13 +88,53 @@ impl DaemonCli { pub async fn print_conversations(&mut self) -> Result<()> { let conversations = KordophoneRepository::get_conversations(&self.proxy())?; - println!("Conversations: {:?}", conversations); + println!("Number of conversations: {}", conversations.len()); + + for conversation in conversations { + println!("{}", ConversationPrinter::new(&conversation.into())); + } + Ok(()) } pub async fn sync_conversations(&mut self) -> Result<()> { - let success = KordophoneRepository::sync_all_conversations(&self.proxy())?; - println!("Initiated sync"); + KordophoneRepository::sync_all_conversations(&self.proxy()) + .map_err(|e| anyhow::anyhow!("Failed to sync conversations: {}", e)) + } + + pub async fn config(&mut self, cmd: ConfigCommands) -> Result<()> { + match cmd { + ConfigCommands::Print => self.print_settings().await, + ConfigCommands::SetServerUrl { url } => self.set_server_url(url).await, + ConfigCommands::SetUsername { username } => self.set_username(username).await, + ConfigCommands::SetCredentialItem { item } => self.set_credential_item(item).await, + } + } + + pub async fn print_settings(&mut self) -> Result<()> { + let server_url = KordophoneSettings::server_url(&self.proxy())?; + let username = KordophoneSettings::username(&self.proxy())?; + let credential_item = KordophoneSettings::credential_item(&self.proxy())?; + + println!("Server URL: {}", server_url); + println!("Username: {}", username); + println!("Credential Item: {}", credential_item); Ok(()) } + + pub async fn set_server_url(&mut self, url: String) -> Result<()> { + KordophoneSettings::set_server_url(&self.proxy(), url) + .map_err(|e| anyhow::anyhow!("Failed to set server URL: {}", e)) + } + + pub async fn set_username(&mut self, username: String) -> Result<()> { + KordophoneSettings::set_username(&self.proxy(), username) + .map_err(|e| anyhow::anyhow!("Failed to set username: {}", e)) + } + + pub async fn set_credential_item(&mut self, item: String) -> Result<()> { + KordophoneSettings::set_credential_item(&self.proxy(), item.into()) + .map_err(|e| anyhow::anyhow!("Failed to set credential item: {}", e)) + } + } \ No newline at end of file diff --git a/kpcli/src/printers.rs b/kpcli/src/printers.rs index 81930d1..96a56e3 100644 --- a/kpcli/src/printers.rs +++ b/kpcli/src/printers.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use time::OffsetDateTime; use pretty::RcDoc; +use dbus::arg::{self, RefArg}; pub struct PrintableConversation { pub guid: String, @@ -37,6 +38,25 @@ impl From for PrintableConversation { } } +impl From for PrintableConversation { + fn from(value: arg::PropMap) -> Self { + Self { + guid: value.get("guid").unwrap().as_str().unwrap().to_string(), + date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap()).unwrap(), + unread_count: value.get("unread_count").unwrap().as_i64().unwrap().try_into().unwrap(), + last_message_preview: value.get("last_message_preview").unwrap().as_str().map(|s| s.to_string()), + participants: value.get("participants") + .unwrap() + .0 + .as_iter() + .unwrap() + .map(|s| s.as_str().unwrap().to_string()) + .collect(), + display_name: value.get("display_name").unwrap().as_str().map(|s| s.to_string()), + } + } +} + pub struct PrintableMessage { pub guid: String, pub date: OffsetDateTime,