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}