diff --git a/Cargo.lock b/Cargo.lock index adc1cbb..b03e9ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -955,15 +955,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.1.16" @@ -1752,6 +1743,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +[[package]] +name = "markdown" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + [[package]] name = "markup5ever" version = "0.35.0" @@ -2355,25 +2355,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "pulldown-cmark" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" -dependencies = [ - "bitflags 2.9.1", - "getopts", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" - [[package]] name = "qoi" version = "0.4.1" @@ -3320,7 +3301,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "12.0.2" +version = "12.0.0" dependencies = [ "async-recursion", "base16ct", @@ -3353,14 +3334,13 @@ dependencies = [ [[package]] name = "tetratto-shared" -version = "12.0.6" +version = "12.0.0" dependencies = [ "ammonia", "chrono", "hex_fmt", - "pulldown-cmark", + "markdown", "rand 0.9.1", - "regex", "serde", "sha2", "snowflaked", @@ -3891,6 +3871,12 @@ version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -3912,12 +3898,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" -[[package]] -name = "unicode-width" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" - [[package]] name = "untrusted" version = "0.9.0" diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 82cf197..1504ba5 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -4,11 +4,15 @@ use nanoneo::{ }; use pathbufd::PathBufD; use regex::Regex; -use std::{collections::HashMap, fs::read_to_string, sync::LazyLock, time::SystemTime}; +use std::{ + collections::HashMap, + fs::{exists, read_to_string, write}, + sync::LazyLock, + time::SystemTime, +}; use tera::Context; use tetratto_core::{ config::Config, - html::{pull_icons, ICONS}, model::{ auth::{DefaultTimelineChoice, User}, permissions::{FinePermission, SecondaryPermission}, @@ -153,6 +157,38 @@ pub const TETRATTO_BUNNY: &[u8] = include_bytes!("./public/images/tetratto_bunny pub(crate) static HTML_FOOTER: LazyLock> = LazyLock::new(|| RwLock::new(String::new())); +/// A container for all loaded icons. +pub(crate) static ICONS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +/// Pull an icon given its name and insert it into [`ICONS`]. +pub(crate) async fn pull_icon(icon: &str, icons_dir: &str) { + let writer = &mut ICONS.write().await; + + let icon_url = format!( + "https://raw.githubusercontent.com/lucide-icons/lucide/refs/heads/main/icons/{icon}.svg" + ); + + let file_path = PathBufD::current().extend(&[icons_dir, &format!("{icon}.svg")]); + + if exists(&file_path).unwrap() { + writer.insert(icon.to_string(), read_to_string(&file_path).unwrap()); + return; + } + + println!("download icon: {icon}"); + let svg = reqwest::get(icon_url) + .await + .unwrap() + .text() + .await + .unwrap() + .replace("\n", ""); + + write(&file_path, &svg).unwrap(); + writer.insert(icon.to_string(), svg); +} + macro_rules! vendor_icon { ($name:literal, $icon:ident, $icons_dir:expr) => {{ let writer = &mut ICONS.write().await; @@ -225,8 +261,56 @@ pub(crate) async fn replace_in_html( input = input.replace(cap.get(0).unwrap().as_str(), &replace_with); } - // icons - input = pull_icons(input, &config.dirs.icons).await; + // icon (with class) + let icon_with_class = + Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap(); + + for cap in icon_with_class.captures_iter(&input.clone()) { + let cap_str = &cap.get(3).unwrap().as_str().replace("\"", ""); + let icon = &(if cap_str.contains(" }}") { + cap_str.split(" }}").next().unwrap().to_string() + } else { + cap_str.to_string() + }); + + pull_icon(icon, &config.dirs.icons).await; + + let reader = ICONS.read().await; + let icon_text = reader.get(icon).unwrap().replace( + "", &format!("{reader}"), 1); diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index bd692f3..3246755 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -258,7 +258,6 @@ version = "1.0.0" "developer:label.change_homepage" = "Change homepage" "developer:label.change_redirect" = "Change redirect URL" "developer:label.change_quota_status" = "Change quota status" -"developer:label.change_storage_capacity" = "Change storage capacity" "developer:label.manage_scopes" = "Manage scopes" "developer:label.scopes" = "Scopes" "developer:label.guides_and_help" = "Guides & help" diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index b0098b7..bfac36f 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -36,13 +36,12 @@ pub(crate) type InnerState = (DataManager, Tera, Client, Option); pub(crate) type State = Arc>; fn render_markdown(value: &Value, _: &HashMap) -> tera::Result { - Ok(tetratto_shared::markdown::render_markdown( - &CustomEmoji::replace(value.as_str().unwrap()), - true, + Ok( + tetratto_shared::markdown::render_markdown(&CustomEmoji::replace(value.as_str().unwrap())) + .replace("\\@", "@") + .replace("%5C@", "@") + .into(), ) - .replace("\\@", "@") - .replace("%5C@", "@") - .into()) } fn render_emojis(value: &Value, _: &HashMap) -> tera::Result { diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 8475223..d3c1a7f 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -2415,7 +2415,7 @@ (ul ("style" "margin-bottom: var(--pad-4)") (li - (text "Increased app storage limit (500 KB->25 MB)")) + (text "Increased app storage limit (500 KB->5 MB)")) (li (text "Ability to create forges")) (li diff --git a/crates/app/src/public/html/developer/app.lisp b/crates/app/src/public/html/developer/app.lisp index d19fb10..b1661e8 100644 --- a/crates/app/src/public/html/developer/app.lisp +++ b/crates/app/src/public/html/developer/app.lisp @@ -44,28 +44,6 @@ ("value" "Unlimited") ("selected" "{% if app.quota_status == 'Unlimited' -%}true{% else %}false{%- endif %}") (text "Unlimited"))))) - (div - ("class" "card-nest") - (div - ("class" "card small flex items-center gap-2") - (icon (text "database-zap")) - (b (str (text "developer:label.change_storage_capacity")))) - (div - ("class" "card") - (select - ("onchange" "save_storage_capacity(event)") - (option - ("value" "Tier1") - ("selected" "{% if app.storage_capacity == 'Tier1' -%}true{% else %}false{%- endif %}") - (text "Tier 1 (25 MB)")) - (option - ("value" "Tier2") - ("selected" "{% if app.storage_capacity == 'Tier2' -%}true{% else %}false{%- endif %}") - (text "Tier 2 (50 MB)")) - (option - ("value" "Tier3") - ("selected" "{% if app.storage_capacity == 'Tier3' -%}true{% else %}false{%- endif %}") - (text "Tier 3 (100 MB)"))))) (text "{%- endif %}") (div ("class" "card-nest") @@ -254,26 +232,6 @@ }); }; - globalThis.save_storage_capacity = (event) => { - const selected = event.target.selectedOptions[0]; - fetch(\"/api/v1/apps/{{ app.id }}/storage_capacity\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - storage_capacity: selected.value, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - globalThis.change_title = async (e) => { e.preventDefault(); diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 5a84aac..2b68c90 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -406,7 +406,6 @@ MANAGE_SERVICES: 1 << 3, MANAGE_PRODUCTS: 1 << 4, DEVELOPER_PASS: 1 << 5, - MANAGE_LETTERS: 1 << 6, }, \"secondary_role\", \"add_permission_to_secondary_role\", diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index e10dec9..7e4d6fb 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -107,7 +107,6 @@ (p (text "{{ profile.settings.status }}")) (text "{%- endif %}") - (text "{% if not profile.settings.hide_social_follows or (user and user.id == profile.id) -%}") (div ("class" "w-full flex") (a @@ -124,7 +123,6 @@ (text "{{ profile.following_count }}")) (span (text "{{ text \"auth:label.following\" }}")))) - (text "{%- endif %}") (text "{% if is_following_you -%}") (b ("class" "notification chip w-content flex items-center gap-2") diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 64d3a30..59b64d0 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -1889,14 +1889,6 @@ \"{{ profile.settings.hide_from_social_lists }}\", \"checkbox\", ], - [ - [ - \"hide_social_follows\", - \"Hide followers/following links on my profile\", - ], - \"{{ profile.settings.hide_social_follows }}\", - \"checkbox\", - ], [[], \"Questions\", \"title\"], [ [ diff --git a/crates/app/src/public/js/app_sdk.js b/crates/app/src/public/js/app_sdk.js index 7a6c834..cd21e6a 100644 --- a/crates/app/src/public/js/app_sdk.js +++ b/crates/app/src/public/js/app_sdk.js @@ -72,25 +72,6 @@ export default function tetratto({ ); } - async function check_ip(ip) { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/bans/${ip}`, { - method: "GET", - headers: { - "Atto-Secret-Key": api_key, - }, - }) - ).text(), - ), - ); - } - async function query(body) { if (!api_key) { throw Error("No API key provided."); @@ -304,7 +285,6 @@ export default function tetratto({ api_key, // app data app, - check_ip, query, insert, update, diff --git a/crates/app/src/routes/api/v1/app_data.rs b/crates/app/src/routes/api/v1/app_data.rs index b5fa212..d5e8c3f 100644 --- a/crates/app/src/routes/api/v1/app_data.rs +++ b/crates/app/src/routes/api/v1/app_data.rs @@ -72,7 +72,7 @@ pub async fn create_request( // check size let new_size = app.data_used + req.value.len(); - if new_size > AppData::user_limit(&owner, &app) { + if new_size > AppData::user_limit(&owner) { return Json(Error::AppHitStorageLimit.into()); } @@ -155,19 +155,12 @@ pub async fn update_value_request( let size_without = app.data_used - app_data.value.len(); let new_size = size_without + req.value.len(); - if new_size > AppData::user_limit(&owner, &app) { + if new_size > AppData::user_limit(&owner) { return Json(Error::AppHitStorageLimit.into()); } // ... - // we only need to add the delta size (the next size - the old size) - if let Err(e) = data - .add_app_data_used( - app.id, - (req.value.len() as i32) - (app_data.value.len() as i32), - ) - .await - { + if let Err(e) = data.add_app_data_used(app.id, req.value.len() as i32).await { return Json(e.into()); } diff --git a/crates/app/src/routes/api/v1/apps.rs b/crates/app/src/routes/api/v1/apps.rs index eac16ba..3b5cd60 100644 --- a/crates/app/src/routes/api/v1/apps.rs +++ b/crates/app/src/routes/api/v1/apps.rs @@ -15,7 +15,7 @@ use tetratto_core::model::{ ApiReturn, Error, }; use tetratto_shared::{hash::random_id, unix_epoch_timestamp}; -use super::{CreateApp, UpdateAppStorageCapacity}; +use super::CreateApp; pub async fn create_request( jar: CookieJar, @@ -138,35 +138,6 @@ pub async fn update_quota_status_request( } } -pub async fn update_storage_capacity_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if !user.permissions.check(FinePermission::MANAGE_APPS) { - return Json(Error::NotAllowed.into()); - } - - match data - .update_app_storage_capacity(id, req.storage_capacity) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "App updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - pub async fn update_scopes_request( jar: CookieJar, Extension(data): Extension, diff --git a/crates/app/src/routes/api/v1/auth/ipbans.rs b/crates/app/src/routes/api/v1/auth/ipbans.rs index a8eb856..7163091 100644 --- a/crates/app/src/routes/api/v1/auth/ipbans.rs +++ b/crates/app/src/routes/api/v1/auth/ipbans.rs @@ -1,34 +1,12 @@ use crate::{ - get_app_from_key, get_user_from_token, + State, get_user_from_token, model::{ApiReturn, Error}, routes::api::v1::CreateIpBan, - State, }; -use axum::{extract::Path, http::HeaderMap, response::IntoResponse, Extension, Json}; +use axum::{Extension, Json, extract::Path, response::IntoResponse}; use crate::cookie::CookieJar; use tetratto_core::model::{addr::RemoteAddr, auth::IpBan, permissions::FinePermission}; -/// Check if the given IP is banned. -pub async fn check_request( - headers: HeaderMap, - Path(ip): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - if get_app_from_key!(data, headers).is_none() { - return Json(Error::NotAllowed.into()); - } - - Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: data - .get_ipban_by_addr(&RemoteAddr::from(ip.as_str())) - .await - .is_ok(), - }) -} - /// Create a new IP ban. pub async fn create_request( jar: CookieJar, diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 8a5d95a..6b56e9b 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -20,9 +20,9 @@ use axum::{ routing::{any, delete, get, post, put}, Router, }; -use serde::Deserialize; +use serde::{Deserialize}; use tetratto_core::model::{ - apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota, DeveloperPassStorageQuota}, + apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota}, auth::AchievementName, communities::{ CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, @@ -432,10 +432,6 @@ pub fn routes() -> Router { "/apps/{id}/quota_status", post(apps::update_quota_status_request), ) - .route( - "/apps/{id}/storage_capacity", - post(apps::update_storage_capacity_request), - ) .route("/apps/{id}/scopes", post(apps::update_scopes_request)) .route("/apps/{id}/grant", post(apps::grant_request)) .route("/apps/{id}/roll", post(apps::roll_api_key_request)) @@ -495,7 +491,6 @@ pub fn routes() -> Router { post(communities::communities::update_membership_role), ) // ipbans - .route("/bans/{ip}", get(auth::ipbans::check_request)) .route("/bans/{ip}", post(auth::ipbans::create_request)) .route("/bans/{ip}", delete(auth::ipbans::delete_request)) // reports @@ -1036,11 +1031,6 @@ pub struct UpdateAppQuotaStatus { pub quota_status: AppQuota, } -#[derive(Deserialize)] -pub struct UpdateAppStorageCapacity { - pub storage_capacity: DeveloperPassStorageQuota, -} - #[derive(Deserialize)] pub struct UpdateAppScopes { pub scopes: Vec, diff --git a/crates/app/src/routes/api/v1/notes.rs b/crates/app/src/routes/api/v1/notes.rs index 979dbf7..bba335e 100644 --- a/crates/app/src/routes/api/v1/notes.rs +++ b/crates/app/src/routes/api/v1/notes.rs @@ -267,7 +267,7 @@ pub async fn delete_by_dir_request( } pub async fn render_markdown_request(Json(req): Json) -> impl IntoResponse { - tetratto_shared::markdown::render_markdown(&CustomEmoji::replace(&req.content), true) + tetratto_shared::markdown::render_markdown(&CustomEmoji::replace(&req.content)) .replace("\\@", "@") .replace("%5C@", "@") } diff --git a/crates/app/src/routes/pages/developer.rs b/crates/app/src/routes/pages/developer.rs index a9e5f92..0d421f7 100644 --- a/crates/app/src/routes/pages/developer.rs +++ b/crates/app/src/routes/pages/developer.rs @@ -62,7 +62,7 @@ pub async fn app_request( )); } - let data_limit = AppData::user_limit(&user, &app); + let data_limit = AppData::user_limit(&user); let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index 3f6274b..15a3ee8 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -731,21 +731,6 @@ pub async fn following_request( check_user_blocked_or_private!(user, other_user, data, jar); - // check hide_social_follows - if other_user.settings.hide_social_follows { - if let Some(ref ua) = user { - if ua.id != other_user.id { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &user).await, - )); - } - } else { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &user).await, - )); - } - } - // fetch data let list = match data .0 @@ -841,21 +826,6 @@ pub async fn followers_request( check_user_blocked_or_private!(user, other_user, data, jar); - // check hide_social_follows - if other_user.settings.hide_social_follows { - if let Some(ref ua) = user { - if ua.id != other_user.id { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &user).await, - )); - } - } else { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &user).await, - )); - } - } - // fetch data let list = match data .0 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index bd7ac03..98a0947 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tetratto-core" description = "The core behind Tetratto" -version = "12.0.2" +version = "12.0.0" edition = "2024" authors.workspace = true repository.workspace = true @@ -18,7 +18,7 @@ default = ["database", "types", "sdk"] pathbufd = "0.1.4" serde = { version = "1.0.219", features = ["derive"] } toml = "0.9.2" -tetratto-shared = { version = "12.0.6", path = "../shared" } +tetratto-shared = { version = "12.0.0", path = "../shared" } tetratto-l10n = { version = "12.0.0", path = "../l10n" } serde_json = "1.0.141" totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"], optional = true } @@ -35,4 +35,6 @@ oiseau = { version = "0.1.2", default-features = false, features = [ "redis", ], optional = true } paste = { version = "1.0.15", optional = true } + +[dev-dependencies] tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } diff --git a/crates/core/src/database/app_data.rs b/crates/core/src/database/app_data.rs index 9aeafc1..d6225fc 100644 --- a/crates/core/src/database/app_data.rs +++ b/crates/core/src/database/app_data.rs @@ -5,7 +5,7 @@ use crate::{auto_method, DataManager}; use oiseau::{PostgresRow, execute, get, query_row, query_rows, params}; pub const FREE_DATA_LIMIT: usize = 512_000; -pub const PASS_DATA_LIMIT: usize = 26_214_400; +pub const PASS_DATA_LIMIT: usize = 5_242_880; impl DataManager { /// Get a [`AppData`] from an SQL row. @@ -117,13 +117,13 @@ impl DataManager { let app = self.get_app_by_id(data.app).await?; // check values - if data.key.len() < 1 { + if data.key.len() < 2 { return Err(Error::DataTooShort("key".to_string())); - } else if data.key.len() > 128 { + } else if data.key.len() > 32 { return Err(Error::DataTooLong("key".to_string())); } - if data.value.len() < 1 { + if data.value.len() < 2 { return Err(Error::DataTooShort("value".to_string())); } else if data.value.len() > Self::MAXIMUM_DATA_SIZE { return Err(Error::DataTooLong("value".to_string())); diff --git a/crates/core/src/database/apps.rs b/crates/core/src/database/apps.rs index 72334a8..b605cb6 100644 --- a/crates/core/src/database/apps.rs +++ b/crates/core/src/database/apps.rs @@ -1,6 +1,6 @@ use oiseau::cache::Cache; use crate::model::{ - apps::{AppQuota, ThirdPartyApp, DeveloperPassStorageQuota}, + apps::{AppQuota, ThirdPartyApp}, auth::User, oauth::AppScope, permissions::{FinePermission, SecondaryPermission}, @@ -25,7 +25,6 @@ impl DataManager { scopes: serde_json::from_str(&get!(x->9(String))).unwrap(), api_key: get!(x->10(String)), data_used: get!(x->11(i32)) as usize, - storage_capacity: serde_json::from_str(&get!(x->12(String))).unwrap(), } } @@ -96,7 +95,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", + "INSERT INTO apps VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", params![ &(data.id as i64), &(data.created as i64), @@ -109,8 +108,7 @@ impl DataManager { &(data.grants as i32), &serde_json::to_string(&data.scopes).unwrap(), &data.api_key, - &(data.data_used as i32), - &serde_json::to_string(&data.storage_capacity).unwrap(), + &(data.data_used as i32) ] ); @@ -169,7 +167,6 @@ impl DataManager { auto_method!(update_app_quota_status(AppQuota)@get_app_by_id -> "UPDATE apps SET quota_status = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app); auto_method!(update_app_scopes(Vec)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET scopes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app); auto_method!(update_app_api_key(&str)@get_app_by_id -> "UPDATE apps SET api_key = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); - auto_method!(update_app_storage_capacity(DeveloperPassStorageQuota)@get_app_by_id -> "UPDATE apps SET storage_capacity = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_app); auto_method!(update_app_data_used(i32)@get_app_by_id -> "UPDATE apps SET data_used = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); auto_method!(add_app_data_used(i32)@get_app_by_id -> "UPDATE apps SET data_used = data_used + $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 4ced643..3bd8678 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -100,21 +100,14 @@ impl DataManager { tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(), permissions: FinePermission::from_bits(get!(x->7(i32)) as u32).unwrap(), is_verified: get!(x->8(i32)) as i8 == 1, - notification_count: { - let x = get!(x->9(i32)) as usize; - // we're a little too close to the maximum count, clearly something's gone wrong - if x > usize::MAX - 1000 { 0 } else { x } - }, + notification_count: get!(x->9(i32)) as usize, follower_count: get!(x->10(i32)) as usize, following_count: get!(x->11(i32)) as usize, last_seen: get!(x->12(i64)) as usize, totp: get!(x->13(String)), recovery_codes: serde_json::from_str(&get!(x->14(String)).to_string()).unwrap(), post_count: get!(x->15(i32)) as usize, - request_count: { - let x = get!(x->16(i32)) as usize; - if x > usize::MAX - 1000 { 0 } else { x } - }, + request_count: get!(x->16(i32)) as usize, connections: serde_json::from_str(&get!(x->17(String)).to_string()).unwrap(), stripe_id: get!(x->18(String)), grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(), diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 5e10783..d37c330 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -44,7 +44,6 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_SERVICES).unwrap(); execute!(&conn, common::CREATE_TABLE_PRODUCTS).unwrap(); execute!(&conn, common::CREATE_TABLE_APP_DATA).unwrap(); - execute!(&conn, common::CREATE_TABLE_LETTERS).unwrap(); for x in common::VERSION_MIGRATIONS.split(";") { execute!(&conn, x).unwrap(); diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index bccbfb9..d2239a6 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -32,4 +32,3 @@ pub const CREATE_TABLE_DOMAINS: &str = include_str!("./sql/create_domains.sql"); pub const CREATE_TABLE_SERVICES: &str = include_str!("./sql/create_services.sql"); pub const CREATE_TABLE_PRODUCTS: &str = include_str!("./sql/create_products.sql"); pub const CREATE_TABLE_APP_DATA: &str = include_str!("./sql/create_app_data.sql"); -pub const CREATE_TABLE_LETTERS: &str = include_str!("./sql/create_letters.sql"); diff --git a/crates/core/src/database/drivers/sql/create_apps.sql b/crates/core/src/database/drivers/sql/create_apps.sql index 70f2b8d..d01ed41 100644 --- a/crates/core/src/database/drivers/sql/create_apps.sql +++ b/crates/core/src/database/drivers/sql/create_apps.sql @@ -9,6 +9,5 @@ CREATE TABLE IF NOT EXISTS apps ( banned INT NOT NULL, grants INT NOT NULL, scopes TEXT NOT NULL, - data_used INT NOT NULL CHECK (data_used >= 0), - storage_capacity TEXT NOT NULL + data_used INT NOT NULL CHECK (data_used >= 0) ) diff --git a/crates/core/src/database/drivers/sql/create_letters.sql b/crates/core/src/database/drivers/sql/create_letters.sql deleted file mode 100644 index f3100eb..0000000 --- a/crates/core/src/database/drivers/sql/create_letters.sql +++ /dev/null @@ -1,9 +0,0 @@ -CREATE TABLE IF NOT EXISTS letters ( - id BIGINT NOT NULL PRIMARY KEY, - created BIGINT NOT NULL, - owner BIGINT NOT NULL, - receivers TEXT NOT NULL, - subject TEXT NOT NULL, - content TEXT NOT NULL, - read_by TEXT NOT NULL -) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql index c101e7d..c0c863a 100644 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ b/crates/core/src/database/drivers/sql/version_migrations.sql @@ -5,11 +5,3 @@ ADD COLUMN IF NOT EXISTS channel_mutes TEXT DEFAULT '[]'; -- users is_deactivated ALTER TABLE users ADD COLUMN IF NOT EXISTS is_deactivated INT DEFAULT 0; - --- apps storage_capacity -ALTER TABLE apps -ADD COLUMN IF NOT EXISTS storage_capacity TEXT DEFAULT '"Tier1"'; - --- letters replying_to -ALTER TABLE letters -ADD COLUMN IF NOT EXISTS replying_to TEXT DEFAULT 0; diff --git a/crates/core/src/database/letters.rs b/crates/core/src/database/letters.rs deleted file mode 100644 index fe9bbac..0000000 --- a/crates/core/src/database/letters.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::model::{auth::User, mail::Letter, permissions::SecondaryPermission, Error, Result}; -use crate::{auto_method, DataManager}; -use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow}; - -impl DataManager { - /// Get a [`Letter`] from an SQL row. - pub(crate) fn get_letter_from_row(x: &PostgresRow) -> Letter { - Letter { - id: get!(x->0(i64)) as usize, - created: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - receivers: serde_json::from_str(&get!(x->3(String))).unwrap(), - subject: get!(x->4(String)), - content: get!(x->5(String)), - read_by: serde_json::from_str(&get!(x->6(String))).unwrap(), - replying_to: get!(x->7(i32)) as usize, - } - } - - auto_method!(get_letter_by_id(usize as i64)@get_letter_from_row -> "SELECT * FROM letters WHERE id = $1" --name="letter" --returns=Letter --cache-key-tmpl="atto.letter:{}"); - - /// Get all letters by user. - /// - /// # Arguments - /// * `id` - the ID of the user to fetch letters for - pub async fn get_letters_by_user(&self, id: usize) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM letters WHERE owner = $1 ORDER BY created DESC", - &[&(id as i64)], - |x| { Self::get_letter_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("letter".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all letters by user (where user is a receiver). - /// - /// # Arguments - /// * `id` - the ID of the user to fetch letters for - pub async fn get_received_letters_by_user(&self, id: usize) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM letters WHERE receivers LIKE $1 ORDER BY created DESC", - &[&format!("%\"{id}\"%")], - |x| { Self::get_letter_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("letter".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all letters which are replying to the given letter. - /// - /// # Arguments - /// * `id` - the ID of the letter to fetch letters for - pub async fn get_letters_by_replying_to(&self, id: usize) -> Result> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_rows!( - &conn, - "SELECT * FROM letters WHERE replying_to = $1 ORDER BY created DESC", - &[&(id as i64)], - |x| { Self::get_letter_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("letter".to_string())); - } - - Ok(res.unwrap()) - } - - /// Create a new letter in the database. - /// - /// # Arguments - /// * `data` - a mock [`Letter`] object to insert - pub async fn create_letter(&self, data: Letter) -> Result { - // check values - if data.subject.len() < 2 { - return Err(Error::DataTooShort("subject".to_string())); - } else if data.subject.len() > 256 { - return Err(Error::DataTooLong("subject".to_string())); - } - - if data.content.len() < 2 { - return Err(Error::DataTooShort("content".to_string())); - } else if data.content.len() > 16384 { - return Err(Error::DataTooLong("content".to_string())); - } - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "INSERT INTO letters VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", - params![ - &(data.id as i64), - &(data.created as i64), - &(data.owner as i64), - &serde_json::to_string(&data.receivers).unwrap(), - &data.subject, - &data.content, - &serde_json::to_string(&data.read_by).unwrap(), - &(data.replying_to as i64) - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(data) - } - - pub async fn delete_letter(&self, id: usize, user: &User) -> Result<()> { - let letter = self.get_letter_by_id(id).await?; - - // check user permission - if user.id != letter.owner - && !user - .secondary_permissions - .check(SecondaryPermission::MANAGE_LETTERS) - { - return Err(Error::NotAllowed); - } - - // ... - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!(&conn, "DELETE FROM letters WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // ... - self.0.1.remove(format!("atto.letter:{}", id)).await; - Ok(()) - } - - auto_method!(update_letter_read_by(Vec) -> "UPDATE letters SET read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.letter:{}"); -} diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 218bcd6..80b77a1 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -14,7 +14,6 @@ mod invite_codes; mod ipbans; mod ipblocks; mod journals; -mod letters; mod memberships; mod message_reactions; mod messages; diff --git a/crates/core/src/database/questions.rs b/crates/core/src/database/questions.rs index 3703d4f..7722250 100644 --- a/crates/core/src/database/questions.rs +++ b/crates/core/src/database/questions.rs @@ -408,10 +408,6 @@ impl DataManager { // check muted phrases for phrase in receiver.settings.muted { - if phrase.is_empty() { - continue; - } - if data.content.contains(&phrase) { // act like the question was created so theyre less likely to try and send it again or bypass return Ok(0); diff --git a/crates/core/src/html.rs b/crates/core/src/html.rs deleted file mode 100644 index 73c42e1..0000000 --- a/crates/core/src/html.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{ - collections::HashMap, - fs::{exists, read_to_string, write}, - sync::LazyLock, -}; -use tokio::sync::RwLock; - -use pathbufd::PathBufD; - -/// A container for all loaded icons. -pub static ICONS: LazyLock>> = - LazyLock::new(|| RwLock::new(HashMap::new())); - -/// Pull an icon given its name and insert it into [`ICONS`]. -pub async fn pull_icon(icon: &str, icons_dir: &str) { - let writer = &mut ICONS.write().await; - - let icon_url = format!( - "https://raw.githubusercontent.com/lucide-icons/lucide/refs/heads/main/icons/{icon}.svg" - ); - - let file_path = PathBufD::current().extend(&[icons_dir, &format!("{icon}.svg")]); - - if exists(&file_path).unwrap_or(false) { - writer.insert(icon.to_string(), read_to_string(&file_path).unwrap()); - return; - } - - println!("download icon: {icon}"); - let svg = reqwest::get(icon_url) - .await - .unwrap() - .text() - .await - .unwrap() - .replace("\n", ""); - - write(&file_path, &svg).unwrap(); - writer.insert(icon.to_string(), svg); -} - -/// Read a string and pull all icons found within it. -pub async fn pull_icons(mut input: String, icon_dir: &str) -> String { - // icon (with class) - let icon_with_class = - regex::Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*c\\((.*?)\\)\\s*(\\}\\})").unwrap(); - - for cap in icon_with_class.captures_iter(&input.clone()) { - let cap_str = &cap.get(3).unwrap().as_str().replace("\"", ""); - let icon = &(if cap_str.contains(" }}") { - cap_str.split(" }}").next().unwrap().to_string() - } else { - cap_str.to_string() - }); - - pull_icon(icon, icon_dir).await; - - let reader = ICONS.read().await; - let icon_text = reader.get(icon).unwrap().replace( - " Self { - Self::Tier1 - } -} - -impl DeveloperPassStorageQuota { - pub fn limit(&self) -> usize { - match self { - DeveloperPassStorageQuota::Tier1 => 26214400, - DeveloperPassStorageQuota::Tier2 => 52428800, - DeveloperPassStorageQuota::Tier3 => 104857600, - } - } -} - /// An app is required to request grants on user accounts. /// /// Users must approve grants through a web portal. @@ -119,8 +90,6 @@ pub struct ThirdPartyApp { pub api_key: String, /// The number of bytes the app's app_data rows are using. pub data_used: usize, - /// The app's storage capacity. - pub storage_capacity: DeveloperPassStorageQuota, } impl ThirdPartyApp { @@ -133,13 +102,12 @@ impl ThirdPartyApp { title, homepage, redirect, - quota_status: AppQuota::default(), + quota_status: AppQuota::Limited, banned: false, grants: 0, scopes: Vec::new(), api_key: String::new(), data_used: 0, - storage_capacity: DeveloperPassStorageQuota::default(), } } } @@ -164,16 +132,12 @@ impl AppData { } /// Get the data limit of a given user. - pub fn user_limit(user: &User, app: &ThirdPartyApp) -> usize { + pub fn user_limit(user: &User) -> usize { if user .secondary_permissions .check(SecondaryPermission::DEVELOPER_PASS) { - if app.storage_capacity != DeveloperPassStorageQuota::Tier1 { - app.storage_capacity.limit() - } else { - PASS_DATA_LIMIT - } + PASS_DATA_LIMIT } else { FREE_DATA_LIMIT } diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 2e45b73..37d2bf9 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; + use super::{ oauth::AuthGrant, permissions::{FinePermission, SecondaryPermission}, @@ -337,10 +338,6 @@ pub struct UserSettings { /// Biography shown on `profile/private.lisp` page. #[serde(default)] pub private_biography: String, - /// If the followers/following links are hidden from the user's profile. - /// Will also revoke access to their respective pages. - #[serde(default)] - pub hide_social_follows: bool, } #[derive(Clone, Debug, Serialize, Deserialize, Default)] diff --git a/crates/core/src/model/mail.rs b/crates/core/src/model/mail.rs deleted file mode 100644 index 8336821..0000000 --- a/crates/core/src/model/mail.rs +++ /dev/null @@ -1,44 +0,0 @@ -use serde::{Serialize, Deserialize}; -use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; - -/// A letter is the most basic structure of the mail system. Letters are sent -/// and received by users. -#[derive(Serialize, Deserialize)] -pub struct Letter { - pub id: usize, - pub created: usize, - pub owner: usize, - pub receivers: Vec, - pub subject: String, - pub content: String, - /// The ID of every use who has read the letter. Can be checked in the UI - /// with `user.id in letter.read_by`. - /// - /// This field can be updated by anyone in the letter's `receivers` field. - /// Other fields in the letter can only be updated by the letter's `owner`. - pub read_by: Vec, - /// The ID of the letter this letter is replying to. - pub replying_to: usize, -} - -impl Letter { - /// Create a new [`Letter`]. - pub fn new( - owner: usize, - receivers: Vec, - subject: String, - content: String, - replying_to: usize, - ) -> Self { - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - created: unix_epoch_timestamp(), - owner, - receivers, - subject, - content, - read_by: Vec::new(), - replying_to, - } - } -} diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 06c4149..7d7f19e 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -7,7 +7,6 @@ pub mod communities; pub mod communities_permissions; pub mod journals; pub mod littleweb; -pub mod mail; pub mod moderation; pub mod oauth; pub mod permissions; diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 61ebb61..796b9f1 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -178,7 +178,6 @@ bitflags! { const MANAGE_SERVICES = 1 << 3; const MANAGE_PRODUCTS = 1 << 4; const DEVELOPER_PASS = 1 << 5; - const MANAGE_LETTERS = 1 << 6; const _ = !0; } diff --git a/crates/core/src/sdk.rs b/crates/core/src/sdk.rs index 0e5add6..71ba502 100644 --- a/crates/core/src/sdk.rs +++ b/crates/core/src/sdk.rs @@ -75,22 +75,6 @@ impl DataClient { } } - /// Check if the given IP is IP banned from the Tetratto host. You will only know - /// if the IP is banned or not, meaning you will not be shown the reason if it - /// is banned. - pub async fn check_ip(&self, ip: &str) -> Result { - match self - .http - .get(format!("{}/api/v1/bans/{}", self.host, ip)) - .header("Atto-Secret-Key", &self.api_key) - .send() - .await - { - Ok(x) => api_return_ok!(bool, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - /// Query the app's data. pub async fn query(&self, query: &SimplifiedQuery) -> Result { match self diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index a866bd1..02b14fc 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tetratto-shared" description = "Shared stuff for Tetratto" -version = "12.0.6" +version = "12.0.0" edition = "2024" authors.workspace = true repository.workspace = true @@ -10,10 +10,9 @@ license.workspace = true [dependencies] ammonia = "4.1.1" chrono = "0.4.41" +markdown = "1.0.0" hex_fmt = "0.3.0" -pulldown-cmark = "0.13.0" rand = "0.9.1" -regex = "1.11.1" serde = { version = "1.0.219", features = ["derive"] } sha2 = "0.10.9" snowflaked = "1.0.3" diff --git a/crates/shared/src/markdown.rs b/crates/shared/src/markdown.rs index 022e23d..82d6b79 100644 --- a/crates/shared/src/markdown.rs +++ b/crates/shared/src/markdown.rs @@ -1,42 +1,36 @@ use ammonia::Builder; -use pulldown_cmark::{Parser, Options, html::push_html}; +use markdown::{to_html_with_options, Options, CompileOptions, ParseOptions, Constructs}; use std::collections::HashSet; -pub fn render_markdown_dirty(input: &str) -> String { - let input = &autolinks(&parse_alignment(&parse_backslash_breaks(input))); - - let mut options = Options::empty(); - options.insert(Options::ENABLE_STRIKETHROUGH); - options.insert(Options::ENABLE_GFM); - options.insert(Options::ENABLE_FOOTNOTES); - options.insert(Options::ENABLE_TABLES); - options.insert(Options::ENABLE_HEADING_ATTRIBUTES); - options.insert(Options::ENABLE_SUBSCRIPT); - - let parser = Parser::new_ext(input, options); - - let mut html = String::new(); - push_html(&mut html, parser); - - html -} - -pub fn clean_html(html: String, allowed_attributes: HashSet<&str>) -> String { - Builder::default() - .generic_attributes(allowed_attributes) - .add_tags(&[ - "video", "source", "img", "b", "span", "p", "i", "strong", "em", "a", "align", - ]) - .rm_tags(&["script", "style", "link", "canvas"]) - .add_tag_attributes("a", &["href", "target"]) - .add_url_schemes(&["atto"]) - .clean(&html.replace("