Private
Public Access
1
0

kordophone: add support for /messages

This commit is contained in:
2025-01-20 19:43:21 -08:00
parent 793faab721
commit a8104c379c
8 changed files with 206 additions and 6 deletions

View File

@@ -9,7 +9,10 @@ use hyper::{Body, Client, Method, Request, Uri};
use async_trait::async_trait; use async_trait::async_trait;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{model::{Conversation, JwtToken}, APIInterface}; use crate::{
model::{Conversation, ConversationID, JwtToken, Message},
APIInterface
};
type HttpClient = Client<hyper::client::HttpConnector>; type HttpClient = Client<hyper::client::HttpConnector>;
@@ -111,6 +114,12 @@ impl APIInterface for HTTPAPIClient {
self.auth_token = Some(token.clone()); self.auth_token = Some(token.clone());
Ok(token) Ok(token)
} }
async fn get_messages(&mut self, conversation_id: &ConversationID) -> Result<Vec<Message>, Self::Error> {
let endpoint = format!("messages?guid={}", conversation_id);
let messages: Vec<Message> = self.request(&endpoint, Method::GET).await?;
Ok(messages)
}
} }
impl HTTPAPIClient { impl HTTPAPIClient {
@@ -261,4 +270,18 @@ mod test {
let conversations = client.get_conversations().await.unwrap(); let conversations = client.get_conversations().await.unwrap();
assert!(!conversations.is_empty()); 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());
}
} }

View File

@@ -1,5 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
pub use crate::model::Conversation; pub use crate::model::{
Conversation, Message, ConversationID
};
use crate::model::JwtToken; use crate::model::JwtToken;
pub mod http_client; pub mod http_client;
@@ -17,6 +19,9 @@ pub trait APIInterface {
// (GET) /conversations // (GET) /conversations
async fn get_conversations(&mut self) -> Result<Vec<Conversation>, Self::Error>; async fn get_conversations(&mut self) -> Result<Vec<Conversation>, Self::Error>;
// (GET) /messages
async fn get_messages(&mut self, conversation_id: &ConversationID) -> Result<Vec<Message>, Self::Error>;
// (POST) /authenticate // (POST) /authenticate
async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error>; async fn authenticate(&mut self, credentials: Credentials) -> Result<JwtToken, Self::Error>;
} }

View File

@@ -2,6 +2,10 @@ use serde::Deserialize;
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
use super::Identifiable;
pub type ConversationID = <Conversation as Identifiable>::ID;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct Conversation { pub struct Conversation {
pub guid: String, 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)] #[derive(Default)]
pub struct ConversationBuilder { pub struct ConversationBuilder {
guid: Option<String>, guid: Option<String>,

View File

@@ -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<String>,
#[serde(with = "time::serde::iso8601")]
pub date: OffsetDateTime,
}
impl Message {
pub fn builder() -> MessageBuilder {
MessageBuilder::new()
}
}
#[derive(Default)]
pub struct MessageBuilder {
guid: Option<String>,
text: Option<String>,
sender: Option<String>,
date: Option<OffsetDateTime>,
}
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()),
}
}
}

View File

@@ -1,5 +1,15 @@
pub mod conversation; pub mod conversation;
pub mod message;
pub use conversation::Conversation; pub use conversation::Conversation;
pub use conversation::ConversationID;
pub use message::Message;
pub mod jwt; pub mod jwt;
pub use jwt::JwtToken; pub use jwt::JwtToken;
pub trait Identifiable {
type ID;
fn id(&self) -> &Self::ID;
}

View File

