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()))
74 .into_response();
75 }
76 };
77 let mut personal_details_pref: Option<Value> = None;
78 let mut preferences: Vec<Value> = prefs
79 .into_iter()
80 .filter(|row| {
81 row.name == APP_BSKY_NAMESPACE
82 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE))
83 })
84 .filter_map(|row| {
85 if row.name == DECLARED_AGE_PREF {
86 return None;
87 }
88 if row.name == PERSONAL_DETAILS_PREF {
89 if !has_full_access {
90 return None;
91 }
92 personal_details_pref = serde_json::from_value(row.value_json.clone()).ok();
93 }
94 serde_json::from_value(row.value_json).ok()
95 })
96 .collect();
97 if let Some(age) = personal_details_pref
98 .as_ref()
99 .and_then(|pref| pref.get("birthDate"))
100 .and_then(|v| v.as_str())
101 .and_then(get_age_from_datestring)
102 {
103 let declared_age_pref = serde_json::json!({
104 "$type": DECLARED_AGE_PREF,
105 "isOverAge13": age >= 13,
106 "isOverAge16": age >= 16,
107 "isOverAge18": age >= 18,
108 });
109 preferences.push(declared_age_pref);
110 }
111 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response()
112}
113
114#[derive(Deserialize)]
115pub struct PutPreferencesInput {
116 pub preferences: Vec<Value>,
117}
118pub async fn put_preferences(
119 State(state): State<AppState>,
120 headers: axum::http::HeaderMap,
121 Json(input): Json<PutPreferencesInput>,
122) -> Response {
123 let token = match crate::auth::extract_bearer_token_from_header(
124 headers.get("Authorization").and_then(|h| h.to_str().ok()),
125 ) {
126 Some(t) => t,
127 None => {
128 return ApiError::AuthenticationRequired.into_response();
129 }
130 };
131 let auth_user =
132 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
133 Ok(user) => user,
134 Err(_) => {
135 return ApiError::AuthenticationFailed(None).into_response();
136 }
137 };
138 let has_full_access = auth_user.permissions().has_full_access();
139 let user_id: uuid::Uuid =
140 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", &*auth_user.did)
141 .fetch_optional(&state.db)
142 .await
143 {
144 Ok(Some(id)) => id,
145 _ => {
146 return ApiError::InternalError(Some("User not found".into())).into_response();
147 }
148 };
149 if input.preferences.len() > MAX_PREFERENCES_COUNT {
150 return ApiError::InvalidRequest(format!(
151 "Too many preferences: {} exceeds limit of {}",
152 input.preferences.len(),
153 MAX_PREFERENCES_COUNT
154 ))
155 .into_response();
156 }
157 let mut forbidden_prefs: Vec<String> = Vec::new();
158 for pref in &input.preferences {
159 let pref_str = serde_json::to_string(pref).unwrap_or_default();
160 if pref_str.len() > MAX_PREFERENCE_SIZE {
161 return ApiError::InvalidRequest(format!(
162 "Preference too large: {} bytes exceeds limit of {}",
163 pref_str.len(),
164 MAX_PREFERENCE_SIZE
165 ))
166 .into_response();
167 }
168 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
169 Some(t) => t,
170 None => {
171 return ApiError::InvalidRequest("Preference is missing a $type".into())
172 .into_response();
173 }
174 };
175 if !pref_type.starts_with(APP_BSKY_NAMESPACE) {
176 return ApiError::InvalidRequest(format!(
177 "Some preferences are not in the {} namespace",
178 APP_BSKY_NAMESPACE
179 ))
180 .into_response();
181 }
182 if pref_type == PERSONAL_DETAILS_PREF && !has_full_access {
183 forbidden_prefs.push(pref_type.to_string());
184 }
185 }
186 if !forbidden_prefs.is_empty() {
187 return ApiError::InvalidRequest(format!(
188 "Do not have authorization to set preferences: {}",
189 forbidden_prefs.join(", ")
190 ))
191 .into_response();
192 }
193 let mut tx = match state.db.begin().await {
194 Ok(tx) => tx,
195 Err(_) => {
196 return ApiError::InternalError(Some("Failed to start transaction".into()))
197 .into_response();
198 }
199 };
200 let delete_result = sqlx::query!(
201 "DELETE FROM account_preferences WHERE user_id = $1 AND (name = $2 OR name LIKE $3)",
202 user_id,
203 APP_BSKY_NAMESPACE,
204 format!("{}.%", APP_BSKY_NAMESPACE)
205 )
206 .execute(&mut *tx)
207 .await;
208 if delete_result.is_err() {
209 let _ = tx.rollback().await;
210 return ApiError::InternalError(Some("Failed to clear preferences".into())).into_response();
211 }
212 for pref in input.preferences {
213 let pref_type = match pref.get("$type").and_then(|t| t.as_str()) {
214 Some(t) => t,
215 None => continue,
216 };
217 if pref_type == DECLARED_AGE_PREF {
218 continue;
219 }
220 let insert_result = sqlx::query!(
221 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)",
222 user_id,
223 pref_type,
224 pref
225 )
226 .execute(&mut *tx)
227 .await;
228 if insert_result.is_err() {
229 let _ = tx.rollback().await;
230 return ApiError::InternalError(Some("Failed to save preference".into()))
231 .into_response();
232 }
233 }
234 if tx.commit().await.is_err() {
235 return ApiError::InternalError(Some("Failed to commit transaction".into()))
236 .into_response();
237 }
238 StatusCode::OK.into_response()
239}