use serde::de::DeserializeOwned; use serde::Serialize; use std::fmt; #[derive(Debug)] pub enum AdminProxyError { RequestFailed(String), PdsError { status: u16, error: String, message: String, }, ParseError(String), } impl fmt::Display for AdminProxyError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AdminProxyError::RequestFailed(msg) => write!(f, "Request failed: {msg}"), AdminProxyError::PdsError { status, error, message, } => write!(f, "PDS error ({status}): {error} - {message}"), AdminProxyError::ParseError(msg) => write!(f, "Parse error: {msg}"), } } } /// Make an authenticated GET request to a PDS XRPC endpoint. pub async fn admin_xrpc_get( pds_base_url: &str, admin_password: &str, endpoint: &str, query_params: &[(&str, &str)], ) -> Result { let url = format!( "{}/xrpc/{}", pds_base_url.trim_end_matches('/'), endpoint ); let client = reqwest::Client::new(); let resp = client .get(&url) .query(query_params) .basic_auth("admin", Some(admin_password)) .send() .await .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; if !resp.status().is_success() { let status = resp.status().as_u16(); let body = resp.text().await.unwrap_or_default(); if let Ok(err_json) = serde_json::from_str::(&body) { return Err(AdminProxyError::PdsError { status, error: err_json["error"] .as_str() .unwrap_or("Unknown") .to_string(), message: err_json["message"] .as_str() .unwrap_or(&body) .to_string(), }); } return Err(AdminProxyError::PdsError { status, error: "Unknown".to_string(), message: body, }); } resp.json::() .await .map_err(|e| AdminProxyError::ParseError(e.to_string())) } /// Make an authenticated POST request to a PDS XRPC endpoint. pub async fn admin_xrpc_post( pds_base_url: &str, admin_password: &str, endpoint: &str, body: &T, ) -> Result { let url = format!( "{}/xrpc/{}", pds_base_url.trim_end_matches('/'), endpoint ); let client = reqwest::Client::new(); let resp = client .post(&url) .json(body) .basic_auth("admin", Some(admin_password)) .send() .await .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; if !resp.status().is_success() { let status = resp.status().as_u16(); let body = resp.text().await.unwrap_or_default(); if let Ok(err_json) = serde_json::from_str::(&body) { return Err(AdminProxyError::PdsError { status, error: err_json["error"] .as_str() .unwrap_or("Unknown") .to_string(), message: err_json["message"] .as_str() .unwrap_or(&body) .to_string(), }); } return Err(AdminProxyError::PdsError { status, error: "Unknown".to_string(), message: body, }); } resp.json::() .await .map_err(|e| AdminProxyError::ParseError(e.to_string())) } /// Make an authenticated POST request that returns no meaningful body (just success/failure). pub async fn admin_xrpc_post_no_response( pds_base_url: &str, admin_password: &str, endpoint: &str, body: &T, ) -> Result<(), AdminProxyError> { let url = format!( "{}/xrpc/{}", pds_base_url.trim_end_matches('/'), endpoint ); let client = reqwest::Client::new(); let resp = client .post(&url) .json(body) .basic_auth("admin", Some(admin_password)) .send() .await .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; if !resp.status().is_success() { let status = resp.status().as_u16(); let body = resp.text().await.unwrap_or_default(); if let Ok(err_json) = serde_json::from_str::(&body) { return Err(AdminProxyError::PdsError { status, error: err_json["error"] .as_str() .unwrap_or("Unknown") .to_string(), message: err_json["message"] .as_str() .unwrap_or(&body) .to_string(), }); } return Err(AdminProxyError::PdsError { status, error: "Unknown".to_string(), message: body, }); } Ok(()) } /// Make an unauthenticated GET request that returns text (e.g., _health). pub async fn get_text( pds_base_url: &str, endpoint: &str, ) -> Result { let url = format!( "{}/{}", pds_base_url.trim_end_matches('/'), endpoint ); let client = reqwest::Client::new(); let resp = client .get(&url) .send() .await .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; if !resp.status().is_success() { let status = resp.status().as_u16(); let body = resp.text().await.unwrap_or_default(); return Err(AdminProxyError::PdsError { status, error: "Unknown".to_string(), message: body, }); } resp.text() .await .map_err(|e| AdminProxyError::ParseError(e.to_string())) } /// Make an unauthenticated POST request to an arbitrary base URL (e.g., relay). pub async fn public_xrpc_post( base_url: &str, endpoint: &str, body: &T, ) -> Result<(), AdminProxyError> { let url = format!( "{}/xrpc/{}", base_url.trim_end_matches('/'), endpoint ); let client = reqwest::Client::new(); let resp = client .post(&url) .json(body) .send() .await .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; if !resp.status().is_success() { let status = resp.status().as_u16(); let body = resp.text().await.unwrap_or_default(); if let Ok(err_json) = serde_json::from_str::(&body) { return Err(AdminProxyError::PdsError { status, error: err_json["error"] .as_str() .unwrap_or("Unknown") .to_string(), message: err_json["message"] .as_str() .unwrap_or(&body) .to_string(), }); } return Err(AdminProxyError::PdsError { status, error: "Unknown".to_string(), message: body, }); } Ok(()) } /// Make an unauthenticated GET request with JSON response parsing. pub async fn public_xrpc_get( pds_base_url: &str, endpoint: &str, query_params: &[(&str, &str)], ) -> Result { let url = format!( "{}/xrpc/{}", pds_base_url.trim_end_matches('/'), endpoint ); let client = reqwest::Client::new(); let resp = client .get(&url) .query(query_params) .send() .await .map_err(|e| AdminProxyError::RequestFailed(e.to_string()))?; if !resp.status().is_success() { let status = resp.status().as_u16(); let body = resp.text().await.unwrap_or_default(); if let Ok(err_json) = serde_json::from_str::(&body) { return Err(AdminProxyError::PdsError { status, error: err_json["error"] .as_str() .unwrap_or("Unknown") .to_string(), message: err_json["message"] .as_str() .unwrap_or(&body) .to_string(), }); } return Err(AdminProxyError::PdsError { status, error: "Unknown".to_string(), message: body, }); } resp.json::() .await .map_err(|e| AdminProxyError::ParseError(e.to_string())) }