@@ -1,21 +1,29 @@
use async_trait::async_trait; use async_trait::async_trait;
use std::collections::HashMap;
pub use crate::APIInterface; 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 struct TestClient {
pub version: &'static str, pub version: &'static str,
pub conversations: Vec<Conversation>, pub conversations: Vec<Conversation>,
pub messages: HashMap<ConversationID, Vec<Message>>,
} }
#[derive(Debug)] #[derive(Debug)]
pub enum TestError {} pub enum TestError {
ConversationNotFound,
}
impl TestClient { impl TestClient {
pub fn new() -> TestClient { pub fn new() -> TestClient {
TestClient { TestClient {
version: "KordophoneTest-1.0", version: "KordophoneTest-1.0",
conversations: vec![], conversations: vec![],
messages: HashMap::<ConversationID, Vec<Message>>::new(),
} }
} }
} }
@@ -35,4 +43,12 @@ impl APIInterface for TestClient {
async fn get_conversations(&mut self) -> Result<Vec<Conversation>, Self::Error> { async fn get_conversations(&mut self) -> Result<Vec<Conversation>, Self::Error> {
Ok(self.conversations.clone()) Ok(self.conversations.clone())
} }
async fn get_messages(&mut self, conversation: Conversation) -> Result<Vec<Message>, Self::Error> {
if let Some(messages) = self.messages.get(&conversation.guid) {
return Ok(messages.clone())
}
Err(TestError::ConversationNotFound)
}
} }

View File

@@ -5,7 +5,7 @@ use kordophone::api::http_client::Credentials;
use dotenv; use dotenv;
use anyhow::Result; use anyhow::Result;
use clap::Subcommand; use clap::Subcommand;
use crate::printers::ConversationPrinter; use crate::printers::{ConversationPrinter, MessagePrinter};
pub fn make_api_client_from_env() -> HTTPAPIClient { pub fn make_api_client_from_env() -> HTTPAPIClient {
dotenv::dotenv().ok(); dotenv::dotenv().ok();
@@ -30,6 +30,11 @@ pub enum Commands {
/// Prints all known conversations on the server. /// Prints all known conversations on the server.
Conversations, Conversations,
/// Prints all messages in a conversation.
Messages {
conversation_id: String,
},
/// Prints the server Kordophone version. /// Prints the server Kordophone version.
Version, Version,
} }
@@ -40,6 +45,7 @@ impl Commands {
match cmd { match cmd {
Commands::Version => client.print_version().await, Commands::Version => client.print_version().await,
Commands::Conversations => client.print_conversations().await, Commands::Conversations => client.print_conversations().await,
Commands::Messages { conversation_id } => client.print_messages(conversation_id).await,
} }
} }
} }
@@ -68,6 +74,14 @@ impl ClientCli {
Ok(()) 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(())
}
} }

View File

@@ -37,6 +37,24 @@ impl From<kordophone_db::models::Conversation> for PrintableConversation {
} }
} }
pub struct PrintableMessage {
pub guid: String,
pub date: OffsetDateTime,
pub sender: String,
pub text: String,
}
impl From<kordophone::model::Message> for PrintableMessage {
fn from(value: kordophone::model::Message) -> Self {
Self {
guid: value.guid,
date: value.date,
sender: value.sender.unwrap_or("<me>".to_string()),
text: value.text,
}
}
}
pub struct ConversationPrinter<'a> { pub struct ConversationPrinter<'a> {
doc: RcDoc<'a, PrintableConversation> doc: RcDoc<'a, PrintableConversation>
} }
@@ -56,6 +74,9 @@ impl<'a> ConversationPrinter<'a> {
.append(RcDoc::line()) .append(RcDoc::line())
.append("Date: ") .append("Date: ")
.append(conversation.date.to_string()) .append(conversation.date.to_string())
.append(RcDoc::line())
.append("Unread Count: ")
.append(conversation.unread_count.to_string())
.append(RcDoc::line()) .append(RcDoc::line())
.append("Participants: ") .append("Participants: ")
.append("[") .append("[")
@@ -90,3 +111,35 @@ impl<'a> Display for ConversationPrinter<'a> {
self.doc.render_fmt(180, f) 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!("<Message: \"{}\"", &message.guid))
.append(
RcDoc::line()
.append("Date: ")
.append(message.date.to_string())
.append(RcDoc::line())
.append("Sender: ")
.append(&message.sender)
.append(RcDoc::line())
.append("Body: ")
.append(&message.text)
.nest(4)
)
.append(RcDoc::line())
.append(">");
MessagePrinter { doc }
}
}