diff options
Diffstat (limited to 'adenosine-cli/src/lib.rs')
-rw-r--r-- | adenosine-cli/src/lib.rs | 243 |
1 files changed, 0 insertions, 243 deletions
diff --git a/adenosine-cli/src/lib.rs b/adenosine-cli/src/lib.rs index 0db2cb4..3eb56cc 100644 --- a/adenosine-cli/src/lib.rs +++ b/adenosine-cli/src/lib.rs @@ -2,226 +2,9 @@ use anyhow::anyhow; pub use anyhow::Result; use lazy_static::lazy_static; use regex::Regex; -use reqwest::header; use serde_json::Value; use std::collections::HashMap; use std::str::FromStr; -use std::time::Duration; - -pub mod identifiers; -use identifiers::Nsid; - -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: &Nsid, - params: Option<HashMap<String, String>>, - ) -> Result<Option<Value>> { - log::debug!("XRPC GET endpoint={} params={:?}", nsid, params); - let params: HashMap<String, String> = params.unwrap_or_default(); - let res = self - .http_client - .get(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms) - .send()?; - // TODO: refactor this error handling stuff into single method - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request (400): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } else if res.status() == 500 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Internal Error (500): {}", - 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: &Nsid, - params: Option<HashMap<String, String>>, - output: &mut W, - ) -> Result<u64> { - let params: HashMap<String, String> = params.unwrap_or_default(); - let res = self - .http_client - .get(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms) - .send()?; - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request (400): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } else if res.status() == 500 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Internal Error (500): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } - let mut res = res.error_for_status()?; - Ok(res.copy_to(output)?) - } - - pub fn post( - &self, - nsid: &Nsid, - params: Option<HashMap<String, String>>, - body: Option<Value>, - ) -> Result<Option<Value>> { - let params: HashMap<String, String> = params.unwrap_or_default(); - log::debug!( - "XRPC POST endpoint={} params={:?} body={:?}", - nsid, - params, - body - ); - let mut req = self - .http_client - .post(format!("{}/xrpc/{}", self.host, nsid)) - .query(¶ms); - req = if let Some(b) = body { - req.json(&b) - } else { - req - }; - let res = req.send()?; - if res.status() == 400 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Bad Request (400): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } else if res.status() == 500 { - let val: Value = res.json()?; - return Err(anyhow!( - "XRPC Internal Error (500): {}", - val["message"].as_str().unwrap_or("unknown") - )); - } - let res = res.error_for_status()?; - if res.content_length() == Some(0) { - Ok(None) - } else { - Ok(res.json()?) - } - } - - pub fn post_cbor_from_reader<R: std::io::Read>( - &self, - nsid: &Nsid, - params: Option<HashMap<String, String>>, - input: &mut R, - ) -> Result<Option<Value>> { - let params: HashMap<String, String> = params.unwrap_or_default(); - 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(¶ms) - .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)?; - // trying to also support pulling "aud" as DID; not sure this is actually correct use of - // UCAN/JWT semantics? - let did = obj["sub"] - .as_str() - .or(obj["aud"].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()); - // JWT from atproto ("sub") - assert_eq!( - parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6cGxjOmV4M3NpNTI3Y2QyYW9nYnZpZGtvb296YyIsImlhdCI6MTY2NjgyOTM5M30.UvZgTqvaJICONa1wIUT1bny7u3hqVAqWhWy3qeuyZrE").unwrap(), - "did:plc:ex3si527cd2aogbvidkooozc", - ); - // UCAN from adenosine-pds ("aud") - assert_eq!( - parse_did_from_jwt("eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsInVjdiI6IjAuOS4wLWNhbmFyeSJ9.eyJhdHQiOltdLCJhdWQiOiJkaWQ6cGxjOnM3b25ieWphN2MzeXJzZ3Zob2xrbHM1YiIsImV4cCI6MTY3NTM4Mzg2NywiZmN0IjpbXSwiaXNzIjoiZGlkOmtleTp6RG5hZWRHVGJkb0Frb1NlOG96a3k1WHAzMjZTVFpUSm50aDlHY2dxaTZQYjNzYjczIiwibm5jIjoiTnZURDhENWZjNXFpalIyMWJ1V2Z1ZE02dzlBM2drSy1ac3RtUW03b21pdyIsInByZiI6W119.QwZkb9R17tNhXnY_roqFYgdiIgUnSC18FYWQb3PcH6BU1R5l4W_T4XdACyczPGfM-jAnF2r2loBXDntYVS6N5A").unwrap(), - "did:plc:s7onbyja7c3yrsgvholkls5b", - ); - assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err()); -} /// Represents fields/content specified on the command line. /// @@ -322,29 +105,3 @@ pub fn value_from_fields(fields: Vec<ArgField>) -> Value { } Value::Object(serde_json::map::Map::from_iter(map.into_iter())) } - -/// Helper to generate the current timestamp as right now, UTC, formatted as an RFC 3339 string. -/// -/// Currently, bluesky PDS expects millisecond precision, so we use that. -/// -/// Returns something like "2022-11-22T09:21:15.640Z" -pub fn created_at_now() -> String { - let now = time::OffsetDateTime::now_utc(); - // remove microsecond precision, but retain millisecond precision - let ms = now.millisecond(); - let now = now.replace_microsecond(0).unwrap(); - let now = now.replace_millisecond(ms).unwrap(); - now.format(&time::format_description::well_known::Rfc3339) - .unwrap() -} - -#[test] -fn test_created_at_now() { - // eg: 2022-11-22T09:20:44.123Z - let ts = created_at_now(); - println!("{}", ts); - assert_eq!(&ts[4..5], "-"); - assert_eq!(&ts[7..8], "-"); - assert_eq!(&ts[10..11], "T"); - assert_eq!(&ts[23..24], "Z"); -} |