brk_cli 0.3.0-alpha.6

A command line interface to run a BRK instance
use std::{
    fs,
    path::{Path, PathBuf},
};

use brk_error::{Error, Result};
use brk_rpc::{Auth, Client};
use brk_server::Website;
use brk_types::Port;
use owo_colors::OwoColorize;
use serde::{Deserialize, Deserializer, Serialize};

use crate::{default_brk_path, dot_brk_path, fix_user_path};

#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct Config {
    #[serde(default, deserialize_with = "default_on_error")]
    brkdir: Option<String>,

    #[serde(default, deserialize_with = "default_on_error")]
    brkport: Option<Port>,

    #[serde(default, deserialize_with = "default_on_error")]
    website: Option<Website>,

    #[serde(default, deserialize_with = "default_on_error")]
    bitcoindir: Option<String>,

    #[serde(default, deserialize_with = "default_on_error")]
    blocksdir: Option<String>,

    #[serde(default, deserialize_with = "default_on_error")]
    rpcconnect: Option<String>,

    #[serde(default, deserialize_with = "default_on_error")]
    rpcport: Option<u16>,

    #[serde(default, deserialize_with = "default_on_error")]
    rpccookiefile: Option<String>,

    #[serde(default, deserialize_with = "default_on_error")]
    rpcuser: Option<String>,

    #[serde(default, deserialize_with = "default_on_error")]
    rpcpassword: Option<String>,
}

impl Config {
    pub fn import() -> Result<Self> {
        let config_args = Self::parse_args();

        let path = dot_brk_path();

        let _ = fs::create_dir_all(&path);

        let path = path.join("config.toml");

        let mut config = Self::read(&path);

        if let Some(v) = config_args.brkdir {
            config.brkdir = Some(v);
        }
        if let Some(v) = config_args.brkport {
            config.brkport = Some(v);
        }
        if let Some(v) = config_args.website {
            config.website = Some(v);
        }
        if let Some(v) = config_args.bitcoindir {
            config.bitcoindir = Some(v);
        }
        if let Some(v) = config_args.blocksdir {
            config.blocksdir = Some(v);
        }
        if let Some(v) = config_args.rpcconnect {
            config.rpcconnect = Some(v);
        }
        if let Some(v) = config_args.rpcport {
            config.rpcport = Some(v);
        }
        if let Some(v) = config_args.rpccookiefile {
            config.rpccookiefile = Some(v);
        }
        if let Some(v) = config_args.rpcuser {
            config.rpcuser = Some(v);
        }
        if let Some(v) = config_args.rpcpassword {
            config.rpcpassword = Some(v);
        }

        config.check();

        Ok(config)
    }

    fn parse_args() -> Self {
        use lexopt::prelude::*;

        let mut config = Self::default();
        let mut parser = lexopt::Parser::from_env();

        while let Some(arg) = parser.next().unwrap() {
            match arg {
                Short('h') | Long("help") => {
                    Self::print_help();
                    std::process::exit(0);
                }
                Short('V') | Long("version") => {
                    println!("brk {}", env!("CARGO_PKG_VERSION"));
                    std::process::exit(0);
                }
                Long("brkdir") => config.brkdir = Some(parser.value().unwrap().parse().unwrap()),
                Long("brkport") => config.brkport = Some(parser.value().unwrap().parse().unwrap()),
                Long("website") => config.website = Some(parser.value().unwrap().parse().unwrap()),
                Long("bitcoindir") => {
                    config.bitcoindir = Some(parser.value().unwrap().parse().unwrap())
                }
                Long("blocksdir") => {
                    config.blocksdir = Some(parser.value().unwrap().parse().unwrap())
                }
                Long("rpcconnect") => {
                    config.rpcconnect = Some(parser.value().unwrap().parse().unwrap())
                }
                Long("rpcport") => config.rpcport = Some(parser.value().unwrap().parse().unwrap()),
                Long("rpccookiefile") => {
                    config.rpccookiefile = Some(parser.value().unwrap().parse().unwrap())
                }
                Long("rpcuser") => config.rpcuser = Some(parser.value().unwrap().parse().unwrap()),
                Long("rpcpassword") => {
                    config.rpcpassword = Some(parser.value().unwrap().parse().unwrap())
                }
                _ => {
                    eprintln!("{}", arg.unexpected());
                    std::process::exit(1);
                }
            }
        }

        config
    }

