diff options
Diffstat (limited to 'adenosine-cli')
| -rw-r--r-- | adenosine-cli/Cargo.toml | 3 | ||||
| -rw-r--r-- | adenosine-cli/README.md | 2 | ||||
| -rw-r--r-- | adenosine-cli/plan.txt | 6 | ||||
| -rw-r--r-- | adenosine-cli/src/lib.rs | 85 | ||||
| -rw-r--r-- | adenosine-cli/src/main.rs | 193 | 
5 files changed, 285 insertions, 4 deletions
| diff --git a/adenosine-cli/Cargo.toml b/adenosine-cli/Cargo.toml index 8d42961..8220e8f 100644 --- a/adenosine-cli/Cargo.toml +++ b/adenosine-cli/Cargo.toml @@ -15,11 +15,14 @@ structopt = "*"  # NOTE: could try 'rustls-tls' feature instead of default native TLS?  reqwest = { version = "0.11", features = ["blocking", "json"] }  serde_json = "*" +log = "*" +env_logger = "*"  # uncertain about these...  anyhow = "1.0"  atty = "0.2"  colored_json = "*" +termcolor = "*"  [package.metadata.deb]  maintainer = "Bryan Newbold <bnewbold@robocracy.org>" diff --git a/adenosine-cli/README.md b/adenosine-cli/README.md index a5a692c..30fb999 100644 --- a/adenosine-cli/README.md +++ b/adenosine-cli/README.md @@ -2,4 +2,4 @@  `adenosine-cli`: atproto command-line client  ============================================ - +Like the `http` command (HTTPie), but for the AT Protocol (<https://atproto.com>). diff --git a/adenosine-cli/plan.txt b/adenosine-cli/plan.txt index fc7b6dc..4eb594c 100644 --- a/adenosine-cli/plan.txt +++ b/adenosine-cli/plan.txt @@ -6,7 +6,7 @@ just talks to the remote.  config variables: -    ATP_SERVICE: prefix, eg http://localhost:1234 +    ATP_HOST: prefix, eg http://localhost:1234      ATP_AUTH_TOKEN: JWT string  at-uri can be either a global `at://` URI string, or a user-local referene of @@ -63,3 +63,7 @@ file; '-' to read a JSON file from stdin.          profile <name>?          search-users <query> +--- + +plan: +- first version which uses entirely schema-less / generic XRPC client diff --git a/adenosine-cli/src/lib.rs b/adenosine-cli/src/lib.rs new file mode 100644 index 0000000..704c1d9 --- /dev/null +++ b/adenosine-cli/src/lib.rs @@ -0,0 +1,85 @@ +use anyhow::anyhow; +pub use anyhow::Result; +use reqwest::header; +use serde_json::Value; +use std::collections::HashMap; +use std::str::FromStr; +use std::time::Duration; + +static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum XrpcMethod { +    Get, +    Post, +} + +impl FromStr for XrpcMethod { +    type Err = anyhow::Error; + +    fn from_str(s: &str) -> Result<Self, Self::Err> { +        match s { +            "get" => Ok(XrpcMethod::Get), +            "post" => Ok(XrpcMethod::Post), +            _ => Err(anyhow!("unknown method: {}", s)), +        } +    } +} + +pub struct XrpcClient { +    http_client: reqwest::blocking::Client, +    host: String, +} + +impl XrpcClient { +    pub fn new(host: String, auth_token: Option<String>) -> Result<Self> { +        let mut headers = header::HeaderMap::new(); +        if let Some(token) = &auth_token { +            let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {}", token))?; +            auth_value.set_sensitive(true); +            headers.insert(header::AUTHORIZATION, auth_value); +        }; + +        let http_client = reqwest::blocking::Client::builder() +            .default_headers(headers) +            .user_agent(APP_USER_AGENT) +            .timeout(Duration::from_secs(30)) +            //.danger_accept_invalid_certs(true) +            .build() +            .expect("ERROR :: Could not build reqwest client"); + +        Ok(XrpcClient { http_client, host }) +    } + +    pub fn get( +        &self, +        nsid: String, +        params: Option<HashMap<String, String>>, +    ) -> Result<Option<Value>> { +        let params: HashMap<String, String> = params.unwrap_or(HashMap::new()); +        let res = self +            .http_client +            .get(format!("{}/xrpc/{}", self.host, nsid)) +            .query(¶ms) +            .send()? +            .error_for_status()?; +        Ok(res.json()?) +    } + +    pub fn post( +        &self, +        nsid: String, +        params: Option<HashMap<String, String>>, +        body: Value, +    ) -> Result<Option<Value>> { +        let params: HashMap<String, String> = params.unwrap_or(HashMap::new()); +        let res = self +            .http_client +            .get(format!("{}/xrpc/{}", self.host, nsid)) +            .query(¶ms) +            .json(&body) +            .send()? +            .error_for_status()?; +        Ok(res.json()?) +    } +} diff --git a/adenosine-cli/src/main.rs b/adenosine-cli/src/main.rs index e7a11a9..0246fd6 100644 --- a/adenosine-cli/src/main.rs +++ b/adenosine-cli/src/main.rs @@ -1,3 +1,192 @@ -fn main() { -    println!("Hello, world!"); +use adenosine_cli::*; +use anyhow::anyhow; +use serde_json::Value; +use std::collections::HashMap; + +use colored_json::to_colored_json_auto; +use log::{self, debug, info}; +use std::io::Write; +use std::path::PathBuf; +use structopt::StructOpt; +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + +#[derive(StructOpt)] +#[structopt(rename_all = "kebab-case", about = "CLI interface for AT Protocol")] +struct Opt { +    #[structopt( +        global = true, +        long = "--host", +        env = "ATP_HOST", +        default_value = "https://localhost:8080" +    )] +    atp_host: String, + +    // API auth tokens can be generated from the account page in the fatcat.wiki web interface +    #[structopt( +        global = true, +        long = "--auth-token", +        env = "ATP_AUTH_TOKEN", +        hide_env_values = true +    )] +    auth_token: Option<String>, + +    /// Log more messages. Pass multiple times for ever more verbosity +    /// +    /// By default, it'll only report errors. Passing `-v` one time also prints +    /// warnings, `-vv` enables info logging, `-vvv` debug, and `-vvvv` trace. +    #[structopt(global = true, long, short = "v", parse(from_occurrences))] +    verbose: i8, + +    #[structopt(long = "--shell-completions", hidden = true)] +    shell_completions: Option<structopt::clap::Shell>, + +    #[structopt(subcommand)] +    cmd: Command, +} + +#[derive(StructOpt)] +enum AccountCommand { +    /// Register a new account +    Register { +        #[structopt(long, short)] +        email: String, + +        #[structopt(long, short)] +        username: String, + +        #[structopt(long, short)] +        password: String, +    }, +    Delete, +    Login, +    Logout, +    Info, +    CreateRevocationKey, +} + +#[derive(StructOpt)] +enum Command { +    Get { +        uri: String, +    }, + +    Xrpc { +        method: XrpcMethod, +        nsid: String, +        params: Option<String>, +    }, + +    /// Sub-commands for managing account +    Account { +        #[structopt(subcommand)] +        cmd: AccountCommand, +    }, + +    /// Summarize connection and authentication with API +    Status, +} + +fn main() -> Result<()> { +    let opt = Opt::from_args(); + +    let log_level = match opt.verbose { +        std::i8::MIN..=-1 => "none", +        0 => "error", +        1 => "warn", +        2 => "info", +        3 => "debug", +        4..=std::i8::MAX => "trace", +    }; +    // hyper logging is very verbose, so crank that down even if everything else is more verbose +    let log_filter = format!("{},hyper=error", log_level); +    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_filter)) +        .format_timestamp(None) +        .init(); + +    debug!("Args parsed, starting up"); + +    #[cfg(windows)] +    colored_json::enable_ansi_support(); + +    if let Some(shell) = opt.shell_completions { +        Opt::clap().gen_completions_to("adenosine", shell, &mut std::io::stdout()); +        std::process::exit(0); +    } + +    if let Err(err) = run(opt) { +        // Be graceful about some errors +        if let Some(io_err) = err.root_cause().downcast_ref::<std::io::Error>() { +            if let std::io::ErrorKind::BrokenPipe = io_err.kind() { +                // presumably due to something like writing to stdout and piped to `head -n10` and +                // stdout was closed +                debug!("got BrokenPipe error, assuming stdout closed as expected and exiting with success"); +                std::process::exit(0); +            } +        } +        let mut color_stderr = StandardStream::stderr(if atty::is(atty::Stream::Stderr) { +            ColorChoice::Auto +        } else { +            ColorChoice::Never +        }); +        color_stderr.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?; +        eprintln!("Error: {:?}", err); +        color_stderr.set_color(&ColorSpec::new())?; +        std::process::exit(1); +    } +    Ok(()) +} + +fn run(opt: Opt) -> Result<()> { +    let xrpc_client = XrpcClient::new(opt.atp_host, opt.auth_token)?; + +    match opt.cmd { +        Command::Xrpc { +            method, +            nsid, +            params, +        } => { +            let body: Value = ().into(); +            let res = match method { +                // XXX: parse params +                XrpcMethod::Get => xrpc_client.get(nsid, None)?, +                XrpcMethod::Post => xrpc_client.post(nsid, None, body)?, +            }; +            if let Some(val) = res { +                writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&val)?)? +            }; +        } +        Command::Get { uri } => { +            println!("GET: {}", uri); +            /* +            let result = specifier.get_from_api(&mut api_client, expand, hide)?; +            if toml { +                writeln!(&mut std::io::stdout(), "{}", result.to_toml_string()?)? +            } else { +                // "if json" +                writeln!( +                    &mut std::io::stdout(), +                    "{}", +                    to_colored_json_auto(&result.to_json_value()?)? +                )? +            } +            */ +        } +        Command::Account { +            cmd: +                AccountCommand::Register { +                    email, +                    username, +                    password, +                }, +        } => { +            println!( +                "REGISTER: email={} username={} password={}", +                email, username, password +            ); +        } +        _ => { +            unimplemented!("some command"); +        } +    } +    Ok(())  } | 
