kordophone: add support for /messages
This commit is contained in:
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
67
kordophone/src/model/message.rs
Normal file
67
kordophone/src/model/message.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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("[")
|
||||||
@@ -89,4 +110,36 @@ impl<'a> Display for ConversationPrinter<'a> {
|
|||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user