From 07b55f861528c0cd555842cb9a89c1ad69ccc7c2 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Fri, 2 May 2025 12:03:56 -0700 Subject: [PATCH] client: implements send_message --- kordophone/src/api/http_client.rs | 36 ++++++++++++--- kordophone/src/api/mod.rs | 8 +++- kordophone/src/model/mod.rs | 4 ++ kordophone/src/model/outgoing_message.rs | 56 ++++++++++++++++++++++++ kordophone/src/tests/test_client.rs | 19 +++++++- kpcli/src/client/mod.rs | 19 ++++++++ 6 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 kordophone/src/model/outgoing_message.rs diff --git a/kordophone/src/api/http_client.rs b/kordophone/src/api/http_client.rs index bfc2488..1a9c3c4 100644 --- a/kordophone/src/api/http_client.rs +++ b/kordophone/src/api/http_client.rs @@ -20,7 +20,17 @@ use tokio_tungstenite::connect_async; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use crate::{ - model::{Conversation, ConversationID, JwtToken, Message, MessageID, UpdateItem, Event}, + model::{ + Conversation, + ConversationID, + JwtToken, + Message, + MessageID, + UpdateItem, + Event, + OutgoingMessage, + }, + APIInterface }; @@ -215,6 +225,19 @@ impl APIInterface for HTTPAPIClient { Ok(messages) } + async fn send_message( + &mut self, + outgoing_message: OutgoingMessage, + ) -> Result { + let message: Message = self.request_with_body( + "sendMessage", + Method::POST, + || serde_json::to_string(&outgoing_message).unwrap().into() + ).await?; + + Ok(message) + } + async fn open_event_socket(&mut self) -> Result { use tungstenite::http::StatusCode; use tungstenite::handshake::client::Request as TungsteniteRequest; @@ -285,24 +308,23 @@ impl HTTPAPIClient { } async fn request(&mut self, endpoint: &str, method: Method) -> Result { - self.request_with_body(endpoint, method, || { Body::empty() }).await + self.request_with_body(endpoint, method, Body::empty).await } - async fn request_with_body(&mut self, endpoint: &str, method: Method, body_fn: B) -> Result - where T: DeserializeOwned, B: Fn() -> Body + async fn request_with_body(&mut self, endpoint: &str, method: Method, body_fn: impl Fn() -> Body) -> Result + where T: DeserializeOwned { self.request_with_body_retry(endpoint, method, body_fn, true).await } - async fn request_with_body_retry( + async fn request_with_body_retry( &mut self, endpoint: &str, method: Method, - body_fn: B, + body_fn: impl Fn() -> Body, retry_auth: bool) -> Result where T: DeserializeOwned, - B: Fn() -> Body { use hyper::StatusCode; diff --git a/kordophone/src/api/mod.rs b/kordophone/src/api/mod.rs index 5f35bfd..81f8fae 100644 --- a/kordophone/src/api/mod.rs +++ b/kordophone/src/api/mod.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; pub use crate::model::{ - Conversation, Message, ConversationID, MessageID, + Conversation, Message, ConversationID, MessageID, OutgoingMessage, }; pub mod auth; @@ -35,6 +35,12 @@ pub trait APIInterface { after: Option, ) -> Result, Self::Error>; + // (POST) /sendMessage + async fn send_message( + &mut self, + outgoing_message: OutgoingMessage, + ) -> Result; + // (POST) /authenticate async fn authenticate(&mut self, credentials: Credentials) -> Result; diff --git a/kordophone/src/model/mod.rs b/kordophone/src/model/mod.rs index b54ce2e..cc659aa 100644 --- a/kordophone/src/model/mod.rs +++ b/kordophone/src/model/mod.rs @@ -1,6 +1,7 @@ pub mod conversation; pub mod event; pub mod message; +pub mod outgoing_message; pub mod update; pub use conversation::Conversation; @@ -9,6 +10,9 @@ pub use conversation::ConversationID; pub use message::Message; pub use message::MessageID; +pub use outgoing_message::OutgoingMessage; +pub use outgoing_message::OutgoingMessageBuilder; + pub use update::UpdateItem; pub use event::Event; diff --git a/kordophone/src/model/outgoing_message.rs b/kordophone/src/model/outgoing_message.rs new file mode 100644 index 0000000..1f13d2f --- /dev/null +++ b/kordophone/src/model/outgoing_message.rs @@ -0,0 +1,56 @@ +use serde::Serialize; +use super::conversation::ConversationID; + +#[derive(Debug, Clone, Serialize)] +pub struct OutgoingMessage { + #[serde(rename = "body")] + pub text: String, + + #[serde(rename = "guid")] + pub conversation_id: ConversationID, + + #[serde(rename = "fileTransferGUIDs")] + pub file_transfer_guids: Vec, +} + +impl OutgoingMessage { + pub fn builder() -> OutgoingMessageBuilder { + OutgoingMessageBuilder::new() + } +} + +#[derive(Default)] +pub struct OutgoingMessageBuilder { + text: Option, + conversation_id: Option, + file_transfer_guids: Option>, +} + +impl OutgoingMessageBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn text(mut self, text: String) -> Self { + self.text = Some(text); + self + } + + pub fn conversation_id(mut self, conversation_id: ConversationID) -> Self { + self.conversation_id = Some(conversation_id); + self + } + + pub fn file_transfer_guids(mut self, file_transfer_guids: Vec) -> Self { + self.file_transfer_guids = Some(file_transfer_guids); + self + } + + pub fn build(self) -> OutgoingMessage { + OutgoingMessage { + text: self.text.unwrap(), + conversation_id: self.conversation_id.unwrap(), + file_transfer_guids: self.file_transfer_guids.unwrap_or_default(), + } + } +} \ No newline at end of file diff --git a/kordophone/src/tests/test_client.rs b/kordophone/src/tests/test_client.rs index 3e06f3a..fdc3eff 100644 --- a/kordophone/src/tests/test_client.rs +++ b/kordophone/src/tests/test_client.rs @@ -1,10 +1,13 @@ use async_trait::async_trait; use std::collections::HashMap; +use time::OffsetDateTime; +use uuid::Uuid; + pub use crate::APIInterface; use crate::{ api::http_client::Credentials, - model::{Conversation, ConversationID, JwtToken, Message, MessageID, UpdateItem, Event}, + model::{Conversation, ConversationID, JwtToken, Message, MessageID, UpdateItem, Event, OutgoingMessage}, api::event_socket::EventSocket, }; @@ -88,6 +91,20 @@ impl APIInterface for TestClient { Err(TestError::ConversationNotFound) } + async fn send_message( + &mut self, + outgoing_message: OutgoingMessage, + ) -> Result { + let message = Message::builder() + .guid(Uuid::new_v4().to_string()) + .text(outgoing_message.text) + .date(OffsetDateTime::now_utc()) + .build(); + + self.messages.entry(outgoing_message.conversation_id).or_insert(vec![]).push(message.clone()); + Ok(message) + } + async fn open_event_socket(&mut self) -> Result { Ok(TestEventSocket::new()) } diff --git a/kpcli/src/client/mod.rs b/kpcli/src/client/mod.rs index 87a0e08..d805f9e 100644 --- a/kpcli/src/client/mod.rs +++ b/kpcli/src/client/mod.rs @@ -8,6 +8,7 @@ use anyhow::Result; use clap::Subcommand; use crate::printers::{ConversationPrinter, MessagePrinter}; use kordophone::model::event::Event; +use kordophone::model::outgoing_message::OutgoingMessage; use futures_util::StreamExt; @@ -47,6 +48,12 @@ pub enum Commands { /// Prints all raw updates from the server. RawUpdates, + + /// Sends a message to the server. + SendMessage { + conversation_id: String, + message: String, + }, } impl Commands { @@ -58,6 +65,7 @@ impl Commands { Commands::Messages { conversation_id } => client.print_messages(conversation_id).await, Commands::RawUpdates => client.print_raw_updates().await, Commands::Events => client.print_events().await, + Commands::SendMessage { conversation_id, message } => client.send_message(conversation_id, message).await, } } } @@ -123,6 +131,17 @@ impl ClientCli { Ok(()) } + + pub async fn send_message(&mut self, conversation_id: String, message: String) -> Result<()> { + let outgoing_message = OutgoingMessage::builder() + .conversation_id(conversation_id) + .text(message) + .build(); + + let message = self.api.send_message(outgoing_message).await?; + println!("Message sent: {}", message.guid); + Ok(()) + } }