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}