summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBryan Newbold <bnewbold@robocracy.org>2022-10-26 23:28:35 -0700
committerBryan Newbold <bnewbold@robocracy.org>2022-10-26 23:28:57 -0700
commitb4f643ef26d9568044f343ca3537222726768f73 (patch)
tree8965a2a75f0ac153cb33d9eea6fcea7ee1360a76
parentfea10888148094b64c7ca96ec670b68b55beaef5 (diff)
downloadadenosine-b4f643ef26d9568044f343ca3537222726768f73.tar.gz
adenosine-b4f643ef26d9568044f343ca3537222726768f73.zip
cli: initial progress
-rw-r--r--Cargo.lock66
-rw-r--r--adenosine-cli/Cargo.toml3
-rw-r--r--adenosine-cli/README.md2
-rw-r--r--adenosine-cli/plan.txt6
-rw-r--r--adenosine-cli/src/lib.rs85
-rw-r--r--adenosine-cli/src/main.rs193
-rw-r--r--extra/adenosine.157
-rw-r--r--extra/adenosine.1.scdoc40
8 files changed, 448 insertions, 4 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 713165c..40bc638 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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(&params)
+ .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(&params)
+ .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