aboutsummaryrefslogtreecommitdiffstats
path: root/adenosine/src
diff options
context:
space:
mode:
authorbryan newbold <bnewbold@robocracy.org>2023-02-19 17:01:07 -0800
committerbryan newbold <bnewbold@robocracy.org>2023-02-19 17:19:39 -0800
commitec2bf0c54245cd84f492847d2a1e070919b14a53 (patch)
treedbeb5b28c8b7e06eb9ac192d14ea4fdec81bb1e7 /adenosine/src
parentb8ba815b4cafdff48694d14c994e862738d342ef (diff)
downloadadenosine-ec2bf0c54245cd84f492847d2a1e070919b14a53.tar.gz
adenosine-ec2bf0c54245cd84f492847d2a1e070919b14a53.zip
more refactoring of common code and APIs
Diffstat (limited to 'adenosine/src')
-rw-r--r--adenosine/src/app_bsky/mod.rs165
-rw-r--r--adenosine/src/auth.rs37
-rw-r--r--adenosine/src/com_atproto/mod.rs29
-rw-r--r--adenosine/src/com_atproto/repo/mod.rs53
-rw-r--r--adenosine/src/ipld.rs47
-rw-r--r--adenosine/src/lib.rs231
-rw-r--r--adenosine/src/plc.rs (renamed from adenosine/src/did.rs)0
-rw-r--r--adenosine/src/xrpc.rs186
8 files changed, 525 insertions, 223 deletions
diff --git a/adenosine/src/app_bsky/mod.rs b/adenosine/src/app_bsky/mod.rs
new file mode 100644
index 0000000..18a0449
--- /dev/null
+++ b/adenosine/src/app_bsky/mod.rs
@@ -0,0 +1,165 @@
+/// app.bsky types (manually entered)
+use serde_json::Value;
+
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Subject {
+ pub uri: String,
+ // TODO: CID is required
+ pub cid: Option<String>,
+}
+
+/// Generic over Re-post and Like
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct RefRecord {
+ pub subject: Subject,
+ pub createdAt: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FollowSubject {
+ pub did: String,
+ // pub declarationCid: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FollowRecord {
+ pub subject: FollowSubject,
+ pub createdAt: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct ProfileRecord {
+ pub displayName: String,
+ pub description: Option<String>,
+}
+
+// app.bsky.system.actorUser or app.bsky.system.actorScene
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Declaration {
+ pub actorType: String,
+}
+
+// actorType: app.bsky.system.actorUser
+// cid: bafyreid27zk7lbis4zw5fz4podbvbs4fc5ivwji3dmrwa6zggnj4bnd57u
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct DeclRef {
+ pub actorType: String,
+ pub cid: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Profile {
+ pub did: String,
+ pub declaration: DeclRef,
+ pub handle: String,
+ // for simple accounts, 'creator' is just the did
+ pub creator: String,
+ pub displayName: Option<String>,
+ pub description: Option<String>,
+ pub followersCount: u64,
+ pub followsCount: u64,
+ pub membersCount: u64,
+ pub postsCount: u64,
+ pub myState: serde_json::Value,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct GenericFeed {
+ pub feed: Vec<FeedItem>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct User {
+ pub did: String,
+ pub handle: String,
+ pub displayName: Option<String>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FeedItem {
+ pub uri: String,
+ pub cid: String,
+ pub author: User,
+ pub repostedBy: Option<User>,
+ pub record: Value,
+ //pub embed?: RecordEmbed | ExternalEmbed | UnknownEmbed,
+ pub embed: Option<Value>,
+ pub replyCount: u64,
+ pub repostCount: u64,
+ pub upvoteCount: u64,
+ pub downvoteCount: u64,
+ pub indexedAt: String,
+ pub myState: Option<Value>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Post {
+ pub text: String,
+ pub reply: Option<PostReply>,
+ pub createdAt: Option<String>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct PostReply {
+ pub parent: Subject,
+ pub root: Subject,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct PostThread {
+ pub thread: ThreadItem,
+}
+
+// TODO: 'parent' and 'replies' should allow "NotFoundPost" for references that point to an unknown
+// URI
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct ThreadItem {
+ pub uri: String,
+ pub cid: String,
+ pub author: User,
+ pub record: Value,
+ //pub embed?: RecordEmbed | ExternalEmbed | UnknownEmbed,
+ pub embed: Option<Value>,
+ pub parent: Option<Box<ThreadItem>>,
+ pub replyCount: u64,
+ pub replies: Option<Vec<ThreadItem>>,
+ pub repostCount: u64,
+ pub upvoteCount: u64,
+ pub downvoteCount: u64,
+ pub indexedAt: String,
+ pub myState: Option<Value>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct FollowTarget {
+ // TODO: nested follow list?
+ pub subject: Subject,
+ pub did: String,
+ pub handle: String,
+ pub displayName: Option<String>,
+ pub createdAt: Option<String>,
+ pub indexedAt: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Follow {
+ // TODO: nested follow list?
+ pub subject: Subject,
+ pub follows: FollowTarget,
+}
diff --git a/adenosine/src/auth.rs b/adenosine/src/auth.rs
new file mode 100644
index 0000000..ff931ef
--- /dev/null
+++ b/adenosine/src/auth.rs
@@ -0,0 +1,37 @@
+use anyhow::anyhow;
+pub use anyhow::Result;
+use serde_json::Value;
+
+/// 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());
+}
diff --git a/adenosine/src/com_atproto/mod.rs b/adenosine/src/com_atproto/mod.rs
new file mode 100644
index 0000000..8e2317a
--- /dev/null
+++ b/adenosine/src/com_atproto/mod.rs
@@ -0,0 +1,29 @@
+// com.atproto types (manually entered)
+
+pub mod repo;
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
+pub struct AccountRequest {
+ pub email: String,
+ pub handle: String,
+ pub password: String,
+ pub inviteCode: Option<String>,
+ pub recoveryKey: Option<String>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
+pub struct SessionRequest {
+ pub handle: String,
+ pub password: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Session {
+ pub did: String,
+ pub name: String,
+ pub accessJwt: String,
+ pub refreshJwt: String,
+}
diff --git a/adenosine/src/com_atproto/repo/mod.rs b/adenosine/src/com_atproto/repo/mod.rs
new file mode 100644
index 0000000..aa66e98
--- /dev/null
+++ b/adenosine/src/com_atproto/repo/mod.rs
@@ -0,0 +1,53 @@
+/// com.atproto.repo types (manually entered)
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct Describe {
+ pub name: String,
+ pub did: String,
+ pub didDoc: serde_json::Value,
+ pub collections: Vec<String>,
+ pub nameIsCorrect: bool,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct CreateRecord {
+ pub did: String,
+ pub collection: String,
+ pub record: serde_json::Value,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct PutRecord {
+ pub did: String,
+ pub collection: String,
+ pub rkey: String,
+ pub record: serde_json::Value,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct DeleteRecord {
+ pub did: String,
+ pub collection: String,
+ pub rkey: String,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct BatchWriteBody {
+ pub did: String,
+ pub writes: Vec<BatchWrite>,
+}
+
+#[allow(non_snake_case)]
+#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
+pub struct BatchWrite {
+ #[serde(rename = "type")]
+ pub op_type: String,
+ pub collection: String,
+ pub rkey: Option<String>,
+ pub value: serde_json::Value,
+}
diff --git a/adenosine/src/ipld.rs b/adenosine/src/ipld.rs
new file mode 100644
index 0000000..65a56e4
--- /dev/null
+++ b/adenosine/src/ipld.rs
@@ -0,0 +1,47 @@
+use libipld::{Cid, Ipld};
+use serde_json::{json, Value};
+use std::collections::BTreeMap;
+use std::str::FromStr;
+
+/// Intentionally serializing with this instead of DAG-JSON, because ATP schemas don't encode CID
+/// links in any special way, they just pass the CID as a string.
+pub fn ipld_into_json_value(val: Ipld) -> Value {
+ match val {
+ Ipld::Null => Value::Null,
+ Ipld::Bool(b) => Value::Bool(b),
+ Ipld::Integer(v) => json!(v),
+ Ipld::Float(v) => json!(v),
+ Ipld::String(s) => Value::String(s),
+ Ipld::Bytes(b) => Value::String(data_encoding::BASE64_NOPAD.encode(&b)),
+ Ipld::List(l) => Value::Array(l.into_iter().map(ipld_into_json_value).collect()),
+ Ipld::Map(m) => Value::Object(serde_json::Map::from_iter(
+ m.into_iter().map(|(k, v)| (k, ipld_into_json_value(v))),
+ )),
+ Ipld::Link(c) => Value::String(c.to_string()),
+ }
+}
+
+/// Crude reverse generation
+///
+/// Does not handle base64 to bytes, and the link generation is pretty simple (object elements with
+/// key "car"). Numbers always come through as f64 (float).
+pub fn json_value_into_ipld(val: Value) -> Ipld {
+ match val {
+ Value::Null => Ipld::Null,
+ Value::Bool(b) => Ipld::Bool(b),
+ Value::String(s) => Ipld::String(s),
+ // TODO: handle numbers better?
+ Value::Number(v) => Ipld::Float(v.as_f64().unwrap()),
+ Value::Array(l) => Ipld::List(l.into_iter().map(json_value_into_ipld).collect()),
+ Value::Object(m) => {
+ let map: BTreeMap<String, Ipld> = BTreeMap::from_iter(m.into_iter().map(|(k, v)| {
+ if k == "car" && v.is_string() {
+ (k, Ipld::Link(Cid::from_str(v.as_str().unwrap()).unwrap()))
+ } else {
+ (k, json_value_into_ipld(v))
+ }
+ }));
+ Ipld::Map(map)
+ }
+ }
+}
diff --git a/adenosine/src/lib.rs b/adenosine/src/lib.rs
index dc4a1b9..49701a1 100644
--- a/adenosine/src/lib.rs
+++ b/adenosine/src/lib.rs
@@ -1,232 +1,17 @@
-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;
-
+pub mod app_bsky;
+pub mod auth;
pub mod car;
+pub mod com_atproto;
pub mod crypto;
-pub mod did;
pub mod identifiers;
+pub mod ipld;
pub mod mst;
+pub mod plc;
pub mod repo;
-pub mod ucan_p256;
-pub mod vendored;
-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");
+pub mod xrpc;
- 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(&params)
- .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(&params)
- .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(&params);
- 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(&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)?;
- // 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());
-}
+mod ucan_p256;
+mod vendored;
/// Helper to generate the current timestamp as right now, UTC, formatted as an RFC 3339 string.
///
diff --git a/adenosine/src/did.rs b/adenosine/src/plc.rs
index c7d7d10..c7d7d10 100644
--- a/adenosine/src/did.rs
+++ b/adenosine/src/plc.rs
diff --git a/adenosine/src/xrpc.rs b/adenosine/src/xrpc.rs
new file mode 100644
index 0000000..382c7fb
--- /dev/null
+++ b/adenosine/src/xrpc.rs
@@ -0,0 +1,186 @@
+use crate::identifiers::Nsid;
+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: &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(&params)
+ .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(&params)
+ .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(&params);
+ 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(&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
+}