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 reqwest::Client; 10use serde::{Deserialize, Serialize}; 11use serde_json::{json, Value}; 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 53 let record_row = sqlx::query!( 54 "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = 'app.bsky.actor.profile' AND rkey = 'self'", 55 user_id 56 ) 57 .fetch_optional(&state.db) 58 .await 59 .ok()??; 60 61 let cid: cid::Cid = record_row.record_cid.parse().ok()?; 62 let block_bytes = state.block_store.get(&cid).await.ok()??; 63 serde_ipld_dagcbor::from_slice(&block_bytes).ok() 64} 65 66fn munge_profile_with_local(profile: &mut ProfileViewDetailed, local_record: &Value) { 67 if let Some(display_name) = local_record.get("displayName").and_then(|v| v.as_str()) { 68 profile.display_name = Some(display_name.to_string()); 69 } 70 if let Some(description) = local_record.get("description").and_then(|v| v.as_str()) { 71 profile.description = Some(description.to_string()); 72 } 73} 74 75async fn proxy_to_appview( 76 method: &str, 77 params: &HashMap<String, String>, 78 auth_header: Option<&str>, 79) -> Result<(StatusCode, Value), Response> { 80 let appview_url = match std::env::var("APPVIEW_URL") { 81 Ok(url) => url, 82 Err(_) => { 83 return Err( 84 (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "No upstream AppView configured"}))).into_response() 85 ); 86 } 87 }; 88 89 let target_url = format!("{}/xrpc/{}", appview_url, method); 90 info!("Proxying GET request to {}", target_url); 91 92 let client = Client::new(); 93 let mut request_builder = client.get(&target_url).query(params); 94 95 if let Some(auth) = auth_header { 96 request_builder = request_builder.header("Authorization", auth); 97 } 98 99 match request_builder.send().await { 100 Ok(resp) => { 101 let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); 102 match resp.json::<Value>().await { 103 Ok(body) => Ok((status, body)), 104 Err(e) => { 105 error!("Error parsing proxy response: {:?}", e); 106 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response()) 107 } 108 } 109 } 110 Err(e) => { 111 error!("Error sending proxy request: {:?}", e); 112 if e.is_timeout() { 113 Err((StatusCode::GATEWAY_TIMEOUT, Json(json!({"error": "UpstreamTimeout"}))).into_response()) 114 } else { 115 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response()) 116 } 117 } 118 } 119} 120 121pub async fn get_profile( 122 State(state): State<AppState>, 123 headers: axum::http::HeaderMap, 124 Query(params): Query<GetProfileParams>, 125) -> Response { 126 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 127 128 let auth_did = auth_header.and_then(|h| { 129 let token = crate::auth::extract_bearer_token_from_header(Some(h))?; 130 crate::auth::get_did_from_token(&token).ok() 131 }); 132 133 let mut query_params = HashMap::new(); 134 query_params.insert("actor".to_string(), params.actor.clone()); 135 136 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfile", &query_params, auth_header).await { 137 Ok(r) => r, 138 Err(e) => return e, 139 }; 140 141 if !status.is_success() { 142 return (status, Json(body)).into_response(); 143 } 144 145 let mut profile: ProfileViewDetailed = match serde_json::from_value(body) { 146 Ok(p) => p, 147 Err(_) => { 148 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profile response"}))).into_response(); 149 } 150 }; 151 152 if let Some(ref did) = auth_did { 153 if profile.did == *did { 154 if let Some(local_record) = get_local_profile_record(&state, did).await { 155 munge_profile_with_local(&mut profile, &local_record); 156 } 157 } 158 } 159 160 (StatusCode::OK, Json(profile)).into_response() 161} 162 163pub async fn get_profiles( 164 State(state): State<AppState>, 165 headers: axum::http::HeaderMap, 166 Query(params): Query<GetProfilesParams>, 167) -> Response { 168 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok()); 169 170 let auth_did = auth_header.and_then(|h| { 171 let token = crate::auth::extract_bearer_token_from_header(Some(h))?; 172 crate::auth::get_did_from_token(&token).ok() 173 }); 174 175 let mut query_params = HashMap::new(); 176 query_params.insert("actors".to_string(), params.actors.clone()); 177 178 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfiles", &query_params, auth_header).await { 179 Ok(r) => r, 180 Err(e) => return e, 181 }; 182 183 if !status.is_success() { 184 return (status, Json(body)).into_response(); 185 } 186 187 let mut output: GetProfilesOutput = match serde_json::from_value(body) { 188 Ok(p) => p, 189 Err(_) => { 190 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profiles response"}))).into_response(); 191 } 192 }; 193 194 if let Some(ref did) = auth_did { 195 for profile in &mut output.profiles { 196 if profile.did == *did { 197 if let Some(local_record) = get_local_profile_record(&state, did).await { 198 munge_profile_with_local(profile, &local_record); 199 } 200 break; 201 } 202 } 203 } 204 205 (StatusCode::OK, Json(output)).into_response() 206}