    fn print_help() {
        let v = env!("CARGO_PKG_VERSION");

        println!("{} {}", "brk".bold(), v.bright_black());
        println!("Bitcoin Research Kit");
        println!();
        println!("{}", "USAGE:".bold());
        println!(
            "    {} brk {}",
            "[ENV]".bright_black(),
            "[OPTIONS]".bright_black()
        );
        println!();
        println!("{}", "OPTIONS:".bold());
        println!("    -h, --help                Print help");
        println!("    -V, --version             Print version");
        println!();
        println!(
            "    --brkdir {}           Output directory {}",
            "<PATH>".bright_black(),
            "[~/.brk]".bright_black()
        );
        println!(
            "    --brkport {}          Server port {}",
            "<PORT>".bright_black(),
            "[3110]".bright_black()
        );
        println!(
            "    --website {}     Website {}",
            "<BOOL|PATH>".bright_black(),
            "[true]".bright_black()
        );
        println!();
        println!(
            "    --bitcoindir {}       Bitcoin directory {}",
            "<PATH>".bright_black(),
            "[OS default]".bright_black()
        );
        println!(
            "    --blocksdir {}        Blocks directory {}",
            "<PATH>".bright_black(),
            "[<bitcoindir>/blocks]".bright_black()
        );
        println!();
        println!(
            "    --rpcconnect {}         RPC host {}",
            "<IP>".bright_black(),
            "[localhost]".bright_black()
        );
        println!(
            "    --rpcport {}          RPC port {}",
            "<PORT>".bright_black(),
            "[8332]".bright_black()
        );
        println!(
            "    --rpccookiefile {}    RPC cookie file {}",
            "<PATH>".bright_black(),
            "[<bitcoindir>/.cookie]".bright_black()
        );
        println!(
            "    --rpcuser {}      RPC username",
            "<USERNAME>".bright_black()
        );
        println!(
            "    --rpcpassword {}  RPC password",
            "<PASSWORD>".bright_black()
        );
        println!();
        println!("{}", "ENVIRONMENT:".bold());
        println!(
            "    LOG={}               Log level {}",
            "<LEVEL>".bright_black(),
            "[info]".bright_black()
        );
        println!(
            "    RUST_LOG={}          Full log filter",
            "<RULES>".bright_black()
        );
        println!();
        println!("{}", "CONFIG:".bold());
        println!(
            "    Edit {} to persist settings:",
            "~/.brk/config.toml".bright_black()
        );
        println!("    {}", "brkdir = \"/path/to/data\"".bright_black());
        println!(
            "    {}",
            "bitcoindir = \"/path/to/.bitcoin\"".bright_black()
        );
    }

    fn check(&self) {
        if !self.bitcoindir().is_dir() {
            println!("{:?} isn't a valid directory", self.bitcoindir());
            println!("Please use the --bitcoindir parameter to set a valid path.");
            println!("Run the program with '-h' for help.");
            std::process::exit(1);
        }

        if !self.blocksdir().is_dir() {
            println!("{:?} isn't a valid directory", self.blocksdir());
            println!("Please use the --blocksdir parameter to set a valid path.");
            println!("Run the program with '-h' for help.");
            std::process::exit(1);
        }

        if !self.brkdir().is_dir() {
            println!("{:?} isn't a valid directory", self.brkdir());
            println!("Please use the --brkdir parameter to set a valid path.");
            println!("Run the program with '-h' for help.");
            std::process::exit(1);
        }

        if self.rpc_auth().is_err() {
            println!(
                "Unsuccessful authentication with the RPC client.
First make sure that `bitcoind` is running. If it is then please either set --rpccookiefile or --rpcuser and --rpcpassword as the default values seemed to have failed.
Finally, you can run the program with '-h' for help."
            );
            std::process::exit(1);
        }
    }

    fn read(path: &Path) -> Self {
        fs::read_to_string(path).map_or_else(
            |_| Config::default(),
            |contents| toml::from_str(&contents).unwrap_or_default(),
        )
    }

    pub fn rpc(&self) -> Result<Client> {
        Client::new(
            &format!(
                "http://{}:{}",
                self.rpcconnect().unwrap_or(&"localhost".to_string()),
                self.rpcport().unwrap_or(8332)
            ),
            self.rpc_auth()?,
        )
    }

    fn rpc_auth(&self) -> Result<Auth> {
        let cookie = self.path_cookiefile();

        if cookie.is_file() {
            Ok(Auth::CookieFile(cookie))
        } else if self.rpcuser.is_some() && self.rpcpassword.is_some() {
            Ok(Auth::UserPass(
                self.rpcuser.clone().unwrap(),
                self.rpcpassword.clone().unwrap(),
            ))
        } else {
            Err(Error::AuthFailed)
        }
    }

    fn rpcconnect(&self) -> Option<&String> {
        self.rpcconnect.as_ref()
    }

    fn rpcport(&self) -> Option<u16> {
        self.rpcport
    }

    pub fn bitcoindir(&self) -> PathBuf {
        self.bitcoindir
            .as_ref()
            .map_or_else(Client::default_bitcoin_path, |s| fix_user_path(s.as_ref()))
    }

    pub fn blocksdir(&self) -> PathBuf {
        self.blocksdir.as_ref().map_or_else(
            || self.bitcoindir().join("blocks"),
            |blocksdir| fix_user_path(blocksdir.as_str()),
        )
    }

    pub fn brkdir(&self) -> PathBuf {
        self.brkdir
            .as_ref()
            .map_or_else(default_brk_path, |s| fix_user_path(s.as_ref()))
    }

    fn path_cookiefile(&self) -> PathBuf {
        self.rpccookiefile.as_ref().map_or_else(
            || self.bitcoindir().join(".cookie"),
            |p| fix_user_path(p.as_str()),
        )
    }

    pub fn website(&self) -> Website {
        self.website.clone().unwrap_or_default()
    }

    pub fn brkport(&self) -> Option<Port> {
        self.brkport
    }
}

fn default_on_error<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de> + Default,
{
    match T::deserialize(deserializer) {
        Ok(v) => Ok(v),
        Err(_) => Ok(T::default()),
    }
}