this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::State,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use serde::{Deserialize, Serialize};
9use serde_json::{Value, json};
10
11const APP_BSKY_NAMESPACE: &str = "app.bsky";
12const MAX_PREFERENCES_COUNT: usize = 100;
13const MAX_PREFERENCE_SIZE: usize = 10_000;
14
15#[derive(Serialize)]
16pub struct GetPreferencesOutput {
17 pub preferences: Vec<Value>,
18}
19pub async fn get_preferences(
20 State(state): State<AppState>,
21 headers: axum::http::HeaderMap,
22) -> Response {
23 let token = match crate::auth::extract_bearer_token_from_header(
24 headers.get("Authorization").and_then(|h| h.to_str().ok()),
25 ) {
26 Some(t) => t,
27 None => {
28 return (
29 StatusCode::UNAUTHORIZED,
30 Json(json!({"error": "AuthenticationRequired"})),
31 )
32 .into_response();
33 }
34 };
35 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
36 Ok(user) => user,
37 Err(_) => {
38 return (
39 StatusCode::UNAUTHORIZED,
40 Json(json!({"error": "AuthenticationFailed"})),
41 )
42 .into_response();
43 }
44 };
45 let user_id: uuid::Uuid =
46 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
47 .fetch_optional(&state.db)
48 .await
49 {
50 Ok(Some(id)) => id,
51 _ => {
52 return (
53 StatusCode::INTERNAL_SERVER_ERROR,
54 Json(json!({"error": "InternalError", "message": "User not found"})),
55 )
56 .into_response();
57 }
58 };
59 let prefs_result = sqlx::query!(
60 "SELECT name, value_json FROM account_preferences WHERE user_id = $1",
61 user_id
62 )
63 .fetch_all(&state.db)
64 .await;
65 let prefs = match prefs_result {
66 Ok(rows) => rows,
67 Err(_) => {
68 return (
69 StatusCode::INTERNAL_SERVER_ERROR,
70 Json(json!({"error": "InternalError", "message": "Failed to fetch preferences"})),
71 )
72 .into_response();
73 }
74 };
75 let preferences: Vec<Value> = prefs
76 .into_iter()
77 .filter(|row| {
78 row.name == APP_BSKY_NAMESPACE
79 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE))
80 })
81 .filter_map(|row| {
82 if row.name == "app.bsky.actor.defs#declaredAgePref" {
83 return None;
84 }
85 serde_json::from_value(row.value_json).ok()
86 })
87 .collect();
88 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
89}
90
91#[derive(Deserialize)]
92pub struct PutPreferencesInput {
93 pub preferences: Vec<Value>,
94}
95pub async fn put_preferences(
96 State(state): State<AppState>,
97 headers: axum::http::HeaderMap,
98 Json(input): Json<PutPreferencesInput>,
99) -> Response {
100 let token = match crate::auth::extract_bearer_token_from_header(
101 headers.get("Authorization").and_then(|h| h.to_str().ok()),
102 ) {
103 Some(t) => t,
104 None => {
105 return (
106 StatusCode::UNAUTHORIZED,
107 Json(json!({"error": "AuthenticationRequired"})),
108 )
109 .into_response();
110 }
111 };
112 let auth_user = match crate::auth::validate_bearer_token(&state.db, &token).await {
113 Ok(user) => user,
114 Err(_) => {
115 return (
116 StatusCode::UNAUTHORIZED,
117 Json(json!({"error": "AuthenticationFailed"})),
118 )
119 .into_response();
120 }
121 };
122 let user_id: uuid::Uuid =
123 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
124 .fetch_optional(&state.db)
125 .await
126 {
127 Ok(Some(id)) => id,
128 _ => {
129 return (
130 StatusCode::INTERNAL_SERVER_ERROR,
131 Json(json!({"error": "InternalError", "message": "User not found"})),
132 )
133 .into_response();
134 }
135 };
136 if input.preferences.len() > MAX_PREFERENCES_COUNT {
137 return (
138 StatusCode::BAD_REQUEST,
139 Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})),
140 )
141 .into_response();
142 }
143 for pref in &input.preferences {
144 let pref_str = serde_json::to_string(pref).unwrap_or_default();
145 if pref_str.len() > MAX_PREFERENCE_SIZE {
146 return (
147 StatusCode::BAD_REQUEST,
148 Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})),
149 )
150 .into_response();
151 }
152 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
153 Some(t) => t,
154 None => {
155 return (
156 StatusCode::BAD_REQUEST,
157 Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})),
158 )
159 .into_response();
160 }
161 };
162 if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
163 return (
164 StatusCode::BAD_REQUEST,
165 Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})),
166 )
167 .into_response();
168 }
169 if pref_type == "app.bsky.actor.defs#declaredAgePref" {
170 return (
171 StatusCode::BAD_REQUEST,
172 Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})),
173 )
174 .into_response();
175 }
176 }
177 let mut tx = match state.db.begin().await {
178 Ok(tx) => tx,
179 Err(_) => {
180 return (
181 StatusCode::INTERNAL_SERVER_ERROR,
182 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})),
183 )
184 .into_response();
185 }
186 };
187 let delete_result = sqlx::query!(
188 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)",
189 user_id,
190 APP_BSKY_NAMESPACE,
191 format!("{}.%", APP_BSKY_NAMESPACE)
192 )
193 .execute(&mut *tx)
194 .await;
195 if delete_result.is_err() {
196 let _ = tx.rollback().await;
197 return (
198 StatusCode::INTERNAL_SERVER_ERROR,
199 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})),
200 )
201 .into_response();
202 }
203 for pref in input.preferences {
204 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
205 Some(t) => t,
206 None => continue,
207 };
208 let insert_result = sqlx::query!(
209 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
210 user_id,
211 pref_type,
212 pref
213 )
214 .execute(&mut *tx)
215 .await;
216 if insert_result.is_err() {
217 let _ = tx.rollback().await;
218 return (
219 StatusCode::INTERNAL_SERVER_ERROR,
220 Json(json!({"error": "InternalError", "message": "Failed to save preference"})),
221 )
222 .into_response();
223 }
224 }
225 if tx.commit().await.is_err() {
226 return (
227 StatusCode::INTERNAL_SERVER_ERROR,
228 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})),
229 )
230 .into_response();
231 }
232 StatusCode::OK.into_response()
233}