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#[derive(Deserialize)]
15pub struct GetProfileParams {
16 pub actor: String,
17}
18#[derive(Deserialize)]
19pub struct GetProfilesParams {
20 pub actors: String,
21}
22#[derive(Serialize, Deserialize, Clone)]
23#[serde(rename_all = "camelCase")]
24pub struct ProfileViewDetailed {
25 pub did: String,
26 pub handle: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub display_name: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub description: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub avatar: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub banner: Option<String>,
35 #[serde(flatten)]
36 pub extra: HashMap<String, Value>,
37}
38#[derive(Serialize, Deserialize)]
39pub struct GetProfilesOutput {
40 pub profiles: Vec<ProfileViewDetailed>,
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}
58fn munge_profile_with_local(profile: &mut ProfileViewDetailed, local_record: &Value) {
59 if let Some(display_name) = local_record.get("displayName").and_then(|v| v.as_str()) {
60 profile.display_name = Some(display_name.to_string());
61 }
62 if let Some(description) = local_record.get("description").and_then(|v| v.as_str()) {
63 profile.description = Some(description.to_string());
64 }
65}
66async fn proxy_to_appview(
67 method: &str,
68 params: &HashMap<String, String>,
69 auth_header: Option<&str>,
70) -> Result<(StatusCode, Value), Response> {
71 let appview_url = match std::env::var("APPVIEW_URL") {
72 Ok(url) => url,
73 Err(_) => {
74 return Err(
75 (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "No upstream AppView configured"}))).into_response()
76 );
77 }
78 };
79 let target_url = format!("{}/xrpc/{}", appview_url, method);
80 info!("Proxying GET request to {}", target_url);
81 let client = proxy_client();
82 let mut request_builder = client.get(&target_url).query(params);
83 if let Some(auth) = auth_header {
84 request_builder = request_builder.header("Authorization", auth);
85 }
86 match request_builder.send().await {
87 Ok(resp) => {
88 let status = StatusCode::from_u16(resp.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY);
89 match resp.json::<Value>().await {
90 Ok(body) => Ok((status, body)),
91 Err(e) => {
92 error!("Error parsing proxy response: {:?}", e);
93 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response())
94 }
95 }
96 }
97 Err(e) => {
98 error!("Error sending proxy request: {:?}", e);
99 if e.is_timeout() {
100 Err((StatusCode::GATEWAY_TIMEOUT, Json(json!({"error": "UpstreamTimeout"}))).into_response())
101 } else {
102 Err((StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError"}))).into_response())
103 }
104 }
105 }
106}
107pub async fn get_profile(
108 State(state): State<AppState>,
109 headers: axum::http::HeaderMap,
110 Query(params): Query<GetProfileParams>,
111) -> Response {
112 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
113 let auth_did = if let Some(h) = auth_header {
114 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
115 match crate::auth::validate_bearer_token(&state.db, &token).await {
116 Ok(user) => Some(user.did),
117 Err(_) => None,
118 }
119 } else {
120 None
121 }
122 } else {
123 None
124 };
125 let mut query_params = HashMap::new();
126 query_params.insert("actor".to_string(), params.actor.clone());
127 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfile", &query_params, auth_header).await {
128 Ok(r) => r,
129 Err(e) => return e,
130 };
131 if !status.is_success() {
132 return (status, Json(body)).into_response();
133 }
134 let mut profile: ProfileViewDetailed = match serde_json::from_value(body) {
135 Ok(p) => p,
136 Err(_) => {
137 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profile response"}))).into_response();
138 }
139 };
140 if let Some(ref did) = auth_did {
141 if profile.did == *did {
142 if let Some(local_record) = get_local_profile_record(&state, did).await {
143 munge_profile_with_local(&mut profile, &local_record);
144 }
145 }
146 }
147 (StatusCode::OK, Json(profile)).into_response()
148}
149pub async fn get_profiles(
150 State(state): State<AppState>,
151 headers: axum::http::HeaderMap,
152 Query(params): Query<GetProfilesParams>,
153) -> Response {
154 let auth_header = headers.get("Authorization").and_then(|h| h.to_str().ok());
155 let auth_did = if let Some(h) = auth_header {
156 if let Some(token) = crate::auth::extract_bearer_token_from_header(Some(h)) {
157 match crate::auth::validate_bearer_token(&state.db, &token).await {
158 Ok(user) => Some(user.did),
159 Err(_) => None,
160 }
161 } else {
162 None
163 }
164 } else {
165 None
166 };
167 let mut query_params = HashMap::new();
168 query_params.insert("actors".to_string(), params.actors.clone());
169 let (status, body) = match proxy_to_appview("app.bsky.actor.getProfiles", &query_params, auth_header).await {
170 Ok(r) => r,
171 Err(e) => return e,
172 };
173 if !status.is_success() {
174 return (status, Json(body)).into_response();
175 }
176 let mut output: GetProfilesOutput = match serde_json::from_value(body) {
177 Ok(p) => p,
178 Err(_) => {
179 return (StatusCode::BAD_GATEWAY, Json(json!({"error": "UpstreamError", "message": "Invalid profiles response"}))).into_response();
180 }
181 };
182 if let Some(ref did) = auth_did {
183 for profile in &mut output.profiles {
184 if profile.did == *did {
185 if let Some(local_record) = get_local_profile_record(&state, did).await {
186 munge_profile_with_local(profile, &local_record);
187 }
188 break;
189 }
190 }
191 }
192 (StatusCode::OK, Json(output)).into_response()
193}