diff --git a/core/kordophone-db/src/tests/mod.rs b/core/kordophone-db/src/tests/mod.rs index f631fd3..83719ee 100644 --- a/core/kordophone-db/src/tests/mod.rs +++ b/core/kordophone-db/src/tests/mod.rs @@ -26,7 +26,7 @@ fn participants_vec_equal_ignoring_id(a: &[Participant], b: &[Participant]) -> b // For each participant in a, check if there is a matching participant in b a.iter().all(|a_participant| { b.iter().any(|b_participant| participants_equal_ignoring_id(a_participant, b_participant)) - }) && + }) && // Also check the reverse to ensure no duplicates b.iter().all(|b_participant| { a.iter().any(|a_participant| participants_equal_ignoring_id(b_participant, a_participant)) diff --git a/core/kordophone/src/tests/mod.rs b/core/kordophone/src/tests/mod.rs index 61fb496..6b0c872 100644 --- a/core/kordophone/src/tests/mod.rs +++ b/core/kordophone/src/tests/mod.rs @@ -57,9 +57,7 @@ pub mod api_interface { async fn test_delete_conversation() { let mut client = TestClient::new(); - let test_convo = Conversation::builder() - .display_name("Delete Me") - .build(); + let test_convo = Conversation::builder().display_name("Delete Me").build(); client.conversations.push(test_convo.clone()); diff --git a/core/kordophoned-client/src/platform/linux.rs b/core/kordophoned-client/src/platform/linux.rs index b8b1fa0..2f82ff0 100644 --- a/core/kordophoned-client/src/platform/linux.rs +++ b/core/kordophoned-client/src/platform/linux.rs @@ -114,11 +114,23 @@ impl DaemonClient for DBusClient { .collect()) } - fn send_message(&mut self, conversation_id: String, text: String) -> Result> { + fn reply(&mut self, conversation_id: String, text: String) -> Result> { let attachment_guids: Vec<&str> = vec![]; - let outgoing_id = KordophoneRepository::send_message( + let outgoing_id = + KordophoneRepository::reply(&self.proxy(), &conversation_id, &text, attachment_guids)?; + Ok(Some(outgoing_id)) + } + + fn new_conversation( + &mut self, + handle_ids: Vec, + text: String, + ) -> Result> { + let attachment_guids: Vec<&str> = vec![]; + let handle_ids: Vec<&str> = handle_ids.iter().map(String::as_str).collect(); + let outgoing_id = KordophoneRepository::new_conversation( &self.proxy(), - &conversation_id, + handle_ids, &text, attachment_guids, )?; diff --git a/core/kordophoned-client/src/platform/macos.rs b/core/kordophoned-client/src/platform/macos.rs index 58ffec9..488c93e 100644 --- a/core/kordophoned-client/src/platform/macos.rs +++ b/core/kordophoned-client/src/platform/macos.rs @@ -201,7 +201,7 @@ impl DaemonClient for XpcClient { Ok(messages) } - fn send_message(&mut self, conversation_id: String, text: String) -> Result> { + fn reply(&mut self, conversation_id: String, text: String) -> Result> { let mut args = HashMap::new(); args.insert( Self::key("conversation_id"), @@ -211,7 +211,34 @@ impl DaemonClient for XpcClient { let reply = self .transport - .send_with_reply(Self::request("SendMessage", Some(args))); + .send_with_reply(Self::request("Reply", Some(args))); + let Message::Dictionary(map) = reply else { + anyhow::bail!("Unexpected send response"); + }; + + Ok(Self::get_string(&map, "uuid")) + } + + fn new_conversation( + &mut self, + handle_ids: Vec, + text: String, + ) -> Result> { + let mut args = HashMap::new(); + args.insert( + Self::key("handle_ids"), + Message::Array( + handle_ids + .into_iter() + .map(|handle_id| Message::String(Self::key(&handle_id))) + .collect(), + ), + ); + args.insert(Self::key("text"), Message::String(Self::key(&text))); + + let reply = self + .transport + .send_with_reply(Self::request("NewConversation", Some(args))); let Message::Dictionary(map) = reply else { anyhow::bail!("Unexpected send response"); }; diff --git a/core/kordophoned-client/src/worker.rs b/core/kordophoned-client/src/worker.rs index 6e9cb0d..32f5678 100644 --- a/core/kordophoned-client/src/worker.rs +++ b/core/kordophoned-client/src/worker.rs @@ -24,10 +24,14 @@ pub enum Request { RefreshMessages { conversation_id: String, }, - SendMessage { + Reply { conversation_id: String, text: String, }, + NewConversation { + handle_ids: Vec, + text: String, + }, MarkRead { conversation_id: String, }, @@ -42,8 +46,8 @@ pub enum Event { conversation_id: String, messages: Vec, }, - MessageSent { - conversation_id: String, + MessageQueued { + conversation_id: Option, outgoing_id: Option, }, MarkedRead, @@ -80,33 +84,38 @@ pub fn spawn_worker( loop { match request_rx.recv_timeout(Duration::from_millis(100)) { Ok(req) => { - let res = - match req { - Request::RefreshConversations => { - client.get_conversations(200, 0).map(Event::Conversations) - } - Request::RefreshMessages { conversation_id } => client - .get_messages(conversation_id.clone(), None) - .map(|messages| Event::Messages { - conversation_id, - messages, - }), - Request::SendMessage { + let res = match req { + Request::RefreshConversations => { + client.get_conversations(200, 0).map(Event::Conversations) + } + Request::RefreshMessages { conversation_id } => client + .get_messages(conversation_id.clone(), None) + .map(|messages| Event::Messages { conversation_id, - text, - } => client.send_message(conversation_id.clone(), text).map( - |outgoing_id| Event::MessageSent { - conversation_id, - outgoing_id, - }, - ), - Request::MarkRead { conversation_id } => client - .mark_conversation_as_read(conversation_id.clone()) - .map(|_| Event::MarkedRead), - Request::SyncConversation { conversation_id } => client - .sync_conversation(conversation_id.clone()) - .map(|_| Event::ConversationSyncTriggered { conversation_id }), - }; + messages, + }), + Request::Reply { + conversation_id, + text, + } => client + .reply(conversation_id.clone(), text) + .map(|outgoing_id| Event::MessageQueued { + conversation_id: Some(conversation_id), + outgoing_id, + }), + Request::NewConversation { handle_ids, text } => client + .new_conversation(handle_ids, text) + .map(|outgoing_id| Event::MessageQueued { + conversation_id: None, + outgoing_id, + }), + Request::MarkRead { conversation_id } => client + .mark_conversation_as_read(conversation_id.clone()) + .map(|_| Event::MarkedRead), + Request::SyncConversation { conversation_id } => client + .sync_conversation(conversation_id.clone()) + .map(|_| Event::ConversationSyncTriggered { conversation_id }), + }; match res { Ok(evt) => { @@ -135,7 +144,9 @@ pub(crate) trait DaemonClient { conversation_id: String, last_message_id: Option, ) -> Result>; - fn send_message(&mut self, conversation_id: String, text: String) -> Result>; + fn reply(&mut self, conversation_id: String, text: String) -> Result>; + fn new_conversation(&mut self, handle_ids: Vec, text: String) + -> Result>; fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()>; fn sync_conversation(&mut self, conversation_id: String) -> Result<()>; fn install_signal_handlers(&mut self, _event_tx: mpsc::Sender) -> Result<()> { diff --git a/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml b/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml index 14ad915..47ac5c8 100644 --- a/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml +++ b/core/kordophoned/include/net.buzzert.kordophonecd.Server.xml @@ -83,7 +83,7 @@ - + @@ -91,9 +91,28 @@ + + + + + + + + + + created, + Err(e) => { + log::error!( + target: target::EVENT, + "Failed to ensure conversation {} exists for sent message {}: {}", + conversation_id, + message.id, + e + ); + return; + } + }; + + if conversation_created { + self.signal_sender + .send(Signal::ConversationsUpdated) + .await + .unwrap(); + } + // Insert the message into the database. log::debug!(target: target::EVENT, "inserting sent message into database: {}", message.id); - self.database + if let Err(e) = self + .database .lock() .await .with_repository(|r| { @@ -363,13 +407,24 @@ impl Daemon { ) }) .await - .unwrap(); + { + log::error!( + target: target::EVENT, + "Failed to persist sent message {} for conversation {}: {}", + message.id, + conversation_id, + e + ); + return; + } // Remove from outgoing messages. log::debug!(target: target::EVENT, "Removing message from outgoing messages: {}", outgoing_message.guid); + for messages in self.outgoing_messages.values_mut() { + messages.retain(|m| m.guid != outgoing_message.guid); + } self.outgoing_messages - .get_mut(&conversation_id) - .map(|messages| messages.retain(|m| m.guid != outgoing_message.guid)); + .retain(|_, messages| !messages.is_empty()); // Send message updated signal. self.signal_sender @@ -517,24 +572,87 @@ impl Daemon { result } + async fn ensure_conversation_exists_for_sent_message( + &mut self, + conversation_id: &ConversationID, + outgoing_message: &OutgoingMessage, + message: &Message, + ) -> Result { + let conversation_exists = self + .database + .lock() + .await + .with_repository(|r| r.get_conversation_by_guid(conversation_id)) + .await? + .is_some(); + + if conversation_exists { + return Ok(false); + } + + let participants = Self::participants_for_outgoing_message(outgoing_message); + let mut builder = Conversation::builder() + .guid(conversation_id) + .date(message.date) + .unread_count(0) + .participants(participants); + + if !message.text.trim().is_empty() { + builder = builder.last_message_preview(&message.text); + } + + let conversation = builder.build(); + log::info!( + target: target::EVENT, + "Creating local conversation {} from sent message {}", + conversation_id, + message.id + ); + + self.database + .lock() + .await + .with_repository(|r| r.insert_conversation(conversation)) + .await?; + + Ok(true) + } + + fn participants_for_outgoing_message(outgoing_message: &OutgoingMessage) -> Vec { + let handle_ids = match &outgoing_message.target { + OutgoingMessageTarget::Conversation(_) => return Vec::new(), + OutgoingMessageTarget::Handles(handle_ids) => handle_ids, + }; + + let mut contact_resolver = ContactResolver::new(DefaultContactResolverBackend::default()); + handle_ids + .iter() + .map(|handle| DbParticipant::Remote { + handle: handle.clone(), + contact_id: contact_resolver.resolve_contact_id(handle), + }) + .collect() + } + async fn enqueue_outgoing_message( &mut self, text: String, - conversation_id: String, + target: OutgoingMessageTarget, attachment_guids: Vec, ) -> Uuid { - let conversation_id = conversation_id.clone(); let outgoing_message = OutgoingMessage::builder() .text(text) - .conversation_id(conversation_id.clone()) + .target(target) .file_transfer_guids(attachment_guids) .build(); - // Keep a record of this so we can provide a consistent model to the client. - self.outgoing_messages - .entry(conversation_id) - .or_insert(vec![]) - .push(outgoing_message.clone()); + if let Some(conversation_id) = outgoing_message.conversation_id().cloned() { + // Keep a record of replies so we can provide a consistent model to the client. + self.outgoing_messages + .entry(conversation_id) + .or_insert(vec![]) + .push(outgoing_message.clone()); + } let guid = outgoing_message.guid.clone(); self.post_office_sink diff --git a/core/kordophoned/src/dbus/agent.rs b/core/kordophoned/src/dbus/agent.rs index 7e94fff..69dc4d6 100644 --- a/core/kordophoned/src/dbus/agent.rs +++ b/core/kordophoned/src/dbus/agent.rs @@ -388,13 +388,23 @@ impl DbusRepository for DBusAgent { self.send_event_sync(Event::DeleteAllConversations) } - fn send_message( + fn reply( &mut self, conversation_id: String, 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::Reply(conversation_id, text, attachment_guids, r)) + .map(|uuid| uuid.to_string()) + } + + fn new_conversation( + &mut self, + handle_ids: Vec, + text: String, + attachment_guids: Vec, + ) -> Result { + self.send_event_sync(|r| Event::NewConversation(handle_ids, text, attachment_guids, r)) .map(|uuid| uuid.to_string()) } diff --git a/core/kordophoned/src/xpc/agent.rs b/core/kordophoned/src/xpc/agent.rs index 6ebdf79..06a5f1c 100644 --- a/core/kordophoned/src/xpc/agent.rs +++ b/core/kordophoned/src/xpc/agent.rs @@ -127,7 +127,7 @@ impl XpcAgent { // Drop any cleanup resource now that payload is constructed and sent. drop(result.cleanup); - + log::trace!(target: LOG_TARGET, "XPC reply sent for method: {}", method); } else { log::warn!(target: LOG_TARGET, "No reply port for method: {}", method); diff --git a/core/kordophoned/src/xpc/rpc.rs b/core/kordophoned/src/xpc/rpc.rs index c057468..47c3d69 100644 --- a/core/kordophoned/src/xpc/rpc.rs +++ b/core/kordophoned/src/xpc/rpc.rs @@ -254,8 +254,8 @@ pub async fn dispatch( Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))), }, - // SendMessage - "SendMessage" => { + // Reply + "Reply" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, None => { @@ -286,12 +286,64 @@ pub async fn dispatch( _ => Vec::new(), }; match agent - .send_event(|r| Event::SendMessage(conversation_id, text, attachment_guids, r)) + .send_event(|r| Event::Reply(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, "type", "ReplyResponse"); + dict_put_str(&mut reply, "uuid", &uuid.to_string()); + DispatchResult::new(Message::Dictionary(reply)) + } + Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))), + } + } + + // NewConversation + "NewConversation" => { + let args = match get_dictionary_field(root, "arguments") { + Some(a) => a, + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } + }; + let handle_ids: Vec = match args.get(&cstr("handle_ids")) { + 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(), + }; + if handle_ids.is_empty() { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing handle_ids", + )); + } + 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::NewConversation(handle_ids, text, attachment_guids, r)) + .await + { + Ok(uuid) => { + let mut reply: XpcMap = HashMap::new(); + dict_put_str(&mut reply, "type", "NewConversationResponse"); dict_put_str(&mut reply, "uuid", &uuid.to_string()); DispatchResult::new(Message::Dictionary(reply)) } diff --git a/core/kpcli/src/client/mod.rs b/core/kpcli/src/client/mod.rs index e2d08a1..774885a 100644 --- a/core/kpcli/src/client/mod.rs +++ b/core/kpcli/src/client/mod.rs @@ -93,7 +93,9 @@ impl Commands { Commands::Mark { conversation_id } => { client.mark_conversation_as_read(conversation_id).await } - Commands::Delete { conversation_id } => client.delete_conversation(conversation_id).await, + Commands::Delete { conversation_id } => { + client.delete_conversation(conversation_id).await + } } } } @@ -211,11 +213,7 @@ impl ClientCli { Ok(()) } - async fn send_message( - &mut self, - target: OutgoingMessageTarget, - message: String, - ) -> Result<()> { + async fn send_message(&mut self, target: OutgoingMessageTarget, message: String) -> Result<()> { let outgoing_message = OutgoingMessage::builder() .target(target) .text(message) @@ -255,8 +253,11 @@ impl ClientCli { } pub async fn reply(&mut self, conversation_id: String, message: String) -> Result<()> { - self.send_message(OutgoingMessageTarget::Conversation(conversation_id), message) - .await + self.send_message( + OutgoingMessageTarget::Conversation(conversation_id), + message, + ) + .await } pub async fn new_message(&mut self, handle_ids: Vec, message: String) -> Result<()> { diff --git a/core/kpcli/src/daemon/dbus.rs b/core/kpcli/src/daemon/dbus.rs index dc4e444..570ad5f 100644 --- a/core/kpcli/src/daemon/dbus.rs +++ b/core/kpcli/src/daemon/dbus.rs @@ -109,15 +109,20 @@ impl DaemonInterface for DBusDaemonInterface { Ok(()) } - async fn enqueue_outgoing_message( - &mut self, - conversation_id: String, - text: String, - ) -> Result<()> { + async fn reply(&mut self, conversation_id: String, text: String) -> Result<()> { let attachment_guids: Vec<&str> = vec![]; - let outgoing_message_id = KordophoneRepository::send_message( + let outgoing_message_id = + KordophoneRepository::reply(&self.proxy(), &conversation_id, &text, attachment_guids)?; + println!("Outgoing message ID: {}", outgoing_message_id); + Ok(()) + } + + async fn new_conversation(&mut self, handle_ids: Vec, text: String) -> Result<()> { + let attachment_guids: Vec<&str> = vec![]; + let handle_ids: Vec<&str> = handle_ids.iter().map(String::as_str).collect(); + let outgoing_message_id = KordophoneRepository::new_conversation( &self.proxy(), - &conversation_id, + handle_ids, &text, attachment_guids, )?; diff --git a/core/kpcli/src/daemon/mod.rs b/core/kpcli/src/daemon/mod.rs index 872ab1b..394856e 100644 --- a/core/kpcli/src/daemon/mod.rs +++ b/core/kpcli/src/daemon/mod.rs @@ -21,11 +21,8 @@ pub trait DaemonInterface { conversation_id: String, last_message_id: Option, ) -> Result<()>; - async fn enqueue_outgoing_message( - &mut self, - conversation_id: String, - text: String, - ) -> Result<()>; + async fn reply(&mut self, conversation_id: String, text: String) -> Result<()>; + async fn new_conversation(&mut self, handle_ids: Vec, text: String) -> Result<()>; async fn wait_for_signals(&mut self) -> Result<()>; async fn config(&mut self, cmd: ConfigCommands) -> Result<()>; async fn delete_all_conversations(&mut self) -> Result<()>; @@ -73,11 +70,12 @@ impl DaemonInterface for StubDaemonInterface { "Daemon interface not implemented on this platform" )) } - async fn enqueue_outgoing_message( - &mut self, - _conversation_id: String, - _text: String, - ) -> Result<()> { + async fn reply(&mut self, _conversation_id: String, _text: String) -> Result<()> { + Err(anyhow::anyhow!( + "Daemon interface not implemented on this platform" + )) + } + async fn new_conversation(&mut self, _handle_ids: Vec, _text: String) -> Result<()> { Err(anyhow::anyhow!( "Daemon interface not implemented on this platform" )) @@ -161,12 +159,20 @@ pub enum Commands { /// Deletes all conversations. DeleteAllConversations, - /// Enqueues an outgoing message to be sent to a conversation. - SendMessage { + /// Replies to an existing conversation. + #[command(alias = "send-message")] + Reply { conversation_id: String, text: String, }, + /// Starts a new conversation with one or more resolved handles. + New { + #[arg(long = "handle", required = true)] + handle_ids: Vec, + text: String, + }, + /// Downloads an attachment from the server to the attachment store. Returns the path to the attachment. DownloadAttachment { attachment_id: String }, @@ -208,10 +214,11 @@ impl Commands { .await } Commands::DeleteAllConversations => client.delete_all_conversations().await, - Commands::SendMessage { + Commands::Reply { conversation_id, text, - } => client.enqueue_outgoing_message(conversation_id, text).await, + } => client.reply(conversation_id, text).await, + Commands::New { handle_ids, text } => client.new_conversation(handle_ids, text).await, Commands::UploadAttachment { path } => client.upload_attachment(path).await, Commands::DownloadAttachment { attachment_id } => { client.download_attachment(attachment_id).await diff --git a/core/kpcli/src/daemon/xpc.rs b/core/kpcli/src/daemon/xpc.rs index e703781..fb4fd8a 100644 --- a/core/kpcli/src/daemon/xpc.rs +++ b/core/kpcli/src/daemon/xpc.rs @@ -371,11 +371,7 @@ impl DaemonInterface for XpcDaemonInterface { _ => Err(anyhow::anyhow!("Unexpected messages payload")), } } - async fn enqueue_outgoing_message( - &mut self, - _conversation_id: String, - _text: String, - ) -> Result<()> { + async fn reply(&mut self, _conversation_id: String, _text: String) -> Result<()> { let mach_port_name = Self::build_service_name()?; let mut client = XPCClient::connect(&mach_port_name); let mut args = HashMap::new(); @@ -387,10 +383,34 @@ impl DaemonInterface for XpcDaemonInterface { Self::key("text"), Message::String(CString::new(_text).unwrap()), ); - let reply = self - .call_method(&mut client, "SendMessage", Some(args)) + let response = self.call_method(&mut client, "Reply", Some(args)).await?; + if let Some(uuid) = Self::get_string(&response, "uuid") { + println!("Outgoing message ID: {}", uuid.to_string_lossy()); + } + Ok(()) + } + + async fn new_conversation(&mut self, handle_ids: Vec, text: String) -> Result<()> { + 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("handle_ids"), + Message::Array( + handle_ids + .into_iter() + .map(|handle_id| Message::String(CString::new(handle_id).unwrap())) + .collect(), + ), + ); + args.insert( + Self::key("text"), + Message::String(CString::new(text).unwrap()), + ); + let response = self + .call_method(&mut client, "NewConversation", Some(args)) .await?; - if let Some(uuid) = Self::get_string(&reply, "uuid") { + if let Some(uuid) = Self::get_string(&response, "uuid") { println!("Outgoing message ID: {}", uuid.to_string_lossy()); } Ok(()) diff --git a/core/kptui/src/main.rs b/core/kptui/src/main.rs index 783818b..1139c2a 100644 --- a/core/kptui/src/main.rs +++ b/core/kptui/src/main.rs @@ -378,20 +378,22 @@ fn run_app( app.pinned_to_bottom = was_pinned; } } - daemon::Event::MessageSent { + daemon::Event::MessageQueued { conversation_id, outgoing_id, } => { - if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) { - app.status = outgoing_id - .as_deref() - .map(|id| format!("Sent ({id})")) - .unwrap_or_else(|| "Sent".to_string()); - app.refresh_messages_in_flight = false; - request_tx - .send(daemon::Request::RefreshMessages { conversation_id }) - .ok(); - app.refresh_messages_in_flight = true; + if let Some(conversation_id) = conversation_id { + if app.active_conversation_id.as_deref() == Some(conversation_id.as_str()) { + app.status = outgoing_id + .as_deref() + .map(|id| format!("Sent ({id})")) + .unwrap_or_else(|| "Sent".to_string()); + app.refresh_messages_in_flight = false; + request_tx + .send(daemon::Request::RefreshMessages { conversation_id }) + .ok(); + app.refresh_messages_in_flight = true; + } } } daemon::Event::MarkedRead => {} @@ -638,7 +640,7 @@ fn handle_chat_keys( return; }; request_tx - .send(daemon::Request::SendMessage { + .send(daemon::Request::Reply { conversation_id, text, }) diff --git a/gtk/src/service/interface/dbusservice.vala b/gtk/src/service/interface/dbusservice.vala index b7b89ac..df645e3 100644 --- a/gtk/src/service/interface/dbusservice.vala +++ b/gtk/src/service/interface/dbusservice.vala @@ -50,8 +50,11 @@ namespace DBusService { [DBus (name = "GetMessages")] public abstract GLib.HashTable[] get_messages(string conversation_id, string last_message_id) throws DBusError, IOError; - [DBus (name = "SendMessage")] - public abstract string send_message(string conversation_id, string text, string[] attachment_guids) throws DBusError, IOError; + [DBus (name = "Reply")] + public abstract string reply(string conversation_id, string text, string[] attachment_guids) throws DBusError, IOError; + + [DBus (name = "NewConversation")] + public abstract string new_conversation(string[] handle_ids, string text, string[] attachment_guids) throws DBusError, IOError; [DBus (name = "MessagesUpdated")] public signal void messages_updated(string conversation_id); diff --git a/gtk/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml b/gtk/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml index f0dcf0b..e8b868c 100644 --- a/gtk/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml +++ b/gtk/src/service/interface/xml/net.buzzert.kordophonecd.Server.xml @@ -83,7 +83,7 @@ - + @@ -91,9 +91,28 @@ + + + + + + + + + + ) async throws { + public func reply(conversationId: String, message: String, transferGuids: Set) async throws { var args: [String: xpc_object_t] = [:] args["conversation_id"] = xpcString(conversationId) args["text"] = xpcString(message) @@ -142,7 +142,20 @@ final class XPCClient args["attachment_guids"] = xpcStringArray(transferGuids) } - let req = makeRequest(method: "SendMessage", arguments: args) + let req = makeRequest(method: "Reply", arguments: args) + guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError } + } + + public func newConversation(handleIds: Set, message: String, transferGuids: Set) async throws { + var args: [String: xpc_object_t] = [:] + args["handle_ids"] = xpcStringArray(handleIds) + args["text"] = xpcString(message) + + if !transferGuids.isEmpty { + args["attachment_guids"] = xpcStringArray(transferGuids) + } + + let req = makeRequest(method: "NewConversation", arguments: args) guard let reply = try await sendSync(req), xpc_get_type(reply) == XPC_TYPE_DICTIONARY else { throw Error.typeError } } @@ -411,4 +424,3 @@ extension xpc_object_t ) } } -