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 = if let Some(h) = auth_header {
129 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
130 match crate::auth::validate_bearer_token(&state.db, &token).await {
131 Ok(user) => Some(user.did),
132 Err(_) => None,
133 }
134 } else {
135 None
136 }
137 } else {
138 None
139 };
140
141 let mut query_params = HashMap::new();
142 query_params.insert("actor".to_string(), params.actor.clone());
143
144 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfile", &query_params, auth_header).await {
145 Ok(r) => r,
146 Err(e) => return e,
147 };
148
149 if !status.is_success() {
150 return (status, Json(body)).into_response();
151 }
152
153 let mut profile: ProfileViewDetailed = match serde_json::from_value(body) {
154 Ok(p) => p,
155 Err(_) => {
156 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profile response"}))).into_response();
157 }
158 };
159
160 if let Some(ref did) = auth_did {
161 if profile.did == *did {
162 if let Some(local_record) = get_local_profile_record(&state, did).await {
163 munge_profile_with_local(&mut profile, &local_record);
164 }
165 }
166 }
167
168 (StatusCode::OK, Json(profile)).into_response()
169}
170
171pub async fn get_profiles(
172 State(state): State<AppState>,
173 headers: axum::http::HeaderMap,
174 Query(params): Query<GetProfilesParams>,
175) -> Response {
176 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
177
178 let auth_did = if let Some(h) = auth_header {
179 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
180 match crate::auth::validate_bearer_token(&state.db, &token).await {
181 Ok(user) => Some(user.did),
182 Err(_) => None,
183 }
184 } else {
185 None
186 }
187 } else {
188 None
189 };
190
191 let mut query_params = HashMap::new();
192 query_params.insert("actors".to_string(), params.actors.clone());
193
194 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfiles", &query_params, auth_header).await {
195 Ok(r) => r,
196 Err(e) => return e,
197 };
198
199 if !status.is_success() {
200 return (status, Json(body)).into_response();
201 }
202
203 let mut output: GetProfilesOutput = match serde_json::from_value(body) {
204 Ok(p) => p,
205 Err(_) => {
206 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profiles response"}))).into_response();
207 }
208 };
209
210 if let Some(ref did) = auth_did {
211 for profile in &mut output.profiles {
212 if profile.did == *did {
213 if let Some(local_record) = get_local_profile_record(&state, did).await {
214 munge_profile_with_local(profile, &local_record);
215 }
216 break;
217 }
218 }
219 }
220
221 (StatusCode::OK, Json(output)).into_response()
222}