aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--adenosine-cli/src/bin/adenosine.rs213
-rw-r--r--adenosine/src/xrpc.rs65
2 files changed, 196 insertions, 82 deletions
diff --git a/adenosine-cli/src/bin/adenosine.rs b/adenosine-cli/src/bin/adenosine.rs
index 86b43eb..f3429b6 100644
--- a/adenosine-cli/src/bin/adenosine.rs
+++ b/adenosine-cli/src/bin/adenosine.rs
@@ -38,6 +38,24 @@ struct Opt {
)]
auth_token: Option<String>,
+ /// Authentication handle (username), for commands that need it
+ #[structopt(
+ global = true,
+ long = "--auth-handle",
+ env = "ATP_AUTH_HANDLE",
+ hide_env_values = true
+ )]
+ auth_handle: Option<String>,
+
+ /// Authentication password, for commands that need it
+ #[structopt(
+ global = true,
+ long = "--auth-password",
+ env = "ATP_AUTH_PASSWORD",
+ hide_env_values = true
+ )]
+ auth_password: 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
@@ -280,8 +298,28 @@ fn print_result_json(result: Option<Value>) -> Result<()> {
Ok(())
}
+/// Helper for endpoints that require authentication.
+///
+/// If an author token already exists, use it to refresh the session. If no auth token is provided,
+/// tries using handle/password to login, resulting in a session auth token.
+///
+/// Returns DID passed from session token if auth was successful, otherwise an Error.
+fn require_auth_did(opt: &Opt, xrpc_client: &mut XrpcClient) -> Result<Did> {
+ if opt.auth_token.is_some() {
+ // TODO: currently clobbers session
+ //xrpc_client.auth_refresh()?;
+ } else if let (Some(handle), Some(passwd)) = (&opt.auth_handle, &opt.auth_password) {
+ xrpc_client.auth_login(handle, passwd)?;
+ } else {
+ return Err(anyhow!(
+ "command requires auth, but have neither token orhandle/password"
+ ));
+ }
+ xrpc_client.auth_did()
+}
+
fn run(opt: Opt) -> Result<()> {
- let xrpc_client = XrpcClient::new(opt.atp_host.clone(), opt.auth_token.clone())?;
+ let mut 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)?)
@@ -366,9 +404,12 @@ fn run(opt: Opt) -> Result<()> {
}
None
}
- Command::Create { collection, fields } => {
- let did = jwt_did.ok_or(anyhow!("need auth token"))?;
- let val = value_from_fields(fields);
+ Command::Create {
+ ref collection,
+ ref fields,
+ } => {
+ let did = require_auth_did(&opt, &mut xrpc_client)?;
+ let val = value_from_fields(fields.clone());
xrpc_client.post(
&Nsid::from_str("com.atproto.repo.createRecord")?,
None,
@@ -380,10 +421,17 @@ fn run(opt: Opt) -> Result<()> {
})),
)?
}
- Command::Update { uri, fields } => {
+ Command::Update {
+ ref uri,
+ ref fields,
+ } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
let did = uri.repository.to_string();
- let collection = uri.collection.ok_or(anyhow!("collection required"))?;
- let rkey = uri.record.ok_or(anyhow!("record key required"))?;
+ let collection = uri
+ .collection
+ .clone()
+ .ok_or(anyhow!("collection required"))?;
+ let rkey = uri.record.clone().ok_or(anyhow!("record key required"))?;
params.insert("did".to_string(), did.clone());
params.insert("collection".to_string(), collection.clone());
params.insert("rkey".to_string(), rkey.clone());
@@ -391,7 +439,7 @@ fn run(opt: Opt) -> Result<()> {
let mut record = xrpc_client
.get(&Nsid::from_str("com.atproto.repo.getRecord")?, Some(params))?
.unwrap_or(json!({}));
- update_value_from_fields(fields, &mut record);
+ update_value_from_fields(fields.clone(), &mut record);
xrpc_client.post(
&Nsid::from_str("com.atproto.repo.putRecord")?,
None,
@@ -403,10 +451,14 @@ fn run(opt: Opt) -> Result<()> {
})),
)?
}
- Command::Delete { uri } => {
+ Command::Delete { ref uri } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
let did = uri.repository.to_string();
- let collection = uri.collection.ok_or(anyhow!("collection required"))?;
- let rkey = uri.record.ok_or(anyhow!("record key required"))?;
+ let collection = uri
+ .collection
+ .clone()
+ .ok_or(anyhow!("collection required"))?;
+ let rkey = uri.record.clone().ok_or(anyhow!("record key required"))?;
xrpc_client.post(
&Nsid::from_str("com.atproto.repo.deleteRecord")?,
None,
@@ -418,15 +470,18 @@ fn run(opt: Opt) -> Result<()> {
)?
}
Command::Xrpc {
- method,
- nsid,
- fields,
+ ref method,
+ ref nsid,
+ ref fields,
} => {
update_params_from_fields(&fields, &mut params);
- let body = value_from_fields(fields);
+ let body = value_from_fields(fields.clone());
match method {
XrpcMethod::Get => xrpc_client.get(&nsid, Some(params))?,
- XrpcMethod::Post => xrpc_client.post(&nsid, Some(params), Some(body))?,
+ XrpcMethod::Post => {
+ require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.post(&nsid, Some(params), Some(body))?
+ }
}
}
Command::Account {
@@ -538,66 +593,84 @@ fn run(opt: Opt) -> Result<()> {
}
Command::Bsky {
cmd: BskyCommand::Timeline,
- } => xrpc_client.get(&Nsid::from_str("app.bsky.feed.getTimeline")?, None)?,
+ } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.get(&Nsid::from_str("app.bsky.feed.getTimeline")?, None)?
+ }
Command::Bsky {
cmd: BskyCommand::Notifications,
- } => xrpc_client.get(&Nsid::from_str("app.bsky.notifications.get")?, None)?,
+ } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.get(&Nsid::from_str("app.bsky.notifications.get")?, None)?
+ }
Command::Bsky {
- cmd: BskyCommand::Post { text },
- } => xrpc_client.post(
- &Nsid::from_str("com.atproto.repo.createRecord")?,
- None,
- Some(json!({
- "did": jwt_did.ok_or(anyhow!("need auth token"))?,
- "collection": "app.bsky.feed.post",
- "record": {
- "text": text,
- "createdAt": created_at_now(),
- },
- })),
- )?,
+ cmd: BskyCommand::Post { ref text },
+ } => {
+ let did = require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.post(
+ &Nsid::from_str("com.atproto.repo.createRecord")?,
+ None,
+ Some(json!({
+ "did": did,
+ "collection": "app.bsky.feed.post",
+ "record": {
+ "text": text,
+ "createdAt": created_at_now(),
+ },
+ })),
+ )?
+ }
Command::Bsky {
- cmd: BskyCommand::Repost { uri },
- } => xrpc_client.post(
- &Nsid::from_str("com.atproto.repo.createRecord")?,
- None,
- Some(json!({
- "did": jwt_did.ok_or(anyhow!("need auth token"))?,
- "collection": "app.bsky.feed.repost",
- "record": {
- "subject": uri.to_string(),
- "createdAt": created_at_now(),
- }
- })),
- )?,
+ cmd: BskyCommand::Repost { ref uri },
+ } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.post(
+ &Nsid::from_str("com.atproto.repo.createRecord")?,
+ None,
+ Some(json!({
+ "did": jwt_did.ok_or(anyhow!("need auth token"))?,
+ "collection": "app.bsky.feed.repost",
+ "record": {
+ "subject": uri.to_string(),
+ "createdAt": created_at_now(),
+ }
+ })),
+ )?
+ }
Command::Bsky {
- cmd: BskyCommand::Like { uri },
- } => xrpc_client.post(
- &Nsid::from_str("com.atproto.repo.createRecord")?,
- None,
- Some(json!({
- "did": jwt_did.ok_or(anyhow!("need auth token"))?,
- "collection": "app.bsky.feed.like",
- "record": {
- "subject": { "uri": uri.to_string(), "cid": "TODO" },
- "createdAt": created_at_now(),
- },
- })),
- )?,
+ cmd: BskyCommand::Like { ref uri },
+ } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.post(
+ &Nsid::from_str("com.atproto.repo.createRecord")?,
+ None,
+ Some(json!({
+ "did": jwt_did.ok_or(anyhow!("need auth token"))?,
+ "collection": "app.bsky.feed.like",
+ "record": {
+ "subject": { "uri": uri.to_string(), "cid": "TODO" },
+ "createdAt": created_at_now(),
+ },
+ })),
+ )?
+ }
Command::Bsky {
- cmd: BskyCommand::Follow { uri },
- } => xrpc_client.post(
- &Nsid::from_str("com.atproto.repo.createRecord")?,
- None,
- Some(json!({
- "did": jwt_did.ok_or(anyhow!("need auth token"))?,
- "collection": "app.bsky.graph.follow",
- "record": {
- "subject": { "did": uri.to_string() },
- "createdAt": created_at_now(),
- }
- })),
- )?,
+ cmd: BskyCommand::Follow { ref uri },
+ } => {
+ require_auth_did(&opt, &mut xrpc_client)?;
+ xrpc_client.post(
+ &Nsid::from_str("com.atproto.repo.createRecord")?,
+ None,
+ Some(json!({
+ "did": jwt_did.ok_or(anyhow!("need auth token"))?,
+ "collection": "app.bsky.graph.follow",
+ "record": {
+ "subject": { "did": uri.to_string() },
+ "createdAt": created_at_now(),
+ }
+ })),
+ )?
+ }
Command::Bsky {
cmd: BskyCommand::Profile { name },
} => {
diff --git a/adenosine/src/xrpc.rs b/adenosine/src/xrpc.rs
index 97caa4d..d958d5e 100644
--- a/adenosine/src/xrpc.rs
+++ b/adenosine/src/xrpc.rs
@@ -1,11 +1,12 @@
-use crate::identifiers::Nsid;
+use crate::identifiers::{Did, Nsid};
+use crate::auth::parse_did_from_jwt;
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;
+use serde_json::{json, Value};
static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
@@ -30,26 +31,64 @@ impl FromStr for XrpcMethod {
pub struct XrpcClient {
http_client: reqwest::blocking::Client,
host: String,
+ auth_token: Option<String>,
+ refresh_token: Option<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 })
+ Ok(XrpcClient { http_client, host, auth_token: auth_token.clone(), refresh_token: auth_token })
+ }
+
+ fn auth_headers(&self) -> reqwest::header::HeaderMap {
+ let mut headers = header::HeaderMap::new();
+ if let Some(token) = &self.auth_token {
+ let mut auth_value = header::HeaderValue::from_str(&format!("Bearer {token}")).expect("header formatting");
+ auth_value.set_sensitive(true);
+ headers.insert(header::AUTHORIZATION, auth_value);
+ };
+ headers
+ }
+
+ /// Creates a new session, and updates current client auth tokens with the result
+ pub fn auth_login(&mut self, handle: &str, password: &str) -> Result <()> {
+ let resp = self.post(
+ &Nsid::from_str("com.atproto.session.create")?,
+ None,
+ Some(json!({
+ "handle": handle,
+ "password": password,
+ })))?;
+ let resp = resp.ok_or(anyhow!("missing session auth info"))?;
+ self.auth_token = resp["accessJwt"].as_str().map(|s| s.to_string());
+ self.refresh_token = resp["refreshJwt"].as_str().map(|s| s.to_string());
+ Ok(())
+ }
+
+ /// Uses refresh token to update auth token
+ pub fn auth_refresh(&mut self) -> Result<()> {
+ self.auth_token = self.refresh_token.clone();
+ let resp = self.post(&Nsid::from_str("com.atproto.session.refresh")?, None, None)?;
+ let resp = resp.ok_or(anyhow!("missing session auth info"))?;
+ self.auth_token = resp["accessJwt"].as_str().map(|s| s.to_string());
+ self.refresh_token = resp["refreshJwt"].as_str().map(|s| s.to_string());
+ Ok(())
+ }
+
+ pub fn auth_did(&self) -> Result<Did> {
+ if let Some(token) = &self.auth_token {
+ return Did::from_str(&parse_did_from_jwt(&token)?)
+ } else {
+ Err(anyhow!("no auth token configured"))
+ }
}
pub fn get(
@@ -62,6 +101,7 @@ impl XrpcClient {
let res = self
.http_client
.get(format!("{}/xrpc/{nsid}", self.host))
+ .headers(self.auth_headers())
.query(&params)
.send()?;
// TODO: refactor this error handling stuff into single method
@@ -92,6 +132,7 @@ impl XrpcClient {
let res = self
.http_client
.get(format!("{}/xrpc/{}", self.host, nsid))
+ .headers(self.auth_headers())
.query(&params)
.send()?;
if res.status() == 400 {
@@ -127,6 +168,7 @@ impl XrpcClient {
let mut req = self
.http_client
.post(format!("{}/xrpc/{}", self.host, nsid))
+ .headers(self.auth_headers())
.query(&params);
req = if let Some(b) = body {
req.json(&b)
@@ -167,6 +209,7 @@ impl XrpcClient {
let res = self
.http_client
.post(format!("{}/xrpc/{}", self.host, nsid))
+ .headers(self.auth_headers())
.query(&params)
.header(reqwest::header::CONTENT_TYPE, "application/cbor")
.body(buf)
@@ -181,6 +224,4 @@ impl XrpcClient {
let res = res.error_for_status()?;
Ok(res.json()?)
}
-
- // reqwest::blocking::Body
}