From b7fabd6c05522a7d7272f02f2ed8bc919bde9e21 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Sat, 23 Aug 2025 19:48:49 -0700 Subject: [PATCH] xpc: implement GetConversations --- kordophoned/src/xpc/agent.rs | 44 +++++++++++++++++++++++++++++ kpcli/src/daemon/xpc.rs | 54 +++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/kordophoned/src/xpc/agent.rs b/kordophoned/src/xpc/agent.rs index a069cfa..61948f5 100644 --- a/kordophoned/src/xpc/agent.rs +++ b/kordophoned/src/xpc/agent.rs @@ -133,6 +133,50 @@ async fn dispatch(agent: &XpcAgent, root: &HashMap) -> Message } } + "GetConversations" => { + // Defaults + let mut limit: i32 = 100; + let mut offset: i32 = 0; + + if let Some(args) = get_dictionary_field(root, "arguments") { + if let Some(Message::String(v)) = args.get(&cstr("limit")) { limit = v.to_string_lossy().parse().unwrap_or(100); } + if let Some(Message::String(v)) = args.get(&cstr("offset")) { offset = v.to_string_lossy().parse().unwrap_or(0); } + } + + match agent.send_event(|r| Event::GetAllConversations(limit, offset, r)).await { + Ok(conversations) => { + // Build array of conversation dictionaries + let mut items: Vec = Vec::with_capacity(conversations.len()); + for conv in conversations { + let mut m: HashMap = HashMap::new(); + m.insert(cstr("guid"), Message::String(cstr(&conv.guid))); + m.insert(cstr("display_name"), Message::String(cstr(&conv.display_name.unwrap_or_default()))); + m.insert(cstr("unread_count"), Message::String(cstr(&(conv.unread_count as i64).to_string()))); + m.insert(cstr("last_message_preview"), Message::String(cstr(&conv.last_message_preview.unwrap_or_default()))); + + // participants -> array of strings + let participants: Vec = conv + .participants + .into_iter() + .map(|p| Message::String(cstr(&p.display_name()))) + .collect(); + m.insert(cstr("participants"), Message::Array(participants)); + + // date as unix timestamp (i64) + m.insert(cstr("date"), Message::String(cstr(&conv.date.and_utc().timestamp().to_string()))); + + items.push(Message::Dictionary(m)); + } + + let mut reply: HashMap = HashMap::new(); + reply.insert(cstr("type"), Message::String(cstr("GetConversationsResponse"))); + reply.insert(cstr("conversations"), Message::Array(items)); + Message::Dictionary(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 1308bfe..efd85fb 100644 --- a/kpcli/src/daemon/xpc.rs +++ b/kpcli/src/daemon/xpc.rs @@ -17,6 +17,7 @@ use futures::{ const SERVICE_NAME: &str = "net.buzzert.kordophonecd\0"; const GET_VERSION_METHOD: &str = "GetVersion"; +const GET_CONVERSATIONS_METHOD: &str = "GetConversations"; // We can't use XPCClient from xpc-connection because of some strange decisions with which flags // are passed to xpc_connection_create_mach_service. @@ -164,7 +165,58 @@ impl DaemonInterface for XpcDaemonInterface { // Remaining methods unimplemented on macOS async fn print_conversations(&mut self) -> Result<()> { - Err(anyhow::anyhow!("Feature not implemented for XPC")) + // Connect + let mach_port_name = Self::build_service_name()?; + let mut client = XPCClient::connect(&mach_port_name); + + // Build arguments: limit=100, offset=0 (string-encoded for portability) + let mut args = HashMap::new(); + args.insert(CString::new("limit").unwrap(), Message::String(CString::new("100").unwrap())); + args.insert(CString::new("offset").unwrap(), Message::String(CString::new("0").unwrap())); + + // Call + let reply = self + .call_method(&mut client, GET_CONVERSATIONS_METHOD, Some(args)) + .await?; + + // Expect an array under "conversations" + match reply.get(&CString::new("conversations").unwrap()) { + Some(Message::Array(items)) => { + println!("Number of conversations: {}", items.len()); + + for item in items { + if let Message::Dictionary(map) = item { + // Convert to PrintableConversation + let guid = Self::get_string(map, "guid").map(|s| s.to_string_lossy().into_owned()).unwrap_or_default(); + let display_name = Self::get_string(map, "display_name").map(|s| s.to_string_lossy().into_owned()); + let last_preview = Self::get_string(map, "last_message_preview").map(|s| s.to_string_lossy().into_owned()); + + let unread_count = match map.get(&CString::new("unread_count").unwrap()) { Some(Message::String(v)) => v.to_string_lossy().parse().unwrap_or(0), _ => 0 }; + let date_ts: i64 = match map.get(&CString::new("date").unwrap()) { Some(Message::String(v)) => v.to_string_lossy().parse().unwrap_or(0), _ => 0 }; + + let participants: Vec = match map.get(&CString::new("participants").unwrap()) { + 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(), + }; + + // Build PrintableConversation directly + let conv = crate::printers::PrintableConversation { + guid, + display_name, + last_message_preview: last_preview, + unread_count, + date: time::OffsetDateTime::from_unix_timestamp(date_ts).unwrap_or_else(|_| time::OffsetDateTime::UNIX_EPOCH), + participants, + }; + + println!("{}", crate::printers::ConversationPrinter::new(&conv)); + } + } + Ok(()) + } + Some(other) => Err(anyhow::anyhow!("Unexpected conversations payload: {:?}", other)), + None => Err(anyhow::anyhow!("Missing conversations in reply")), + } } async fn sync_conversations(&mut self, _conversation_id: Option) -> Result<()> { Err(anyhow::anyhow!("Feature not implemented for XPC"))