diff options
| author | Bryan Newbold <bnewbold@robocracy.org> | 2022-10-26 23:28:35 -0700 | 
|---|---|---|
| committer | Bryan Newbold <bnewbold@robocracy.org> | 2022-10-26 23:28:57 -0700 | 
| commit | b4f643ef26d9568044f343ca3537222726768f73 (patch) | |
| tree | 8965a2a75f0ac153cb33d9eea6fcea7ee1360a76 | |
| parent | fea10888148094b64c7ca96ec670b68b55beaef5 (diff) | |
| download | adenosine-b4f643ef26d9568044f343ca3537222726768f73.tar.gz adenosine-b4f643ef26d9568044f343ca3537222726768f73.zip  | |
cli: initial progress
| -rw-r--r-- | Cargo.lock | 66 | ||||
| -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 | ||||
| -rw-r--r-- | extra/adenosine.1 | 57 | ||||
| -rw-r--r-- | extra/adenosine.1.scdoc | 40 | 
8 files changed, 448 insertions, 4 deletions
@@ -9,9 +9,21 @@ dependencies = [   "anyhow",   "atty",   "colored_json", + "env_logger", + "log",   "reqwest",   "serde_json",   "structopt", + "termcolor", +] + +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr",  ]  [[package]] @@ -135,6 +147,19 @@ dependencies = [  ]  [[package]] +name = "env_logger" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]]  name = "fastrand"  version = "1.8.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -299,6 +324,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"  [[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]]  name = "hyper"  version = "0.14.20"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -585,6 +616,23 @@ dependencies = [  ]  [[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]]  name = "remove_dir_all"  version = "0.5.3"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -773,6 +821,15 @@ dependencies = [  ]  [[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]]  name = "textwrap"  version = "0.11.0"  source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1040,6 +1097,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"  checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"  [[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]]  name = "winapi-x86_64-pc-windows-gnu"  version = "0.4.0"  source = "registry+https://github.com/rust-lang/crates.io-index" 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(())  } diff --git a/extra/adenosine.1 b/extra/adenosine.1 new file mode 100644 index 0000000..7a490d3 --- /dev/null +++ b/extra/adenosine.1 @@ -0,0 +1,57 @@ +.\" Generated by scdoc 1.11.1 +.\" Complete documentation for this program is not available as a GNU info page +.ie \n(.g .ds Aq \(aq +.el       .ds Aq ' +.nh +.ad l +.\" Begin generated content: +.TH "adenosine" "1" "2022-10-27" "adenosine Client Tool Manual Page" +.P +.SH NAME +.P +adenosine - command-line client for AT protocol (atproto.\&com) +.P +.SH SYNOPSIS +.P +adenosine [OPTIONS] <COMMAND> <ARGS> +.P +.SH DESCRIPTION +.P +TODO +.P +.SH COMMANDS +.P +.SS Other Commands +.P +.RS 4 +\fBstatus\fR [--json] +.RS 4 +Summarizes connection and authentication to the API server.\& Useful for debugging +.P +.RE +.RE +.SH OPTIONS +.P +\fB-h, --help\fR +.RS 4 +Prints help information +.P +.RE +\fB-V, --version\fR +.RS 4 +Prints version information +.P +.RE +\fB-v, --verbose\fR +.RS 4 +Pass many times for more log output +By default, it'll only report errors.\& Passing `-v` one time also prints warnings, `-vv` enables info logging, `-vvv` debug, and `-vvvv` trace.\& +.P +.RE +\fB--host <atp-host>\fR [env: ATP_HOST] +.P +\fB--auth-token <auth-token>\fR [env: ATP_AUTH_TOKEN] +.P +.SH EXAMPLES +.P +TODO diff --git a/extra/adenosine.1.scdoc b/extra/adenosine.1.scdoc new file mode 100644 index 0000000..2934e19 --- /dev/null +++ b/extra/adenosine.1.scdoc @@ -0,0 +1,40 @@ +adenosine(1) "adenosine Client Tool Manual Page" + +# NAME + +adenosine - command-line client for AT protocol (atproto.com) + +# SYNOPSIS + +adenosine [OPTIONS] <COMMAND> <ARGS> + +# DESCRIPTION + +TODO + +# COMMANDS + +## Other Commands + +	*status* [--json] +		Summarizes connection and authentication to the API server. Useful for debugging + +# OPTIONS + +*-h, --help* +	Prints help information + +*-V, --version* +	Prints version information + +*-v, --verbose* +	Pass many times for more log output +	By default, it'll only report errors. Passing `-v` one time also prints warnings, `-vv` enables info logging, `-vvv` debug, and `-vvvv` trace. + +*--host <atp-host>* [env: ATP_HOST] + +*--auth-token <auth-token>* [env: ATP_AUTH_TOKEN] + +# EXAMPLES + +TODO  | 
