use anyhow::Context as _;
use super::StoredSession;
use super::crypto::{KeyPair, dpop_nonce_retry, dpop_proof, http_client};
pub async fn get_record_oauth(
session: &mut StoredSession,
collection: &str,
rkey: &str,
) -> anyhow::Result<Option<serde_json::Value>> {
let http = http_client()?;
let dpop_key = KeyPair::from_bytes(&session.dpop_scalar)?;
let mut url = url::Url::parse(&format!(
"{}/xrpc/com.atproto.repo.getRecord",
session.pds_url.trim_end_matches('/')
))
.context("parse getRecord url")?;
url.query_pairs_mut()
.append_pair("repo", session.owner_did.as_str())
.append_pair("collection", collection)
.append_pair("rkey", rkey);
let mut nonce = session.dpop_nonce.clone();
loop {
let proof = dpop_proof(
&dpop_key,
"GET",
url.as_str(),
nonce.as_deref(),
Some(&session.access_token),
)?;
let resp = http
.get(url.clone())
.header("Authorization", format!("DPoP {}", session.access_token))
.header("DPoP", proof)
.send()
.await
.context("send getRecord request")?;
if dpop_nonce_retry(&resp, &mut nonce) {
session.dpop_nonce = nonce.clone();
continue;
}
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if status == reqwest::StatusCode::NOT_FOUND {
return Ok(None);
}
if status == reqwest::StatusCode::BAD_REQUEST
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&text)
&& matches!(
v["error"].as_str(),
Some("RecordNotFound" | "CouldNotFindRecord")
)
{
return Ok(None);
}
anyhow::ensure!(
status.is_success(),
"getRecord failed with HTTP {status}: {text}"
);
let v: serde_json::Value =
serde_json::from_str(&text).context("parse getRecord response")?;
return Ok(Some(v));
}
}
pub async fn create_record_oauth(
session: &mut StoredSession,
collection: &str,
rkey: &str,
record: &serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
pds_post_oauth(
session,
"com.atproto.repo.createRecord",
&serde_json::json!({
"repo": session.owner_did,
"collection": collection,
"rkey": rkey,
"record": record,
}),
)
.await
}
pub async fn put_record_oauth(
session: &mut StoredSession,
collection: &str,
rkey: &str,
record: &serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
pds_post_oauth(
session,
"com.atproto.repo.putRecord",
&serde_json::json!({
"repo": session.owner_did,
"collection": collection,
"rkey": rkey,
"record": record,
}),
)
.await
}
pub async fn delete_record_oauth(
session: &mut StoredSession,
collection: &str,
rkey: &str,
swap_record: Option<&str>,
) -> anyhow::Result<()> {
let mut body = serde_json::json!({
"repo": session.owner_did,
"collection": collection,
"rkey": rkey,
});
if let Some(swap) = swap_record {
body["swapRecord"] = serde_json::json!(swap);
}
let _ = pds_post_oauth(session, "com.atproto.repo.deleteRecord", &body).await?;
Ok(())
}
async fn pds_post_oauth(
session: &mut StoredSession,
nsid: &str,
body: &serde_json::Value,
) -> anyhow::Result<serde_json::Value> {
let http = http_client()?;
let dpop_key = KeyPair::from_bytes(&session.dpop_scalar)?;
let url = format!("{}/xrpc/{nsid}", session.pds_url.trim_end_matches('/'));
let mut nonce = session.dpop_nonce.clone();
loop {
let proof = dpop_proof(
&dpop_key,
"POST",
&url,
nonce.as_deref(),
Some(&session.access_token),
)?;
let resp = http
.post(&url)
.header("Authorization", format!("DPoP {}", session.access_token))
.header("DPoP", proof)
.header("Content-Type", "application/json")
.json(body)
.send()
.await
.with_context(|| format!("send {nsid} request"))?;
if dpop_nonce_retry(&resp, &mut nonce) {
session.dpop_nonce = nonce.clone();
continue;
}
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
anyhow::ensure!(
status.is_success(),
"{nsid} failed with HTTP {status}: {text}"
);
if text.trim().is_empty() {
return Ok(serde_json::json!({}));
}
return serde_json::from_str(&text).with_context(|| format!("parse {nsid} response"));
}
}