this repo has no description
1use crate::api::proxy_client::proxy_client; 2use crate::state::AppState; 3use axum::{ 4 Json, 5 extract::{Query, State}, 6 http::StatusCode, 7 response::{IntoResponse, Response}, 8}; 9use jacquard_repo::storage::BlockStore; 10use serde::{Deserialize, Serialize}; 11use serde_json::{Value, json}; 12use std::collections::HashMap; 13use tracing::{error, info}; 14 15#[derive(Deserialize)] 16pub struct GetProfileParams { 17 pub actor: String, 18} 19 20#[derive(Deserialize)] 21pub struct GetProfilesParams { 22 pub actors: String, 23} 24 25#[derive(Serialize, Deserialize, Clone)] 26#[serde(rename_all = "camelCase")] 27pub struct ProfileViewDetailed { 28 pub did: String, 29 pub handle: String, 30 #[serde(skip_serializing_if = "Option::is_none")] 31 pub display_name: Option<String>, 32 #[serde(skip_serializing_if = "Option::is_none")] 33 pub description: Option<String>, 34 #[serde(skip_serializing_if = "Option::is_none")] 35 pub avatar: Option<String>, 36 #[serde(skip_serializing_if = "Option::is_none")] 37 pub banner: Option<String>, 38 #[serde(flatten)] 39 pub extra: HashMap<String, Value>, 40} 41 42#[derive(Serialize, Deserialize)] 43pub struct GetProfilesOutput { 44 pub profiles: Vec<ProfileViewDetailed>, 45} 46 47async fn get_local_profile_record(state: &AppState, did: &str) -> Option<Value> { 48 let user_id: uuid::Uuid = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 49 .fetch_optional(&state.db) 50 .await 51 .ok()??; 52 let record_row = sqlx::query!( 53 "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = 'app.bsky.actor.profile' AND rkey = 'self'", 54 user_id 55 ) 56 .fetch_optional(&state.db) 57 .await 58 .ok()??; 59 let cid: cid::Cid = record_row.record_cid.parse().ok()?; 60 let block_bytes = state.block_store.get(&cid).await.ok()??; 61 serde_ipld_dagcbor::from_slice(&block_bytes).ok() 62} 63 64fn munge_profile_with_local(profile: &mut ProfileViewDetailed, local_record: &Value) { 65 if let Some(display_name) = local_record.get("displayName").and_then(|v| v.as_str()) { 66 profile.display_name = Some(display_name.to_string()); 67 } 68 if let Some(description) = local_record.get("description").and_then(|v| v.as_str()) { 69 profile.description = Some(description.to_string()); 70 } 71} 72 73async fn proxy_to_appview( 74 method: &str, 75 params: &HashMap<String, String>, 76 auth_did: &str, 77 auth_key_bytes: Option<&[u8]>, 78) -> Result<(StatusCode, Value), Response> { 79 let appview_url = match std::env::var("APPVIEW_URL") { 80 Ok(url) => url, 81 Err(_) => { 82 return Err(( 83 StatusCode::BAD_GATEWAY, 84 Json( 85 json!({"error": "UpstreamError", "message": "No upstream AppView configured"}), 86 ), 87 ) 88 .into_response()); 89 } 90 }; 91 let target_url = format!("{}/xrpc/{}", appview_url, method); 92 info!("Proxying GET request to {}", target_url); 93 let client = proxy_client(); 94 let mut request_builder = client.get(&target_url).query(params); 95 if let Some(key_bytes) = auth_key_bytes { 96 let appview_did = 97 std::env::var("APPVIEW_DID").unwrap_or_else(|_| "did:web:api.bsky.app".to_string()); 98 match crate::auth::create_service_token(auth_did, &appview_did, method, key_bytes) { 99 Ok(service_token) => { 100 request_builder = 101 request_builder.header("Authorization", format!("Bearer {}", service_token)); 102 } 103 Err(e) => { 104 error!("Failed to create service token: {:?}", e); 105 return Err(( 106 StatusCode::INTERNAL_SERVER_ERROR, 107 Json(json!({"error": "InternalError"})), 108 ) 109 .into_response()); 110 } 111 } 112 } 113 match request_builder.send().await { 114 Ok(resp) => { 115 let status = 116 StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); 117 match resp.json::<Value>().await { 118 Ok(body) => Ok((status, body)), 119 Err(e) => { 120 error!("Error parsing proxy response: {:?}", e); 121 Err(( 122 StatusCode::BAD_GATEWAY, 123 Json(json!({"error": "UpstreamError"})), 124 ) 125 .into_response()) 126 } 127 } 128 } 129 Err(e) => { 130 error!("Error sending proxy request: {:?}", e); 131 if e.is_timeout() { 132 Err(( 133 StatusCode::GATEWAY_TIMEOUT, 134 Json(json!({"error": "UpstreamTimeout"})), 135 ) 136 .into_response()) 137 } else { 138 Err(( 139 StatusCode::BAD_GATEWAY, 140 Json(json!({"error": "UpstreamError"})), 141 ) 142 .into_response()) 143 } 144 } 145 } 146} 147 148pub async fn get_profile( 149 State(state): State<AppState>, 150 headers: axum::http::HeaderMap, 151 Query(params): Query<GetProfileParams>, 152) -> Response { 153 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 154 let auth_user = if let Some(h) = auth_header { 155 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) { 156 crate::auth::validate_bearer_token(&state.db, &token) 157 .await 158 .ok() 159 } else { 160 None 161 } 162 } else { 163 None 164 }; 165 let auth_did = auth_user.as_ref().map(|u| u.did.clone()); 166 let auth_key_bytes = auth_user.as_ref().and_then(|u| u.key_bytes.clone()); 167 let mut query_params = HashMap::new(); 168 query_params.insert("actor".to_string(), params.actor.clone()); 169 let (status, body) = match proxy_to_appview( 170 "app.bsky.actor.getProfile", 171 &query_params, 172 auth_did.as_deref().unwrap_or(""), 173 auth_key_bytes.as_deref(), 174 ) 175 .await 176 { 177 Ok(r) => r, 178 Err(e) => return e, 179 }; 180 if !status.is_success() { 181 return (status, Json(body)).into_response(); 182 } 183 let mut profile: ProfileViewDetailed = match serde_json::from_value(body) { 184 Ok(p) => p, 185 Err(_) => { 186 return ( 187 StatusCode::BAD_GATEWAY, 188 Json(json!({"error": "UpstreamError", "message": "Invalid profile response"})), 189 ) 190 .into_response(); 191 } 192 }; 193 if let Some(ref did) = auth_did 194 && profile.did == *did 195 && let Some(local_record) = get_local_profile_record(&state, did).await { 196 munge_profile_with_local(&mut profile, &local_record); 197 } 198 (StatusCode::OK, Json(profile)).into_response() 199} 200 201pub async fn get_profiles( 202 State(state): State<AppState>, 203 headers: axum::http::HeaderMap, 204 Query(params): Query<GetProfilesParams>, 205) -> Response { 206 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 207 let auth_user = if let Some(h) = auth_header { 208 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) { 209 crate::auth::validate_bearer_token(&state.db, &token) 210 .await 211 .ok() 212 } else { 213 None 214 } 215 } else { 216 None 217 }; 218 let auth_did = auth_user.as_ref().map(|u| u.did.clone()); 219 let auth_key_bytes = auth_user.as_ref().and_then(|u| u.key_bytes.clone()); 220 let mut query_params = HashMap::new(); 221 query_params.insert("actors".to_string(), params.actors.clone()); 222 let (status, body) = match proxy_to_appview( 223 "app.bsky.actor.getProfiles", 224 &query_params, 225 auth_did.as_deref().unwrap_or(""), 226 auth_key_bytes.as_deref(), 227 ) 228 .await 229 { 230 Ok(r) => r, 231 Err(e) => return e, 232 }; 233 if !status.is_success() { 234 return (status, Json(body)).into_response(); 235 } 236 let mut output: GetProfilesOutput = match serde_json::from_value(body) { 237 Ok(p) => p, 238 Err(_) => { 239 return ( 240 StatusCode::BAD_GATEWAY, 241 Json(json!({"error": "UpstreamError", "message": "Invalid profiles response"})), 242 ) 243 .into_response(); 244 } 245 }; 246 if let Some(ref did) = auth_did { 247 for profile in &mut output.profiles { 248 if profile.did == *did { 249 if let Some(local_record) = get_local_profile_record(&state, did).await { 250 munge_profile_with_local(profile, &local_record); 251 } 252 break; 253 } 254 } 255 } 256 (StatusCode::OK, Json(output)).into_response() 257}