From c2a697f2c175406a9952bfc12dc8501edef25f82 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 1 Apr 2026 15:29:37 -0700 Subject: [PATCH] new message: initial commit --- core/kordophone/src/api/http_client.rs | 69 +++++-- core/kordophone/src/api/mod.rs | 13 +- core/kordophone/src/model/handle.rs | 28 +++ core/kordophone/src/model/mod.rs | 9 + core/kordophone/src/model/outgoing_message.rs | 46 ++++- .../src/model/send_message_response.rs | 12 ++ core/kordophone/src/tests/mod.rs | 26 ++- core/kordophone/src/tests/test_client.rs | 72 +++++-- core/kordophoned-client/build.rs | 1 - core/kordophoned-client/src/lib.rs | 1 - core/kordophoned-client/src/platform/linux.rs | 1 - core/kordophoned-client/src/platform/macos.rs | 24 ++- core/kordophoned-client/src/platform/mod.rs | 1 - core/kordophoned-client/src/worker.rs | 81 +++++--- core/kordophoned/build.rs | 1 - core/kordophoned/src/daemon/mod.rs | 5 +- core/kordophoned/src/daemon/post_office.rs | 28 ++- core/kordophoned/src/dbus/agent.rs | 74 +++---- core/kordophoned/src/xpc/mod.rs | 10 +- core/kordophoned/src/xpc/rpc.rs | 141 ++++++++++--- core/kpcli/src/client/mod.rs | 20 +- core/utilities/src/bin/snoozer.rs | 82 ++++---- .../Operations/MBIMSendMessageOperation.m | 191 +++++++++++++----- 23 files changed, 674 insertions(+), 262 deletions(-) create mode 100644 core/kordophone/src/model/handle.rs create mode 100644 core/kordophone/src/model/send_message_response.rs diff --git a/core/kordophone/src/api/http_client.rs b/core/kordophone/src/api/http_client.rs index 91482ab..558943d 100644 --- a/core/kordophone/src/api/http_client.rs +++ b/core/kordophone/src/api/http_client.rs @@ -24,7 +24,7 @@ use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use crate::{ model::{ Conversation, ConversationID, Event, JwtToken, Message, MessageID, OutgoingMessage, - UpdateItem, + OutgoingMessageTarget, ResolveHandleResponse, SendMessageResponse, UpdateItem, }, APIInterface, }; @@ -312,16 +312,26 @@ impl APIInterface for HTTPAPIClient { async fn send_message( &mut self, outgoing_message: &OutgoingMessage, - ) -> Result { - let message: Message = self + ) -> Result { + let message: SendMessageResponse = self .deserialized_response_with_body("sendMessage", Method::POST, || { - serde_json::to_string(&outgoing_message).unwrap().into() + Self::send_message_request_body(outgoing_message) }) .await?; Ok(message) } + async fn resolve_handle( + &mut self, + handle_id: &str, + ) -> Result { + let endpoint = format!("resolveHandle?id={}", urlencoding::encode(handle_id)); + let response: ResolveHandleResponse = + self.deserialized_response(&endpoint, Method::GET).await?; + Ok(response) + } + async fn fetch_attachment_data( &mut self, guid: &str, @@ -394,8 +404,7 @@ impl APIInterface for HTTPAPIClient { None => "updates".to_string(), }; - let uri = self - .uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?; + let uri = self.uri_for_endpoint(&endpoint, Some(self.websocket_scheme()))?; loop { log::debug!("Connecting to websocket: {:?}", uri); @@ -426,18 +435,20 @@ impl APIInterface for HTTPAPIClient { log::debug!("Websocket request: {:?}", request); - let mut should_retry = true; // retry once after authenticating. + let should_retry = true; // retry once after authenticating. match connect_async(request).await.map_err(Error::from) { Ok((socket, response)) => { log::debug!("Websocket connected: {:?}", response.status()); - break Ok(WebsocketEventSocket::new(socket)) + break Ok(WebsocketEventSocket::new(socket)); } Err(e) => match &e { Error::ClientError(ce) => match ce.as_str() { "HTTP error: 401 Unauthorized" | "Unauthorized" => { // Try to authenticate if let Some(credentials) = &self.auth_store.get_credentials().await { - log::warn!("Websocket connection failed, attempting to authenticate"); + log::warn!( + "Websocket connection failed, attempting to authenticate" + ); let new_token = self.authenticate(credentials.clone()).await?; self.auth_store.set_token(new_token.to_string()).await; @@ -473,16 +484,44 @@ impl HTTPAPIClient { .build(); let client = Client::builder().build::<_, Body>(https); - HTTPAPIClient { base_url, auth_store, client } + HTTPAPIClient { + base_url, + auth_store, + client, + } + } + + fn send_message_request_body(outgoing_message: &OutgoingMessage) -> Body { + #[derive(Serialize)] + struct SendMessageRequest<'a> { + #[serde(rename = "body")] + text: &'a str, + #[serde(rename = "guid", skip_serializing_if = "Option::is_none")] + conversation_id: Option<&'a ConversationID>, + #[serde(rename = "handleIDs", skip_serializing_if = "Option::is_none")] + handle_ids: Option<&'a [String]>, + #[serde(rename = "fileTransferGUIDs")] + file_transfer_guids: &'a Vec, + } + + let (conversation_id, handle_ids) = match &outgoing_message.target { + OutgoingMessageTarget::Conversation(conversation_id) => (Some(conversation_id), None), + OutgoingMessageTarget::Handles(handle_ids) => (None, Some(handle_ids.as_slice())), + }; + + serde_json::to_string(&SendMessageRequest { + text: &outgoing_message.text, + conversation_id, + handle_ids, + file_transfer_guids: &outgoing_message.file_transfer_guids, + }) + .unwrap() + .into() } fn uri_for_endpoint(&self, endpoint: &str, scheme: Option<&str>) -> Result { let mut parts = self.base_url.clone().into_parts(); - let root_path: PathBuf = parts - .path_and_query - .ok_or(Error::URLError)? - .path() - .into(); + let root_path: PathBuf = parts.path_and_query.ok_or(Error::URLError)?.path().into(); let path = root_path.join(endpoint); let path_str = path.to_str().ok_or(Error::URLError)?; diff --git a/core/kordophone/src/api/mod.rs b/core/kordophone/src/api/mod.rs index c5a9cd4..b1eaba8 100644 --- a/core/kordophone/src/api/mod.rs +++ b/core/kordophone/src/api/mod.rs @@ -1,4 +1,7 @@ -pub use crate::model::{Conversation, ConversationID, Message, MessageID, OutgoingMessage}; +pub use crate::model::{ + Conversation, ConversationID, Message, MessageID, OutgoingMessage, ResolveHandleResponse, + SendMessageResponse, +}; use async_trait::async_trait; use bytes::Bytes; @@ -42,7 +45,13 @@ pub trait APIInterface { async fn send_message( &mut self, outgoing_message: &OutgoingMessage, - ) -> Result; + ) -> Result; + + // (GET) /resolveHandle + async fn resolve_handle( + &mut self, + handle_id: &str, + ) -> Result; // (GET) /attachment async fn fetch_attachment_data( diff --git a/core/kordophone/src/model/handle.rs b/core/kordophone/src/model/handle.rs new file mode 100644 index 0000000..1ac5e07 --- /dev/null +++ b/core/kordophone/src/model/handle.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; + +use super::conversation::ConversationID; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ResolvedHandle { + pub id: String, + pub name: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum HandleResolutionStatus { + Valid, + Invalid, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ResolveHandleResponse { + #[serde(rename = "resolvedHandle")] + pub resolved_handle: ResolvedHandle, + + pub status: HandleResolutionStatus, + + #[serde(rename = "existingChat")] + pub existing_chat: Option, +} diff --git a/core/kordophone/src/model/mod.rs b/core/kordophone/src/model/mod.rs index 70538c7..13d96ea 100644 --- a/core/kordophone/src/model/mod.rs +++ b/core/kordophone/src/model/mod.rs @@ -1,7 +1,9 @@ pub mod conversation; pub mod event; +pub mod handle; pub mod message; pub mod outgoing_message; +pub mod send_message_response; pub mod update; pub use conversation::Conversation; @@ -10,8 +12,15 @@ pub use conversation::ConversationID; pub use message::Message; pub use message::MessageID; +pub use handle::HandleResolutionStatus; +pub use handle::ResolveHandleResponse; +pub use handle::ResolvedHandle; + pub use outgoing_message::OutgoingMessage; pub use outgoing_message::OutgoingMessageBuilder; +pub use outgoing_message::OutgoingMessageTarget; + +pub use send_message_response::SendMessageResponse; pub use update::UpdateItem; diff --git a/core/kordophone/src/model/outgoing_message.rs b/core/kordophone/src/model/outgoing_message.rs index 93da21f..854b1d8 100644 --- a/core/kordophone/src/model/outgoing_message.rs +++ b/core/kordophone/src/model/outgoing_message.rs @@ -1,23 +1,23 @@ use super::conversation::ConversationID; use chrono::NaiveDateTime; -use serde::Serialize; use uuid::Uuid; -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OutgoingMessageTarget { + Conversation(ConversationID), + Handles(Vec), +} + +#[derive(Debug, Clone)] pub struct OutgoingMessage { - #[serde(skip)] pub guid: Uuid, - #[serde(skip)] pub date: NaiveDateTime, - #[serde(rename = "body")] pub text: String, - #[serde(rename = "guid")] - pub conversation_id: ConversationID, + pub target: OutgoingMessageTarget, - #[serde(rename = "fileTransferGUIDs")] pub file_transfer_guids: Vec, } @@ -25,13 +25,27 @@ impl OutgoingMessage { pub fn builder() -> OutgoingMessageBuilder { OutgoingMessageBuilder::new() } + + pub fn conversation_id(&self) -> Option<&ConversationID> { + match &self.target { + OutgoingMessageTarget::Conversation(conversation_id) => Some(conversation_id), + OutgoingMessageTarget::Handles(_) => None, + } + } + + pub fn handle_ids(&self) -> Option<&[String]> { + match &self.target { + OutgoingMessageTarget::Conversation(_) => None, + OutgoingMessageTarget::Handles(handle_ids) => Some(handle_ids.as_slice()), + } + } } #[derive(Default)] pub struct OutgoingMessageBuilder { guid: Option, text: Option, - conversation_id: Option, + target: Option, file_transfer_guids: Option>, } @@ -50,8 +64,18 @@ impl OutgoingMessageBuilder { self } + pub fn target(mut self, target: OutgoingMessageTarget) -> Self { + self.target = Some(target); + self + } + pub fn conversation_id(mut self, conversation_id: ConversationID) -> Self { - self.conversation_id = Some(conversation_id); + self.target = Some(OutgoingMessageTarget::Conversation(conversation_id)); + self + } + + pub fn handle_ids(mut self, handle_ids: Vec) -> Self { + self.target = Some(OutgoingMessageTarget::Handles(handle_ids)); self } @@ -64,7 +88,7 @@ impl OutgoingMessageBuilder { OutgoingMessage { guid: self.guid.unwrap_or_else(Uuid::new_v4), text: self.text.unwrap(), - conversation_id: self.conversation_id.unwrap(), + target: self.target.unwrap(), file_transfer_guids: self.file_transfer_guids.unwrap_or_default(), date: chrono::Utc::now().naive_utc(), } diff --git a/core/kordophone/src/model/send_message_response.rs b/core/kordophone/src/model/send_message_response.rs new file mode 100644 index 0000000..be90cef --- /dev/null +++ b/core/kordophone/src/model/send_message_response.rs @@ -0,0 +1,12 @@ +use serde::Deserialize; + +use super::{conversation::ConversationID, message::Message}; + +#[derive(Debug, Clone, Deserialize)] +pub struct SendMessageResponse { + #[serde(flatten)] + pub message: Message, + + #[serde(rename = "conversationGUID")] + pub conversation_id: Option, +} diff --git a/core/kordophone/src/tests/mod.rs b/core/kordophone/src/tests/mod.rs index ee8b46e..1de7245 100644 --- a/core/kordophone/src/tests/mod.rs +++ b/core/kordophone/src/tests/mod.rs @@ -3,7 +3,7 @@ use self::test_client::TestClient; use crate::APIInterface; pub mod api_interface { - use crate::model::Conversation; + use crate::model::{Conversation, HandleResolutionStatus, OutgoingMessage}; use super::*; @@ -28,4 +28,28 @@ pub mod api_interface { assert_eq!(conversations.len(), 1); assert_eq!(conversations[0].display_name, test_convo.display_name); } + + #[tokio::test] + async fn test_resolve_handle() { + let mut client = TestClient::new(); + + let resolved = client.resolve_handle("user@example.com").await.unwrap(); + assert_eq!(resolved.resolved_handle.id, "user@example.com"); + assert_eq!(resolved.status, HandleResolutionStatus::Valid); + assert_eq!(resolved.existing_chat, None); + } + + #[tokio::test] + async fn test_send_message_with_handles() { + let mut client = TestClient::new(); + + let outgoing_message = OutgoingMessage::builder() + .text("hello".to_string()) + .handle_ids(vec!["user@example.com".to_string()]) + .build(); + + let sent = client.send_message(&outgoing_message).await.unwrap(); + assert_eq!(sent.message.text, "hello"); + assert_eq!(sent.conversation_id, None); + } } diff --git a/core/kordophone/src/tests/test_client.rs b/core/kordophone/src/tests/test_client.rs index 3801765..fc0fb86 100644 --- a/core/kordophone/src/tests/test_client.rs +++ b/core/kordophone/src/tests/test_client.rs @@ -9,14 +9,17 @@ use crate::{ api::event_socket::{EventSocket, SinkMessage, SocketEvent, SocketUpdate}, api::http_client::Credentials, model::{ - Conversation, ConversationID, Event, JwtToken, Message, MessageID, OutgoingMessage, - UpdateItem, + Conversation, ConversationID, Event, HandleResolutionStatus, JwtToken, Message, MessageID, + OutgoingMessage, OutgoingMessageTarget, ResolveHandleResponse, ResolvedHandle, + SendMessageResponse, }, }; use bytes::Bytes; +use futures_util::sink::drain; use futures_util::stream::BoxStream; use futures_util::Sink; +use futures_util::SinkExt; use futures_util::StreamExt; pub struct TestClient { @@ -63,13 +66,18 @@ impl EventSocket for TestEventSocket { impl Sink, ) { ( - futures_util::stream::iter(self.events.into_iter().map(Ok)).boxed(), - futures_util::sink::sink(), + futures_util::stream::iter( + self.events + .into_iter() + .map(|event| Ok(SocketEvent::Update(event))), + ) + .boxed(), + drain().sink_map_err(|err| match err {}), ) } async fn raw_updates(self) -> Self::UpdateStream { - let results: Vec, TestError>> = vec![]; + let results: Vec> = vec![]; futures_util::stream::iter(results.into_iter()).boxed() } } @@ -94,9 +102,9 @@ impl APIInterface for TestClient { async fn get_messages( &mut self, conversation_id: &ConversationID, - limit: Option, - before: Option, - after: Option, + _limit: Option, + _before: Option, + _after: Option, ) -> Result, Self::Error> { if let Some(messages) = self.messages.get(conversation_id) { return Ok(messages.clone()); @@ -108,18 +116,42 @@ impl APIInterface for TestClient { async fn send_message( &mut self, outgoing_message: &OutgoingMessage, - ) -> Result { + ) -> Result { let message = Message::builder() .guid(Uuid::new_v4().to_string()) .text(outgoing_message.text.clone()) .date(OffsetDateTime::now_utc()) .build(); - self.messages - .entry(outgoing_message.conversation_id.clone()) - .or_insert(vec![]) - .push(message.clone()); - Ok(message) + let conversation_id = match &outgoing_message.target { + OutgoingMessageTarget::Conversation(conversation_id) => { + self.messages + .entry(conversation_id.clone()) + .or_insert(vec![]) + .push(message.clone()); + None + } + OutgoingMessageTarget::Handles(_) => None, + }; + + Ok(SendMessageResponse { + message, + conversation_id, + }) + } + + async fn resolve_handle( + &mut self, + handle_id: &str, + ) -> Result { + Ok(ResolveHandleResponse { + resolved_handle: ResolvedHandle { + id: handle_id.to_string(), + name: None, + }, + status: HandleResolutionStatus::Valid, + existing_chat: None, + }) } async fn open_event_socket( @@ -131,17 +163,17 @@ impl APIInterface for TestClient { async fn fetch_attachment_data( &mut self, - guid: &str, - preview: bool, + _guid: &str, + _preview: bool, ) -> Result { Ok(futures_util::stream::iter(vec![Ok(Bytes::from_static(b"test"))]).boxed()) } async fn upload_attachment( &mut self, - data: tokio::io::BufReader, - filename: &str, - size: u64, + _data: tokio::io::BufReader, + _filename: &str, + _size: u64, ) -> Result where R: tokio::io::AsyncRead + Unpin + Send + Sync + 'static, @@ -151,7 +183,7 @@ impl APIInterface for TestClient { async fn mark_conversation_as_read( &mut self, - conversation_id: &ConversationID, + _conversation_id: &ConversationID, ) -> Result<(), Self::Error> { Ok(()) } diff --git a/core/kordophoned-client/build.rs b/core/kordophoned-client/build.rs index 126eab7..670f409 100644 --- a/core/kordophoned-client/build.rs +++ b/core/kordophoned-client/build.rs @@ -23,4 +23,3 @@ fn main() { println!("cargo:rerun-if-changed={}", KORDOPHONE_XML); } - diff --git a/core/kordophoned-client/src/lib.rs b/core/kordophoned-client/src/lib.rs index 6e97842..ad6ba72 100644 --- a/core/kordophoned-client/src/lib.rs +++ b/core/kordophoned-client/src/lib.rs @@ -2,4 +2,3 @@ mod platform; mod worker; pub use worker::{spawn_worker, ChatMessage, ConversationSummary, Event, Request}; - diff --git a/core/kordophoned-client/src/platform/linux.rs b/core/kordophoned-client/src/platform/linux.rs index b45f6c9..b8b1fa0 100644 --- a/core/kordophoned-client/src/platform/linux.rs +++ b/core/kordophoned-client/src/platform/linux.rs @@ -186,4 +186,3 @@ impl DaemonClient for DBusClient { Ok(()) } } - diff --git a/core/kordophoned-client/src/platform/macos.rs b/core/kordophoned-client/src/platform/macos.rs index 94c1de8..58ffec9 100644 --- a/core/kordophoned-client/src/platform/macos.rs +++ b/core/kordophoned-client/src/platform/macos.rs @@ -95,8 +95,14 @@ impl XpcClient { impl DaemonClient for XpcClient { fn get_conversations(&mut self, limit: i32, offset: i32) -> Result> { let mut args = HashMap::new(); - args.insert(Self::key("limit"), Message::String(Self::key(&limit.to_string()))); - args.insert(Self::key("offset"), Message::String(Self::key(&offset.to_string()))); + args.insert( + Self::key("limit"), + Message::String(Self::key(&limit.to_string())), + ); + args.insert( + Self::key("offset"), + Message::String(Self::key(&offset.to_string())), + ); let reply = self .transport @@ -112,7 +118,9 @@ impl DaemonClient for XpcClient { let mut conversations = Vec::new(); for item in items { - let Message::Dictionary(conv) = item else { continue }; + let Message::Dictionary(conv) = item else { + continue; + }; let id = Self::get_string(conv, "guid").unwrap_or_default(); let display_name = Self::get_string(conv, "display_name").unwrap_or_default(); let preview = Self::get_string(conv, "last_message_preview").unwrap_or_default(); @@ -162,7 +170,10 @@ impl DaemonClient for XpcClient { Message::String(Self::key(&conversation_id)), ); if let Some(last) = last_message_id { - args.insert(Self::key("last_message_id"), Message::String(Self::key(&last))); + args.insert( + Self::key("last_message_id"), + Message::String(Self::key(&last)), + ); } let reply = self @@ -178,7 +189,9 @@ impl DaemonClient for XpcClient { let mut messages = Vec::new(); for item in items { - let Message::Dictionary(msg) = item else { continue }; + let Message::Dictionary(msg) = item else { + continue; + }; messages.push(ChatMessage { sender: Self::get_string(msg, "sender").unwrap_or_default(), text: Self::get_string(msg, "text").unwrap_or_default(), @@ -230,4 +243,3 @@ impl DaemonClient for XpcClient { Ok(()) } } - diff --git a/core/kordophoned-client/src/platform/mod.rs b/core/kordophoned-client/src/platform/mod.rs index 68e7ab7..0a6747e 100644 --- a/core/kordophoned-client/src/platform/mod.rs +++ b/core/kordophoned-client/src/platform/mod.rs @@ -21,4 +21,3 @@ pub(crate) fn new_daemon_client() -> Result> { anyhow::bail!("Unsupported platform") } } - diff --git a/core/kordophoned-client/src/worker.rs b/core/kordophoned-client/src/worker.rs index 2f31a2f..6e9cb0d 100644 --- a/core/kordophoned-client/src/worker.rs +++ b/core/kordophoned-client/src/worker.rs @@ -21,10 +21,19 @@ pub struct ChatMessage { pub enum Request { RefreshConversations, - RefreshMessages { conversation_id: String }, - SendMessage { conversation_id: String, text: String }, - MarkRead { conversation_id: String }, - SyncConversation { conversation_id: String }, + RefreshMessages { + conversation_id: String, + }, + SendMessage { + conversation_id: String, + text: String, + }, + MarkRead { + conversation_id: String, + }, + SyncConversation { + conversation_id: String, + }, } pub enum Event { @@ -38,9 +47,13 @@ pub enum Event { outgoing_id: Option, }, MarkedRead, - ConversationSyncTriggered { conversation_id: String }, + ConversationSyncTriggered { + conversation_id: String, + }, ConversationsUpdated, - MessagesUpdated { conversation_id: String }, + MessagesUpdated { + conversation_id: String, + }, UpdateStreamReconnected, Error(String), } @@ -59,38 +72,41 @@ pub fn spawn_worker( }; if let Err(e) = client.install_signal_handlers(event_tx.clone()) { - let _ = event_tx.send(Event::Error(format!("Failed to install daemon signals: {e}"))); + let _ = event_tx.send(Event::Error(format!( + "Failed to install daemon signals: {e}" + ))); } 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 { + 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 { conversation_id, - messages, - }), - Request::SendMessage { - 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 }), - }; + 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 }), + }; match res { Ok(evt) => { @@ -130,4 +146,3 @@ pub(crate) trait DaemonClient { Ok(()) } } - diff --git a/core/kordophoned/build.rs b/core/kordophoned/build.rs index f7e9e8a..4db9d7e 100644 --- a/core/kordophoned/build.rs +++ b/core/kordophoned/build.rs @@ -27,4 +27,3 @@ fn main() { println!("cargo:rerun-if-changed={}", KORDOPHONE_XML); } - diff --git a/core/kordophoned/src/daemon/mod.rs b/core/kordophoned/src/daemon/mod.rs index 54fae37..d701e2a 100644 --- a/core/kordophoned/src/daemon/mod.rs +++ b/core/kordophoned/src/daemon/mod.rs @@ -477,9 +477,8 @@ impl Daemon { .await; // Convert DB messages to daemon model, substituting local_id when an alias exists. - let mut result: Vec = Vec::with_capacity( - db_messages.len() + outgoing_messages.len(), - ); + let mut result: Vec = + Vec::with_capacity(db_messages.len() + outgoing_messages.len()); for m in db_messages.into_iter() { let server_id = m.id.clone(); let mut dm: Message = m.into(); diff --git a/core/kordophoned/src/daemon/post_office.rs b/core/kordophoned/src/daemon/post_office.rs index 60c52ae..3474a59 100644 --- a/core/kordophoned/src/daemon/post_office.rs +++ b/core/kordophoned/src/daemon/post_office.rs @@ -8,6 +8,7 @@ use tokio_condvar::Condvar; use crate::daemon::events::Event as DaemonEvent; use kordophone::api::APIInterface; use kordophone::model::outgoing_message::OutgoingMessage; +use kordophone::model::OutgoingMessageTarget; use anyhow::Result; @@ -102,10 +103,29 @@ impl Result> PostOffice { Ok(sent_message) => { log::info!(target: target::POST_OFFICE, "Message sent successfully: {}", message.guid); - let conversation_id = message.conversation_id.clone(); - let event = - DaemonEvent::MessageSent(sent_message.into(), message, conversation_id); - event_sink.send(event).await.unwrap(); + let conversation_id = sent_message.conversation_id.clone().or_else(|| { + match &message.target { + OutgoingMessageTarget::Conversation(conversation_id) => { + Some(conversation_id.clone()) + } + OutgoingMessageTarget::Handles(_) => None, + } + }); + + if let Some(conversation_id) = conversation_id { + let event = DaemonEvent::MessageSent( + sent_message.message.into(), + message, + conversation_id, + ); + event_sink.send(event).await.unwrap(); + } else { + log::error!( + target: target::POST_OFFICE, + "Message sent but no conversation id was available for {}", + message.guid + ); + } } Err(e) => { diff --git a/core/kordophoned/src/dbus/agent.rs b/core/kordophoned/src/dbus/agent.rs index 3a9b8fa..7e94fff 100644 --- a/core/kordophoned/src/dbus/agent.rs +++ b/core/kordophoned/src/dbus/agent.rs @@ -318,49 +318,49 @@ impl DbusRepository for DBusAgent { .attachments .into_iter() .map(|attachment| { - attachment_count += 1; - let mut attachment_map = arg::PropMap::new(); - attachment_map.insert( - "guid".into(), - arg::Variant(Box::new(attachment.guid.clone())), - ); - attachment_map.insert( - "downloaded".into(), - arg::Variant(Box::new(attachment.is_downloaded(false))), - ); - attachment_map.insert( - "preview_downloaded".into(), - arg::Variant(Box::new(attachment.is_downloaded(true))), - ); + attachment_count += 1; + let mut attachment_map = arg::PropMap::new(); + attachment_map.insert( + "guid".into(), + arg::Variant(Box::new(attachment.guid.clone())), + ); + attachment_map.insert( + "downloaded".into(), + arg::Variant(Box::new(attachment.is_downloaded(false))), + ); + attachment_map.insert( + "preview_downloaded".into(), + arg::Variant(Box::new(attachment.is_downloaded(true))), + ); - if let Some(ref metadata) = attachment.metadata { - let mut metadata_map = arg::PropMap::new(); + if let Some(ref metadata) = attachment.metadata { + let mut metadata_map = arg::PropMap::new(); - if let Some(ref attribution_info) = metadata.attribution_info { - let mut attribution_map = arg::PropMap::new(); - if let Some(width) = attribution_info.width { - attribution_map.insert( - "width".into(), - arg::Variant(Box::new(width as i32)), + if let Some(ref attribution_info) = metadata.attribution_info { + let mut attribution_map = arg::PropMap::new(); + if let Some(width) = attribution_info.width { + attribution_map.insert( + "width".into(), + arg::Variant(Box::new(width as i32)), + ); + } + if let Some(height) = attribution_info.height { + attribution_map.insert( + "height".into(), + arg::Variant(Box::new(height as i32)), + ); + } + metadata_map.insert( + "attribution_info".into(), + arg::Variant(Box::new(attribution_map)), ); } - if let Some(height) = attribution_info.height { - attribution_map.insert( - "height".into(), - arg::Variant(Box::new(height as i32)), - ); - } - metadata_map.insert( - "attribution_info".into(), - arg::Variant(Box::new(attribution_map)), + + attachment_map.insert( + "metadata".into(), + arg::Variant(Box::new(metadata_map)), ); } - - attachment_map.insert( - "metadata".into(), - arg::Variant(Box::new(metadata_map)), - ); - } attachment_map }) .collect(); diff --git a/core/kordophoned/src/xpc/mod.rs b/core/kordophoned/src/xpc/mod.rs index d2bf926..cd9cef5 100644 --- a/core/kordophoned/src/xpc/mod.rs +++ b/core/kordophoned/src/xpc/mod.rs @@ -15,10 +15,16 @@ pub struct DispatchResult { impl DispatchResult { pub fn new(message: Message) -> Self { - Self { message, cleanup: None } + Self { + message, + cleanup: None, + } } pub fn with_cleanup(message: Message, cleanup: T) -> Self { - Self { message, cleanup: Some(Box::new(cleanup)) } + Self { + message, + cleanup: Some(Box::new(cleanup)), + } } } diff --git a/core/kordophoned/src/xpc/rpc.rs b/core/kordophoned/src/xpc/rpc.rs index 0d8b5c3..c057468 100644 --- a/core/kordophoned/src/xpc/rpc.rs +++ b/core/kordophoned/src/xpc/rpc.rs @@ -105,7 +105,12 @@ pub async fn dispatch( .and_then(|m| dict_get_str(m, "conversation_id")) { Some(id) => id, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; match agent .send_event(|r| Event::SyncConversation(conversation_id, r)) @@ -122,7 +127,12 @@ pub async fn dispatch( .and_then(|m| dict_get_str(m, "conversation_id")) { Some(id) => id, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; match agent .send_event(|r| Event::MarkConversationAsRead(conversation_id, r)) @@ -137,11 +147,21 @@ pub async fn dispatch( "GetMessages" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let conversation_id = match dict_get_str(args, "conversation_id") { Some(id) => id, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing conversation_id", + )) + } }; let last_message_id = dict_get_str(args, "last_message_id"); match agent @@ -158,13 +178,10 @@ pub async fn dispatch( dict_put_str(&mut m, "sender", &msg.sender.display_name()); // Include attachment GUIDs for the client to resolve/download - let attachment_guids: Vec = msg - .attachments - .iter() - .map(|a| a.guid.clone()) - .collect(); + let attachment_guids: Vec = + msg.attachments.iter().map(|a| a.guid.clone()).collect(); m.insert(cstr("attachment_guids"), array_from_strs(attachment_guids)); - + // Full attachments array with metadata (mirrors DBus fields) let mut attachments_items: Vec = Vec::new(); for attachment in msg.attachments.iter() { @@ -193,12 +210,23 @@ pub async fn dispatch( if let Some(attribution_info) = &metadata.attribution_info { let mut attribution_map: XpcMap = HashMap::new(); if let Some(width) = attribution_info.width { - dict_put_i64_as_str(&mut attribution_map, "width", width as i64); + dict_put_i64_as_str( + &mut attribution_map, + "width", + width as i64, + ); } if let Some(height) = attribution_info.height { - dict_put_i64_as_str(&mut attribution_map, "height", height as i64); + dict_put_i64_as_str( + &mut attribution_map, + "height", + height as i64, + ); } - metadata_map.insert(cstr("attribution_info"), Message::Dictionary(attribution_map)); + metadata_map.insert( + cstr("attribution_info"), + Message::Dictionary(attribution_map), + ); } if !metadata_map.is_empty() { a.insert(cstr("metadata"), Message::Dictionary(metadata_map)); @@ -208,7 +236,7 @@ pub async fn dispatch( attachments_items.push(Message::Dictionary(a)); } m.insert(cstr("attachments"), Message::Array(attachments_items)); - + items.push(Message::Dictionary(m)); } let mut reply: XpcMap = HashMap::new(); @@ -230,11 +258,21 @@ pub async fn dispatch( "SendMessage" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let conversation_id = match dict_get_str(args, "conversation_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing conversation_id")), + None => { + return DispatchResult::new(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")) { @@ -265,11 +303,21 @@ pub async fn dispatch( "GetAttachmentInfo" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing attachment_id", + )) + } }; match agent .send_event(|r| Event::GetAttachment(attachment_id, r)) @@ -308,11 +356,21 @@ pub async fn dispatch( "OpenAttachmentFd" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing attachment_id", + )) + } }; let preview = dict_get_str(args, "preview") .map(|s| s == "true") @@ -324,7 +382,7 @@ pub async fn dispatch( { Ok(attachment) => { use std::os::fd::AsRawFd; - + let path = attachment.get_path_for_preview(preview); match std::fs::OpenOptions::new().read(true).open(&path) { Ok(file) => { @@ -335,9 +393,14 @@ pub async fn dispatch( dict_put_str(&mut reply, "type", "OpenAttachmentFdResponse"); reply.insert(cstr("fd"), Message::Fd(fd)); - DispatchResult { message: Message::Dictionary(reply), cleanup: Some(Box::new(file)) } + DispatchResult { + message: Message::Dictionary(reply), + cleanup: Some(Box::new(file)), + } + } + Err(e) => { + DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e))) } - Err(e) => DispatchResult::new(make_error_reply("OpenFailed", &format!("{}", e))), } } Err(e) => DispatchResult::new(make_error_reply("DaemonError", &format!("{}", e))), @@ -348,11 +411,21 @@ pub async fn dispatch( "DownloadAttachment" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let attachment_id = match dict_get_str(args, "attachment_id") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing attachment_id")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing attachment_id", + )) + } }; let preview = dict_get_str(args, "preview") .map(|s| s == "true") @@ -371,11 +444,18 @@ pub async fn dispatch( use std::path::PathBuf; let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let path = match dict_get_str(args, "path") { Some(v) => v, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")), + None => { + return DispatchResult::new(make_error_reply("InvalidRequest", "Missing path")) + } }; match agent .send_event(|r| Event::UploadAttachment(PathBuf::from(path), r)) @@ -413,7 +493,12 @@ pub async fn dispatch( "UpdateSettings" => { let args = match get_dictionary_field(root, "arguments") { Some(a) => a, - None => return DispatchResult::new(make_error_reply("InvalidRequest", "Missing arguments")), + None => { + return DispatchResult::new(make_error_reply( + "InvalidRequest", + "Missing arguments", + )) + } }; let server_url = dict_get_str(args, "server_url"); let username = dict_get_str(args, "username"); diff --git a/core/kpcli/src/client/mod.rs b/core/kpcli/src/client/mod.rs index bce27c5..ae1b0ba 100644 --- a/core/kpcli/src/client/mod.rs +++ b/core/kpcli/src/client/mod.rs @@ -146,17 +146,15 @@ impl ClientCli { loop { match stream.next().await.unwrap() { - Ok(update) => { - match update { - SocketUpdate::Update(updates) => { - for update in updates { - println!("Got update: {:?}", update); - } - } - SocketUpdate::Pong => { - println!("Pong"); + Ok(update) => match update { + SocketUpdate::Update(updates) => { + for update in updates { + println!("Got update: {:?}", update); } } + SocketUpdate::Pong => { + println!("Pong"); + } }, Err(e) => { @@ -175,8 +173,8 @@ impl ClientCli { .text(message) .build(); - let message = self.api.send_message(&outgoing_message).await?; - println!("Message sent: {}", message.guid); + let response = self.api.send_message(&outgoing_message).await?; + println!("Message sent: {}", response.message.guid); Ok(()) } diff --git a/core/utilities/src/bin/snoozer.rs b/core/utilities/src/bin/snoozer.rs index 28f6c6c..825ecc6 100644 --- a/core/utilities/src/bin/snoozer.rs +++ b/core/utilities/src/bin/snoozer.rs @@ -1,13 +1,13 @@ use std::env; use std::process; -use kordophone::{ - api::{HTTPAPIClient, InMemoryAuthenticationStore, EventSocket}, - model::{ConversationID, event::EventData}, - APIInterface, -}; -use kordophone::api::http_client::Credentials; use kordophone::api::AuthenticationStore; +use kordophone::api::http_client::Credentials; +use kordophone::{ + APIInterface, + api::{EventSocket, HTTPAPIClient, InMemoryAuthenticationStore}, + model::{ConversationID, event::EventData}, +}; use futures_util::StreamExt; use hyper::Uri; @@ -18,7 +18,10 @@ async fn main() -> Result<(), Box> { let args: Vec = env::args().collect(); if args.len() < 2 { - eprintln!("Usage: {} [conversation_id2] [conversation_id3] ...", args[0]); + eprintln!( + "Usage: {} [conversation_id2] [conversation_id3] ...", + args[0] + ); eprintln!("Environment variables required:"); eprintln!(" KORDOPHONE_API_URL - Server URL"); eprintln!(" KORDOPHONE_USERNAME - Username for authentication"); @@ -30,65 +33,74 @@ async fn main() -> Result<(), Box> { let server_url: Uri = env::var("KORDOPHONE_API_URL") .map_err(|_| "KORDOPHONE_API_URL environment variable not set")? .parse()?; - + let username = env::var("KORDOPHONE_USERNAME") .map_err(|_| "KORDOPHONE_USERNAME environment variable not set")?; - + let password = env::var("KORDOPHONE_PASSWORD") .map_err(|_| "KORDOPHONE_PASSWORD environment variable not set")?; - + let credentials = Credentials { username, password }; - + // Collect all conversation IDs from command line arguments - let target_conversation_ids: Vec = args[1..].iter() - .map(|id| id.clone()) - .collect(); - - println!("Monitoring {} conversation(s) for updates: {:?}", - target_conversation_ids.len(), target_conversation_ids); - + let target_conversation_ids: Vec = + args[1..].iter().map(|id| id.clone()).collect(); + + println!( + "Monitoring {} conversation(s) for updates: {:?}", + target_conversation_ids.len(), + target_conversation_ids + ); + let auth_store = InMemoryAuthenticationStore::new(Some(credentials.clone())); let mut client = HTTPAPIClient::new(server_url, auth_store); let _ = client.authenticate(credentials).await?; - + // Open event socket let event_socket = client.open_event_socket(None).await?; let (mut stream, _sink) = event_socket.events().await; - + println!("Connected to event stream, waiting for updates..."); - + // Process events while let Some(event_result) = stream.next().await { match event_result { Ok(socket_event) => { match socket_event { - kordophone::api::event_socket::SocketEvent::Update(event) => { - match event.data { - EventData::MessageReceived(conversation, _message) => { - if target_conversation_ids.contains(&conversation.guid) { - println!("Message update detected for conversation {}, marking as read...", conversation.guid); - match client.mark_conversation_as_read(&conversation.guid).await { - Ok(_) => println!("Successfully marked conversation {} as read", conversation.guid), - Err(e) => eprintln!("Failed to mark conversation {} as read: {:?}", conversation.guid, e), - } + kordophone::api::event_socket::SocketEvent::Update(event) => match event.data { + EventData::MessageReceived(conversation, _message) => { + if target_conversation_ids.contains(&conversation.guid) { + println!( + "Message update detected for conversation {}, marking as read...", + conversation.guid + ); + match client.mark_conversation_as_read(&conversation.guid).await { + Ok(_) => println!( + "Successfully marked conversation {} as read", + conversation.guid + ), + Err(e) => eprintln!( + "Failed to mark conversation {} as read: {:?}", + conversation.guid, e + ), } - }, - - _ => {} + } } + + _ => {} }, kordophone::api::event_socket::SocketEvent::Pong => { // Ignore pong messages } } - }, + } Err(e) => { eprintln!("Error receiving event: {:?}", e); break; } } } - + println!("Event stream ended"); Ok(()) } diff --git a/server/kordophone/Bridge/Operations/MBIMSendMessageOperation.m b/server/kordophone/Bridge/Operations/MBIMSendMessageOperation.m index 85b7b82..4e44864 100644 --- a/server/kordophone/Bridge/Operations/MBIMSendMessageOperation.m +++ b/server/kordophone/Bridge/Operations/MBIMSendMessageOperation.m @@ -10,6 +10,7 @@ #import "IMCore_ClassDump.h" #import "IMMessageItem+Encoded.h" +#import "MBIMErrorResponse.h" @implementation MBIMSendMessageOperation @@ -20,40 +21,93 @@ return @"sendMessage"; } -- (IMMessage *)_sendMessage:(NSString *)messageBody toChatWithGUID:(NSString *)chatGUID attachmentGUIDs:(NSArray *)guids +- (nullable IMChat *)_chatForHandleIDs:(NSArray *)handleIDs registry:(IMChatRegistry *)registry { - __block IMMessage *result = nil; - - dispatch_sync([[self class] sharedIMAccessQueue], ^{ - IMChat *chat = [[IMChatRegistry sharedInstance] existingChatWithGUID:chatGUID]; - - // TODO: chat might not be an iMessage chat! - IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]]; - IMHandle *senderHandle = [iMessageAccount loginIMHandle]; - - NSAttributedString *replyAttrString = [[NSAttributedString alloc] initWithString:messageBody]; - NSAttributedString *attrStringWithFileTransfers = IMCreateSuperFormatStringWithAppendedFileTransfers(replyAttrString, guids); - - IMMessage *reply = [IMMessage fromMeIMHandle:senderHandle - withText:attrStringWithFileTransfers - fileTransferGUIDs:guids - flags:(kIMMessageFinished | kIMMessageIsFromMe)]; - - for (NSString *guid in [reply fileTransferGUIDs]) { - [[IMFileTransferCenter sharedInstance] assignTransfer:guid toHandle:chat.recipient]; + IMAccount *iMessageAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]]; + if (!iMessageAccount) { + MBIMLogError(@"Unable to find an iMessage account for message send."); + return nil; + } + + NSMutableArray *handles = [NSMutableArray arrayWithCapacity:[handleIDs count]]; + for (NSString *handleID in handleIDs) { + IMHandle *handle = [iMessageAccount imHandleWithID:handleID]; + if (!handle) { + MBIMLogError(@"Couldn't resolve IMHandle for id %@", handleID); + return nil; } - + + [handles addObject:handle]; + } + + if ([handles count] == 1) { + IMHandle *handle = [handles firstObject]; + IMChat *chat = [registry existingChatWithHandle:handle allowAlternativeService:NO]; if (!chat) { - MBIMLogInfo(@"Chat does not exist: %@", chatGUID); - } else { - result = reply; - - dispatch_async(dispatch_get_main_queue(), ^{ - [chat sendMessage:reply]; - }); + chat = [registry chatWithHandle:handle]; } + + return chat; + } + + IMChat *chat = [registry existingChatWithHandles:handles + allowAlternativeService:NO + groupID:nil + displayName:nil + joinedChatsOnly:YES]; + + if (!chat) { + chat = [registry chatWithHandles:handles displayName:nil joinedChatsOnly:YES]; + } + + return chat; +} + +- (nullable NSDictionary *)_sendMessage:(NSString *)messageBody toChat:(IMChat *)chat attachmentGUIDs:(NSArray *)guids includeConversationGUID:(BOOL)includeConversationGUID +{ + if (!chat) { + return nil; + } + + IMAccount *sendingAccount = [chat account]; + if (!sendingAccount) { + sendingAccount = [[IMAccountController sharedInstance] bestAccountForService:[IMServiceImpl iMessageService]]; + } + + IMHandle *senderHandle = [sendingAccount loginIMHandle]; + if (!senderHandle) { + MBIMLogError(@"Unable to determine sender handle for message send."); + return nil; + } + + NSAttributedString *replyAttrString = [[NSAttributedString alloc] initWithString:messageBody]; + NSAttributedString *attrStringWithFileTransfers = IMCreateSuperFormatStringWithAppendedFileTransfers(replyAttrString, guids); + + IMMessage *reply = [IMMessage fromMeIMHandle:senderHandle + withText:attrStringWithFileTransfers + fileTransferGUIDs:guids + flags:(kIMMessageFinished | kIMMessageIsFromMe)]; + + for (NSString *guid in [reply fileTransferGUIDs]) { + [[IMFileTransferCenter sharedInstance] assignTransfer:guid toMessage:reply account:sendingAccount]; + } + + NSMutableDictionary *result = [[reply mbim_dictionaryRepresentation] mutableCopy]; + if (includeConversationGUID) { + NSString *conversationGUID = [chat guid]; + if (!conversationGUID) { + conversationGUID = [[[IMChatRegistry sharedInstance] allGUIDsForChat:chat] firstObject]; + } + + if (conversationGUID) { + result[@"conversationGUID"] = conversationGUID; + } + } + + dispatch_async(dispatch_get_main_queue(), ^{ + [chat sendMessage:reply]; }); - + return result; } @@ -79,7 +133,7 @@ - (void)main { - NSObject *response = [[HTTPErrorResponse alloc] initWithErrorCode:500]; + __block NSObject *response = [[HTTPErrorResponse alloc] initWithErrorCode:500]; NSError *error = nil; NSDictionary *args = [NSJSONSerialization JSONObjectWithData:self.requestBodyData options:0 error:&error]; @@ -90,31 +144,70 @@ NSString *guid = [args objectForKey:@"guid"]; NSString *messageBody = [args objectForKey:@"body"]; - if (!guid || !messageBody) { + NSArray *rawHandleIDs = [args objectForKey:@"handleIDs"]; + BOOL hasGUID = [guid isKindOfClass:[NSString class]] && [guid length] > 0; + BOOL hasHandleIDs = [rawHandleIDs isKindOfClass:[NSArray class]] && [rawHandleIDs count] > 0; + + if (![messageBody isKindOfClass:[NSString class]] || (!hasGUID && !hasHandleIDs) || (hasGUID && hasHandleIDs)) { + response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"sendMessage requires body and exactly one of guid or handleIDs."]; self.serverCompletionBlock(response); return; } - // tapbacks -#if 0 - IMMessage *acknowledgment = [IMMessage instantMessageWithAssociatedMessageContent: /* [NSString stringWithFormat:@"%@ \"%%@\"", tapbackAction] */ - flags:0 - associatedMessageGUID:guid - associatedMessageType:IMAssociatedMessageTypeAcknowledgmentHeart - associatedMessageRange:[imMessage messagePartRange] - messageSummaryInfo:[self adjustMessageSummaryInfoForSending:message] - threadIdentifier:[imMessage threadIdentifier]]; -#endif - - NSArray *transferGUIDs = [args objectForKey:@"fileTransferGUIDs"]; - if (!transferGUIDs) { - transferGUIDs = @[]; + NSMutableArray *handleIDs = [NSMutableArray array]; + if (hasHandleIDs) { + for (id handleID in rawHandleIDs) { + if ([handleID isKindOfClass:[NSString class]] && [handleID length] > 0) { + [handleIDs addObject:handleID]; + } + } + + handleIDs = [[[NSOrderedSet orderedSetWithArray:handleIDs] array] mutableCopy]; + if ([handleIDs count] == 0) { + response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"No valid handle IDs provided."]; + self.serverCompletionBlock(response); + return; + } } - - IMMessage *result = [self _sendMessage:messageBody toChatWithGUID:guid attachmentGUIDs:transferGUIDs]; - if (result) { - response = [MBIMJSONDataResponse responseWithJSONObject:[result mbim_dictionaryRepresentation]]; + + NSArray *rawTransferGUIDs = [args objectForKey:@"fileTransferGUIDs"]; + NSMutableArray *transferGUIDs = [NSMutableArray array]; + if ([rawTransferGUIDs isKindOfClass:[NSArray class]]) { + for (id transferGUID in rawTransferGUIDs) { + if ([transferGUID isKindOfClass:[NSString class]] && [transferGUID length] > 0) { + [transferGUIDs addObject:transferGUID]; + } + } } + + dispatch_sync([[self class] sharedIMAccessQueue], ^{ + IMChatRegistry *registry = [IMChatRegistry sharedInstance]; + IMChat *chat = nil; + BOOL includeConversationGUID = NO; + + if (hasGUID) { + chat = [registry existingChatWithGUID:guid]; + if (!chat) { + response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Chat does not exist for the provided guid."]; + return; + } + } else { + chat = [self _chatForHandleIDs:handleIDs registry:registry]; + includeConversationGUID = YES; + if (!chat) { + response = [[MBIMErrorResponse alloc] initWithErrorCode:500 message:@"Unable to create or locate a chat for the provided handles."]; + return; + } + } + + NSDictionary *result = [self _sendMessage:messageBody + toChat:chat + attachmentGUIDs:transferGUIDs + includeConversationGUID:includeConversationGUID]; + if (result) { + response = [MBIMJSONDataResponse responseWithJSONObject:result]; + } + }); self.serverCompletionBlock(response); }