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
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, Json(json!({"error": "UpstreamError", "message": "No upstream AppView configured"}))).into_response()
84 );
85 }
86 };
87 let target_url = format!("{}/xrpc/{}", appview_url, method);
88 info!("Proxying GET request to {}", target_url);
89 let client = proxy_client();
90 let mut request_builder = client.get(&target_url).query(params);
91 if let Some(key_bytes) = auth_key_bytes {
92 let appview_did = std::env::var("APPVIEW_DID").unwrap_or_else(|_| "did:web:api.bsky.app".to_string());
93 match crate::auth::create_service_token(auth_did, &appview_did, method, key_bytes) {
94 Ok(service_token) => {
95 request_builder = request_builder.header("Authorization", format!("Bearer {}", service_token));
96 }
97 Err(e) => {
98 error!("Failed to create service token: {:?}", e);
99 return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response());
100 }
101 }
102 }
103 match request_builder.send().await {
104 Ok(resp) => {
105 let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
106 match resp.json::<Value>().await {
107 Ok(body) => Ok((status, body)),
108 Err(e) => {
109 error!("Error parsing proxy response: {:?}", e);
110 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response())
111 }
112 }
113 }
114 Err(e) => {
115 error!("Error sending proxy request: {:?}", e);
116 if e.is_timeout() {
117 Err((StatusCode::GATEWAY_TIMEOUT, Json(json!({"error": "UpstreamTimeout"}))).into_response())
118 } else {
119 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response())
120 }
121 }
122 }
123}
124
125pub async fn get_profile(
126 State(state): State<AppState>,
127 headers: axum::http::HeaderMap,
128 Query(params): Query<GetProfileParams>,
129) -> Response {
130 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
131 let auth_user = if let Some(h) = auth_header {
132 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
133 crate::auth::validate_bearer_token(&state.db, &token).await.ok()
134 } else {
135 None
136 }
137 } else {
138 None
139 };
140 let auth_did = auth_user.as_ref().map(|u| u.did.clone());
141 let auth_key_bytes = auth_user.as_ref().and_then(|u| u.key_bytes.clone());
142 let mut query_params = HashMap::new();
143 query_params.insert("actor".to_string(), params.actor.clone());
144 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfile", &query_params, auth_did.as_deref().unwrap_or(""), auth_key_bytes.as_deref()).await {
145 Ok(r) => r,
146 Err(e) => return e,
147 };
148 if !status.is_success() {
149 return (status, Json(body)).into_response();
150 }
151 let mut profile: ProfileViewDetailed = match serde_json::from_value(body) {
152 Ok(p) => p,
153 Err(_) => {
154 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profile response"}))).into_response();
155 }
156 };
157 if let Some(ref did) = auth_did {
158 if profile.did == *did {
159 if let Some(local_record) = get_local_profile_record(&state, did).await {
160 munge_profile_with_local(&mut profile, &local_record);
161 }
162 }
163 }
164 (StatusCode::OK, Json(profile)).into_response()
165}
166
167pub async fn get_profiles(
168 State(state): State<AppState>,
169 headers: axum::http::HeaderMap,
170 Query(params): Query<GetProfilesParams>,
171) -> Response {
172 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
173 let auth_user = if let Some(h) = auth_header {
174 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
175 crate::auth::validate_bearer_token(&state.db, &token).await.ok()
176 } else {
177 None
178 }
179 } else {
180 None
181 };
182 let auth_did = auth_user.as_ref().map(|u| u.did.clone());
183 let auth_key_bytes = auth_user.as_ref().and_then(|u| u.key_bytes.clone());
184 let mut query_params = HashMap::new();
185 query_params.insert("actors".to_string(), params.actors.clone());
186 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfiles", &query_params, auth_did.as_deref().unwrap_or(""), auth_key_bytes.as_deref()).await {
187 Ok(r) => r,
188 Err(e) => return e,
189 };
190 if !status.is_success() {
191 return (status, Json(body)).into_response();
192 }
193 let mut output: GetProfilesOutput = match serde_json::from_value(body) {
194 Ok(p) => p,
195 Err(_) => {
196 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profiles response"}))).into_response();
197 }
198 };
199 if let Some(ref did) = auth_did {
200 for profile in &mut output.profiles {
201 if profile.did == *did {
202 if let Some(local_record) = get_local_profile_record(&state, did).await {
203 munge_profile_with_local(profile, &local_record);
204 }
205 break;
206 }
207 }
208 }
209 (StatusCode::OK, Json(output)).into_response()
210}