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(age) = personal_details_pref
112 .as_ref()
113 .and_then(|pref| pref.get("birthDate"))
114 .and_then(|v| v.as_str())
115 .and_then(get_age_from_datestring)
116 {
117 let declared_age_pref = json!({
118 "$type": DECLARED_AGE_PREF,
119 "isOverAge13": age >= 13,
120 "isOverAge16": age >= 16,
121 "isOverAge18": age >= 18,
122 });
123 preferences.push(declared_age_pref);
124 }
125 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
126}
127
128#[derive(Deserialize)]
129pub struct PutPreferencesInput {
130 pub preferences: Vec<Value>,
131}
132pub async fn put_preferences(
133 State(state): State<AppState>,
134 headers: axum::http::HeaderMap,
135 Json(input): Json<PutPreferencesInput>,
136) -> Response {
137 let token = match crate::auth::extract_bearer_token_from_header(
138 headers.get("Authorization").and_then(|h| h.to_str().ok()),
139 ) {
140 Some(t) => t,
141 None => {
142 return (
143 StatusCode::UNAUTHORIZED,
144 Json(json!({"error": "AuthenticationRequired"})),
145 )
146 .into_response();
147 }
148 };
149 let auth_user =
150 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
151 Ok(user) => user,
152 Err(_) => {
153 return (
154 StatusCode::UNAUTHORIZED,
155 Json(json!({"error": "AuthenticationFailed"})),
156 )
157 .into_response();
158 }
159 };
160 let has_full_access = auth_user.permissions().has_full_access();
161 let user_id: uuid::Uuid =
162 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did)
163 .fetch_optional(&state.db)
164 .await
165 {
166 Ok(Some(id)) => id,
167 _ => {
168 return (
169 StatusCode::INTERNAL_SERVER_ERROR,
170 Json(json!({"error": "InternalError", "message": "User not found"})),
171 )
172 .into_response();
173 }
174 };
175 if input.preferences.len() > MAX_PREFERENCES_COUNT {
176 return (
177 StatusCode::BAD_REQUEST,
178 Json(json!({"error": "InvalidRequest", "message": format!("Too many preferences: {} exceeds limit of {}", input.preferences.len(), MAX_PREFERENCES_COUNT)})),
179 )
180 .into_response();
181 }
182 let mut forbidden_prefs: Vec<String> = Vec::new();
183 for pref in &input.preferences {
184 let pref_str = serde_json::to_string(pref).unwrap_or_default();
185 if pref_str.len() > MAX_PREFERENCE_SIZE {
186 return (
187 StatusCode::BAD_REQUEST,
188 Json(json!({"error": "InvalidRequest", "message": format!("Preference too large: {} bytes exceeds limit of {}", pref_str.len(), MAX_PREFERENCE_SIZE)})),
189 )
190 .into_response();
191 }
192 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
193 Some(t) => t,
194 None => {
195 return (
196 StatusCode::BAD_REQUEST,
197 Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})),
198 )
199 .into_response();
200 }
201 };
202 if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
203 return (
204 StatusCode::BAD_REQUEST,
205 Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})),
206 )
207 .into_response();
208 }
209 if pref_type == PERSONAL_DETAILS_PREF && !has_full_access {
210 forbidden_prefs.push(pref_type.to_string());
211 }
212 }
213 if !forbidden_prefs.is_empty() {
214 return (
215 StatusCode::BAD_REQUEST,
216 Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})),
217 )
218 .into_response();
219 }
220 let mut tx = match state.db.begin().await {
221 Ok(tx) => tx,
222 Err(_) => {
223 return (
224 StatusCode::INTERNAL_SERVER_ERROR,
225 Json(json!({"error": "InternalError", "message": "Failed to start transaction"})),
226 )
227 .into_response();
228 }
229 };
230 let delete_result = sqlx::query!(
231 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)",
232 user_id,
233 APP_BSKY_NAMESPACE,
234 format!("{}.%", APP_BSKY_NAMESPACE)
235 )
236 .execute(&mut *tx)
237 .await;
238 if delete_result.is_err() {
239 let _ = tx.rollback().await;
240 return (
241 StatusCode::INTERNAL_SERVER_ERROR,
242 Json(json!({"error": "InternalError", "message": "Failed to clear preferences"})),
243 )
244 .into_response();
245 }
246 for pref in input.preferences {
247 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
248 Some(t) => t,
249 None => continue,
250 };
251 if pref_type == DECLARED_AGE_PREF {
252 continue;
253 }
254 let insert_result = sqlx::query!(
255 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
256 user_id,
257 pref_type,
258 pref
259 )
260 .execute(&mut *tx)
261 .await;
262 if insert_result.is_err() {
263 let _ = tx.rollback().await;
264 return (
265 StatusCode::INTERNAL_SERVER_ERROR,
266 Json(json!({"error": "InternalError", "message": "Failed to save preference"})),
267 )
268 .into_response();
269 }
270 }
271 if tx.commit().await.is_err() {
272 return (
273 StatusCode::INTERNAL_SERVER_ERROR,
274 Json(json!({"error": "InternalError", "message": "Failed to commit transaction"})),
275 )
276 .into_response();
277 }
278 StatusCode::OK.into_response()
279}