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