From 16db2caacca6a3bc48e587810d7ccd1e9bc4f120 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 23 Aug 2025 20:13:33 -0700 Subject: [PATCH] xpc: implement rest of methods in kpcli except signals. --- kordophoned/src/xpc/agent.rs | 159 +++++++++++++++++++++++++++++++++++ kpcli/src/daemon/xpc.rs | 115 +++++++++++++++++++++++-- 2 files changed, 265 insertions(+), 9 deletions(-) diff --git a/kordophoned/src/xpc/agent.rs b/kordophoned/src/xpc/agent.rs index 89dd083..50177a4 100644 --- a/kordophoned/src/xpc/agent.rs +++ b/kordophoned/src/xpc/agent.rs @@ -1,6 +1,7 @@ use crate::xpc::interface::SERVICE_NAME; use futures_util::StreamExt; use kordophoned::daemon::{events::Event, signals::Signal, DaemonResult}; +use kordophoned::daemon::settings::Settings; use std::collections::HashMap; use std::ffi::CString; use std::sync::Arc; @@ -144,6 +145,12 @@ fn array_from_strs(values: impl IntoIterator) -> Message { Message::Array(arr) } +fn make_ok_reply() -> Message { + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "Ok"); + Message::Dictionary(reply) +} + async fn dispatch(agent: &XpcAgent, root: &HashMap) -> Message { // Standardized request: { method: String, arguments: Dictionary? } let method = match dict_get_str(root, "method").or_else(|| dict_get_str(root, "type")) { @@ -224,6 +231,158 @@ async fn dispatch(agent: &XpcAgent, root: &HashMap) -> Message } } + "SyncConversationList" => { + match agent.send_event(Event::SyncConversationList).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "SyncAllConversations" => { + match agent.send_event(Event::SyncAllConversations).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "SyncConversation" => { + let conversation_id = match get_dictionary_field(root, "arguments").and_then(|m| dict_get_str(m, "conversation_id")) { + Some(id) => id, + None => return make_error_reply("InvalidRequest", "Missing conversation_id"), + }; + match agent.send_event(|r| Event::SyncConversation(conversation_id, r)).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "MarkConversationAsRead" => { + let conversation_id = match get_dictionary_field(root, "arguments").and_then(|m| dict_get_str(m, "conversation_id")) { + Some(id) => id, + None => return make_error_reply("InvalidRequest", "Missing conversation_id"), + }; + match agent.send_event(|r| Event::MarkConversationAsRead(conversation_id, r)).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "GetMessages" => { + let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => return make_error_reply("InvalidRequest", "Missing arguments") }; + let conversation_id = match dict_get_str(args, "conversation_id") { Some(id) => id, None => return make_error_reply("InvalidRequest", "Missing conversation_id") }; + let last_message_id = dict_get_str(args, "last_message_id"); + match agent.send_event(|r| Event::GetMessages(conversation_id, last_message_id, r)).await { + Ok(messages) => { + let mut items: Vec = Vec::with_capacity(messages.len()); + for msg in messages { + let mut m: XpcMap = HashMap::new(); + dict_put_str(&mut m, "id", &msg.id); + dict_put_str(&mut m, "text", &msg.text.replace('\u{FFFC}', "")); + dict_put_i64_as_str(&mut m, "date", msg.date.and_utc().timestamp()); + dict_put_str(&mut m, "sender", &msg.sender.display_name()); + items.push(Message::Dictionary(m)); + } + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "GetMessagesResponse"); + reply.insert(cstr("messages"), Message::Array(items)); + Message::Dictionary(reply) + } + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "DeleteAllConversations" => { + match agent.send_event(Event::DeleteAllConversations).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "SendMessage" => { + let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => return make_error_reply("InvalidRequest", "Missing arguments") }; + let conversation_id = match dict_get_str(args, "conversation_id") { Some(v) => v, None => return make_error_reply("InvalidRequest", "Missing conversation_id") }; + let text = dict_get_str(args, "text").unwrap_or_default(); + let attachment_guids: Vec = match args.get(&cstr("attachment_guids")) { + Some(Message::Array(arr)) => arr.iter().filter_map(|m| match m { Message::String(s) => Some(s.to_string_lossy().into_owned()), _ => None }).collect(), + _ => Vec::new(), + }; + match agent.send_event(|r| Event::SendMessage(conversation_id, text, attachment_guids, r)).await { + Ok(uuid) => { + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "SendMessageResponse"); + dict_put_str(&mut reply, "uuid", &uuid.to_string()); + Message::Dictionary(reply) + } + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "GetAttachmentInfo" => { + let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => return make_error_reply("InvalidRequest", "Missing arguments") }; + let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, None => return make_error_reply("InvalidRequest", "Missing attachment_id") }; + match agent.send_event(|r| Event::GetAttachment(attachment_id, r)).await { + Ok(attachment) => { + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "GetAttachmentInfoResponse"); + dict_put_str(&mut reply, "path", &attachment.get_path_for_preview(false).to_string_lossy()); + dict_put_str(&mut reply, "preview_path", &attachment.get_path_for_preview(true).to_string_lossy()); + dict_put_str(&mut reply, "downloaded", &attachment.is_downloaded(false).to_string()); + dict_put_str(&mut reply, "preview_downloaded", &attachment.is_downloaded(true).to_string()); + Message::Dictionary(reply) + } + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "DownloadAttachment" => { + let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => return make_error_reply("InvalidRequest", "Missing arguments") }; + let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, None => return make_error_reply("InvalidRequest", "Missing attachment_id") }; + let preview = dict_get_str(args, "preview").map(|s| s == "true").unwrap_or(false); + match agent.send_event(|r| Event::DownloadAttachment(attachment_id, preview, r)).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "UploadAttachment" => { + use std::path::PathBuf; + let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => return make_error_reply("InvalidRequest", "Missing arguments") }; + let path = match dict_get_str(args, "path") { Some(v) => v, None => return make_error_reply("InvalidRequest", "Missing path") }; + match agent.send_event(|r| Event::UploadAttachment(PathBuf::from(path), r)).await { + Ok(upload_guid) => { + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "UploadAttachmentResponse"); + dict_put_str(&mut reply, "upload_guid", &upload_guid); + Message::Dictionary(reply) + } + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "GetAllSettings" => { + match agent.send_event(Event::GetAllSettings).await { + Ok(settings) => { + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "GetAllSettingsResponse"); + dict_put_str(&mut reply, "server_url", &settings.server_url.unwrap_or_default()); + dict_put_str(&mut reply, "username", &settings.username.unwrap_or_default()); + Message::Dictionary(reply) + } + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + + "UpdateSettings" => { + let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => return make_error_reply("InvalidRequest", "Missing arguments") }; + let server_url = dict_get_str(args, "server_url"); + let username = dict_get_str(args, "username"); + let settings = Settings { server_url, username, token: None }; + match agent.send_event(|r| Event::UpdateSettings(settings, r)).await { + Ok(()) => make_ok_reply(), + Err(e) => make_error_reply("DaemonError", &format!("{}", e)), + } + } + // Unknown method fallback other => make_error_reply("UnknownMethod", other), } diff --git a/kpcli/src/daemon/xpc.rs b/kpcli/src/daemon/xpc.rs index 12851a2..e302f1c 100644 --- a/kpcli/src/daemon/xpc.rs +++ b/kpcli/src/daemon/xpc.rs @@ -273,41 +273,138 @@ impl DaemonInterface for XpcDaemonInterface { } } async fn sync_conversations(&mut self, _conversation_id: Option) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + + if let Some(id) = _conversation_id { + let mut args = HashMap::new(); + args.insert(Self::key("conversation_id"), Message::String(CString::new(id).unwrap())); + let _ = self.call_method(&mut client, "SyncConversation", Some(args)).await?; + return Ok(()); + } + + let _ = self.call_method(&mut client, "SyncAllConversations", None).await?; + Ok(()) } async fn sync_conversations_list(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + let _ = self.call_method(&mut client, "SyncConversationList", None).await?; + Ok(()) } async fn print_messages( &mut self, _conversation_id: String, _last_message_id: Option, ) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + + let mut args = HashMap::new(); + args.insert(Self::key("conversation_id"), Message::String(CString::new(_conversation_id).unwrap())); + if let Some(last) = _last_message_id { + args.insert(Self::key("last_message_id"), Message::String(CString::new(last).unwrap())); + } + + let reply = self.call_method(&mut client, "GetMessages", Some(args)).await?; + match reply.get(&Self::key("messages")) { + Some(Message::Array(items)) => { + println!("Number of messages: {}", items.len()); + for item in items { + if let Message::Dictionary(map) = item { + let guid = Self::get_string(map, "id").map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(); + let sender = Self::get_string(map, "sender").map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(); + let text = Self::get_string(map, "text").map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(); + let date_ts = Self::get_i64_from_str(map, "date").unwrap_or(0); + let msg = crate::printers::PrintableMessage { + guid, + date: time::OffsetDateTime::from_unix_timestamp(date_ts).unwrap_or_else(|_| time::OffsetDateTime::UNIX_EPOCH), + sender, + text, + file_transfer_guids: vec![], + attachment_metadata: None, + }; + println!("{}", crate::printers::MessagePrinter::new(&msg)); + } + } + Ok(()) + } + _ => Err(anyhow::anyhow!("Unexpected messages payload")), + } } async fn enqueue_outgoing_message( &mut self, _conversation_id: String, _text: String, ) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + let mut args = HashMap::new(); + args.insert(Self::key("conversation_id"), Message::String(CString::new(_conversation_id).unwrap())); + args.insert(Self::key("text"), Message::String(CString::new(_text).unwrap())); + let reply = self.call_method(&mut client, "SendMessage", Some(args)).await?; + if let Some(uuid) = Self::get_string(&reply, "uuid") { println!("Outgoing message ID: {}", uuid.to_string_lossy()); } + Ok(()) } 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")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + match _cmd { + ConfigCommands::Print => { + let reply = self.call_method(&mut client, "GetAllSettings", None).await?; + let server_url = Self::get_string(&reply, "server_url").map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(); + let username = Self::get_string(&reply, "username").map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(); + let table = prettytable::table!([b->"Server URL", &server_url], [b->"Username", &username]); + table.printstd(); + Ok(()) + } + ConfigCommands::SetServerUrl { url } => { + let mut args = HashMap::new(); + args.insert(Self::key("server_url"), Message::String(CString::new(url).unwrap())); + let _ = self.call_method(&mut client, "UpdateSettings", Some(args)).await?; + Ok(()) + } + ConfigCommands::SetUsername { username } => { + let mut args = HashMap::new(); + args.insert(Self::key("username"), Message::String(CString::new(username).unwrap())); + let _ = self.call_method(&mut client, "UpdateSettings", Some(args)).await?; + Ok(()) + } + } } async fn delete_all_conversations(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + let _ = self.call_method(&mut client, "DeleteAllConversations", None).await?; + Ok(()) } async fn download_attachment(&mut self, _attachment_id: String) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + let mut args = HashMap::new(); + args.insert(Self::key("attachment_id"), Message::String(CString::new(_attachment_id).unwrap())); + args.insert(Self::key("preview"), Message::String(CString::new("false").unwrap())); + let _ = self.call_method(&mut client, "DownloadAttachment", Some(args)).await?; + Ok(()) } async fn upload_attachment(&mut self, _path: String) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + let mut args = HashMap::new(); + args.insert(Self::key("path"), Message::String(CString::new(_path).unwrap())); + let reply = self.call_method(&mut client, "UploadAttachment", Some(args)).await?; + if let Some(guid) = Self::get_string(&reply, "upload_guid") { println!("Upload GUID: {}", guid.to_string_lossy()); } + Ok(()) } async fn mark_conversation_as_read(&mut self, _conversation_id: String) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + let mut args = HashMap::new(); + args.insert(Self::key("conversation_id"), Message::String(CString::new(_conversation_id).unwrap())); + let _ = self.call_method(&mut client, "MarkConversationAsRead", Some(args)).await?; + Ok(()) } }