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 chrono::{Datelike, NaiveDate, Utc};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11
12const APP_BSKY_NAMESPACE: &str = "app.bsky";
13const MAX_PREFERENCES_COUNT: usize = 100;
14const MAX_PREFERENCE_SIZE: usize = 10_000;
15const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref";
16const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref";
17
18fn get_age_from_datestring(birth_date: &str) -> Option<i32> {
19 let bday = NaiveDate::parse_from_str(birth_date, "%Y-%m-%d").ok()?;
20 let today = Utc::now().date_naive();
21 let mut age = today.year() - bday.year();
22 let m = today.month() as i32 - bday.month() as i32;
23 if m < 0 || (m == 0 && today.day() < bday.day()) {
24 age -= 1;
25 }
26 Some(age)
27}
28
29#[derive(Serialize)]
30pub struct GetPreferencesOutput {
31 pub preferences: Vec<Value>,
32}
33pub async fn get_preferences(
34 State(state): State<AppState>,
35 headers: axum::http::HeaderMap,
36) -> Response {
37 let token = match crate::auth::extract_bearer_token_from_header(
38 headers.get("Authorization").and_then(|h| h.to_str().ok()),
39 ) {
40 Some(t) => t,
41 None => {
42 return (
43 StatusCode::UNAUTHORIZED,
44 Json(json!({"error": "AuthenticationRequired"})),
45 )
46 .into_response();
47 }
48 };
49 let auth_user =
50 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
51 Ok(user) => user,
52 Err(_) => {
53 return (
54 StatusCode::UNAUTHORIZED,
55 Json(json!({"error": "AuthenticationFailed"})),
56 )
57 .into_response();
58 }
59 };
60 let has_full_access = auth_user.permissions().has_full_access();
61 let user_id: uuid::Uuid =
62 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
63 .fetch_optional(&state.db)
64 .await
65 {
66 Ok(Some(id)) => id,
67 _ => {
68 return (
69 StatusCode::INTERNAL_SERVER_ERROR,
70 Json(json!({"error": "InternalError", "message": "User not found"})),
71 )
72 .into_response();
73 }
74 };
75 let prefs_result = sqlx::query!(
76 "SELECT name, value_json FROM account_preferences WHERE user_id = $1",
77 user_id
78 )
79 .fetch_all(&state.db)
80 .await;
81 let prefs = match prefs_result {
82 Ok(rows) => rows,
83 Err(_) => {
84 return (
85 StatusCode::INTERNAL_SERVER_ERROR,
86 Json(json!({"error": "InternalError", "message": "Failed to fetch preferences"})),
87 )
88 .into_response();
89 }
90 };
91 let mut personal_details_pref: Option<Value> = None;
92 let mut preferences: Vec<Value> = prefs
93 .into_iter()
94 .filter(|row| {
95 row.name == APP_BSKY_NAMESPACE
96 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE))
97 })
98 .filter_map(|row| {
99 if row.name == DECLARED_AGE_PREF {
100 return None;
101 }
102 if row.name == PERSONAL_DETAILS_PREF {
103 if !has_full_access {
104 return None;
105 }
106 personal_details_pref = serde_json::from_value(row.value_json.clone()).ok();
107 }
108 serde_json::from_value(row.value_json).ok()
109 })
110 .collect();
111 if let Some(ref pref) = personal_details_pref {
112 if let Some(birth_date) = pref.get("birthDate").and_then(|v| v.as_str()) {
113 if let Some(age) = get_age_from_datestring(birth_date) {
114 let declared_age_pref = json!({
115 "$type": DECLARED_AGE_PREF,
116 "isOverAge13": age >= 13,
117 "isOverAge16": age >= 16,
118 "isOverAge18": age >= 18,
119 });
120 preferences.push(declared_age_pref);
121 }
122 }
123 }
124 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
125}
126
127#[derive(Deserialize)]
128pub struct PutPreferencesInput {
129 pub preferences: Vec<Value>,
130}
131pub async fn put_preferences(
132 State(state): State<AppState>,
133 headers: axum::http::HeaderMap,
134 Json(input): Json<PutPreferencesInput>,
135) -> Response {
136 let token = match crate::auth::extract_bearer_token_from_header(
137 headers.get("Authorization").and_then(|h| h.to_str().ok()),
138 ) {
139 Some(t) => t,
140 None => {
141 return (
142 StatusCode::UNAUTHORIZED,
143 Json(json!({"error": "AuthenticationRequired"})),
144 )
145 .into_response();
146 }
147 };
148 let auth_user =
149 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
150 Ok(user) => user,
151 Err(_) => {
152 return (
153 StatusCode::UNAUTHORIZED,
154 Json(json!({"error": "AuthenticationFailed"})),
155 )
156 .into_response();
157 }
158 };
159 let has_full_access = auth_user.permissions().has_full_access();
160 let user_id: uuid::Uuid = match sqlx::query_scalar!(
161 "SELECT id FROM users WHERE did = $1",
162 auth_user.did
163 )
164 .fetch_optional(&state.db)
165 .await
166 {
167 Ok(Some(id)) => id,
168 _ => {
169 return (
170 StatusCode::INTERNAL_SERVER_ERROR,
171 Json(json!({"error": "InternalError", "message": "User not found"})),
172 )
173 .into_response();
174 }
175 };
176 if input.preferences.len() > MAX_PREFERENCES_COUNT {
177 return (
178 StatusCode::BAD_REQUEST,
179 Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})),
180 )
181 .into_response();
182 }
183 let mut forbidden_prefs: Vec<String> = Vec::new();
184 for pref in &input.preferences {
185 let pref_str = serde_json::to_string(pref).unwrap_or_default();
186 if pref_str.len() > MAX_PREFERENCE_SIZE {
187 return (
188 StatusCode::BAD_REQUEST,
189 Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})),
190 )
191 .into_response();
192 }
193 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
194 Some(t) => t,
195 None => {
196 return (
197 StatusCode::BAD_REQUEST,
198 Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})),
199 )
200 .into_response();
201 }
202 };
203 if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
204 return (
205 StatusCode::BAD_REQUEST,
206 Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})),
207 )
208 .into_response();
209 }
210 if pref_type == PERSONAL_DETAILS_PREF && !has_full_access {
211 forbidden_prefs.push(pref_type.to_string());
212 }
213 }
214 if !forbidden_prefs.is_empty() {
215 return (
216 StatusCode::BAD_REQUEST,
217 Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})),
218 )
219 .into_response();
220 }
221 let mut tx = match state.db.begin().await {
222 Ok(tx) => tx,
223 Err(_) => {
224 return (
225 StatusCode::INTERNAL_SERVER_ERROR,
226 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})),
227 )
228 .into_response();
229 }
230 };
231 let delete_result = sqlx::query!(
232 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)",
233 user_id,
234 APP_BSKY_NAMESPACE,
235 format!("{}.%", APP_BSKY_NAMESPACE)
236 )
237 .execute(&mut *tx)
238 .await;
239 if delete_result.is_err() {
240 let _ = tx.rollback().await;
241 return (
242 StatusCode::INTERNAL_SERVER_ERROR,
243 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})),
244 )
245 .into_response();
246 }
247 for pref in input.preferences {
248 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
249 Some(t) => t,
250 None => continue,
251 };
252 if pref_type == DECLARED_AGE_PREF {
253 continue;
254 }
255 let insert_result = sqlx::query!(
256 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
257 user_id,
258 pref_type,
259 pref
260 )
261 .execute(&mut *tx)
262 .await;
263 if insert_result.is_err() {
264 let _ = tx.rollback().await;
265 return (
266 StatusCode::INTERNAL_SERVER_ERROR,
267 Json(json!({"error": "InternalError", "message": "Failed to save preference"})),
268 )
269 .into_response();
270 }
271 }
272 if tx.commit().await.is_err() {
273 return (
274 StatusCode::INTERNAL_SERVER_ERROR,
275 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})),
276 )
277 .into_response();
278 }
279 StatusCode::OK.into_response()
280}