From 973373426aed534afc79e8d089e20df2ad6d3133 Mon Sep 17 00:00:00 2001 From: trisua Date: Mon, 30 Jun 2025 18:49:41 -0400 Subject: [PATCH] add: policy achievements --- crates/app/src/public/html/components.lisp | 10 +++--- .../app/src/public/html/profile/settings.lisp | 2 +- crates/app/src/public/js/me.js | 21 +++++++++++ crates/app/src/routes/api/v1/auth/profile.rs | 35 +++++++++++++++++-- crates/app/src/routes/api/v1/mod.rs | 11 ++++++ crates/core/src/model/auth.rs | 13 ++++++- example/tetratto.toml | 4 +-- 7 files changed, 83 insertions(+), 13 deletions(-) diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index ba7f4e3..e863b65 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -1135,15 +1135,13 @@ (icon (text "rabbit")) (str (text "general:link.reference"))) - (a - ("href" "{{ config.policies.terms_of_service }}") - ("class" "button") + (button + ("onclick" "trigger('me::achievement', ['OpenTos']); Turbo.visit('{{ config.policies.terms_of_service }}')") (icon (text "heart-handshake")) (text "Terms of service")) - (a - ("href" "{{ config.policies.privacy }}") - ("class" "button") + (button + ("onclick" "trigger('me::achievement', ['OpenPrivacyPolicy']); Turbo.visit('{{ config.policies.privacy }}')") (icon (text "cookie")) (text "Privacy policy")) (b ("class" "title") (str (text "general:label.account"))) diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index b191b8a..dce30d2 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -1522,7 +1522,7 @@ [ [ \"hide_associated_blocked_users\", - \"Hide users that you've blocked on your other accounts from timelines.\", + \"Hide users that you've blocked on your other accounts from timelines\", ], \"{{ profile.settings.hide_associated_blocked_users }}\", \"checkbox\", diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index e346a6b..0c7ac10 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -342,6 +342,27 @@ }, ); + self.define("achievement", async (_, name) => { + fetch("/api/v1/auth/user/me/achievement", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + }), + }) + .then((res) => res.json()) + .then((res) => { + if (!res.ok) { + trigger("atto::toast", [ + res.ok ? "success" : "error", + res.message, + ]); + } + }); + }); + self.define("report", (_, asset, asset_type) => { window.open( `/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`, diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index 1977d95..f66e9bf 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -3,8 +3,9 @@ use crate::{ get_user_from_token, model::{ApiReturn, Error}, routes::api::v1::{ - AppendAssociations, DeleteUser, DisableTotp, RefreshGrantToken, UpdateSecondaryUserRole, - UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername, + AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken, + UpdateSecondaryUserRole, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, + UpdateUserUsername, }, State, }; @@ -21,7 +22,7 @@ use futures_util::{sink::SinkExt, stream::StreamExt}; use tetratto_core::{ cache::Cache, model::{ - auth::{AchievementName, InviteCode, Token, UserSettings}, + auth::{AchievementName, InviteCode, Token, UserSettings, SELF_SERVE_ACHIEVEMENTS}, moderation::AuditLogEntry, oauth, permissions::FinePermission, @@ -920,3 +921,31 @@ pub async fn generate_invite_codes_request( payload: Some((out_string, errors_string)), }) } + +/// Award an achievement to the current user. +/// Only works with specific "self-serve" achievements. +pub async fn self_serve_achievement_request( + jar: CookieJar, + Extension(data): Extension, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let mut user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !SELF_SERVE_ACHIEVEMENTS.contains(&req.name) { + return Json(Error::MiscError("Cannot grant this achievement manually".to_string()).into()); + } + + // award achievement + match data.add_achievement(&mut user, req.name.into()).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Achievement granted".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index b87dbdc..3cd4423 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -19,6 +19,7 @@ use axum::{ use serde::Deserialize; use tetratto_core::model::{ apps::AppQuota, + auth::AchievementName, communities::{ CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, PollOption, PostContext, @@ -387,6 +388,10 @@ pub fn routes() -> Router { "/auth/user/{id}/grants/{app}/refresh", post(auth::profile::refresh_grant_request), ) + .route( + "/auth/user/me/achievement", + post(auth::profile::self_serve_achievement_request), + ) // apps .route("/apps", post(apps::create_request)) .route("/apps/{id}/title", post(apps::update_title_request)) @@ -976,7 +981,13 @@ pub struct AddJournalDir { pub struct RemoveJournalDir { pub dir: String, } + #[derive(Deserialize)] pub struct UpdateNoteTags { pub tags: Vec, } + +#[derive(Deserialize)] +pub struct AwardAchievement { + pub name: AchievementName, +} diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index c2c68c1..bac4ae6 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -493,7 +493,10 @@ pub struct ExternalConnectionData { } /// The total number of achievements needed to 100% Tetratto! -pub const ACHIEVEMENTS: usize = 28; +pub const ACHIEVEMENTS: usize = 30; +/// "self-serve" achievements can be granted by the user through the API. +pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = + &[AchievementName::OpenTos, AchievementName::OpenPrivacyPolicy]; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum AchievementName { @@ -525,6 +528,8 @@ pub enum AchievementName { EditNote, CreatePostWithTitle, CreateRepost, + OpenTos, + OpenPrivacyPolicy, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -565,6 +570,8 @@ impl AchievementName { Self::EditNote => "I take it back!", Self::CreatePostWithTitle => "Must declutter", Self::CreateRepost => "More than a like or comment...", + Self::OpenTos => "Well informed!", + Self::OpenPrivacyPolicy => "Privacy conscious", } } @@ -598,6 +605,8 @@ impl AchievementName { Self::EditNote => "Edit a note.", Self::CreatePostWithTitle => "Create a post with a title.", Self::CreateRepost => "Create a repost or quote.", + Self::OpenTos => "Open the terms of service.", + Self::OpenPrivacyPolicy => "Open the privacy policy.", } } @@ -633,6 +642,8 @@ impl AchievementName { Self::EditNote => Uncommon, Self::CreatePostWithTitle => Common, Self::CreateRepost => Common, + Self::OpenTos => Uncommon, + Self::OpenPrivacyPolicy => Uncommon, } } } diff --git a/example/tetratto.toml b/example/tetratto.toml index 488bc89..0f36100 100644 --- a/example/tetratto.toml +++ b/example/tetratto.toml @@ -38,8 +38,8 @@ user = "user" password = "postgres" [policies] -terms_of_service = "/public/tos.html" -privacy = "/public/privacy.html" +terms_of_service = "/doc/tos.md" +privacy = "/doc/privacy.md" [turnstile] site_key = "1x00000000000000000000AA"