relay-cli 0.1.2

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

use crate::{
    args::Args,
    parsers::parse_address,
    progress::Progress,
    records::{get_agent_record, get_user_record},
    sign::sign_request,
    storage::Storage,
    transport::agent_base_url,
};
use relay_lib::{
    core::{Uuid, Version, chrono::Utc},
    prelude::{Address, MetaEnvelope, PrivateEnvelope, e2e, sign},
};

#[derive(Serialize)]
struct SendReq {
    pub envelope: MetaEnvelope,
}

#[derive(Debug, Clone, clap::Parser)]
#[clap(about = "Send a message to a user")]
pub struct SendCmd {
    #[clap(value_parser = parse_address)]
    pub address: Address,

    #[clap(short, long, value_parser = parse_address)]
    pub to: Address,

    #[clap(short, long)]
    pub message: Option<String>,
}

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

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

        progress.step("Fetching recipient keys", "Fetched recipient keys");
        let recipient = get_user_record(self.to.agent(), self.to.user().clone()).map_err(|_| {
            progress.abort("Failed to fetch recipient user record");
        })?;

        progress.step("Reading identity", "Read identity");
        let identity = match storage.root.get_identity(&self.address) {
            Some(id) => id.clone(),
            None => {
                println!("No identity found for address: {}", self.address);
                return Ok(());
            }
        };

        progress.step("Composing message", "Composed message");
        let message = match self.message.clone() {
            Some(msg) => msg,
            None => {
                progress.pause();
                let message = edit::edit(
                    "\n# Please write your message here.\n# Lines starting with '#' will be ignored\n",
                )?;
                progress.resume();
                message
                    .trim()
                    .split('\n')
                    .filter(|line| !line.starts_with('#'))
                    .collect::<Vec<&str>>()
                    .join("\n")
            }
        };

        let private = PrivateEnvelope {
            content_type: "plain/text".to_string(),
            message: message.as_bytes().to_vec(),
        };

        progress.step("Encrypting message", "Encrypted message");
        let rng = relay_lib::crypto::OsRng;
        let payload = serde_json::to_vec(&private).expect("Failed to serialize payload");
        let (meta, ciphertext) =
            e2e::encrypt(rng, &recipient, &payload, &[]).context("encrypting")?;

        progress.step("Signing message", "Signed message");
        let signed = sign::sign(&ciphertext, &identity.signing_key());

        let envelope = MetaEnvelope {
            gid: Uuid::new_v4(),
            version: Version::parse("0.0.1").unwrap(),
            from: self.address.clone(),
            to: self.to.clone(),
            timestamp: Utc::now(),
            crypto: meta,
            payload: signed,
        };

        progress.step("Sending message", "Sent message");
        let request = sign_request(
            &self.address.canonical(),
            SendReq { envelope },
            &agent,
            &identity.signing_key(),
        )?;

        let resp = reqwest::blocking::Client::new()
            .post(agent_base_url(self.address.agent())?.join("/send/user")?)
            .json(&request)
            .send()
            .context("Failed to send messages request")?;

        progress.commit();

        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 send message. Status: {}", status));
        }

        progress.arrow(&format!(
            "Message successfully sent to `{}`",
            self.to.canonical()
        ));
        Ok(())
    }
}