use axum::{ Json, extract::State, http::StatusCode, response::{IntoResponse, Response}, }; use chrono::{Datelike, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; use serde_json::Value; use tranquil_pds::api::error::ApiError; use tranquil_pds::auth::{Auth, NotTakendown, Permissive}; use tranquil_pds::state::AppState; const APP_BSKY_NAMESPACE: &str = "app.bsky"; const MAX_PREFERENCE_SIZE: usize = 10_000; const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref"; const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref"; fn get_age_from_datestring(birth_date: &str) -> Option { let bday = NaiveDate::parse_from_str(birth_date, "%Y-%m-%d").ok()?; let today = Utc::now().date_naive(); let mut age = today.year() - bday.year(); let m = i32::try_from(today.month()).unwrap_or(0) - i32::try_from(bday.month()).unwrap_or(0); if m < 0 || (m == 0 && today.day() < bday.day()) { age -= 1; } Some(age) } #[derive(Serialize)] pub struct GetPreferencesOutput { pub preferences: Vec, } pub async fn get_preferences(State(state): State, auth: Auth) -> Response { let has_full_access = auth.permissions().has_full_access(); let user_id: uuid::Uuid = match state.repos.user.get_id_by_did(&auth.did).await { Ok(Some(id)) => id, _ => { return ApiError::InternalError(Some("User not found".into())).into_response(); } }; let prefs = match state.repos.infra.get_account_preferences(user_id).await { Ok(rows) => rows, Err(_) => { return ApiError::InternalError(Some("Failed to fetch preferences".into())) .into_response(); } }; let mut personal_details_pref: Option = None; let mut preferences: Vec = prefs .into_iter() .filter(|(name, _)| { name == APP_BSKY_NAMESPACE || name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) }) .filter_map(|(name, value_json)| { if name == DECLARED_AGE_PREF { return None; } if name == PERSONAL_DETAILS_PREF { if !has_full_access { return None; } personal_details_pref = serde_json::from_value(value_json.clone()).ok(); } serde_json::from_value(value_json).ok() }) .collect(); if let Some(age) = personal_details_pref .as_ref() .and_then(|pref| pref.get("birthDate")) .and_then(Value::as_str) .and_then(get_age_from_datestring) { let declared_age_pref = serde_json::json!({ "$type": DECLARED_AGE_PREF, "isOverAge13": age >= 13, "isOverAge16": age >= 16, "isOverAge18": age >= 18, }); preferences.push(declared_age_pref); } (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() } #[derive(Deserialize)] pub struct PutPreferencesInput { pub preferences: Vec, } pub async fn put_preferences( State(state): State, auth: Auth, Json(input): Json, ) -> Response { let max_preferences_count: usize = tranquil_config::get().server.max_preferences_count; let has_full_access = auth.permissions().has_full_access(); let user_id: uuid::Uuid = match state.repos.user.get_id_by_did(&auth.did).await { Ok(Some(id)) => id, _ => { return ApiError::InternalError(Some("User not found".into())).into_response(); } }; if input.preferences.len() > max_preferences_count { return ApiError::InvalidRequest(format!( "Too many preferences: {} exceeds limit of {}", input.preferences.len(), max_preferences_count )) .into_response(); } enum PrefValidation { Ok(Option), TooLarge(usize), MissingType, WrongNamespace, } let validation_results: Vec = input .preferences .iter() .map(|pref| { let pref_str = serde_json::to_string(pref).unwrap_or_default(); if pref_str.len() > MAX_PREFERENCE_SIZE { return PrefValidation::TooLarge(pref_str.len()); } let pref_type = match pref.get("$type").and_then(Value::as_str) { Some(t) => t, None => return PrefValidation::MissingType, }; if !pref_type.starts_with(APP_BSKY_NAMESPACE) { return PrefValidation::WrongNamespace; } if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { PrefValidation::Ok(Some(pref_type.to_string())) } else { PrefValidation::Ok(None) } }) .collect(); if let Some(err) = validation_results.iter().find_map(|v| match v { PrefValidation::TooLarge(size) => Some( ApiError::InvalidRequest(format!( "Preference too large: {} bytes exceeds limit of {}", size, MAX_PREFERENCE_SIZE )) .into_response(), ), PrefValidation::MissingType => { Some(ApiError::InvalidRequest("Preference is missing a $type".into()).into_response()) } PrefValidation::WrongNamespace => Some( ApiError::InvalidRequest(format!( "Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE )) .into_response(), ), PrefValidation::Ok(_) => None, }) { return err; } let forbidden_prefs: Vec = validation_results .into_iter() .filter_map(|v| match v { PrefValidation::Ok(Some(s)) => Some(s), _ => None, }) .collect(); if !forbidden_prefs.is_empty() { return ApiError::InvalidRequest(format!( "Do not have authorization to set preferences: {}", forbidden_prefs.join(", ") )) .into_response(); } let prefs_to_save: Vec<(String, Value)> = input .preferences .into_iter() .filter_map(|pref| { let pref_type = pref.get("$type").and_then(Value::as_str)?; if pref_type == DECLARED_AGE_PREF { return None; } Some((pref_type.to_string(), pref)) }) .collect(); if state .repos .infra .replace_namespace_preferences(user_id, APP_BSKY_NAMESPACE, prefs_to_save) .await .is_err() { return ApiError::InternalError(Some("Failed to save preferences".into())).into_response(); } StatusCode::OK.into_response() }