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