Highly ambitious ATProtocol AppView service and sdks
at main 118 lines 5.0 kB view raw
1//! GraphQL schema extension for blob uploads 2 3use async_graphql::dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, TypeRef}; 4use async_graphql::Error; 5use base64::engine::general_purpose; 6use base64::Engine; 7 8use crate::atproto_extensions::upload_blob as atproto_upload_blob; 9use crate::auth; 10use crate::graphql::schema_builder::BlobContainer; 11 12/// Creates the BlobUploadResponse GraphQL type 13pub fn create_blob_upload_response_type() -> Object { 14 let mut response = Object::new("BlobUploadResponse"); 15 16 // Return the Blob type instead of JSON to ensure consistent ref field handling 17 response = response.field(Field::new("blob", TypeRef::named_nn("Blob"), |ctx| { 18 FieldFuture::new(async move { 19 // The BlobContainer is passed through from the mutation resolver 20 // The Blob type resolver will handle extracting the fields 21 let container = ctx.parent_value.try_downcast_ref::<BlobContainer>()?; 22 Ok(Some(FieldValue::owned_any(container.clone()))) 23 }) 24 })); 25 26 response 27} 28 29/// Add uploadBlob mutation to the Mutation type 30pub fn add_upload_blob_mutation( 31 mutation: Object, 32 auth_base_url: String, 33) -> Object { 34 mutation.field( 35 Field::new( 36 "uploadBlob", 37 TypeRef::named_nn("BlobUploadResponse"), 38 move |ctx| { 39 let auth_base = auth_base_url.clone(); 40 41 FieldFuture::new(async move { 42 // Get GraphQL context which contains auth info (same pattern as create/update mutations) 43 let gql_ctx = ctx.data::<crate::graphql::GraphQLContext>() 44 .map_err(|_| Error::new("Missing GraphQL context"))?; 45 46 // Check if user is authenticated 47 let token = gql_ctx.auth_token.as_ref() 48 .ok_or_else(|| Error::new("Authentication required"))?; 49 50 // Get data and mimeType arguments 51 let data_base64 = ctx 52 .args 53 .get("data") 54 .ok_or_else(|| Error::new("data argument is required"))? 55 .string()?; 56 57 let mime_type = ctx 58 .args 59 .get("mimeType") 60 .ok_or_else(|| Error::new("mimeType argument is required"))? 61 .string()?; 62 63 // Decode base64 data 64 let blob_data = general_purpose::STANDARD 65 .decode(data_base64) 66 .map_err(|e| Error::new(format!("Invalid base64 data: {}", e)))?; 67 68 // Verify OAuth token to get user info (needed for DID) 69 let user_info = auth::verify_oauth_token_cached( 70 token, 71 &auth_base, 72 gql_ctx.auth_cache.clone(), 73 ) 74 .await 75 .map_err(|e| Error::new(format!("Invalid token: {}", e)))?; 76 77 // Get ATProto DPoP auth and PDS URL for this user 78 let (dpop_auth, pds_url) = auth::get_atproto_auth_for_user_cached( 79 token, 80 &auth_base, 81 gql_ctx.auth_cache.clone(), 82 ) 83 .await 84 .map_err(|e| Error::new(format!("Failed to get ATProto auth: {}", e)))?; 85 86 // Upload blob to user's PDS 87 let http_client = reqwest::Client::new(); 88 let upload_result = atproto_upload_blob( 89 &http_client, 90 &dpop_auth, 91 &pds_url, 92 blob_data, 93 mime_type, 94 ) 95 .await 96 .map_err(|e| Error::new(format!("Failed to upload blob: {}", e)))?; 97 98 // Extract the DID from user info 99 let did = user_info.did.unwrap_or(user_info.sub); 100 101 // Create BlobContainer with flattened ref field (CID string) 102 // This ensures the GraphQL Blob type returns ref as a String, not an object 103 let blob_container = BlobContainer { 104 blob_ref: upload_result.blob.r#ref.link.clone(), // Extract CID from ref.$link 105 mime_type: upload_result.blob.mime_type.clone(), 106 size: upload_result.blob.size as i64, 107 did, 108 }; 109 110 Ok(Some(FieldValue::owned_any(blob_container))) 111 }) 112 }, 113 ) 114 .argument(InputValue::new("data", TypeRef::named_nn(TypeRef::STRING))) 115 .argument(InputValue::new("mimeType", TypeRef::named_nn(TypeRef::STRING))) 116 .description("Upload a blob to the user's AT Protocol repository"), 117 ) 118}