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