add: policy achievements

This commit is contained in:
trisua 2025-06-30 18:49:41 -04:00
parent d90b08720a
commit 973373426a
7 changed files with 83 additions and 13 deletions

View file

@ -1135,15 +1135,13 @@
(icon (text "rabbit")) (icon (text "rabbit"))
(str (text "general:link.reference"))) (str (text "general:link.reference")))
(a (button
("href" "{{ config.policies.terms_of_service }}") ("onclick" "trigger('me::achievement', ['OpenTos']); Turbo.visit('{{ config.policies.terms_of_service }}')")
("class" "button")
(icon (text "heart-handshake")) (icon (text "heart-handshake"))
(text "Terms of service")) (text "Terms of service"))
(a (button
("href" "{{ config.policies.privacy }}") ("onclick" "trigger('me::achievement', ['OpenPrivacyPolicy']); Turbo.visit('{{ config.policies.privacy }}')")
("class" "button")
(icon (text "cookie")) (icon (text "cookie"))
(text "Privacy policy")) (text "Privacy policy"))
(b ("class" "title") (str (text "general:label.account"))) (b ("class" "title") (str (text "general:label.account")))

View file

@ -1522,7 +1522,7 @@
[ [
[ [
\"hide_associated_blocked_users\", \"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 }}\", \"{{ profile.settings.hide_associated_blocked_users }}\",
\"checkbox\", \"checkbox\",

View file

@ -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) => { self.define("report", (_, asset, asset_type) => {
window.open( window.open(
`/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`, `/mod_panel/file_report?asset=${asset}&asset_type=${asset_type}`,

View file

@ -3,8 +3,9 @@ use crate::{
get_user_from_token, get_user_from_token,
model::{ApiReturn, Error}, model::{ApiReturn, Error},
routes::api::v1::{ routes::api::v1::{
AppendAssociations, DeleteUser, DisableTotp, RefreshGrantToken, UpdateSecondaryUserRole, AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken,
UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername, UpdateSecondaryUserRole, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole,
UpdateUserUsername,
}, },
State, State,
}; };
@ -21,7 +22,7 @@ use futures_util::{sink::SinkExt, stream::StreamExt};
use tetratto_core::{ use tetratto_core::{
cache::Cache, cache::Cache,
model::{ model::{
auth::{AchievementName, InviteCode, Token, UserSettings}, auth::{AchievementName, InviteCode, Token, UserSettings, SELF_SERVE_ACHIEVEMENTS},
moderation::AuditLogEntry, moderation::AuditLogEntry,
oauth, oauth,
permissions::FinePermission, permissions::FinePermission,
@ -920,3 +921,31 @@ pub async fn generate_invite_codes_request(
payload: Some((out_string, errors_string)), 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<State>,
Json(req): Json<AwardAchievement>,
) -> 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()),
}
}

View file

@ -19,6 +19,7 @@ use axum::{
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::{ use tetratto_core::model::{
apps::AppQuota, apps::AppQuota,
auth::AchievementName,
communities::{ communities::{
CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess,
PollOption, PostContext, PollOption, PostContext,
@ -387,6 +388,10 @@ pub fn routes() -> Router {
"/auth/user/{id}/grants/{app}/refresh", "/auth/user/{id}/grants/{app}/refresh",
post(auth::profile::refresh_grant_request), post(auth::profile::refresh_grant_request),
) )
.route(
"/auth/user/me/achievement",
post(auth::profile::self_serve_achievement_request),
)
// apps // apps
.route("/apps", post(apps::create_request)) .route("/apps", post(apps::create_request))
.route("/apps/{id}/title", post(apps::update_title_request)) .route("/apps/{id}/title", post(apps::update_title_request))
@ -976,7 +981,13 @@ pub struct AddJournalDir {
pub struct RemoveJournalDir { pub struct RemoveJournalDir {
pub dir: String, pub dir: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateNoteTags { pub struct UpdateNoteTags {
pub tags: Vec<String>, pub tags: Vec<String>,
} }
#[derive(Deserialize)]
pub struct AwardAchievement {
pub name: AchievementName,
}

View file

@ -493,7 +493,10 @@ pub struct ExternalConnectionData {
} }
/// The total number of achievements needed to 100% Tetratto! /// 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)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum AchievementName { pub enum AchievementName {
@ -525,6 +528,8 @@ pub enum AchievementName {
EditNote, EditNote,
CreatePostWithTitle, CreatePostWithTitle,
CreateRepost, CreateRepost,
OpenTos,
OpenPrivacyPolicy,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -565,6 +570,8 @@ impl AchievementName {
Self::EditNote => "I take it back!", Self::EditNote => "I take it back!",
Self::CreatePostWithTitle => "Must declutter", Self::CreatePostWithTitle => "Must declutter",
Self::CreateRepost => "More than a like or comment...", 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::EditNote => "Edit a note.",
Self::CreatePostWithTitle => "Create a post with a title.", Self::CreatePostWithTitle => "Create a post with a title.",
Self::CreateRepost => "Create a repost or quote.", 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::EditNote => Uncommon,
Self::CreatePostWithTitle => Common, Self::CreatePostWithTitle => Common,
Self::CreateRepost => Common, Self::CreateRepost => Common,
Self::OpenTos => Uncommon,
Self::OpenPrivacyPolicy => Uncommon,
} }
} }
} }

View file

@ -38,8 +38,8 @@ user = "user"
password = "postgres" password = "postgres"
[policies] [policies]
terms_of_service = "/public/tos.html" terms_of_service = "/doc/tos.md"
privacy = "/public/privacy.html" privacy = "/doc/privacy.md"
[turnstile] [turnstile]
site_key = "1x00000000000000000000AA" site_key = "1x00000000000000000000AA"