this repo has no description
1use crate::state::AppState; 2use axum::{ 3 extract::{Query, State}, 4 http::StatusCode, 5 response::{IntoResponse, Response}, 6 Json, 7}; 8use jacquard_repo::storage::BlockStore; 9use crate::api::proxy_client::proxy_client; 10use serde::{Deserialize, Serialize}; 11use serde_json::{json, Value}; 12use std::collections::HashMap; 13use tracing::{error, info}; 14#[derive(Deserialize)] 15pub struct GetProfileParams { 16 pub actor: String, 17} 18#[derive(Deserialize)] 19pub struct GetProfilesParams { 20 pub actors: String, 21} 22#[derive(Serialize, Deserialize, Clone)] 23#[serde(rename_all = "camelCase")] 24pub struct ProfileViewDetailed { 25 pub did: String, 26 pub handle: String, 27 #[serde(skip_serializing_if = "Option::is_none")] 28 pub display_name: Option<String>, 29 #[serde(skip_serializing_if = "Option::is_none")] 30 pub description: Option<String>, 31 #[serde(skip_serializing_if = "Option::is_none")] 32 pub avatar: Option<String>, 33 #[serde(skip_serializing_if = "Option::is_none")] 34 pub banner: Option<String>, 35 #[serde(flatten)] 36 pub extra: HashMap<String, Value>, 37} 38#[derive(Serialize, Deserialize)] 39pub struct GetProfilesOutput { 40 pub profiles: Vec<ProfileViewDetailed>, 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} 58fn munge_profile_with_local(profile: &mut ProfileViewDetailed, local_record: &Value) { 59 if let Some(display_name) = local_record.get("displayName").and_then(|v| v.as_str()) { 60 profile.display_name = Some(display_name.to_string()); 61 } 62 if let Some(description) = local_record.get("description").and_then(|v| v.as_str()) { 63 profile.description = Some(description.to_string()); 64 } 65} 66async fn proxy_to_appview( 67 method: &str, 68 params: &HashMap<String, String>, 69 auth_header: Option<&str>, 70) -> Result<(StatusCode, Value), Response> { 71 let appview_url = match std::env::var("APPVIEW_URL") { 72 Ok(url) => url, 73 Err(_) => { 74 return Err( 75 (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "No upstream AppView configured"}))).into_response() 76 ); 77 } 78 }; 79 let target_url = format!("{}/xrpc/{}", appview_url, method); 80 info!("Proxying GET request to {}", target_url); 81 let client = proxy_client(); 82 let mut request_builder = client.get(&target_url).query(params); 83 if let Some(auth) = auth_header { 84 request_builder = request_builder.header("Authorization", auth); 85 } 86 match request_builder.send().await { 87 Ok(resp) => { 88 let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); 89 match resp.json::<Value>().await { 90 Ok(body) => Ok((status, body)), 91 Err(e) => { 92 error!("Error parsing proxy response: {:?}", e); 93 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response()) 94 } 95 } 96 } 97 Err(e) => { 98 error!("Error sending proxy request: {:?}", e); 99 if e.is_timeout() { 100 Err((StatusCode::GATEWAY_TIMEOUT, Json(json!({"error": "UpstreamTimeout"}))).into_response()) 101 } else { 102 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response()) 103 } 104 } 105 } 106} 107pub async fn get_profile( 108 State(state): State<AppState>, 109 headers: axum::http::HeaderMap, 110 Query(params): Query<GetProfileParams>, 111) -> Response { 112 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 113 let auth_did = if let Some(h) = auth_header { 114 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) { 115 match crate::auth::validate_bearer_token(&state.db, &token).await { 116 Ok(user) => Some(user.did), 117 Err(_) => None, 118 } 119 } else { 120 None 121 } 122 } else { 123 None 124 }; 125 let mut query_params = HashMap::new(); 126 query_params.insert("actor".to_string(), params.actor.clone()); 127 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfile", &query_params, auth_header).await { 128 Ok(r) => r, 129 Err(e) => return e, 130 }; 131 if !status.is_success() { 132 return (status, Json(body)).into_response(); 133 } 134 let mut profile: ProfileViewDetailed = match serde_json::from_value(body) { 135 Ok(p) => p, 136 Err(_) => { 137 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profile response"}))).into_response(); 138 } 139 }; 140 if let Some(ref did) = auth_did { 141 if profile.did == *did { 142 if let Some(local_record) = get_local_profile_record(&state, did).await { 143 munge_profile_with_local(&mut profile, &local_record); 144 } 145 } 146 } 147 (StatusCode::OK, Json(profile)).into_response() 148} 149pub async fn get_profiles( 150 State(state): State<AppState>, 151 headers: axum::http::HeaderMap, 152 Query(params): Query<GetProfilesParams>, 153) -> Response { 154 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 155 let auth_did = if let Some(h) = auth_header { 156 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) { 157 match crate::auth::validate_bearer_token(&state.db, &token).await { 158 Ok(user) => Some(user.did), 159 Err(_) => None, 160 } 161 } else { 162 None 163 } 164 } else { 165 None 166 }; 167 let mut query_params = HashMap::new(); 168 query_params.insert("actors".to_string(), params.actors.clone()); 169 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfiles", &query_params, auth_header).await { 170 Ok(r) => r, 171 Err(e) => return e, 172 }; 173 if !status.is_success() { 174 return (status, Json(body)).into_response(); 175 } 176 let mut output: GetProfilesOutput = match serde_json::from_value(body) { 177 Ok(p) => p, 178 Err(_) => { 179 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profiles response"}))).into_response(); 180 } 181 }; 182 if let Some(ref did) = auth_did { 183 for profile in &mut output.profiles { 184 if profile.did == *did { 185 if let Some(local_record) = get_local_profile_record(&state, did).await { 186 munge_profile_with_local(profile, &local_record); 187 } 188 break; 189 } 190 } 191 } 192 (StatusCode::OK, Json(output)).into_response() 193}