diff --git a/Cargo.lock b/Cargo.lock index 7e5ea67..f29747f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -297,6 +297,27 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "csv" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +dependencies = [ + "memchr", +] + [[package]] name = "ctor" version = "0.2.8" @@ -459,6 +480,16 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.5.0" @@ -467,10 +498,21 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.0", "windows-sys 0.59.0", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -497,6 +539,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "env_filter" version = "0.1.2" @@ -678,6 +726,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + [[package]] name = "http" version = "0.2.12" @@ -788,6 +842,17 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi 0.5.0", + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -888,7 +953,7 @@ dependencies = [ "kordophone", "kordophone-db", "log", - "thiserror", + "thiserror 2.0.12", "tokio", ] @@ -906,6 +971,7 @@ dependencies = [ "kordophone-db", "log", "pretty", + "prettytable", "time", "tokio", ] @@ -1199,6 +1265,20 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "prettytable" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781" +dependencies = [ + "csv", + "encode_unicode", + "is-terminal", + "lazy_static", + "term", + "unicode-width", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -1256,6 +1336,17 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -1264,7 +1355,7 @@ checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 2.0.12", ] [[package]] @@ -1315,6 +1406,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "ryu" version = "1.0.17" @@ -1477,6 +1574,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1495,13 +1603,33 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/kordophone-db/src/repository.rs b/kordophone-db/src/repository.rs index 1371c3c..468ed68 100644 --- a/kordophone-db/src/repository.rs +++ b/kordophone-db/src/repository.rs @@ -207,6 +207,21 @@ impl<'a> Repository<'a> { Ok(result) } + pub fn get_last_message_for_conversation(&mut self, conversation_guid: &str) -> Result> { + use crate::schema::messages::dsl::*; + use crate::schema::conversation_messages::dsl::*; + + let message_record = conversation_messages + .filter(conversation_id.eq(conversation_guid)) + .inner_join(messages) + .select(MessageRecord::as_select()) + .order_by(schema::messages::date.desc()) + .first::(self.connection) + .optional()?; + + Ok(message_record.map(|r| r.into())) + } + // Helper function to get the last inserted row ID // This is a workaround since the Sqlite backend doesn't support `RETURNING` // Huge caveat with this is that it depends on whatever the last insert was, prevents concurrent inserts. diff --git a/kordophone-db/src/tests/mod.rs b/kordophone-db/src/tests/mod.rs index 2b21712..facc721 100644 --- a/kordophone-db/src/tests/mod.rs +++ b/kordophone-db/src/tests/mod.rs @@ -310,6 +310,10 @@ async fn test_insert_messages_batch() { ), } } + + // Make sure the last message is the last one we inserted + let last_message = repository.get_last_message_for_conversation(&conversation_id).unwrap().unwrap(); + assert_eq!(last_message.id, message4.id); }) .await; } diff --git a/kordophoned/include/net.buzzert.kordophonecd.Server.xml b/kordophoned/include/net.buzzert.kordophonecd.Server.xml index 00e98f8..cbe2060 100644 --- a/kordophoned/include/net.buzzert.kordophonecd.Server.xml +++ b/kordophoned/include/net.buzzert.kordophonecd.Server.xml @@ -8,6 +8,8 @@ value="Returns the version of the client daemon."/> + + + + + + + + + + + + + + + diff --git a/kordophoned/src/daemon/events.rs b/kordophoned/src/daemon/events.rs index 9b52874..598a7a0 100644 --- a/kordophoned/src/daemon/events.rs +++ b/kordophoned/src/daemon/events.rs @@ -1,5 +1,5 @@ use tokio::sync::oneshot; -use kordophone_db::models::Conversation; +use kordophone_db::models::{Conversation, Message}; use crate::daemon::settings::Settings; pub type Reply = oneshot::Sender; @@ -20,6 +20,12 @@ pub enum Event { /// Update settings in the database. UpdateSettings(Settings, Reply<()>), + + /// Returns all messages for a conversation from the database. + /// Parameters: + /// - conversation_id: The ID of the conversation to get messages for. + /// - last_message_id: (optional) The ID of the last message to get. If None, all messages are returned. + GetMessages(String, Option, Reply>), } diff --git a/kordophoned/src/daemon/mod.rs b/kordophoned/src/daemon/mod.rs index 71932b2..58dcb44 100644 --- a/kordophoned/src/daemon/mod.rs +++ b/kordophoned/src/daemon/mod.rs @@ -19,7 +19,7 @@ use async_trait::async_trait; use kordophone_db::{ database::{Database, DatabaseAccess}, - models::Conversation, + models::{Conversation, Message}, }; use kordophone::model::JwtToken; @@ -54,6 +54,7 @@ impl TokenStore for DatabaseTokenStore { mod target { pub static SYNC: &str = "sync"; + pub static EVENT: &str = "event"; } pub struct Daemon { @@ -100,7 +101,11 @@ impl Daemon { } pub async fn run(&mut self) { + log::info!("Starting daemon version {}", self.version); + log::debug!("Debug logging enabled."); + while let Some(event) = self.event_receiver.recv().await { + log::debug!(target: target::EVENT, "Received event: {:?}", event); self.handle_event(event).await; } } @@ -148,6 +153,11 @@ impl Daemon { reply.send(()).unwrap(); }, + + Event::GetMessages(conversation_id, last_message_id, reply) => { + let messages = self.get_messages(conversation_id, last_message_id).await; + reply.send(messages).unwrap(); + }, } } @@ -160,6 +170,10 @@ impl Daemon { self.database.lock().await.with_repository(|r| r.all_conversations().unwrap()).await } + async fn get_messages(&mut self, conversation_id: String, last_message_id: Option) -> Vec { + self.database.lock().await.with_repository(|r| r.get_messages_for_conversation(&conversation_id).unwrap()).await + } + async fn sync_all_conversations_impl(database: &mut Arc>, signal_sender: &Sender) -> Result<()> { log::info!(target: target::SYNC, "Starting conversation sync"); @@ -180,8 +194,16 @@ impl Daemon { database.with_repository(|r| r.insert_conversation(conversation)).await?; // Fetch and sync messages for this conversation + let last_message_id = database.with_repository(|r| -> Option { + r.get_last_message_for_conversation(&conversation_id) + .unwrap_or(None) + .map(|m| m.id) + }).await; + log::debug!(target: target::SYNC, "Fetching messages for conversation {}", conversation_id); - let messages = client.get_messages(&conversation_id, None, None, None).await?; + log::debug!(target: target::SYNC, "Last message id: {:?}", last_message_id); + + let messages = client.get_messages(&conversation_id, None, None, last_message_id).await?; let db_messages: Vec = messages.into_iter() .map(|m| kordophone_db::models::Message::from(m)) .collect(); diff --git a/kordophoned/src/dbus/server_impl.rs b/kordophoned/src/dbus/server_impl.rs index 63458a3..6a1523a 100644 --- a/kordophoned/src/dbus/server_impl.rs +++ b/kordophoned/src/dbus/server_impl.rs @@ -74,6 +74,28 @@ impl DbusRepository for ServerImpl { fn sync_all_conversations(&mut self) -> Result<(), dbus::MethodErr> { self.send_event_sync(Event::SyncAllConversations) } + + fn get_messages(&mut self, conversation_id: String, last_message_id: String) -> Result, dbus::MethodErr> { + let last_message_id_opt = if last_message_id.is_empty() { + None + } else { + Some(last_message_id) + }; + + self.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r)) + .and_then(|messages| { + let result = messages.into_iter().map(|msg| { + let mut map = arg::PropMap::new(); + map.insert("id".into(), arg::Variant(Box::new(msg.id))); + map.insert("text".into(), arg::Variant(Box::new(msg.text))); + map.insert("date".into(), arg::Variant(Box::new(msg.date.and_utc().timestamp()))); + map.insert("sender".into(), arg::Variant(Box::new(msg.sender.display_name()))); + map + }).collect(); + + Ok(result) + }) + } } impl DbusSettings for ServerImpl { diff --git a/kordophoned/src/main.rs b/kordophoned/src/main.rs index 9b507aa..fa48fef 100644 --- a/kordophoned/src/main.rs +++ b/kordophoned/src/main.rs @@ -12,9 +12,14 @@ use dbus::interface; use dbus::server_impl::ServerImpl; fn initialize_logging() { + // Weird: is this the best way to do this? + let log_level = std::env::var("RUST_LOG") + .map(|s| s.parse::().unwrap_or(LevelFilter::Info)) + .unwrap_or(LevelFilter::Info); + env_logger::Builder::from_default_env() - .filter_level(LevelFilter::Info) .format_timestamp_secs() + .filter_level(log_level) .init(); } diff --git a/kpcli/Cargo.toml b/kpcli/Cargo.toml index d55b6d7..420baf6 100644 --- a/kpcli/Cargo.toml +++ b/kpcli/Cargo.toml @@ -15,6 +15,7 @@ kordophone = { path = "../kordophone" } kordophone-db = { path = "../kordophone-db" } log = "0.4.22" pretty = { version = "0.12.3", features = ["termcolor"] } +prettytable = "0.10.0" time = "0.3.37" tokio = "1.41.1" diff --git a/kpcli/src/daemon/mod.rs b/kpcli/src/daemon/mod.rs index e2a6644..9cf11b2 100644 --- a/kpcli/src/daemon/mod.rs +++ b/kpcli/src/daemon/mod.rs @@ -1,8 +1,8 @@ use anyhow::Result; use clap::Subcommand; use dbus::blocking::{Connection, Proxy}; +use prettytable::table; use crate::printers::{ConversationPrinter, MessagePrinter}; -use std::future; const DBUS_NAME: &str = "net.buzzert.kordophonecd"; const DBUS_PATH: &str = "/net/buzzert/kordophonecd/daemon"; @@ -34,6 +34,12 @@ pub enum Commands { /// Waits for signals from the daemon. Signals, + + /// Prints the messages for a conversation. + Messages { + conversation_id: String, + last_message_id: Option, + }, } #[derive(Subcommand)] @@ -66,6 +72,7 @@ impl Commands { Commands::Sync => client.sync_conversations().await, Commands::Config { command } => client.config(command).await, Commands::Signals => client.wait_for_signals().await, + Commands::Messages { conversation_id, last_message_id } => client.print_messages(conversation_id, last_message_id).await, } } } @@ -107,6 +114,17 @@ impl DaemonCli { .map_err(|e| anyhow::anyhow!("Failed to sync conversations: {}", e)) } + pub async fn print_messages(&mut self, conversation_id: String, last_message_id: Option) -> Result<()> { + let messages = KordophoneRepository::get_messages(&self.proxy(), &conversation_id, &last_message_id.unwrap_or_default())?; + println!("Number of messages: {}", messages.len()); + + for message in messages { + println!("{}", MessagePrinter::new(&message.into())); + } + + Ok(()) + } + pub async fn wait_for_signals(&mut self) -> Result<()> { use dbus::Message; mod dbus_signals { @@ -136,13 +154,17 @@ impl DaemonCli { } 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())?; + let server_url = KordophoneSettings::server_url(&self.proxy()).unwrap_or_default(); + let username = KordophoneSettings::username(&self.proxy()).unwrap_or_default(); + let credential_item = KordophoneSettings::credential_item(&self.proxy()).unwrap_or_default(); + + let table = table!( + [ b->"Server URL", &server_url ], + [ b->"Username", &username ], + [ b->"Credential Item", &credential_item ] + ); + table.printstd(); - println!("Server URL: {}", server_url); - println!("Username: {}", username); - println!("Credential Item: {}", credential_item); Ok(()) } diff --git a/kpcli/src/printers.rs b/kpcli/src/printers.rs index 96a56e3..5a7146f 100644 --- a/kpcli/src/printers.rs +++ b/kpcli/src/printers.rs @@ -86,6 +86,17 @@ impl From for PrintableMessage { } } +impl From for PrintableMessage { + fn from(value: arg::PropMap) -> Self { + Self { + guid: value.get("id").unwrap().as_str().unwrap().to_string(), + date: OffsetDateTime::from_unix_timestamp(value.get("date").unwrap().as_i64().unwrap()).unwrap(), + sender: value.get("sender").unwrap().as_str().unwrap().to_string(), + text: value.get("text").unwrap().as_str().unwrap().to_string(), + } + } +} + pub struct ConversationPrinter<'a> { doc: RcDoc<'a, PrintableConversation> }