Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
6.9 kB · 201 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202use 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<i32> { 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<Value>,}pub async fn get_preferences(State(state): State<AppState>, auth: Auth<Permissive>) -> 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<Value> = None; let mut preferences: Vec<Value> = 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<Value>,}pub async fn put_preferences( State(state): State<AppState>, auth: Auth<NotTakendown>, Json(input): Json<PutPreferencesInput>,) -> 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<String>), TooLarge(usize), MissingType, WrongNamespace, }
let validation_results: Vec<PrefValidation> = 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<String> = 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()}