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