From a8104c379c620cde922128daac4d3bd5e0b3fff8 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Mon, 20 Jan 2025 19:43:21 -0800 Subject: [PATCH] kordophone: add support for /messages --- kordophone/src/api/http_client.rs | 25 ++++++++++- kordophone/src/api/mod.rs | 7 ++- kordophone/src/model/conversation.rs | 12 +++++ kordophone/src/model/message.rs | 67 ++++++++++++++++++++++++++++ kordophone/src/model/mod.rs | 12 ++++- kordophone/src/tests/test_client.rs | 20 ++++++++- kpcli/src/client/mod.rs | 16 ++++++- kpcli/src/printers.rs | 53 ++++++++++++++++++++++ 8 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 kordophone/src/model/message.rs diff --git a/kordophone/src/api/http_client.rs b/kordophone/src/api/http_client.rs index eb1a5bc..c1e8def 100644 --- a/kordophone/src/api/http_client.rs +++ b/kordophone/src/api/http_client.rs @@ -9,7 +9,10 @@ use hyper::{Body, Client, Method, Request, Uri}; use async_trait::async_trait; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use crate::{model::{Conversation, JwtToken}, APIInterface}; +use crate::{ + model::{Conversation, ConversationID, JwtToken, Message}, + APIInterface +}; type HttpClient = Client; @@ -111,6 +114,12 @@ impl APIInterface for HTTPAPIClient { self.auth_token = Some(token.clone()); Ok(token) } + + async fn get_messages(&mut self, conversation_id: &ConversationID) -> Result, Self::Error> { + let endpoint = format!("messages?guid={}", conversation_id); + let messages: Vec = self.request(&endpoint, Method::GET).await?; + Ok(messages) + } } impl HTTPAPIClient { @@ -261,4 +270,18 @@ mod test { let conversations = client.get_conversations().await.unwrap(); assert!(!conversations.is_empty()); } + + #[tokio::test] + async fn test_messages() { + if !mock_client_is_reachable().await { + log::warn!("Skipping http_client tests (mock server not reachable)"); + return; + } + + let mut client = local_mock_client(); + let conversations = client.get_conversations().await.unwrap(); + let conversation = conversations.first().unwrap(); + let messages = client.get_messages(&conversation.guid).await.unwrap(); + assert!(!messages.is_empty()); + } } diff --git a/kordophone/src/api/mod.rs b/kordophone/src/api/mod.rs index 0cd5c99..581f35a 100644 --- a/kordophone/src/api/mod.rs +++ b/kordophone/src/api/mod.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; -pub use crate::model::Conversation; +pub use crate::model::{ + Conversation, Message, ConversationID +}; use crate::model::JwtToken; pub mod http_client; @@ -17,6 +19,9 @@ pub trait APIInterface { // (GET) /conversations async fn get_conversations(&mut self) -> Result, Self::Error>; + // (GET) /messages + async fn get_messages(&mut self, conversation_id: &ConversationID) -> Result, Self::Error>; + // (POST) /authenticate async fn authenticate(&mut self, credentials: Credentials) -> Result; } diff --git a/kordophone/src/model/conversation.rs b/kordophone/src/model/conversation.rs index 96d2f0d..f7b74c3 100644 --- a/kordophone/src/model/conversation.rs +++ b/kordophone/src/model/conversation.rs @@ -2,6 +2,10 @@ use serde::Deserialize; use time::OffsetDateTime; use uuid::Uuid; +use super::Identifiable; + +pub type ConversationID = ::ID; + #[derive(Debug, Clone, Deserialize)] pub struct Conversation { pub guid: String, @@ -28,6 +32,14 @@ impl Conversation { } } +impl Identifiable for Conversation { + type ID = String; + + fn id(&self) -> &Self::ID { + &self.guid + } +} + #[derive(Default)] pub struct ConversationBuilder { guid: Option, diff --git a/kordophone/src/model/message.rs b/kordophone/src/model/message.rs new file mode 100644 index 0000000..1420536 --- /dev/null +++ b/kordophone/src/model/message.rs @@ -0,0 +1,67 @@ +use serde::Deserialize; +use time::OffsetDateTime; +use uuid::Uuid; + +#[derive(Debug, Clone, Deserialize)] +pub struct Message { + pub guid: String, + + #[serde(rename = "text")] + pub text: String, + + #[serde(rename = "sender")] + pub sender: Option, + + #[serde(with = "time::serde::iso8601")] + pub date: OffsetDateTime, +} + +impl Message { + pub fn builder() -> MessageBuilder { + MessageBuilder::new() + } +} + +#[derive(Default)] +pub struct MessageBuilder { + guid: Option, + text: Option, + sender: Option, + date: Option, +} + +impl MessageBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn guid(mut self, guid: String) -> Self { + self.guid = Some(guid); + self + } + + pub fn text(mut self, text: String) -> Self { + self.text = Some(text); + self + } + + pub fn sender(mut self, sender: String) -> Self { + self.sender = Some(sender); + self + } + + pub fn date(mut self, date: OffsetDateTime) -> Self { + self.date = Some(date); + self + } + + pub fn build(self) -> Message { + Message { + guid: self.guid.unwrap_or(Uuid::new_v4().to_string()), + text: self.text.unwrap_or("".to_string()), + sender: self.sender, + date: self.date.unwrap_or(OffsetDateTime::now_utc()), + } + } +} + diff --git a/kordophone/src/model/mod.rs b/kordophone/src/model/mod.rs index 92607cd..d1c848e 100644 --- a/kordophone/src/model/mod.rs +++ b/kordophone/src/model/mod.rs @@ -1,5 +1,15 @@ pub mod conversation; +pub mod message; + pub use conversation::Conversation; +pub use conversation::ConversationID; + +pub use message::Message; pub mod jwt; -pub use jwt::JwtToken; \ No newline at end of file +pub use jwt::JwtToken; + +pub trait Identifiable { + type ID; + fn id(&self) -> &Self::ID; +} \ No newline at end of file diff --git a/kordophone/src/tests/test_client.rs b/kordophone/src/tests/test_client.rs index 375a449..6f44d6b 100644 --- a/kordophone/src/tests/test_client.rs +++ b/kordophone/src/tests/test_client.rs @@ -1,21 +1,29 @@ use async_trait::async_trait; +use std::collections::HashMap; pub use crate::APIInterface; -use crate::{api::http_client::Credentials, model::{Conversation, JwtToken}}; +use crate::{ + api::http_client::Credentials, + model::{conversation, Conversation, ConversationID, JwtToken, Message} +}; pub struct TestClient { pub version: &'static str, pub conversations: Vec, + pub messages: HashMap>, } #[derive(Debug)] -pub enum TestError {} +pub enum TestError { + ConversationNotFound, +} impl TestClient { pub fn new() -> TestClient { TestClient { version: "KordophoneTest-1.0", conversations: vec![], + messages: HashMap::>::new(), } } } @@ -35,4 +43,12 @@ impl APIInterface for TestClient { async fn get_conversations(&mut self) -> Result, Self::Error> { Ok(self.conversations.clone()) } + + async fn get_messages(&mut self, conversation: Conversation) -> Result, Self::Error> { + if let Some(messages) = self.messages.get(&conversation.guid) { + return Ok(messages.clone()) + } + + Err(TestError::ConversationNotFound) + } } diff --git a/kpcli/src/client/mod.rs b/kpcli/src/client/mod.rs index 55e6333..671db2f 100644 --- a/kpcli/src/client/mod.rs +++ b/kpcli/src/client/mod.rs @@ -5,7 +5,7 @@ use kordophone::api::http_client::Credentials; use dotenv; use anyhow::Result; use clap::Subcommand; -use crate::printers::ConversationPrinter; +use crate::printers::{ConversationPrinter, MessagePrinter}; pub fn make_api_client_from_env() -> HTTPAPIClient { dotenv::dotenv().ok(); @@ -30,6 +30,11 @@ pub enum Commands { /// Prints all known conversations on the server. Conversations, + /// Prints all messages in a conversation. + Messages { + conversation_id: String, + }, + /// Prints the server Kordophone version. Version, } @@ -40,6 +45,7 @@ impl Commands { match cmd { Commands::Version => client.print_version().await, Commands::Conversations => client.print_conversations().await, + Commands::Messages { conversation_id } => client.print_messages(conversation_id).await, } } } @@ -68,6 +74,14 @@ impl ClientCli { Ok(()) } + + pub async fn print_messages(&mut self, conversation_id: String) -> Result<()> { + let messages = self.api.get_messages(&conversation_id).await?; + for message in messages { + println!("{}", MessagePrinter::new(&message.into())); + } + Ok(()) + } } diff --git a/kpcli/src/printers.rs b/kpcli/src/printers.rs index 37b73e8..7ae3b48 100644 --- a/kpcli/src/printers.rs +++ b/kpcli/src/printers.rs @@ -37,6 +37,24 @@ impl From for PrintableConversation { } } +pub struct PrintableMessage { + pub guid: String, + pub date: OffsetDateTime, + pub sender: String, + pub text: String, +} + +impl From for PrintableMessage { + fn from(value: kordophone::model::Message) -> Self { + Self { + guid: value.guid, + date: value.date, + sender: value.sender.unwrap_or("".to_string()), + text: value.text, + } + } +} + pub struct ConversationPrinter<'a> { doc: RcDoc<'a, PrintableConversation> } @@ -56,6 +74,9 @@ impl<'a> ConversationPrinter<'a> { .append(RcDoc::line()) .append("Date: ") .append(conversation.date.to_string()) + .append(RcDoc::line()) + .append("Unread Count: ") + .append(conversation.unread_count.to_string()) .append(RcDoc::line()) .append("Participants: ") .append("[") @@ -89,4 +110,36 @@ impl<'a> Display for ConversationPrinter<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.doc.render_fmt(180, f) } +} + +pub struct MessagePrinter<'a> { + doc: RcDoc<'a, PrintableMessage> +} + +impl<'a> Display for MessagePrinter<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.doc.render_fmt(180, f) + } +} + +impl<'a> MessagePrinter<'a> { + pub fn new(message: &'a PrintableMessage) -> Self { + let doc = RcDoc::text(format!(""); + + MessagePrinter { doc } + } } \ No newline at end of file