forked from
slices.network/slices
Highly ambitious ATProtocol AppView service and sdks
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}