relay-cli 0.1.2

CLI interface for Relay Agents.
use anyhow::{Context, Result};
use base64::{Engine as _, prelude::BASE64_STANDARD};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};

use crate::progress::Progress;
use relay_lib::{
    crypto::{SigningKey, StaticSecret, hex},
    prelude::{Address, InboxId, KeyRecord},
};

#[derive(Clone, Serialize, Deserialize)]
pub struct Identity {
    pub pub_record: KeyRecord,
    pub inboxes: Vec<InboxId>,
    #[serde(with = "hex::serde")]
    pub signing_key: [u8; 32],
    #[serde(with = "hex::serde")]
    pub static_secret: [u8; 32],
}

impl Identity {
    pub fn signing_key(&self) -> SigningKey {
        SigningKey::from_bytes(&self.signing_key)
    }

    pub fn static_secret(&self) -> StaticSecret {
        StaticSecret::from(self.static_secret)
    }

    pub fn print(&self, level: u32, progress: &mut Progress) {
        progress.nested_section(level, &self.pub_record.id);
        progress.nested(
            level + 1,
            &format!("Created {}", self.pub_record.created_at),
        );
        if let Some(expires_at) = self.pub_record.expires_at {
            progress.nested(level + 1, &format!("Expires {}", expires_at));
        } else {
            progress.nested(level + 1, "Never Expires");
        }
        if !self.inboxes.is_empty() {
            let mut inboxes = self.inboxes.clone();
            inboxes.sort_by(|a, b| a.canonical().cmp(b.canonical()));

            progress.nested_section(level + 1, "Inboxes");
            for inbox in inboxes.iter() {
                progress.nested(level + 2, &format!("{}", inbox));
            }
        }
    }
}

#[derive(Clone, Serialize, Deserialize)]
pub struct StorageRoot {
    pub identities: HashMap<Address, Identity>,
}

impl StorageRoot {
    pub fn get_identity(&self, address: &Address) -> Option<&Identity> {
        let mut address = address.clone();
        address.inbox = None;
        self.identities.get(&address)
    }

    pub fn export(&self, progress: &mut Progress, addresses: &[Address]) -> Result<String> {
        progress.step("Validating addresses", "Validated addresses");
        for address in addresses.iter() {
            let mut addr = address.clone();
            addr.inbox = None;
            if !self.identities.contains_key(&addr) {
                progress.abort(&format!(
                    "No identity found for address {}",
                    address.canonical()
                ));
            }
            if address.inbox().is_some() {
                progress.abort("Please specify addresses without inbox IDs");
            }
        }

        progress.step("Exporting identities", "Exported identities");
        let identities = self
            .identities
            .iter()
            .filter(|(addr, _)| addresses.contains(addr))
            .map(|(_, identity)| identity.clone())
            .collect::<Vec<_>>();
        let json = serde_json::to_vec(&identities).context("Could not serialize identities")?;
        Ok(BASE64_STANDARD.encode(json))
    }

    pub fn import(
        &mut self,
        progress: &mut Progress,
        data: &str,
        replace: bool,
    ) -> Result<Vec<Address>> {
        progress.step("Decoding identity data", "Decoded identity data");
        let json = BASE64_STANDARD
            .decode(data)
            .context("Could not decode base64 identity")?;
        let identities: Vec<Identity> =
            serde_json::from_slice(&json).context("Could not deserialize identities")?;
        let identities = identities
            .into_iter()
            .map(|identity| {
                let mut address = Address::parse(&identity.pub_record.id)
                    .expect("Invalid address in imported identity");
                address.inbox = None;
                (address, identity)
            })
            .collect::<HashMap<_, _>>();

        progress.step(
            "Checking for existing identities",
            "Checked for existing identities",
        );
        for (addr, _) in identities.iter() {
            if self.identities.contains_key(addr) {
                if replace {
                    progress.nested(
                        1,
                        &format!("Replacing existing identity for address {}", addr),
                    );
                } else {
                    progress.abort(&format!(
                        "Identity for address {} already exists. Use --replace to overwrite.",
                        addr
                    ));
                }
            }
        }

        progress.step("Importing identities", "Imported identities");
        let addresses: Vec<Address> = identities.keys().cloned().collect();
        self.identities.extend(identities);

        Ok(addresses)
    }
}

#[derive(Clone)]
pub struct Storage {
    pub root: StorageRoot,
}

impl Default for Storage {
    fn default() -> Self {
        Self {
            root: StorageRoot {
                identities: HashMap::new(),
            },
        }
    }
}

impl Storage {
    pub fn path() -> Result<PathBuf> {
        let mut path = dirs::data_dir().context("Could not find data directory")?;
        path.push("relay_cli");
        std::fs::create_dir_all(&path).context("Could not create data directory")?;
        path.push("storage.ron");
        Ok(path)
    }

    pub fn init() -> Result<Self> {
        if !Self::path()?.exists() {
            Self::default().save()?;
        }

        let ron = std::fs::read_to_string(Self::path()?).context("Could not read storage file")?;
        let root: StorageRoot =
            ron::de::from_str(&ron).context("Could not deserialize storage file")?;
        Ok(Self { root })
    }

    pub fn save(&self) -> Result<()> {
        let ron = ron::ser::to_string_pretty(&self.root, ron::ser::PrettyConfig::default())
            .context("Could not serialize storage")?;
        std::fs::write(Self::path()?, ron).context("Could not write storage file")?;
        Ok(())
    }

    pub fn delete(&self) -> Result<()> {
        std::fs::remove_file(Self::path()?).context("Could not delete storage file")?;

        let path = Self::path()?;
        let parent = path.parent().context("Could not get parent directory")?;
        std::fs::remove_dir_all(parent).context("Could not delete storage directory")?;

        Ok(())
    }

    pub fn reset(&mut self) {
        *self = Self::default();
    }
}