relay-cli 0.1.2

CLI interface for Relay Agents.
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};

use crate::{
    args::Args, parsers::parse_address, progress::Progress, records::get_agent_record,
    sign::sign_request, storage::Storage, transport::agent_base_url,
};
use relay_lib::prelude::{Address, InboxId, MetaEnvelope, PrivateEnvelope, e2e};

#[derive(Serialize)]
struct InboxMessagesReq {
    pub inbox: Option<InboxId>,
    pub start: Option<u32>,
    pub amount: Option<u32>,
}

#[derive(Deserialize)]
struct InboxMessagesResp {
    #[serde(rename = "inbox")]
    pub _inbox: Option<InboxId>,
    #[serde(rename = "start")]
    pub _start: Option<u32>,
    pub messages: Vec<MetaEnvelope>,
}

#[derive(Debug, Clone, clap::Parser)]
#[clap(about = "Read messages")]
pub struct MessagesCmd {
    #[arg(value_parser = parse_address)]
    pub address: Address,

    #[clap(short, long, default_value_t = false)]
    pub contents: bool,

    #[clap(long, default_value_t = false)]
    pub raw: bool,
}

impl MessagesCmd {
    pub fn execute(&self, _: Args, storage: &mut Storage) -> Result<()> {
        let mut progress = Progress::new(self.raw);

        progress.step("Fetching agent keys", "Fetched agent keys");
        let agent_record = get_agent_record(self.address.agent()).map_err(|_| {
            progress.abort("Failed to fetch agent record");
        })?;

        progress.step("Reading identity", "Read identity");
        let identity = match storage.root.get_identity(&self.address) {
            Some(id) => id.clone(),
            None => {
                progress.abort("No identity found for the given address");
            }
        };

        progress.step("Fetching messages", "Fetched messages");
        let request = sign_request(
            &self.address.canonical(),
            InboxMessagesReq {
                inbox: self.address.inbox().cloned(),
                start: None,
                amount: None,
            },
            &agent_record,
            &identity.signing_key(),
        )
        .map_err(|_| {
            progress.abort("Failed to sign messages request");
        })?;

        let resp = reqwest::blocking::Client::new()
            .get(agent_base_url(self.address.agent())?.join("/inbox/messages")?)
            .json(&request)
            .send()
            .context("Failed to send messages request")
            .map_err(|_| {
                progress.abort("Failed to send messages request");
            })?;

        if resp.status().as_u16() == 404 {
            progress.abort("Inbox not found on the Agent.");
        }
        if !resp.status().is_success() {
            let status = resp.status();
            progress.abort(&format!("Failed to list messages. Status: {}", status));
        }

        progress.step("Processing response", "Processed response");
        let mut response = resp.json::<InboxMessagesResp>()?;

        if response.messages.is_empty() && !self.raw {
            progress.warn_end("No messages found.");
            return Ok(());
        }

        progress.commit();
        response.messages.sort_by_key(|m| m.timestamp);

        if self.raw {
            for message in response.messages {
                println!("{}", message.gid);
            }
            return Ok(());
        }

        for (i, message) in response.messages.iter().enumerate() {
            progress.nested_section(0, &format!("Message {}", message.gid));
            progress.nested(1, &format!("From {}", message.from));
            progress.nested(1, &format!("To {}", message.to));
            progress.nested(1, &format!("Sent at: {}", message.timestamp));

            if self.contents {
                let payload = e2e::decrypt(
                    &identity.static_secret(),
                    &message.crypto,
                    &message.payload.payload,
                    &[],
                )
                .map_err(|_| {
                    progress.abort("Decryption failed");
                })?;

                let private =
                    serde_json::from_slice::<PrivateEnvelope>(&payload).map_err(|_| {
                        progress.abort("Failed to parse private envelope");
                    })?;

                if private.content_type != "plain/text" {
                    progress.abort(&format!("Unsupported content type: {}. Read the message with the `read` command and pass in `--raw` to dump all contents.", private.content_type));
                }

                progress.skip_line();

                println!("{}", String::from_utf8_lossy(&private.message));

                if i != response.messages.len() - 1 {
                    progress.skip_line();
                }
            }
        }

        Ok(())
    }
}