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