Highly ambitious ATProtocol AppView service and sdks
at main 127 lines 4.2 kB view raw
1// Extensions to atproto-client for functionality not yet available 2// This module provides additional AT Protocol functions following the same patterns 3 4use crate::errors::BlobUploadError; 5use atproto_client::client::DPoPAuth; 6use atproto_oauth::dpop::{DpopRetry, request_dpop}; 7use reqwest_chain::ChainMiddleware; 8use reqwest_middleware::ClientBuilder; 9use serde::{Deserialize, Serialize}; 10 11/// Response from blob upload 12#[cfg_attr(debug_assertions, derive(Debug))] 13#[derive(Deserialize, Serialize, Clone)] 14pub struct UploadBlobResponse { 15 /// The uploaded blob reference 16 pub blob: BlobRef, 17} 18 19/// Blob reference structure 20#[cfg_attr(debug_assertions, derive(Debug))] 21#[derive(Deserialize, Clone, Serialize)] 22pub struct BlobRef { 23 #[serde(rename = "$type")] 24 pub blob_type: String, 25 pub r#ref: BlobLink, 26 #[serde(rename = "mimeType")] 27 pub mime_type: String, 28 pub size: u64, 29} 30 31/// Blob link structure 32#[cfg_attr(debug_assertions, derive(Debug))] 33#[derive(Deserialize, Clone, Serialize)] 34pub struct BlobLink { 35 #[serde(rename = "$link")] 36 pub link: String, 37} 38 39/// Upload a blob to AT Protocol repository 40/// This follows the same pattern as create_record, put_record, etc. but handles binary data 41pub async fn upload_blob( 42 http_client: &reqwest::Client, 43 dpop_auth: &DPoPAuth, 44 base_url: &str, 45 blob_data: Vec<u8>, 46 mime_type: &str, 47) -> Result<UploadBlobResponse, BlobUploadError> { 48 // Build the URL using standard string formatting 49 let url = format!( 50 "{}/xrpc/com.atproto.repo.uploadBlob", 51 base_url.trim_end_matches('/') 52 ); 53 54 // For blob uploads, we need to use a different approach than post_dpop_json 55 // since we're sending binary data, not JSON 56 // We need to use the same DPoP mechanism but with binary body 57 58 // Use the internal post_dpop function but for binary data 59 post_dpop_binary(http_client, dpop_auth, &url, blob_data, mime_type) 60 .await 61 .and_then(|value| serde_json::from_value(value).map_err(BlobUploadError::JsonParse)) 62} 63 64/// Internal function to make DPoP-authenticated POST request with binary data 65/// This uses the same DpopRetry middleware pattern as atproto-client's post_dpop_json 66async fn post_dpop_binary( 67 http_client: &reqwest::Client, 68 dpop_auth: &DPoPAuth, 69 url: &str, 70 data: Vec<u8>, 71 content_type: &str, 72) -> Result<serde_json::Value, BlobUploadError> { 73 // Use the exact same pattern as atproto-client's post_dpop_json_with_headers 74 let (dpop_proof_token, dpop_proof_header, dpop_proof_claim) = request_dpop( 75 &dpop_auth.dpop_private_key_data, 76 "POST", 77 url, 78 &dpop_auth.oauth_access_token, 79 ) 80 .map_err(|e| BlobUploadError::DPoPProof(e.to_string()))?; 81 82 // Create DpopRetry middleware (same as atproto-client) 83 let dpop_retry = DpopRetry::new( 84 dpop_proof_header.clone(), 85 dpop_proof_claim.clone(), 86 dpop_auth.dpop_private_key_data.clone(), 87 true, 88 ); 89 90 // Build client with DpopRetry middleware (same as atproto-client) 91 let dpop_retry_client = ClientBuilder::new(http_client.clone()) 92 .with(ChainMiddleware::new(dpop_retry.clone())) 93 .build(); 94 95 // Make the request with automatic nonce retry handling 96 let http_response = dpop_retry_client 97 .post(url) 98 .header( 99 "Authorization", 100 format!("DPoP {}", dpop_auth.oauth_access_token), 101 ) 102 .header("DPoP", &dpop_proof_token) 103 .header("Content-Type", content_type) 104 .body(data) 105 .send() 106 .await 107 .map_err(BlobUploadError::HttpRequest)?; 108 109 if !http_response.status().is_success() { 110 let status = http_response.status(); 111 let error_text = http_response 112 .text() 113 .await 114 .unwrap_or_else(|_| "unknown".to_string()); 115 return Err(BlobUploadError::UploadFailed { 116 status: status.as_u16(), 117 message: error_text, 118 }); 119 } 120 121 let value = http_response 122 .json::<serde_json::Value>() 123 .await 124 .map_err(|e| BlobUploadError::HttpRequest(e.into()))?; 125 126 Ok(value) 127}