diff --git a/Cargo.lock b/Cargo.lock index fcf5c89..07b9d26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.58.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8523b410d7187a43085e7e064416ea32ded16bd0a4e6fc025e21616d01258f" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap 2.34.0", + "env_logger 0.8.4", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -181,6 +204,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "block-buffer" version = "0.10.4" @@ -208,6 +237,15 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -229,6 +267,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "2.34.0" @@ -612,6 +661,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.8" @@ -668,6 +730,31 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-async-runtime-preview" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c03035be1dae627b7e05c6984acb1f2086043fde5249ae51604f1ff20ed037" +dependencies = [ + "futures-core-preview", + "futures-stable-preview", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -675,6 +762,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-channel-preview" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f8aec6b0eb1d281843ec666fba2b71a49610181e3078fbef7a8cbed481821e" +dependencies = [ + "futures-core-preview", ] [[package]] @@ -683,6 +780,55 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +[[package]] +name = "futures-core-preview" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "098785413db44e5dbf3b1fc23c24039a9091bea5acb3eb0d293f386f18aff97d" +dependencies = [ + "either", +] + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-executor-preview" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28ff61425699ca85de5c63c1f135278403518c3398bd15cf4b6fd1d21c9846e4" +dependencies = [ + "futures-channel-preview", + "futures-core-preview", + "futures-util-preview", + "lazy_static", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-io-preview" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa769a6ac904912c1557b4dcf85b93db2bc9ba57c349f9ce43870e49d67f8e1" +dependencies = [ + "futures-core-preview", + "iovec", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -694,12 +840,49 @@ dependencies = [ "syn", ] +[[package]] +name = "futures-preview" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4d575096a4e2cf458f309b5b7bce5c8aaad8e874b8d77f0aa26c08d7ac18f74" +dependencies = [ + "futures-async-runtime-preview", + "futures-channel-preview", + "futures-core-preview", + "futures-executor-preview", + "futures-io-preview", + "futures-sink-preview", + "futures-stable-preview", + "futures-util-preview", +] + [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +[[package]] +name = "futures-sink-preview" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dc4cdc628b934f18a11ba070d589655f68cfec031a16381b0e7784ff0e9cc18" +dependencies = [ + "either", + "futures-channel-preview", + "futures-core-preview", +] + +[[package]] +name = "futures-stable-preview" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6ba960b8bbbc14a9a741cc8ad9c26aff44538ea14be021db905b43f33854da" +dependencies = [ + "futures-core-preview", + "futures-executor-preview", +] + [[package]] name = "futures-task" version = "0.3.31" @@ -712,15 +895,31 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", ] +[[package]] +name = "futures-util-preview" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b29aa737dba9e2e47a5dcd4d58ec7c7c2d5f78e8460f609f857bcf04163235e" +dependencies = [ + "either", + "futures-channel-preview", + "futures-core-preview", + "futures-io-preview", + "futures-sink-preview", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -760,6 +959,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "h2" version = "0.3.26" @@ -857,6 +1062,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + [[package]] name = "hyper" version = "0.14.28" @@ -933,6 +1144,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "is-terminal" version = "0.4.16" @@ -1008,7 +1228,7 @@ dependencies = [ "bytes", "chrono", "ctor", - "env_logger", + "env_logger 0.11.8", "futures-util", "hyper", "hyper-tls", @@ -1057,18 +1277,22 @@ dependencies = [ "dbus-tokio", "dbus-tree", "directories", - "env_logger", + "env_logger 0.11.8", + "futures-preview", "futures-util", "keyring", "kordophone", "kordophone-db", "log", "once_cell", + "serde", "serde_json", "thiserror 2.0.12", "tokio", "tokio-condvar", "uuid", + "xpc-connection", + "xpc-connection-sys", ] [[package]] @@ -1082,7 +1306,8 @@ dependencies = [ "dbus-codegen", "dbus-tree", "dotenv", - "env_logger", + "env_logger 0.11.8", + "futures-preview", "futures-util", "kordophone", "kordophone-db", @@ -1092,6 +1317,7 @@ dependencies = [ "serde_json", "time", "tokio", + "xpc-connection", ] [[package]] @@ -1100,6 +1326,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.172" @@ -1115,6 +1347,16 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.48.5", +] + [[package]] name = "libredox" version = "0.1.3" @@ -1223,6 +1465,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "5.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "num" version = "0.4.3" @@ -1302,6 +1554,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.0", + "libc", +] + [[package]] name = "object" version = "0.32.2" @@ -1390,6 +1652,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1610,6 +1878,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" version = "0.38.34" @@ -1733,6 +2007,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -2219,6 +2499,15 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2421,3 +2710,21 @@ name = "xml-rs" version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" + +[[package]] +name = "xpc-connection" +version = "0.2.3" +source = "git+https://github.com/dfrankland/xpc-connection-rs.git?rev=cd4fb3d#cd4fb3d05edb4292ccb9566ae27cdeb874222d2a" +dependencies = [ + "block", + "futures", + "xpc-connection-sys", +] + +[[package]] +name = "xpc-connection-sys" +version = "0.1.1" +source = "git+https://github.com/dfrankland/xpc-connection-rs.git?rev=cd4fb3d#cd4fb3d05edb4292ccb9566ae27cdeb874222d2a" +dependencies = [ + "bindgen", +] diff --git a/kordophone-db/src/models/conversation.rs b/kordophone-db/src/models/conversation.rs index 4b06f75..bde1597 100644 --- a/kordophone-db/src/models/conversation.rs +++ b/kordophone-db/src/models/conversation.rs @@ -50,12 +50,12 @@ impl Conversation { impl PartialEq for Conversation { fn eq(&self, other: &Self) -> bool { - self.guid == other.guid && - self.unread_count == other.unread_count && - self.display_name == other.display_name && - self.last_message_preview == other.last_message_preview && - self.date == other.date && - self.participants == other.participants + self.guid == other.guid + && self.unread_count == other.unread_count + && self.display_name == other.display_name + && self.last_message_preview == other.last_message_preview + && self.date == other.date + && self.participants == other.participants } } @@ -75,7 +75,10 @@ impl From for Conversation { participants: value .participant_display_names .into_iter() - .map(|p| Participant::Remote { handle: p, contact_id: None }) // todo: this is wrong + .map(|p| Participant::Remote { + handle: p, + contact_id: None, + }) // todo: this is wrong .collect(), } } diff --git a/kordophone-db/src/models/db/message.rs b/kordophone-db/src/models/db/message.rs index 7821183..657a926 100644 --- a/kordophone-db/src/models/db/message.rs +++ b/kordophone-db/src/models/db/message.rs @@ -52,7 +52,10 @@ impl From for Message { .and_then(|json| serde_json::from_str(&json).ok()); let message_sender = match record.sender_participant_handle { - Some(handle) => Participant::Remote { handle, contact_id: None }, + Some(handle) => Participant::Remote { + handle, + contact_id: None, + }, None => Participant::Me, }; Self { diff --git a/kordophone-db/src/models/db/participant.rs b/kordophone-db/src/models/db/participant.rs index 6b22917..087b71e 100644 --- a/kordophone-db/src/models/db/participant.rs +++ b/kordophone-db/src/models/db/participant.rs @@ -22,16 +22,18 @@ pub struct InsertableRecord { impl From for InsertableRecord { fn from(participant: Participant) -> Self { match participant { - Participant::Me => InsertableRecord { - handle: "me".to_string(), - is_me: true, - contact_id: None, - }, - Participant::Remote { handle, contact_id, .. } => InsertableRecord { - handle, - is_me: false, - contact_id, - }, + Participant::Me => InsertableRecord { + handle: "me".to_string(), + is_me: true, + contact_id: None, + }, + Participant::Remote { + handle, contact_id, .. + } => InsertableRecord { + handle, + is_me: false, + contact_id, + }, } } } @@ -62,16 +64,18 @@ impl From for Participant { impl From for Record { fn from(participant: Participant) -> Self { match participant { - Participant::Me => Record { - handle: "me".to_string(), - is_me: true, - contact_id: None, - }, - Participant::Remote { handle, contact_id, .. } => Record { - handle, - is_me: false, - contact_id, - }, + Participant::Me => Record { + handle: "me".to_string(), + is_me: true, + contact_id: None, + }, + Participant::Remote { + handle, contact_id, .. + } => Record { + handle, + is_me: false, + contact_id, + }, } } } diff --git a/kordophone-db/src/models/message.rs b/kordophone-db/src/models/message.rs index 4de27e2..af8b70e 100644 --- a/kordophone-db/src/models/message.rs +++ b/kordophone-db/src/models/message.rs @@ -28,9 +28,13 @@ impl From for Message { contact_id: None, // Weird server quirk: some sender handles are encoded with control characters. - handle: sender.chars() - .filter(|c| !c.is_control() && !matches!(c, - '\u{202A}' | // LRE + handle: sender + .chars() + .filter(|c| { + !c.is_control() + && !matches!( + c, + '\u{202A}' | // LRE '\u{202B}' | // RLE '\u{202C}' | // PDF '\u{202D}' | // LRO @@ -38,8 +42,9 @@ impl From for Message { '\u{2066}' | // LRI '\u{2067}' | // RLI '\u{2068}' | // FSI - '\u{2069}' // PDI - )) + '\u{2069}' // PDI + ) + }) .collect::(), }, diff --git a/kordophone-db/src/models/mod.rs b/kordophone-db/src/models/mod.rs index a8c7e94..13571fc 100644 --- a/kordophone-db/src/models/mod.rs +++ b/kordophone-db/src/models/mod.rs @@ -5,4 +5,4 @@ pub mod participant; pub use conversation::Conversation; pub use message::Message; -pub use participant::Participant; \ No newline at end of file +pub use participant::Participant; diff --git a/kordophone-db/src/repository.rs b/kordophone-db/src/repository.rs index 9c52a99..0f6783b 100644 --- a/kordophone-db/src/repository.rs +++ b/kordophone-db/src/repository.rs @@ -377,7 +377,11 @@ impl<'a> Repository<'a> { fn get_or_create_participant(&mut self, participant: &Participant) -> Option { match participant { Participant::Me => None, - Participant::Remote { handle: p_handle, contact_id: c_id, .. } => { + Participant::Remote { + handle: p_handle, + contact_id: c_id, + .. + } => { use crate::schema::participants::dsl::*; let existing_participant = participants diff --git a/kordophone/src/api/http_client.rs b/kordophone/src/api/http_client.rs index f940cc3..b487d53 100644 --- a/kordophone/src/api/http_client.rs +++ b/kordophone/src/api/http_client.rs @@ -271,10 +271,14 @@ impl APIInterface for HTTPAPIClient { Ok(token) } - async fn mark_conversation_as_read(&mut self, conversation_id: &ConversationID) -> Result<(), Self::Error> { + async fn mark_conversation_as_read( + &mut self, + conversation_id: &ConversationID, + ) -> Result<(), Self::Error> { // SERVER JANK: This should be POST, but it's GET for some reason. let endpoint = format!("markConversation?guid={}", conversation_id); - self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true).await?; + self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true) + .await?; Ok(()) } diff --git a/kordophone/src/api/mod.rs b/kordophone/src/api/mod.rs index 5da1ced..c5a9cd4 100644 --- a/kordophone/src/api/mod.rs +++ b/kordophone/src/api/mod.rs @@ -65,7 +65,10 @@ pub trait APIInterface { async fn authenticate(&mut self, credentials: Credentials) -> Result; // (GET) /markConversation - async fn mark_conversation_as_read(&mut self, conversation_id: &ConversationID) -> Result<(), Self::Error>; + async fn mark_conversation_as_read( + &mut self, + conversation_id: &ConversationID, + ) -> Result<(), Self::Error>; // (WS) /updates async fn open_event_socket( diff --git a/kordophone/src/tests/test_client.rs b/kordophone/src/tests/test_client.rs index c51260e..3801765 100644 --- a/kordophone/src/tests/test_client.rs +++ b/kordophone/src/tests/test_client.rs @@ -149,7 +149,10 @@ impl APIInterface for TestClient { Ok(String::from("test")) } - async fn mark_conversation_as_read(&mut self, conversation_id: &ConversationID) -> Result<(), Self::Error> { + async fn mark_conversation_as_read( + &mut self, + conversation_id: &ConversationID, + ) -> Result<(), Self::Error> { Ok(()) } } diff --git a/kordophoned/Cargo.toml b/kordophoned/Cargo.toml index a596c66..22b005f 100644 --- a/kordophoned/Cargo.toml +++ b/kordophoned/Cargo.toml @@ -21,14 +21,21 @@ tokio-condvar = "0.3.0" uuid = "1.16.0" once_cell = "1.19.0" -[target.'cfg(target_os = "linux")'.dependencies] # D-Bus dependencies only on Linux +[target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9.7", features = ["futures"] } dbus-crossroads = "0.5.2" dbus-tokio = "0.7.6" dbus-tree = "0.9.2" -[target.'cfg(target_os = "linux")'.build-dependencies] # D-Bus codegen only on Linux +[target.'cfg(target_os = "linux")'.build-dependencies] dbus-codegen = "0.10.0" dbus-crossroads = "0.5.1" + +# XPC (libxpc) interface for macOS IPC +[target.'cfg(target_os = "macos")'.dependencies] +futures-preview = "=0.2.2" +xpc-connection = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection" } +xpc-connection-sys = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection-sys" } +serde = { version = "1.0", features = ["derive"] } diff --git a/kordophoned/build.rs b/kordophoned/build.rs index 921915b..4db9d7e 100644 --- a/kordophoned/build.rs +++ b/kordophoned/build.rs @@ -20,8 +20,8 @@ fn main() { let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface"); - let output = dbus_codegen::generate(&xml, &opts) - .expect("Error generating server dbus interface"); + let output = + dbus_codegen::generate(&xml, &opts).expect("Error generating server dbus interface"); std::fs::write(out_path, output).expect("Error writing server dbus code"); diff --git a/kordophoned/src/daemon/contact_resolver/eds.rs b/kordophoned/src/daemon/contact_resolver/eds.rs index 42e2baf..98f5bd5 100644 --- a/kordophoned/src/daemon/contact_resolver/eds.rs +++ b/kordophoned/src/daemon/contact_resolver/eds.rs @@ -1,10 +1,10 @@ use super::ContactResolverBackend; -use dbus::blocking::Connection; use dbus::arg::{RefArg, Variant}; +use dbus::blocking::Connection; use once_cell::sync::OnceCell; use std::collections::HashMap; -use std::time::Duration; use std::sync::Mutex; +use std::time::Duration; #[derive(Clone)] pub struct EDSContactResolverBackend; @@ -40,8 +40,13 @@ static ADDRESS_BOOK_HANDLE: OnceCell> = OnceCell::new() /// Check whether a given well-known name currently has an owner on the bus. fn name_has_owner(conn: &Connection, name: &str) -> bool { - let proxy = conn.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(2)); - let result: Result<(bool,), _> = proxy.method_call("org.freedesktop.DBus", "NameHasOwner", (name.to_string(),)); + let proxy = conn.with_proxy( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + Duration::from_secs(2), + ); + let result: Result<(bool,), _> = + proxy.method_call("org.freedesktop.DBus", "NameHasOwner", (name.to_string(),)); result.map(|(b,)| b).unwrap_or(false) } @@ -99,10 +104,7 @@ fn ensure_address_book_uid(conn: &Connection) -> anyhow::Result { // The GetManagedObjects reply is the usual ObjectManager map. let (managed_objects,): ( - HashMap< - dbus::Path<'static>, - HashMap>>>, - >, + HashMap, HashMap>>>>, ) = source_manager_proxy.method_call( "org.freedesktop.DBus.ObjectManager", "GetManagedObjects", @@ -153,10 +155,7 @@ fn data_contains_address_book_backend(data: &str) -> bool { /// Open the Evolution address book referenced by `source_uid` and return the /// pair `(object_path, bus_name)` that identifies the newly created D-Bus /// proxy. -fn open_address_book( - conn: &Connection, - source_uid: &str, -) -> anyhow::Result<(String, String)> { +fn open_address_book(conn: &Connection, source_uid: &str) -> anyhow::Result<(String, String)> { let factory_proxy = conn.with_proxy( "org.gnome.evolution.dataserver.AddressBook10", "/org/gnome/evolution/dataserver/AddressBookFactory", @@ -177,11 +176,8 @@ fn open_address_book( /// calls the `Open` method once per process. We ignore any error here /// because the backend might already be open. fn ensure_address_book_open(proxy: &dbus::blocking::Proxy<&Connection>) { - let _: Result<(), _> = proxy.method_call( - "org.gnome.evolution.dataserver.AddressBook", - "Open", - (), - ); + let _: Result<(), _> = + proxy.method_call("org.gnome.evolution.dataserver.AddressBook", "Open", ()); } impl ContactResolverBackend for EDSContactResolverBackend { @@ -295,4 +291,4 @@ impl Default for EDSContactResolverBackend { fn default() -> Self { Self } -} \ No newline at end of file +} diff --git a/kordophoned/src/daemon/contact_resolver/generic.rs b/kordophoned/src/daemon/contact_resolver/generic.rs index d3a3604..7e9cd24 100644 --- a/kordophoned/src/daemon/contact_resolver/generic.rs +++ b/kordophoned/src/daemon/contact_resolver/generic.rs @@ -13,4 +13,4 @@ impl ContactResolverBackend for GenericContactResolverBackend { fn get_contact_display_name(&self, contact_id: &Self::ContactID) -> Option { None } -} \ No newline at end of file +} diff --git a/kordophoned/src/daemon/contact_resolver/mod.rs b/kordophoned/src/daemon/contact_resolver/mod.rs index dce3fe3..e391b80 100644 --- a/kordophoned/src/daemon/contact_resolver/mod.rs +++ b/kordophoned/src/daemon/contact_resolver/mod.rs @@ -15,7 +15,9 @@ pub struct EDSContactResolverBackend; #[cfg(not(target_os = "linux"))] impl Default for EDSContactResolverBackend { - fn default() -> Self { EDSContactResolverBackend } + fn default() -> Self { + EDSContactResolverBackend + } } #[cfg(not(target_os = "linux"))] @@ -56,7 +58,11 @@ where T: Default, { pub fn new(backend: T) -> Self { - Self { backend, display_name_cache: HashMap::new(), contact_id_cache: HashMap::new() } + Self { + backend, + display_name_cache: HashMap::new(), + contact_id_cache: HashMap::new(), + } } pub fn resolve_contact_id(&mut self, address: &str) -> Option { @@ -66,7 +72,8 @@ where let id = self.backend.resolve_contact_id(address).map(|id| id.into()); if let Some(ref id) = id { - self.contact_id_cache.insert(address.to_string(), id.clone()); + self.contact_id_cache + .insert(address.to_string(), id.clone()); } id @@ -80,7 +87,8 @@ where let backend_contact_id: T::ContactID = T::ContactID::from((*contact_id).clone()); let display_name = self.backend.get_contact_display_name(&backend_contact_id); if let Some(ref display_name) = display_name { - self.display_name_cache.insert(contact_id.to_string(), display_name.clone()); + self.display_name_cache + .insert(contact_id.to_string(), display_name.clone()); } display_name diff --git a/kordophoned/src/daemon/mod.rs b/kordophoned/src/daemon/mod.rs index e9466af..8481e13 100644 --- a/kordophoned/src/daemon/mod.rs +++ b/kordophoned/src/daemon/mod.rs @@ -522,12 +522,18 @@ impl Daemon { .await? { for p in &saved.participants { - if let DbParticipant::Remote { handle, contact_id: None } = p { + if let DbParticipant::Remote { + handle, + contact_id: None, + } = p + { log::trace!(target: target::SYNC, "Resolving contact id for participant: {}", handle); if let Some(contact) = contact_resolver.resolve_contact_id(handle) { log::trace!(target: target::SYNC, "Resolved contact id for participant: {}", contact); let _ = database - .with_repository(|r| r.update_participant_contact(&handle, &contact)) + .with_repository(|r| { + r.update_participant_contact(&handle, &contact) + }) .await; } else { log::trace!(target: target::SYNC, "No contact id found for participant: {}", handle); @@ -663,11 +669,11 @@ impl Daemon { signal_sender: &Sender, ) -> Result<()> { log::debug!(target: target::DAEMON, "Updating conversation metadata: {}", conversation.guid); - let updated = database.with_repository(|r| r.merge_conversation_metadata(conversation)).await?; + let updated = database + .with_repository(|r| r.merge_conversation_metadata(conversation)) + .await?; if updated { - signal_sender - .send(Signal::ConversationsUpdated) - .await?; + signal_sender.send(Signal::ConversationsUpdated).await?; } Ok(()) diff --git a/kordophoned/src/daemon/models/message.rs b/kordophoned/src/daemon/models/message.rs index ead4f2a..39bab29 100644 --- a/kordophoned/src/daemon/models/message.rs +++ b/kordophoned/src/daemon/models/message.rs @@ -177,7 +177,10 @@ impl From for DbParticipant { fn from(participant: Participant) -> Self { match participant { Participant::Me => DbParticipant::Me, - Participant::Remote { handle, contact_id } => DbParticipant::Remote { handle, contact_id: contact_id.clone() }, + Participant::Remote { handle, contact_id } => DbParticipant::Remote { + handle, + contact_id: contact_id.clone(), + }, } } -} \ No newline at end of file +} diff --git a/kordophoned/src/daemon/update_monitor.rs b/kordophoned/src/daemon/update_monitor.rs index c8be792..936ef5d 100644 --- a/kordophoned/src/daemon/update_monitor.rs +++ b/kordophoned/src/daemon/update_monitor.rs @@ -65,8 +65,9 @@ impl UpdateMonitor { UpdateEventData::ConversationChanged(conversation) => { log::info!(target: target::UPDATES, "Conversation changed: {:?}", conversation); - // Explicitly update the unread count, we assume this is fresh from the notification. - let db_conversation: kordophone_db::models::Conversation = conversation.clone().into(); + // Explicitly update the unread count, we assume this is fresh from the notification. + let db_conversation: kordophone_db::models::Conversation = + conversation.clone().into(); self.send_event(|r| Event::UpdateConversationMetadata(db_conversation, r)) .await .unwrap_or_else(|e| { diff --git a/kordophoned/src/dbus/agent.rs b/kordophoned/src/dbus/agent.rs index bf18163..6cce37a 100644 --- a/kordophoned/src/dbus/agent.rs +++ b/kordophoned/src/dbus/agent.rs @@ -1,15 +1,15 @@ use dbus::arg; use dbus_tree::MethodErr; -use std::{future::Future, thread}; use std::sync::Arc; +use std::{future::Future, thread}; use tokio::sync::{mpsc, oneshot, Mutex}; use crate::daemon::{ + contact_resolver::{ContactResolver, DefaultContactResolverBackend}, events::{Event, Reply}, settings::Settings, signals::Signal, DaemonResult, - contact_resolver::{ContactResolver, DefaultContactResolverBackend}, }; use kordophone_db::models::participant::Participant; @@ -37,7 +37,8 @@ impl DBusAgent { pub async fn run(self) { // Establish a session bus connection. - let (resource, connection) = connection::new_session_sync().expect("Failed to connect to session bus"); + let (resource, connection) = + connection::new_session_sync().expect("Failed to connect to session bus"); // Ensure the D-Bus resource is polled. tokio::spawn(async move { @@ -79,7 +80,10 @@ impl DBusAgent { Signal::ConversationsUpdated => { log::debug!("Sending signal: ConversationsUpdated"); registry - .send_signal(interface::OBJECT_PATH, DbusSignals::ConversationsUpdated {}) + .send_signal( + interface::OBJECT_PATH, + DbusSignals::ConversationsUpdated {}, + ) .unwrap_or_else(|_| { log::error!("Failed to send signal"); 0 @@ -118,7 +122,8 @@ impl DBusAgent { Signal::AttachmentUploaded(upload_guid, attachment_guid) => { log::debug!( "Sending signal: AttachmentUploaded for upload {}, attachment {}", - upload_guid, attachment_guid + upload_guid, + attachment_guid ); registry .send_signal( @@ -154,7 +159,10 @@ impl DBusAgent { std::future::pending::<()>().await; } - pub async fn send_event(&self, make_event: impl FnOnce(Reply) -> Event) -> DaemonResult { + pub async fn send_event( + &self, + make_event: impl FnOnce(Reply) -> Event, + ) -> DaemonResult { let (reply_tx, reply_rx) = oneshot::channel(); self.event_sink .send(make_event(reply_tx)) @@ -172,26 +180,29 @@ impl DBusAgent { .unwrap() .map_err(|e| MethodErr::failed(&format!("Daemon error: {}", e))) } - + fn resolve_participant_display_name(&mut self, participant: &Participant) -> String { match participant { // Me (we should use a special string here...) Participant::Me => "(Me)".to_string(), // Remote participant with a resolved contact_id - Participant::Remote { handle, contact_id: Some(contact_id), .. } => { - self.contact_resolver.get_contact_display_name(contact_id).unwrap_or_else(|| handle.clone()) - } + Participant::Remote { + handle, + contact_id: Some(contact_id), + .. + } => self + .contact_resolver + .get_contact_display_name(contact_id) + .unwrap_or_else(|| handle.clone()), // Remote participant without a resolved contact_id - Participant::Remote { handle, .. } => { - handle.clone() - } + Participant::Remote { handle, .. } => handle.clone(), } } } -// +// // D-Bus repository interface implementation // @@ -203,9 +214,12 @@ impl DbusRepository for DBusAgent { self.send_event_sync(Event::GetVersion) } - fn get_conversations(&mut self, limit: i32, offset: i32) -> Result, MethodErr> { - self - .send_event_sync(|r| Event::GetAllConversations(limit, offset, r)) + fn get_conversations( + &mut self, + limit: i32, + offset: i32, + ) -> Result, MethodErr> { + self.send_event_sync(|r| Event::GetAllConversations(limit, offset, r)) .map(|conversations| { conversations .into_iter() @@ -243,7 +257,6 @@ impl DbusRepository for DBusAgent { }) } - fn sync_conversation_list(&mut self) -> Result<(), MethodErr> { self.send_event_sync(Event::SyncConversationList) } @@ -260,15 +273,18 @@ impl DbusRepository for DBusAgent { self.send_event_sync(|r| Event::MarkConversationAsRead(conversation_id, r)) } - fn get_messages(&mut self, conversation_id: String, last_message_id: String) -> Result, MethodErr> { + fn get_messages( + &mut self, + conversation_id: String, + last_message_id: String, + ) -> Result, 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)) + self.send_event_sync(|r| Event::GetMessages(conversation_id, last_message_id_opt, r)) .map(|messages| { messages .into_iter() @@ -286,7 +302,9 @@ impl DbusRepository for DBusAgent { ); map.insert( "sender".into(), - arg::Variant(Box::new(self.resolve_participant_display_name(&msg.sender.into()))), + arg::Variant(Box::new( + self.resolve_participant_display_name(&msg.sender.into()), + )), ); // Attachments array @@ -312,7 +330,9 @@ impl DbusRepository for DBusAgent { ); attachment_map.insert( "preview_path".into(), - arg::Variant(Box::new(preview_path.to_string_lossy().to_string())), + arg::Variant(Box::new( + preview_path.to_string_lossy().to_string(), + )), ); attachment_map.insert( "downloaded".into(), @@ -374,27 +394,34 @@ impl DbusRepository for DBusAgent { text: String, attachment_guids: Vec, ) -> Result { - self - .send_event_sync(|r| Event::SendMessage(conversation_id, text, attachment_guids, r)) + self.send_event_sync(|r| Event::SendMessage(conversation_id, text, attachment_guids, r)) .map(|uuid| uuid.to_string()) } - fn get_attachment_info(&mut self, attachment_id: String) -> Result<(String, String, bool, bool), MethodErr> { - self.send_event_sync(|r| Event::GetAttachment(attachment_id, r)).map(|attachment| { - let path = attachment.get_path_for_preview(false); - let downloaded = attachment.is_downloaded(false); - let preview_path = attachment.get_path_for_preview(true); - let preview_downloaded = attachment.is_downloaded(true); - ( - path.to_string_lossy().to_string(), - preview_path.to_string_lossy().to_string(), - downloaded, - preview_downloaded, - ) - }) + fn get_attachment_info( + &mut self, + attachment_id: String, + ) -> Result<(String, String, bool, bool), MethodErr> { + self.send_event_sync(|r| Event::GetAttachment(attachment_id, r)) + .map(|attachment| { + let path = attachment.get_path_for_preview(false); + let downloaded = attachment.is_downloaded(false); + let preview_path = attachment.get_path_for_preview(true); + let preview_downloaded = attachment.is_downloaded(true); + ( + path.to_string_lossy().to_string(), + preview_path.to_string_lossy().to_string(), + downloaded, + preview_downloaded, + ) + }) } - fn download_attachment(&mut self, attachment_id: String, preview: bool) -> Result<(), MethodErr> { + fn download_attachment( + &mut self, + attachment_id: String, + preview: bool, + ) -> Result<(), MethodErr> { self.send_event_sync(|r| Event::DownloadAttachment(attachment_id, preview, r)) } @@ -482,4 +509,4 @@ where .join() }) .expect("Error joining runtime thread") -} \ No newline at end of file +} diff --git a/kordophoned/src/dbus/mod.rs b/kordophoned/src/dbus/mod.rs index 1bc9930..8552c4e 100644 --- a/kordophoned/src/dbus/mod.rs +++ b/kordophoned/src/dbus/mod.rs @@ -1,5 +1,5 @@ -pub mod endpoint; pub mod agent; +pub mod endpoint; pub mod interface { #![allow(unused)] diff --git a/kordophoned/src/main.rs b/kordophoned/src/main.rs index db30aff..d56c824 100644 --- a/kordophoned/src/main.rs +++ b/kordophoned/src/main.rs @@ -3,6 +3,9 @@ mod daemon; #[cfg(target_os = "linux")] mod dbus; +#[cfg(target_os = "macos")] +mod xpc; + use log::LevelFilter; use std::future; @@ -33,7 +36,16 @@ async fn start_ipc_agent(daemon: &mut Daemon) { #[cfg(target_os = "macos")] async fn start_ipc_agent(daemon: &mut Daemon) { - // TODO: Implement macOS IPC agent. + // Start the macOS XPC agent (events in, signals out) on a dedicated thread. + let agent = xpc::agent::XpcAgent::new(daemon.event_sender.clone(), daemon.obtain_signal_receiver()); + std::thread::spawn(move || { + // Use a single-threaded Tokio runtime for the XPC agent. + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Unable to create tokio runtime for XPC agent"); + rt.block_on(agent.run()); + }); } #[cfg(not(any(target_os = "linux", target_os = "macos")))] diff --git a/kordophoned/src/xpc/agent.rs b/kordophoned/src/xpc/agent.rs new file mode 100644 index 0000000..e49e405 --- /dev/null +++ b/kordophoned/src/xpc/agent.rs @@ -0,0 +1,38 @@ +use crate::daemon::{events::Event, signals::Signal, DaemonResult}; +use std::sync::Arc; +use tokio::sync::{mpsc, oneshot, Mutex}; + +/// XPC IPC agent that forwards daemon events and signals over libxpc. +#[derive(Clone)] +pub struct XpcAgent { + event_sink: mpsc::Sender, + signal_receiver: Arc>>>, +} + +impl XpcAgent { + /// Create a new XPC agent with an event sink and signal receiver. + pub fn new(event_sink: mpsc::Sender, signal_receiver: mpsc::Receiver) -> Self { + Self { + event_sink, + signal_receiver: Arc::new(Mutex::new(Some(signal_receiver))), + } + } + + /// Run the XPC agent: perform a basic GetVersion IPC call to the daemon and print the result. + pub async fn run(self) { + todo!() + } + + /// Send an event to the daemon and await its reply. + pub async fn send_event( + &self, + make_event: impl FnOnce(crate::daemon::events::Reply) -> Event, + ) -> DaemonResult { + let (tx, rx) = oneshot::channel(); + self.event_sink + .send(make_event(tx)) + .await + .map_err(|_| "Failed to send event")?; + rx.await.map_err(|_| "Failed to receive reply".into()) + } +} diff --git a/kordophoned/src/xpc/endpoint.rs b/kordophoned/src/xpc/endpoint.rs new file mode 100644 index 0000000..459e5c4 --- /dev/null +++ b/kordophoned/src/xpc/endpoint.rs @@ -0,0 +1,27 @@ +#![cfg(target_os = "macos")] +//! XPC registry for registering handlers and emitting signals. + +/// Registry for XPC message handlers and signal emission. +pub struct XpcRegistry; + +impl XpcRegistry { + /// Create a new XPC registry for the service. + pub fn new() -> Self { + XpcRegistry + } + + /// Register a handler for incoming messages at a given endpoint. + pub fn register_handler(&self, _name: &str, _handler: F) + where + F: Fn(&[u8]) -> Vec + Send + Sync + 'static, + { + // TODO: Implement handler registration over libxpc using SERVICE_NAME + let _ = (_name, _handler); + } + + /// Send a signal (notification) to connected clients. + pub fn send_signal(&self, _signal: &str, _data: &T) { + // TODO: Serialize and send signal over XPC + let _ = (_signal, _data); + } +} diff --git a/kordophoned/src/xpc/interface.rs b/kordophoned/src/xpc/interface.rs new file mode 100644 index 0000000..23fa648 --- /dev/null +++ b/kordophoned/src/xpc/interface.rs @@ -0,0 +1,8 @@ +#![cfg(target_os = "macos")] +//! XPC interface definitions for macOS IPC + +/// Mach service name for the XPC interface (must include trailing NUL). +pub const SERVICE_NAME: &str = "net.buzzert.kordophonecd\0"; + +/// Method names for the XPC interface (must include trailing NUL). +pub const GET_VERSION_METHOD: &str = "GetVersion\0"; diff --git a/kordophoned/src/xpc/mod.rs b/kordophoned/src/xpc/mod.rs new file mode 100644 index 0000000..6c335d2 --- /dev/null +++ b/kordophoned/src/xpc/mod.rs @@ -0,0 +1,6 @@ +#![cfg(target_os = "macos")] +//! macOS XPC IPC interface modules. + +pub mod agent; +pub mod endpoint; +pub mod interface; diff --git a/kpcli/Cargo.toml b/kpcli/Cargo.toml index 82f1774..6ecef03 100644 --- a/kpcli/Cargo.toml +++ b/kpcli/Cargo.toml @@ -29,3 +29,8 @@ dbus-tree = "0.9.2" # D-Bus codegen only on Linux [target.'cfg(target_os = "linux")'.build-dependencies] dbus-codegen = "0.10.0" + +# XPC (libxpc) interface only on macOS +[target.'cfg(target_os = "macos")'.dependencies] +futures-preview = "=0.2.2" +xpc-connection = { git = "https://github.com/dfrankland/xpc-connection-rs.git", rev = "cd4fb3d", package = "xpc-connection" } diff --git a/kpcli/build.rs b/kpcli/build.rs index 307f48d..4755d5b 100644 --- a/kpcli/build.rs +++ b/kpcli/build.rs @@ -16,11 +16,10 @@ fn main() { ..Default::default() }; - let xml = std::fs::read_to_string(KORDOPHONE_XML) - .expect("Error reading server dbus interface"); + let xml = std::fs::read_to_string(KORDOPHONE_XML).expect("Error reading server dbus interface"); - let output = dbus_codegen::generate(&xml, &opts) - .expect("Error generating client dbus interface"); + let output = + dbus_codegen::generate(&xml, &opts).expect("Error generating client dbus interface"); std::fs::write(out_path, output).expect("Error writing client dbus code"); diff --git a/kpcli/src/daemon/mod.rs b/kpcli/src/daemon/mod.rs index 19d2491..04d0130 100644 --- a/kpcli/src/daemon/mod.rs +++ b/kpcli/src/daemon/mod.rs @@ -6,6 +6,9 @@ use clap::Subcommand; #[cfg(target_os = "linux")] mod dbus; +#[cfg(target_os = "macos")] +mod xpc; + #[async_trait] pub trait DaemonInterface { async fn print_version(&mut self) -> Result<()>; @@ -40,40 +43,72 @@ impl StubDaemonInterface { #[async_trait] impl DaemonInterface for StubDaemonInterface { async fn print_version(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn print_conversations(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn sync_conversations(&mut self, _conversation_id: Option) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn sync_conversations_list(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } - async fn print_messages(&mut self, _conversation_id: String, _last_message_id: Option) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + async fn print_messages( + &mut self, + _conversation_id: String, + _last_message_id: Option, + ) -> Result<()> { + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } - async fn enqueue_outgoing_message(&mut self, _conversation_id: String, _text: String) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + async fn enqueue_outgoing_message( + &mut self, + _conversation_id: String, + _text: String, + ) -> Result<()> { + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn wait_for_signals(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn config(&mut self, _cmd: ConfigCommands) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn delete_all_conversations(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn download_attachment(&mut self, _attachment_id: String) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn upload_attachment(&mut self, _path: String) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } async fn mark_conversation_as_read(&mut self, _conversation_id: String) -> Result<()> { - Err(anyhow::anyhow!("Daemon interface not implemented on this platform")) + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) } } @@ -82,7 +117,11 @@ pub fn new_daemon_interface() -> Result> { { Ok(Box::new(dbus::DBusDaemonInterface::new()?)) } - #[cfg(not(target_os = "linux"))] + #[cfg(target_os = "macos")] + { + Ok(Box::new(xpc::XpcDaemonInterface::new()?)) + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] { Ok(Box::new(StubDaemonInterface::new()?)) } @@ -162,7 +201,9 @@ impl Commands { conversation_id, last_message_id, } => { - client.print_messages(conversation_id, last_message_id).await + client + .print_messages(conversation_id, last_message_id) + .await } Commands::DeleteAllConversations => client.delete_all_conversations().await, Commands::SendMessage { diff --git a/kpcli/src/daemon/xpc.rs b/kpcli/src/daemon/xpc.rs new file mode 100644 index 0000000..951d514 --- /dev/null +++ b/kpcli/src/daemon/xpc.rs @@ -0,0 +1,100 @@ +#![cfg(target_os = "macos")] +//! macOS XPC implementation of the DaemonInterface for kpcli. + +use super::{ConfigCommands, DaemonInterface}; +use anyhow::Result; +use async_trait::async_trait; +use futures::stream::StreamExt; +use futures::executor::block_on; +use std::collections::HashMap; +use xpc_connection::{Message, XpcConnection}; + +const SERVICE_NAME: &str = "net.buzzert.kordophonecd\0"; +const GET_VERSION_METHOD: &str = "GetVersion\0"; + +/// XPC-based implementation of DaemonInterface that sends method calls to the daemon over libxpc. +pub struct XpcDaemonInterface; + +impl XpcDaemonInterface { + /// Create a new XpcDaemonInterface. No state is held. + pub fn new() -> Result { + Ok(Self) + } +} + +#[async_trait] +impl DaemonInterface for XpcDaemonInterface { + async fn print_version(&mut self) -> Result<()> { + // Open an XPC connection to the daemon service + let mut conn = XpcConnection::new(SERVICE_NAME); + let mut incoming = conn.connect(); + + // Send a GetVersion request as a dictionary message + let mut dict = HashMap::new(); + dict.insert( + GET_VERSION_METHOD.to_string(), + Message::String(String::new()), + ); + conn.send_message(Message::Dictionary(dict)); + + // Wait for a single string reply (futures-preview StreamFuture returns (Option, Stream)) + let (opt_msg, _) = match block_on(incoming.next()) { + Ok(pair) => pair, + Err(e) => { + eprintln!("Error reading XPC reply: {:?}", e); + return Ok(()); + } + }; + if let Some(Message::String(ver_raw)) = opt_msg { + // Trim the trailing NUL if present + let version = ver_raw.trim_end_matches('\0'); + println!("Server version: {}", version); + } else { + eprintln!("Unexpected XPC reply for GetVersion"); + } + Ok(()) + } + + // Remaining methods unimplemented on macOS + async fn print_conversations(&mut self) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn sync_conversations(&mut self, _conversation_id: Option) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn sync_conversations_list(&mut self) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn print_messages( + &mut self, + _conversation_id: String, + _last_message_id: Option, + ) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn enqueue_outgoing_message( + &mut self, + _conversation_id: String, + _text: String, + ) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn wait_for_signals(&mut self) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn config(&mut self, _cmd: ConfigCommands) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn delete_all_conversations(&mut self) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn download_attachment(&mut self, _attachment_id: String) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn upload_attachment(&mut self, _path: String) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } + async fn mark_conversation_as_read(&mut self, _conversation_id: String) -> Result<()> { + Err(anyhow::anyhow!("Feature not implemented for XPC")) + } +}