From b4f643ef26d9568044f343ca3537222726768f73 Mon Sep 17 00:00:00 2001 From: Bryan Newbold Date: Wed, 26 Oct 2022 23:28:35 -0700 Subject: cli: initial progress --- adenosine-cli/Cargo.toml | 3 + adenosine-cli/README.md | 2 +- adenosine-cli/plan.txt | 6 +- adenosine-cli/src/lib.rs | 85 ++++++++++++++++++++ adenosine-cli/src/main.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 adenosine-cli/src/lib.rs (limited to 'adenosine-cli') 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 " 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 (). 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 ? search-users +--- + +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 { + 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) -> Result { + 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>, + ) -> Result> { + let params: HashMap = 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>, + body: Value, + ) -> Result> { + let params: HashMap = 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, + + /// 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(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, + }, + + /// 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::() { + 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(()) } -- cgit v1.2.3