More logging, delete, new convo
This commit is contained in:
@@ -65,7 +65,15 @@ impl std::error::Error for Error {
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
match self {
|
||||
Error::ClientError(message) => write!(f, "{}", message),
|
||||
Error::HTTPError(err) => write!(f, "HTTP transport error: {}", err),
|
||||
Error::SerdeError(err) => write!(f, "JSON error: {}", err),
|
||||
Error::DecodeError(message) => write!(f, "Decode error: {}", message),
|
||||
Error::PongError(err) => write!(f, "WebSocket error: {}", err),
|
||||
Error::URLError => write!(f, "Invalid URL"),
|
||||
Error::Unauthorized => write!(f, "Unauthorized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,6 +292,17 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_conversation(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error> {
|
||||
// SERVER JANK: This should be DELETE or POST, but it's GET for some reason.
|
||||
let endpoint = format!("delete?guid={}", conversation_id);
|
||||
self.response_with_body_retry(&endpoint, Method::GET, Body::empty, true)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_messages(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
@@ -313,6 +332,25 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
&mut self,
|
||||
outgoing_message: &OutgoingMessage,
|
||||
) -> Result<SendMessageResponse, Self::Error> {
|
||||
match &outgoing_message.target {
|
||||
OutgoingMessageTarget::Conversation(conversation_id) => {
|
||||
log::debug!(
|
||||
"Sending message to conversation {} (body_length={}, attachment_count={})",
|
||||
conversation_id,
|
||||
outgoing_message.text.len(),
|
||||
outgoing_message.file_transfer_guids.len()
|
||||
);
|
||||
}
|
||||
OutgoingMessageTarget::Handles(handle_ids) => {
|
||||
log::debug!(
|
||||
"Sending message to resolved handles {:?} (body_length={}, attachment_count={})",
|
||||
handle_ids,
|
||||
outgoing_message.text.len(),
|
||||
outgoing_message.file_transfer_guids.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let message: SendMessageResponse = self
|
||||
.deserialized_response_with_body("sendMessage", Method::POST, || {
|
||||
Self::send_message_request_body(outgoing_message)
|
||||
@@ -326,6 +364,7 @@ impl<K: AuthenticationStore + Send + Sync> APIInterface for HTTPAPIClient<K> {
|
||||
&mut self,
|
||||
handle_id: &str,
|
||||
) -> Result<ResolveHandleResponse, Self::Error> {
|
||||
log::debug!("Resolving handle {}", handle_id);
|
||||
let endpoint = format!("resolveHandle?id={}", urlencoding::encode(handle_id));
|
||||
let response: ResolveHandleResponse =
|
||||
self.deserialized_response(&endpoint, Method::GET).await?;
|
||||
@@ -542,6 +581,18 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
|
||||
}
|
||||
}
|
||||
|
||||
fn log_transport_error(method: &Method, target: &str, err: &hyper::Error) {
|
||||
log::error!("HTTP transport error for {} {}: {}", method, target, err);
|
||||
|
||||
if format!("{:?}", err).contains("IncompleteMessage") {
|
||||
log::error!(
|
||||
"The server closed the connection before a complete response was received for {} {}.",
|
||||
method,
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn deserialized_response<T: DeserializeOwned>(
|
||||
&mut self,
|
||||
endpoint: &str,
|
||||
@@ -575,15 +626,26 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let response = self
|
||||
.response_with_body_retry(endpoint, method, body_fn, retry_auth)
|
||||
.response_with_body_retry(endpoint, method.clone(), body_fn, retry_auth)
|
||||
.await?;
|
||||
|
||||
// Read and parse response body
|
||||
let body = hyper::body::to_bytes(response.into_body()).await?;
|
||||
let body = match hyper::body::to_bytes(response.into_body()).await {
|
||||
Ok(body) => body,
|
||||
Err(err) => {
|
||||
Self::log_transport_error(&method, endpoint, &err);
|
||||
return Err(Error::HTTPError(err));
|
||||
}
|
||||
};
|
||||
let parsed: T = match serde_json::from_slice(&body) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(json_err) => {
|
||||
log::error!("Error deserializing JSON: {:?}", json_err);
|
||||
log::error!(
|
||||
"Error deserializing JSON for {} {}: {:?}",
|
||||
method,
|
||||
endpoint,
|
||||
json_err
|
||||
);
|
||||
log::error!("Body: {:?}", String::from_utf8_lossy(&body));
|
||||
|
||||
// If JSON deserialization fails, try to interpret it as plain text
|
||||
@@ -606,7 +668,8 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
|
||||
use hyper::StatusCode;
|
||||
|
||||
let uri = self.uri_for_endpoint(endpoint, None)?;
|
||||
log::debug!("Requesting {:?} {:?}", method, uri);
|
||||
let uri_string = uri.to_string();
|
||||
log::debug!("Requesting {} {}", method, uri_string);
|
||||
|
||||
let mut build_request = |auth: &Option<String>| {
|
||||
let body = body_fn();
|
||||
@@ -620,13 +683,24 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
|
||||
|
||||
log::trace!("Obtaining token from auth store");
|
||||
let token = self.auth_store.get_token().await;
|
||||
log::trace!("Token: {:?}", token);
|
||||
log::trace!("Token present: {}", token.is_some());
|
||||
|
||||
let request = build_request(&token);
|
||||
log::trace!("Request: {:?}. Sending request...", request);
|
||||
log::trace!(
|
||||
"Sending request: method={} uri={} authenticated={}",
|
||||
method,
|
||||
uri_string,
|
||||
token.is_some()
|
||||
);
|
||||
|
||||
let mut response = self.client.request(request).await?;
|
||||
log::debug!("-> Response: {:}", response.status());
|
||||
let mut response = match self.client.request(request).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
Self::log_transport_error(&method, &uri_string, &err);
|
||||
return Err(Error::HTTPError(err));
|
||||
}
|
||||
};
|
||||
log::debug!("-> Response: {}", response.status());
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => { /* cool */ }
|
||||
@@ -645,7 +719,19 @@ impl<K: AuthenticationStore + Send + Sync> HTTPAPIClient<K> {
|
||||
let new_token = self.authenticate(credentials.clone()).await?;
|
||||
|
||||
let request = build_request(&Some(new_token.to_string()));
|
||||
response = self.client.request(request).await?;
|
||||
log::trace!(
|
||||
"Retrying request after authentication: method={} uri={} authenticated=true",
|
||||
method,
|
||||
uri_string
|
||||
);
|
||||
response = match self.client.request(request).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
Self::log_transport_error(&method, &uri_string, &err);
|
||||
return Err(Error::HTTPError(err));
|
||||
}
|
||||
};
|
||||
log::debug!("-> Retry response: {}", response.status());
|
||||
} else {
|
||||
return Err(Error::ClientError(
|
||||
"Unauthorized, no credentials provided".into(),
|
||||
|
||||
@@ -79,6 +79,12 @@ pub trait APIInterface {
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
// (GET) /delete
|
||||
async fn delete_conversation(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error>;
|
||||
|
||||
// (WS) /updates
|
||||
async fn open_event_socket(
|
||||
&mut self,
|
||||
|
||||
@@ -52,4 +52,20 @@ pub mod api_interface {
|
||||
assert_eq!(sent.message.text, "hello");
|
||||
assert_eq!(sent.conversation_id, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_delete_conversation() {
|
||||
let mut client = TestClient::new();
|
||||
|
||||
let test_convo = Conversation::builder()
|
||||
.display_name("Delete Me")
|
||||
.build();
|
||||
|
||||
client.conversations.push(test_convo.clone());
|
||||
|
||||
client.delete_conversation(&test_convo.guid).await.unwrap();
|
||||
|
||||
let conversations = client.get_conversations().await.unwrap();
|
||||
assert!(conversations.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,4 +187,19 @@ impl APIInterface for TestClient {
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_conversation(
|
||||
&mut self,
|
||||
conversation_id: &ConversationID,
|
||||
) -> Result<(), Self::Error> {
|
||||
let previous_len = self.conversations.len();
|
||||
self.conversations.retain(|c| &c.guid != conversation_id);
|
||||
self.messages.remove(conversation_id);
|
||||
|
||||
if self.conversations.len() == previous_len {
|
||||
return Err(TestError::ConversationNotFound);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ use kordophone::api::InMemoryAuthenticationStore;
|
||||
use kordophone::APIInterface;
|
||||
|
||||
use crate::printers::{ConversationPrinter, MessagePrinter};
|
||||
use anyhow::Result;
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Subcommand;
|
||||
use kordophone::model::event::EventData;
|
||||
use kordophone::model::outgoing_message::OutgoingMessage;
|
||||
use kordophone::model::{HandleResolutionStatus, OutgoingMessage, OutgoingMessageTarget};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
|
||||
@@ -47,14 +47,29 @@ pub enum Commands {
|
||||
/// Prints all raw updates from the server.
|
||||
RawUpdates,
|
||||
|
||||
/// Sends a message to the server.
|
||||
SendMessage {
|
||||
/// Resolves an address to a canonical handle.
|
||||
#[command(alias = "resolve")]
|
||||
ResolveHandle { address: String },
|
||||
|
||||
/// Replies to an existing conversation.
|
||||
#[command(alias = "send-message")]
|
||||
Reply {
|
||||
conversation_id: String,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Starts a new message to one or more resolved handles.
|
||||
New {
|
||||
#[arg(long = "handle", required = true)]
|
||||
handle_ids: Vec<String>,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Marks a conversation as read.
|
||||
Mark { conversation_id: String },
|
||||
|
||||
/// Deletes a conversation from the server.
|
||||
Delete { conversation_id: String },
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
@@ -66,13 +81,19 @@ 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 {
|
||||
Commands::ResolveHandle { address } => client.resolve_handle(address).await,
|
||||
Commands::Reply {
|
||||
conversation_id,
|
||||
message,
|
||||
} => client.send_message(conversation_id, message).await,
|
||||
} => client.reply(conversation_id, message).await,
|
||||
Commands::New {
|
||||
handle_ids,
|
||||
message,
|
||||
} => client.new_message(handle_ids, message).await,
|
||||
Commands::Mark { conversation_id } => {
|
||||
client.mark_conversation_as_read(conversation_id).await
|
||||
}
|
||||
Commands::Delete { conversation_id } => client.delete_conversation(conversation_id).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,20 +188,92 @@ impl ClientCli {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_message(&mut self, conversation_id: String, message: String) -> Result<()> {
|
||||
pub async fn resolve_handle(&mut self, address: String) -> Result<()> {
|
||||
let response = self.api.resolve_handle(&address).await?;
|
||||
let status = match response.status {
|
||||
HandleResolutionStatus::Valid => "valid",
|
||||
HandleResolutionStatus::Invalid => "invalid",
|
||||
HandleResolutionStatus::Unknown => "unknown",
|
||||
};
|
||||
|
||||
println!("Resolved handle: {}", response.resolved_handle.id);
|
||||
|
||||
if let Some(name) = response.resolved_handle.name {
|
||||
println!("Name: {}", name);
|
||||
}
|
||||
|
||||
println!("Status: {}", status);
|
||||
|
||||
if let Some(conversation_id) = response.existing_chat {
|
||||
println!("Existing conversation: {}", conversation_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
&mut self,
|
||||
target: OutgoingMessageTarget,
|
||||
message: String,
|
||||
) -> Result<()> {
|
||||
let outgoing_message = OutgoingMessage::builder()
|
||||
.conversation_id(conversation_id)
|
||||
.target(target)
|
||||
.text(message)
|
||||
.build();
|
||||
|
||||
let response = self.api.send_message(&outgoing_message).await?;
|
||||
println!("Message sent: {}", response.message.guid);
|
||||
if let Some(conversation_id) = response.conversation_id {
|
||||
println!(
|
||||
"Message sent: {} conversation: {}",
|
||||
response.message.guid, conversation_id
|
||||
);
|
||||
} else {
|
||||
println!("Message sent: {}", response.message.guid);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resolve_handle_ids(&mut self, handle_ids: Vec<String>) -> Result<Vec<String>> {
|
||||
let mut resolved_handle_ids = Vec::with_capacity(handle_ids.len());
|
||||
|
||||
for handle_id in handle_ids {
|
||||
let response = self.api.resolve_handle(&handle_id).await?;
|
||||
match response.status {
|
||||
HandleResolutionStatus::Valid => {
|
||||
resolved_handle_ids.push(response.resolved_handle.id);
|
||||
}
|
||||
HandleResolutionStatus::Invalid => {
|
||||
bail!("Handle '{}' is not iMessage-capable.", handle_id);
|
||||
}
|
||||
HandleResolutionStatus::Unknown => {
|
||||
bail!("Handle '{}' could not be resolved.", handle_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_handle_ids)
|
||||
}
|
||||
|
||||
pub async fn reply(&mut self, conversation_id: String, message: String) -> Result<()> {
|
||||
self.send_message(OutgoingMessageTarget::Conversation(conversation_id), message)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn new_message(&mut self, handle_ids: Vec<String>, message: String) -> Result<()> {
|
||||
let resolved_handle_ids = self.resolve_handle_ids(handle_ids).await?;
|
||||
self.send_message(OutgoingMessageTarget::Handles(resolved_handle_ids), message)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn mark_conversation_as_read(&mut self, conversation_id: String) -> Result<()> {
|
||||
self.api.mark_conversation_as_read(&conversation_id).await?;
|
||||
println!("Conversation marked as read: {}", conversation_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_conversation(&mut self, conversation_id: String) -> Result<()> {
|
||||
self.api.delete_conversation(&conversation_id).await?;
|
||||
println!("Conversation deleted: {}", conversation_id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user