mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-08-17 06:46:07 +00:00
some people (me) have a large amount of prefs and moving from ref pds to tranquil ment i lost a few of these. so in this pr i bumped the max prefs to 1000 and made it configurable via the config
202 lines
6.7 KiB
Rust
202 lines
6.7 KiB
Rust
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<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()
|
|
}
|