aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--adenosine-cli/Cargo.toml5
-rw-r--r--adenosine-cli/TODO1
-rw-r--r--adenosine-cli/plan.txt6
-rw-r--r--adenosine-cli/src/lib.rs108
-rw-r--r--adenosine-cli/src/main.rs322
6 files changed, 396 insertions, 47 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 40bc638..3c86937 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,7 @@ version = "0.1.0-dev.0"
dependencies = [
"anyhow",
"atty",
+ "base64",
"colored_json",
"env_logger",
"log",
diff --git a/adenosine-cli/Cargo.toml b/adenosine-cli/Cargo.toml
index 8220e8f..971f59a 100644
--- a/adenosine-cli/Cargo.toml
+++ b/adenosine-cli/Cargo.toml
@@ -15,14 +15,15 @@ structopt = "*"
# NOTE: could try 'rustls-tls' feature instead of default native TLS?
reqwest = { version = "0.11", features = ["blocking", "json"] }
serde_json = "*"
-log = "*"
-env_logger = "*"
+base64 = "*"
# uncertain about these...
anyhow = "1.0"
atty = "0.2"
colored_json = "*"
termcolor = "*"
+log = "*"
+env_logger = "*"
[package.metadata.deb]
maintainer = "Bryan Newbold <bnewbold@robocracy.org>"
diff --git a/adenosine-cli/TODO b/adenosine-cli/TODO
new file mode 100644
index 0000000..4d582e8
--- /dev/null
+++ b/adenosine-cli/TODO
@@ -0,0 +1 @@
+- invite codes in account registration
diff --git a/adenosine-cli/plan.txt b/adenosine-cli/plan.txt
index 4eb594c..668a746 100644
--- a/adenosine-cli/plan.txt
+++ b/adenosine-cli/plan.txt
@@ -40,9 +40,9 @@ file; '-' to read a JSON file from stdin.
create-revocation-key
repo
- root
- export <at-uri>?
- import <in-cbor-filename> [--from <cid>]
+ root <did>?
+ export <did>?
+ import --did <did> <in-cbor-filename> [--from <cid>]
xrpc get|post <nsid> [<params>]+
=> generic method
diff --git a/adenosine-cli/src/lib.rs b/adenosine-cli/src/lib.rs
index 704c1d9..06fade5 100644
--- a/adenosine-cli/src/lib.rs
+++ b/adenosine-cli/src/lib.rs
@@ -53,7 +53,7 @@ impl XrpcClient {
pub fn get(
&self,
- nsid: String,
+ nsid: &str,
params: Option<HashMap<String, String>>,
) -> Result<Option<Value>> {
let params: HashMap<String, String> = params.unwrap_or(HashMap::new());
@@ -61,25 +61,119 @@ impl XrpcClient {
.http_client
.get(format!("{}/xrpc/{}", self.host, nsid))
.query(&params)
- .send()?
- .error_for_status()?;
+ .send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request: {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let res = res.error_for_status()?;
Ok(res.json()?)
}
+ pub fn get_to_writer<W: std::io::Write>(
+ &self,
+ nsid: &str,
+ params: Option<HashMap<String, String>>,
+ output: &mut W,
+ ) -> Result<u64> {
+ let params: HashMap<String, String> = params.unwrap_or(HashMap::new());
+ let res = self
+ .http_client
+ .get(format!("{}/xrpc/{}", self.host, nsid))
+ .query(&params)
+ .send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request: {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let mut res = res.error_for_status()?;
+ Ok(res.copy_to(output)?)
+ }
+
pub fn post(
&self,
- nsid: String,
+ nsid: &str,
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))
+ .post(format!("{}/xrpc/{}", self.host, nsid))
.query(&params)
.json(&body)
- .send()?
- .error_for_status()?;
+ .send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request: {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let res = res.error_for_status()?;
Ok(res.json()?)
}
+
+ pub fn post_cbor_from_reader<R: std::io::Read>(
+ &self,
+ nsid: &str,
+ params: Option<HashMap<String, String>>,
+ input: &mut R,
+ ) -> Result<Option<Value>> {
+ let params: HashMap<String, String> = params.unwrap_or(HashMap::new());
+ let mut buf: Vec<u8> = Vec::new();
+ input.read_to_end(&mut buf)?;
+ let res = self
+ .http_client
+ .post(format!("{}/xrpc/{}", self.host, nsid))
+ .query(&params)
+ .header(reqwest::header::CONTENT_TYPE, "application/cbor")
+ .body(buf)
+ .send()?;
+ if res.status() == 400 {
+ let val: Value = res.json()?;
+ return Err(anyhow!(
+ "XRPC Bad Request: {}",
+ val["message"].as_str().unwrap_or("unknown")
+ ));
+ }
+ let res = res.error_for_status()?;
+ Ok(res.json()?)
+ }
+
+ // reqwest::blocking::Body
+}
+
+/// Tries to parse a DID internal identifier from a JWT (as base64-encoded token)
+pub fn parse_did_from_jwt(jwt: &str) -> Result<String> {
+ let second_b64 = jwt.split(".").nth(1).ok_or(anyhow!("couldn't parse JWT"))?;
+ let second_json: Vec<u8> = base64::decode_config(second_b64, base64::URL_SAFE)?;
+ let obj: Value = serde_json::from_slice(&second_json)?;
+ let did = obj["sub"]
+ .as_str()
+ .ok_or(anyhow!("couldn't find DID subject in JWT"))?
+ .to_string();
+ if !did.starts_with("did:") {
+ return Err(anyhow!("couldn't find DID subject in JWT"));
+ }
+ Ok(did)
}
+
+#[test]
+fn test_parse_jwt() {
+ assert!(parse_did_from_jwt(".").is_err());
+ assert_eq!(
+ parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(),
+ "did:plc:ex3si527cd2aogbvidkooozc",
+ );
+ assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err());
+}
+
+// TODO: parse at-uri
+// at://did:plc:ltk4reuh7rkoy2frnueetpb5/app.bsky.follow/3jg23pbmlhc2a
diff --git a/adenosine-cli/src/main.rs b/adenosine-cli/src/main.rs
index 0246fd6..eea16bc 100644
--- a/adenosine-cli/src/main.rs
+++ b/adenosine-cli/src/main.rs
@@ -1,12 +1,11 @@
use adenosine_cli::*;
use anyhow::anyhow;
-use serde_json::Value;
+use serde_json::{json, Value};
use std::collections::HashMap;
use colored_json::to_colored_json_auto;
-use log::{self, debug, info};
+use log::{self, debug};
use std::io::Write;
-use std::path::PathBuf;
use structopt::StructOpt;
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
@@ -58,10 +57,57 @@ enum AccountCommand {
password: String,
},
Delete,
- Login,
+ Login {
+ #[structopt(long, short)]
+ username: String,
+
+ #[structopt(long, short)]
+ password: String,
+ },
Logout,
Info,
- CreateRevocationKey,
+ // TODO: CreateRevocationKey,
+}
+
+#[derive(StructOpt)]
+enum RepoCommand {
+ Root {
+ #[structopt(long)]
+ did: String,
+ },
+ Export {
+ #[structopt(long)]
+ did: String,
+ #[structopt(long)]
+ from: Option<String>,
+ },
+ Import {
+ // XXX: either path or stdin
+ #[structopt(long)]
+ did: String,
+ },
+}
+
+#[derive(StructOpt)]
+enum BskyCommand {
+ Feed { name: Option<String> },
+ Notifications,
+ Post { text: String },
+ Repost { uri: String },
+ Like { uri: String },
+ // TODO: Repost { uri: String, },
+ Follow { uri: String },
+ // TODO: Unfollow { uri: String, },
+ /*
+ Follows {
+ name: String,
+ },
+ Followers {
+ name: String,
+ },
+ */
+ Profile { name: String },
+ SearchUsers { query: String },
}
#[derive(StructOpt)]
@@ -70,6 +116,30 @@ enum Command {
uri: String,
},
+ Ls {
+ uri: String,
+ },
+
+ Create {
+ collection: String,
+ body: String,
+ },
+ Update {
+ uri: String,
+ params: String,
+ },
+ Delete {
+ uri: String,
+ },
+
+ Describe {
+ name: String,
+ },
+
+ Resolve {
+ name: String,
+ },
+
Xrpc {
method: XrpcMethod,
nsid: String,
@@ -82,6 +152,16 @@ enum Command {
cmd: AccountCommand,
},
+ Repo {
+ #[structopt(subcommand)]
+ cmd: RepoCommand,
+ },
+
+ Bsky {
+ #[structopt(subcommand)]
+ cmd: BskyCommand,
+ },
+
/// Summarize connection and authentication with API
Status,
}
@@ -136,40 +216,73 @@ fn main() -> Result<()> {
Ok(())
}
+fn print_result_json(result: Option<Value>) -> Result<()> {
+ if let Some(val) = result {
+ writeln!(&mut std::io::stdout(), "{}", to_colored_json_auto(&val)?)?
+ };
+ Ok(())
+}
+
fn run(opt: Opt) -> Result<()> {
- let xrpc_client = XrpcClient::new(opt.atp_host, opt.auth_token)?;
+ let xrpc_client = XrpcClient::new(opt.atp_host.clone(), opt.auth_token.clone())?;
+ let mut params: HashMap<String, String> = HashMap::new();
+ let jwt_did: Option<String> = if let Some(ref token) = opt.auth_token {
+ Some(parse_did_from_jwt(token)?)
+ } else {
+ None
+ };
- match opt.cmd {
+ let result = match opt.cmd {
+ Command::Status => {
+ // XXX
+ println!("Configuration");
+ println!(" ATP_HOST: {}", opt.atp_host);
+ if opt.auth_token.is_some() {
+ println!(" ATP_AUTH_TOKEN: <configured>");
+ } else {
+ println!(" ATP_AUTH_TOKEN:");
+ }
+ // TODO: parse JWT?
+ // TODO: connection, auth check
+ // TODO: account username, did, etc
+ None
+ }
+ Command::Describe { name } => {
+ params.insert("user".to_string(), name);
+ xrpc_client.get("com.atproto.repoDescribe", Some(params))?
+ }
+ Command::Resolve { name } => {
+ let mut params: HashMap<String, String> = HashMap::new();
+ params.insert("name".to_string(), name);
+ xrpc_client.get("com.atproto.resolveName", Some(params))?
+ }
+ Command::Get { uri } => {
+ println!("GET: {}", uri);
+ None
+ }
+ Command::Ls { uri } => {
+ unimplemented!()
+ }
+ Command::Create { collection, body } => {
+ unimplemented!()
+ }
+ Command::Update { uri, params } => {
+ unimplemented!()
+ }
+ Command::Delete { uri } => {
+ unimplemented!()
+ }
Command::Xrpc {
method,
nsid,
params,
} => {
let body: Value = ().into();
- let res = match method {
+ 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()?)?
- )?
+ XrpcMethod::Get => xrpc_client.get(&nsid, None)?,
+ XrpcMethod::Post => xrpc_client.post(&nsid, None, body)?,
}
- */
}
Command::Account {
cmd:
@@ -178,15 +291,154 @@ fn run(opt: Opt) -> Result<()> {
username,
password,
},
+ } => xrpc_client.post(
+ "com.atproto.createAccount",
+ None,
+ json!({
+ "email": email,
+ "username": username,
+ "password": password,
+ }),
+ )?,
+ Command::Account {
+ cmd: AccountCommand::Login { username, password },
+ } => xrpc_client.post(
+ "com.atproto.createSession",
+ None,
+ json!({
+ "username": username,
+ "password": password,
+ }),
+ )?,
+ Command::Account {
+ cmd: AccountCommand::Logout,
+ } => xrpc_client.post("com.atproto.deleteSession", None, json!({}))?,
+ Command::Account {
+ cmd: AccountCommand::Delete,
+ } => xrpc_client.post("com.atproto.deleteAccount", None, json!({}))?,
+ Command::Account {
+ cmd: AccountCommand::Info,
+ } => xrpc_client.get("com.atproto.getAccount", None)?,
+ Command::Repo {
+ cmd: RepoCommand::Root { did },
+ } => {
+ params.insert("did".to_string(), did);
+ xrpc_client.get("com.atproto.syncGetRoot", Some(params))?
+ }
+ Command::Repo {
+ cmd: RepoCommand::Export { did, from },
+ } => {
+ params.insert("did".to_string(), did);
+ if let Some(from) = from {
+ params.insert("from".to_string(), from);
+ };
+ xrpc_client.get_to_writer(
+ "com.atproto.syncGetRepo",
+ Some(params),
+ &mut std::io::stdout(),
+ )?;
+ None
+ }
+ Command::Repo {
+ cmd: RepoCommand::Import { did },
+ } => {
+ params.insert("did".to_string(), did);
+ xrpc_client.post_cbor_from_reader(
+ "com.atproto.syncUpdateRepo",
+ Some(params),
+ &mut std::io::stdin(),
+ )?
+ }
+ Command::Bsky {
+ cmd: BskyCommand::Feed { name },
} => {
- println!(
- "REGISTER: email={} username={} password={}",
- email, username, password
+ if let Some(name) = name {
+ params.insert("author".to_string(), name);
+ xrpc_client.get("app.bsky.getAuthorFeed", Some(params))?
+ } else {
+ xrpc_client.get("app.bsky.getHomeFeed", None)?
+ }
+ }
+ Command::Bsky {
+ cmd: BskyCommand::Notifications,
+ } => xrpc_client.get("app.bsky.getNotifications", None)?,
+ Command::Bsky {
+ cmd: BskyCommand::Post { text },
+ } => {
+ params.insert(
+ "did".to_string(),
+ jwt_did.ok_or(anyhow!("need auth token"))?,
);
+ params.insert("collection".to_string(), "app.bsky.post".to_string());
+ xrpc_client.post(
+ "com.atproto.repoCreateRecord",
+ Some(params),
+ json!({
+ "text": text,
+ }),
+ )?
}
- _ => {
- unimplemented!("some command");
+ Command::Bsky {
+ cmd: BskyCommand::Repost { uri },
+ } => {
+ params.insert(
+ "did".to_string(),
+ jwt_did.ok_or(anyhow!("need auth token"))?,
+ );
+ params.insert("collection".to_string(), "app.bsky.repost".to_string());
+ xrpc_client.post(
+ "com.atproto.repoCreateRecord",
+ Some(params),
+ json!({
+ "subject": uri,
+ }),
+ )?
}
- }
+ Command::Bsky {
+ cmd: BskyCommand::Like { uri },
+ } => {
+ params.insert(
+ "did".to_string(),
+ jwt_did.ok_or(anyhow!("need auth token"))?,
+ );
+ params.insert("collection".to_string(), "app.bsky.like".to_string());
+ xrpc_client.post(
+ "com.atproto.repoCreateRecord",
+ Some(params),
+ json!({
+ "subject": uri,
+ }),
+ )?
+ }
+ Command::Bsky {
+ cmd: BskyCommand::Follow { uri },
+ } => {
+ params.insert(
+ "did".to_string(),
+ jwt_did.ok_or(anyhow!("need auth token"))?,
+ );
+ params.insert("collection".to_string(), "app.bsky.follow".to_string());
+ xrpc_client.post(
+ "com.atproto.repoCreateRecord",
+ Some(params),
+ json!({
+ "subject": uri,
+ }),
+ )?
+ }
+ Command::Bsky {
+ cmd: BskyCommand::Profile { name },
+ } => {
+ params.insert("name".to_string(), name);
+ xrpc_client.get("app.bsky.getProfile", Some(params))?
+ }
+ Command::Bsky {
+ cmd: BskyCommand::SearchUsers { query },
+ } => {
+ params.insert("term".to_string(), query);
+ xrpc_client.get("app.bsky.getUsersSearch", Some(params))?
+ }
+ };
+ print_result_json(result)?;
Ok(())
}