diff --git a/.gitignore b/.gitignore index 7bc86aa..f5f83f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target debug/ -.dev diff --git a/Cargo.lock b/Cargo.lock index adc1cbb..c90535f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,9 +34,9 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" [[package]] name = "ammonia" -version = "4.1.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" +checksum = "3ada2ee439075a3e70b6992fce18ac4e407cd05aea9ca3f75d2c0b0c20bbb364" dependencies = [ "cssparser", "html5ever", @@ -337,6 +337,12 @@ dependencies = [ "tokio-postgres", ] +[[package]] +name = "bberry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee0ee2ee1f1a6094d77ba1bf5402f8a8d66e77f6353aff728e37249b2e77458" + [[package]] name = "bit_field" version = "0.10.2" @@ -955,15 +961,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" @@ -1127,11 +1124,12 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.35.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c" dependencies = [ "log", + "mac", "markup5ever", "match_token", ] @@ -1578,17 +1576,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -1753,10 +1740,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] -name = "markup5ever" -version = "0.35.0" +name = "markdown" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +checksum = "a5cab8f2cadc416a82d2e783a1946388b31654d391d1c7d92cc1f03e295b1deb" +dependencies = [ + "unicode-id", +] + +[[package]] +name = "markup5ever" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a8096766c229e8c88a3900c9b44b7e06aa7f7343cc229158c3e58ef8f9973a" dependencies = [ "log", "tendril", @@ -1765,9 +1761,9 @@ dependencies = [ [[package]] name = "match_token" -version = "0.35.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", @@ -1875,12 +1871,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "nanoneo" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1495d19c5bed5372c613d7b4a38e8093b357f4405ce38ba1de2d6586e5c892" - [[package]] name = "native-tls" version = "0.2.14" @@ -2355,25 +2345,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" @@ -2668,9 +2639,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "base64 0.22.1", "bytes", @@ -2688,7 +2659,6 @@ dependencies = [ "js-sys", "log", "mime", - "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", @@ -2904,9 +2874,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -2955,15 +2925,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3230,7 +3191,7 @@ dependencies = [ "cfg-expr", "heck", "pkg-config", - "toml 0.8.23", + "toml", "version-compare", ] @@ -3288,20 +3249,19 @@ dependencies = [ [[package]] name = "tetratto" -version = "12.0.0" +version = "11.0.0" dependencies = [ "ammonia", "async-stripe", "axum", "axum-extra", + "bberry", "cf-turnstile", "contrasted", - "cookie", "emojis", "futures-util", "image", "mime_guess", - "nanoneo", "pathbufd", "regex", "reqwest", @@ -3320,7 +3280,7 @@ dependencies = [ [[package]] name = "tetratto-core" -version = "12.0.2" +version = "11.0.0" dependencies = [ "async-recursion", "base16ct", @@ -3337,30 +3297,28 @@ dependencies = [ "serde_json", "tetratto-l10n", "tetratto-shared", - "tokio", - "toml 0.9.2", + "toml", "totp-rs", ] [[package]] name = "tetratto-l10n" -version = "12.0.0" +version = "11.0.0" dependencies = [ "pathbufd", "serde", - "toml 0.9.2", + "toml", ] [[package]] name = "tetratto-shared" -version = "12.0.6" +version = "11.0.0" dependencies = [ "ammonia", "chrono", "hex_fmt", - "pulldown-cmark", + "markdown", "rand 0.9.1", - "regex", "serde", "sha2", "snowflaked", @@ -3486,18 +3444,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", - "slab", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -3592,26 +3548,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_edit", ] -[[package]] -name = "toml" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow", -] - [[package]] name = "toml_datetime" version = "0.6.11" @@ -3621,15 +3562,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" -dependencies = [ - "serde", -] - [[package]] name = "toml_edit" version = "0.22.27" @@ -3638,25 +3570,17 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", + "toml_write", "winnow", ] [[package]] -name = "toml_parser" -version = "1.0.1" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" -dependencies = [ - "winnow", -] - -[[package]] -name = "toml_writer" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "totp-rs" @@ -3891,6 +3815,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 +3842,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/Cargo.toml b/Cargo.toml index b5beca0..e8d6326 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ members = ["crates/app", "crates/shared", "crates/core", "crates/l10n"] package.authors = ["trisuaso"] package.repository = "https://trisua.com/t/tetratto" package.license = "AGPL-3.0-or-later" -package.homepage = "https://tetratto.com" [profile.dev] incremental = true diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index e5ca15f..8a7eb6e 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -1,11 +1,7 @@ [package] name = "tetratto" -version = "12.0.0" +version = "11.0.0" edition = "2024" -authors.workspace = true -repository.workspace = true -license.workspace = true -homepage.workspace = true [dependencies] pathbufd = "0.1.4" @@ -20,16 +16,17 @@ tower-http = { version = "0.6.6", features = [ "set-header", ] } axum = { version = "0.8.4", features = ["macros", "ws"] } -tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] } axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] } -ammonia = "4.1.1" +ammonia = "4.1.0" tetratto-shared = { path = "../shared" } tetratto-core = { path = "../core" } tetratto-l10n = { path = "../l10n" } + image = "0.25.6" -reqwest = { version = "0.12.22", features = ["json", "stream"] } +reqwest = { version = "0.12.20", features = ["json", "stream"] } regex = "1.11.1" -serde_json = "1.0.141" +serde_json = "1.0.140" mime_guess = "2.0.5" cf-turnstile = "0.2.0" contrasted = "0.1.3" @@ -40,9 +37,7 @@ async-stripe = { version = "0.41.0", features = [ "webhook-events", "billing", "runtime-tokio-hyper", - "connect", ] } emojis = "0.7.0" webp = "0.3.0" -nanoneo = "0.2.0" -cookie = "0.18.1" +bberry = "0.2.0" diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 82cf197..81671fe 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -1,17 +1,21 @@ -use nanoneo::{ +use bberry::{ core::element::{Element, Render}, text, read_param, }; 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}, + permissions::FinePermission, }, }; use tetratto_l10n::LangFile; @@ -36,8 +40,8 @@ pub const ATTO_JS: &str = include_str!("./public/js/atto.js"); pub const ME_JS: &str = include_str!("./public/js/me.js"); pub const STREAMS_JS: &str = include_str!("./public/js/streams.js"); pub const CARP_JS: &str = include_str!("./public/js/carp.js"); +pub const LAYOUT_EDITOR_JS: &str = include_str!("./public/js/layout_editor.js"); pub const PROTO_LINKS_JS: &str = include_str!("./public/js/proto_links.js"); -pub const APP_SDK_JS: &str = include_str!("./public/js/app_sdk.js"); // html pub const BODY: &str = include_str!("./public/html/body.lisp"); @@ -55,7 +59,6 @@ pub const AUTH_BASE: &str = include_str!("./public/html/auth/base.lisp"); pub const AUTH_LOGIN: &str = include_str!("./public/html/auth/login.lisp"); pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.lisp"); pub const AUTH_CONNECTION: &str = include_str!("./public/html/auth/connection.lisp"); -pub const AUTH_SELLER_CONNECTION: &str = include_str!("./public/html/auth/seller_connection.lisp"); pub const PROFILE_BASE: &str = include_str!("./public/html/profile/base.lisp"); pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.lisp"); @@ -137,8 +140,6 @@ pub const LITTLEWEB_SERVICE: &str = include_str!("./public/html/littleweb/servic pub const LITTLEWEB_DOMAIN: &str = include_str!("./public/html/littleweb/domain.lisp"); pub const LITTLEWEB_BROWSER: &str = include_str!("./public/html/littleweb/browser.lisp"); -pub const MARKETPLACE_SELLER: &str = include_str!("./public/html/marketplace/seller.lisp"); - // langs pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); @@ -146,13 +147,44 @@ pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); pub const VENDOR_SPOTIFY_ICON: &str = include_str!("./public/images/vendor/spotify.svg"); pub const VENDOR_LAST_FM_ICON: &str = include_str!("./public/images/vendor/last-fm.svg"); -pub const VENDOR_STRIPE_ICON: &str = include_str!("./public/images/vendor/stripe.svg"); pub const TETRATTO_BUNNY: &[u8] = include_bytes!("./public/images/tetratto_bunny.webp"); 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; @@ -205,7 +237,7 @@ pub(crate) async fn replace_in_html( input.to_string() } else { let start = SystemTime::now(); - let parsed = nanoneo::parse(input); + let parsed = bberry::parse(input); println!("parsed lisp in {}μs", start.elapsed().unwrap().as_micros()); if let Some(plugins) = plugins { @@ -225,8 +257,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); @@ -264,7 +344,6 @@ pub(crate) fn lisp_plugins() -> HashMap Elemen pub(crate) async fn write_assets(config: &Config) -> PathBufD { vendor_icon!("spotify", VENDOR_SPOTIFY_ICON, config.dirs.icons); vendor_icon!("last_fm", VENDOR_LAST_FM_ICON, config.dirs.icons); - vendor_icon!("stripe", VENDOR_STRIPE_ICON, config.dirs.icons); bin_icon!("tetratto_bunny.webp", TETRATTO_BUNNY, config.dirs.assets); // ... @@ -286,7 +365,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"auth/login.html"(crate::assets::AUTH_LOGIN) --config=config --lisp plugins); write_template!(html_path->"auth/register.html"(crate::assets::AUTH_REGISTER) --config=config --lisp plugins); write_template!(html_path->"auth/connection.html"(crate::assets::AUTH_CONNECTION) --config=config --lisp plugins); - write_template!(html_path->"auth/seller_connection.html"(crate::assets::AUTH_SELLER_CONNECTION) --config=config --lisp plugins); write_template!(html_path->"profile/base.html"(crate::assets::PROFILE_BASE) -d "profile" --config=config --lisp plugins); write_template!(html_path->"profile/posts.html"(crate::assets::PROFILE_POSTS) --config=config --lisp plugins); @@ -363,8 +441,6 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"littleweb/domain.html"(crate::assets::LITTLEWEB_DOMAIN) --config=config --lisp plugins); write_template!(html_path->"littleweb/browser.html"(crate::assets::LITTLEWEB_BROWSER) --config=config --lisp plugins); - write_template!(html_path->"marketplace/seller.html"(crate::assets::MARKETPLACE_SELLER) -d "marketplace" --config=config --lisp plugins); - html_path } @@ -432,11 +508,6 @@ pub(crate) async fn initial_context( "is_supporter", &ua.permissions.check(FinePermission::SUPPORTER), ); - ctx.insert( - "has_developer_pass", - &ua.secondary_permissions - .check(SecondaryPermission::DEVELOPER_PASS), - ); ctx.insert("home", &ua.settings.default_timeline.relative_url()); } else { ctx.insert("is_helper", &false); diff --git a/crates/app/src/cookie.rs b/crates/app/src/cookie.rs deleted file mode 100644 index 45fd9a4..0000000 --- a/crates/app/src/cookie.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::convert::Infallible; -use axum::{ - extract::FromRequestParts, - http::{request::Parts, HeaderMap}, -}; -use cookie::{Cookie, CookieJar as CookieCookieJar}; - -/// This is required because "Cookie" his a forbidden header for some fucking reason. -/// Stupidest thing I've ever encountered in JavaScript, absolute fucking insanity. -/// -/// Anyway, most of this shit is just from the original source for axum_extra::extract::CookieJar, -/// just edited to use X-Cookie instead. -/// -/// Stuff from axum_extra will have links to the original provided. -pub struct CookieJar { - jar: CookieCookieJar, -} - -/// -impl FromRequestParts for CookieJar -where - S: Send + Sync, -{ - type Rejection = Infallible; - - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - Ok(Self::from_headers(&parts.headers)) - } -} - -fn cookies_from_request( - header: String, - headers: &HeaderMap, -) -> impl Iterator> + '_ { - headers - .get_all(header) - .into_iter() - .filter_map(|value| value.to_str().ok()) - .flat_map(|value| value.split(';')) - .filter_map(|cookie| Cookie::parse_encoded(cookie.to_owned()).ok()) -} - -impl CookieJar { - /// - /// - /// Modified only to prefer "X-Cookie" header. - pub fn from_headers(headers: &HeaderMap) -> Self { - let mut jar = CookieCookieJar::new(); - - for cookie in cookies_from_request( - if headers.contains_key("X-Cookie") { - "X-Cookie".to_string() - } else { - "Cookie".to_string() - }, - headers, - ) { - jar.add_original(cookie.clone()); - } - - Self { jar } - } - - /// - pub fn get(&self, name: &str) -> Option<&Cookie<'static>> { - self.jar.get(name) - } -} diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index bd692f3..a852d5a 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -32,7 +32,6 @@ version = "1.0.0" "general:action.copy_link" = "Copy link" "general:action.copy_id" = "Copy ID" "general:action.post" = "Post" -"general:action.apply" = "Apply" "general:label.account" = "Account" "general:label.safety" = "Safety" "general:label.share" = "Share" @@ -131,7 +130,6 @@ version = "1.0.0" "communities:label.edit_content" = "Edit content" "communities:label.repost" = "Repost" "communities:label.quote_post" = "Quote post" -"communities:label.ask_about_this" = "Ask about this" "communities:label.search_results" = "Search results" "communities:label.query" = "Query" "communities:label.join_new" = "Join new" @@ -163,7 +161,6 @@ version = "1.0.0" "settings:tab.sessions" = "Sessions" "settings:tab.connections" = "Connections" "settings:tab.images" = "Images" -"settings:tab.presets" = "Presets" "settings:label.change_password" = "Change password" "settings:label.current_password" = "Current password" "settings:label.delete_account" = "Delete account" @@ -183,11 +180,6 @@ version = "1.0.0" "settings:label.ips" = "IPs" "settings:label.generate_invites" = "Generate invites" "settings:label.add_to_stack" = "Add to stack" -"settings:label.alt_text" = "Alt text" -"settings:label.deactivate_account" = "Deactivate account" -"settings:label.activate_account" = "Activate account" -"settings:label.deactivate" = "Deactivate" -"settings:label.account_deactivated" = "Account deactivated" "settings:tab.security" = "Security" "settings:tab.blocks" = "Blocks" "settings:tab.billing" = "Billing" @@ -202,7 +194,6 @@ version = "1.0.0" "mod_panel:label.associations" = "Associations" "mod_panel:label.invited_by" = "Invited by" "mod_panel:label.send_debug_payload" = "Send debug payload" -"mod_panel:label.ban_reason" = "Ban reason" "mod_panel:action.send" = "Send" "requests:label.requests" = "Requests" @@ -226,8 +217,6 @@ version = "1.0.0" "chats:action.add_someone" = "Add someone" "chats:action.kick_member" = "Kick member" "chats:action.mention_user" = "Mention user" -"chats:action.mute" = "Mute" -"chats:action.unmute" = "Unmute" "stacks:link.stacks" = "Stacks" "stacks:label.my_stacks" = "My stacks" @@ -240,7 +229,6 @@ version = "1.0.0" "stacks:label.block_all" = "Block all" "stacks:label.unblock_all" = "Unblock all" -"forge:label.forges" = "Forges" "forge:label.my_forges" = "My forges" "forge:label.create_new" = "Create new forge" "forge:tab.info" = "Info" @@ -249,7 +237,6 @@ version = "1.0.0" "forge:action.close" = "Close" "developer:label.for_developers" = "for Developers" -"developer:label.apps" = "Apps" "developer:label.my_apps" = "My apps" "developer:label.create_new" = "Create new app" "developer:label.homepage" = "Homepage" @@ -258,13 +245,9 @@ 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" -"developer:label.secret_key" = "Secret key" -"developer:label.roll_key" = "Roll key" -"developer:label.data_usage" = "Data usage" "developer:action.delete" = "Delete app" "developer:action.authorize" = "Authorize" @@ -302,9 +285,3 @@ version = "1.0.0" "littleweb:action.edit_site_name" = "Edit site name" "littleweb:action.rename" = "Rename" "littleweb:action.add" = "Add" - -"marketplace:label.products" = "Products" -"marketplace:label.status" = "Status" -"marketplace:action.get_started" = "Get started" -"marketplace:action.finsh_setting_up_account" = "Finish setting up my account" -"marketplace:action.open_seller_dashboard" = "Open seller dashboard" diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs index 69730e0..2c3c03c 100644 --- a/crates/app/src/macros.rs +++ b/crates/app/src/macros.rs @@ -87,10 +87,7 @@ macro_rules! get_user_from_token { { Ok(ua) => { if ua.permissions.check_banned() { - let mut banned_user = tetratto_core::model::auth::User::banned(); - banned_user.ban_reason = ua.ban_reason; - - Some(banned_user) + Some(tetratto_core::model::auth::User::banned()) } else { Some(ua) } @@ -112,7 +109,7 @@ macro_rules! get_user_from_token { Ok((grant, ua)) => { if grant.scopes.contains(&$grant_scope) { if ua.permissions.check_banned() { - None + Some(tetratto_core::model::auth::User::banned()) } else { Some(ua) } @@ -143,20 +140,6 @@ macro_rules! get_user_from_token { None } }}; - - (--browser_session=$browser_session:expr, $db:expr) => {{ - // browser session id - match $db.get_user_by_browser_session(&$browser_session).await { - Ok(ua) => { - if ua.permissions.check_banned() { - None - } else { - Some(ua) - } - } - Err(_) => None, - } - }}; } #[macro_export] @@ -192,27 +175,6 @@ macro_rules! user_banned { #[macro_export] macro_rules! check_user_blocked_or_private { ($user:expr, $other_user:ident, $data:ident, $jar:ident) => { - // check is_deactivated - if ($user.is_none() && $other_user.is_deactivated) - | ($user.is_some() - && !$user - .as_ref() - .unwrap() - .permissions - .check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS) - && $other_user.is_deactivated) - { - return Err(Html( - render_error( - Error::GeneralNotFound("user".to_string()), - &$jar, - &$data, - &$user, - ) - .await, - )); - } - // check require_account if $user.is_none() && $other_user.settings.require_account { return Err(Html( @@ -440,17 +402,3 @@ macro_rules! ignore_users_gen { .concat() }; } - -#[macro_export] -macro_rules! get_app_from_key { - ($db:ident, $headers:ident) => { - if let Some(token) = $headers.get("Atto-Secret-Key") { - match $db.get_app_by_api_key(token.to_str().unwrap()).await { - Ok(x) => Some(x), - Err(_) => None, - } - } else { - None - } - }; -} diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index b0098b7..00ad85f 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -2,18 +2,13 @@ #![doc(html_favicon_url = "/public/favicon.svg")] #![doc(html_logo_url = "/public/tetratto_bunny.webp")] mod assets; -mod cookie; mod image; mod macros; mod routes; mod sanitize; use assets::{init_dirs, write_assets}; -use stripe::Client as StripeClient; -use tetratto_core::model::{ - permissions::{FinePermission, SecondaryPermission}, - uploads::CustomEmoji, -}; +use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji}; pub use tetratto_core::*; use axum::{ @@ -32,17 +27,15 @@ use tracing::{Level, info}; use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc}; use tokio::sync::RwLock; -pub(crate) type InnerState = (DataManager, Tera, Client, Option); -pub(crate) type State = Arc>; +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 { @@ -60,15 +53,6 @@ fn check_supporter(value: &Value, _: &HashMap) -> tera::Result) -> tera::Result { - Ok( - SecondaryPermission::from_bits(value.as_u64().unwrap() as u32) - .unwrap() - .check(SecondaryPermission::DEVELOPER_PASS) - .into(), - ) -} - fn check_staff_badge(value: &Value, _: &HashMap) -> tera::Result { Ok(FinePermission::from_bits(value.as_u64().unwrap() as u32) .unwrap() @@ -123,7 +107,6 @@ async fn main() { tera.register_filter("markdown", render_markdown); tera.register_filter("color", color_escape); tera.register_filter("has_supporter", check_supporter); - tera.register_filter("has_dev_pass", check_dev_pass); tera.register_filter("has_staff_badge", check_staff_badge); tera.register_filter("has_banned", check_banned); tera.register_filter("remove_script_tags", remove_script_tags); @@ -132,13 +115,6 @@ async fn main() { let client = Client::new(); let mut app = Router::new(); - // create stripe client - let stripe_client = if let Some(ref stripe) = config.stripe { - Some(StripeClient::new(stripe.secret.clone())) - } else { - None - }; - // add correct routes if var("LITTLEWEB").is_ok() { app = app.merge(routes::lw_routes()); @@ -147,18 +123,13 @@ async fn main() { .merge(routes::routes(&config)) .layer(SetResponseHeaderLayer::if_not_present( HeaderName::from_static("content-security-policy"), - HeaderValue::from_static("default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors 'self'"), + HeaderValue::from_static("default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' *; frame-ancestors 'self'"), )); } // add junk app = app - .layer(Extension(Arc::new(RwLock::new(( - database, - tera, - client, - stripe_client, - ))))) + .layer(Extension(Arc::new(RwLock::new((database, tera, client))))) .layer(axum::extract::DefaultBodyLimit::max( var("BODY_LIMIT") .unwrap_or("8388608".to_string()) diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index ab6a09d..24c41bd 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -404,7 +404,7 @@ select:focus { .poll_bar { background-color: var(--color-primary); border-radius: var(--radius); - height: 24px; + height: 25px; } .poll_option { @@ -413,22 +413,6 @@ select:focus { overflow-wrap: anywhere; } -.progress_bar { - background: var(--color-super-lowered); - border-radius: var(--circle); - position: relative; - overflow: hidden; - height: 14px; -} - -.progress_bar .poll_bar { - border-radius: var(--circle); - height: 14px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - position: absolute; -} - input[type="checkbox"] { --color: #c9b1bc; appearance: none; @@ -599,9 +583,6 @@ input[type="checkbox"]:checked { border-radius: 6px; height: max-content; font-weight: 600; - display: flex; - justify-content: center; - align-items: center; } .notification.tr { @@ -616,11 +597,6 @@ input[type="checkbox"]:checked { padding: 0; } -.notification:not(.chip) .icon { - width: 100%; - height: 100%; -} - /* chip */ .chip { background: var(--color-primary); @@ -955,7 +931,7 @@ dialog::backdrop { transition: transform 0.15s; } -.dropdown:has(.inner.open) .dropdown_arrow { +.dropdown:has(.inner.open) .dropdown-arrow { transform: rotateZ(180deg); } @@ -1135,7 +1111,7 @@ details[open] > summary { margin-bottom: var(--pad-1); } -details[open]:not(.accordion) > summary::after { +details[open] > summary::after { top: 0; left: 0; width: 5px; @@ -1158,7 +1134,8 @@ details.accordion { } details.accordion summary { - background: var(--color-lowered); + background: var(--background); + border: solid 1px var(--color-super-lowered); border-radius: var(--radius); padding: var(--pad-3) var(--pad-4); margin: 0; @@ -1166,15 +1143,11 @@ details.accordion summary { user-select: none; } -details.accordion summary:hover { - background: var(--color-super-lowered); -} - -details.accordion summary .icon.dropdown_arrow { +details.accordion summary .icon { transition: transform 0.15s; } -details.accordion[open] summary .icon.dropdown_arrow { +details.accordion[open] summary .icon { transform: rotateZ(180deg); } @@ -1184,11 +1157,13 @@ details.accordion[open] summary { } details.accordion .inner { - background: var(--color-raised); + background: var(--background); padding: var(--pad-3) var(--pad-4); border-radius: var(--radius); border-top-left-radius: 0; border-top-right-radius: 0; + border: solid 1px var(--color-super-lowered); + border-top: none; } /* codemirror */ diff --git a/crates/app/src/public/html/auth/register.lisp b/crates/app/src/public/html/auth/register.lisp index 05b3d71..aa94c3d 100644 --- a/crates/app/src/public/html/auth/register.lisp +++ b/crates/app/src/public/html/auth/register.lisp @@ -118,7 +118,7 @@ ("class" "hidden lowered card w-full no_p_margin") ("ui_ident" "purchase_help") (b (text "What does \"Purchase account\" mean?")) - (p (text "Your account will be created, but you cannot use it until you activate it for {{ config.stripe.price_texts.supporter }}.")) + (p (text "Your account will be created, but you cannot use it until you activate it for {{ config.stripe.supporter_price_text }}.")) (p (text "Alternatively, you can provide an invite code to create your account for free."))) (text "{%- endif %}") (button diff --git a/crates/app/src/public/html/auth/seller_connection.lisp b/crates/app/src/public/html/auth/seller_connection.lisp deleted file mode 100644 index 43381da..0000000 --- a/crates/app/src/public/html/auth/seller_connection.lisp +++ /dev/null @@ -1,25 +0,0 @@ -(text "{% extends \"auth/base.html\" %} {% block head %}") -(title - (text "Connection")) - -(text "{% endblock %} {% block title %}Connection{% endblock %} {% block content %}") -(div - ("class" "w-full flex-col gap-2") - ("id" "status") - (b - (text "Working..."))) - -(text "{% if connection_type == \"refresh\" %}") -(script - ("defer" "true") - (text "setTimeout(async () => { - trigger(\"seller::onboarding\"); - }, 1000);")) -(text "{% elif connection_type == \"return\" %}") -(script - ("defer" "true") - (text "setTimeout(async () => { - document.getElementById(\"status\").innerHTML = - `Account updated. You can now close this tab.`; - }, 1000);")) -(text "{%- endif %} {% endblock %}") diff --git a/crates/app/src/public/html/chats/app.lisp b/crates/app/src/public/html/chats/app.lisp index a5bf139..0dc16c3 100644 --- a/crates/app/src/public/html/chats/app.lisp +++ b/crates/app/src/public/html/chats/app.lisp @@ -210,30 +210,6 @@ }); }; - globalThis.mute_channel = async (id, mute = true) => { - await trigger(\"atto::debounce\", [\"channels::mute\"]); - fetch(`/api/v1/channels/${id}/mute`, { - method: mute ? \"POST\" : \"DELETE\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - if (mute) { - document.querySelector(`[ui_ident=channel\\\\.mute\\\\:${id}]`).classList.add(\"hidden\"); - document.querySelector(`[ui_ident=channel\\\\.unmute\\\\:${id}]`).classList.remove(\"hidden\"); - } else { - document.querySelector(`[ui_ident=channel\\\\.mute\\\\:${id}]`).classList.remove(\"hidden\"); - document.querySelector(`[ui_ident=channel\\\\.unmute\\\\:${id}]`).classList.add(\"hidden\"); - } - } - }); - }; - globalThis.update_channel_title = async (id) => { await trigger(\"atto::debounce\", [\"channels::update_title\"]); const title = await trigger(\"atto::prompt\", [\"New channel title:\"]); diff --git a/crates/app/src/public/html/chats/channels.lisp b/crates/app/src/public/html/chats/channels.lisp index f789d32..a87dbeb 100644 --- a/crates/app/src/public/html/chats/channels.lisp +++ b/crates/app/src/public/html/chats/channels.lisp @@ -31,22 +31,6 @@ (text "{{ icon \"user-plus\" }}") (span (text "{{ text \"chats:action.add_someone\" }}"))) - ; mute/unmute - (button - ("class" "lowered small {% if channel.id in user.channel_mutes -%} hidden {%- endif %}") - ("ui_ident" "channel.mute:{{ channel.id }}") - ("onclick" "mute_channel('{{ channel.id }}')") - (icon (text "bell-off")) - (span - (str (text "chats:action.mute")))) - (button - ("class" "lowered small {% if not channel.id in user.channel_mutes -%} hidden {%- endif %}") - ("ui_ident" "channel.unmute:{{ channel.id }}") - ("onclick" "mute_channel('{{ channel.id }}', false)") - (icon (text "bell-ring")) - (span - (str (text "chats:action.unmute")))) - ; ... (text "{%- endif %}") (button ("class" "lowered small") diff --git a/crates/app/src/public/html/communities/list.lisp b/crates/app/src/public/html/communities/list.lisp index cf1cb48..186d4f9 100644 --- a/crates/app/src/public/html/communities/list.lisp +++ b/crates/app/src/public/html/communities/list.lisp @@ -29,6 +29,7 @@ ("minlength" "2") ("maxlength" "32"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}") (div diff --git a/crates/app/src/public/html/communities/question.lisp b/crates/app/src/public/html/communities/question.lisp index 4468d25..975e055 100644 --- a/crates/app/src/public/html/communities/question.lisp +++ b/crates/app/src/public/html/communities/question.lisp @@ -39,6 +39,7 @@ ("class" "flex gap-2") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}") (button + ("class" "primary") (text "{{ text \"requests:label.answer\" }}"))))) (text "{%- endif %}") (div diff --git a/crates/app/src/public/html/communities/search.lisp b/crates/app/src/public/html/communities/search.lisp index a985e91..642d214 100644 --- a/crates/app/src/public/html/communities/search.lisp +++ b/crates/app/src/public/html/communities/search.lisp @@ -28,6 +28,7 @@ ("maxlength" "32") ("value" "{{ text }}"))) (button + ("class" "primary") (text "{{ text \"dialog:action.continue\" }}")))) (div ("class" "card-nest") diff --git a/crates/app/src/public/html/communities/settings.lisp b/crates/app/src/public/html/communities/settings.lisp index fa5ddcf..4213cb9 100644 --- a/crates/app/src/public/html/communities/settings.lisp +++ b/crates/app/src/public/html/communities/settings.lisp @@ -135,6 +135,7 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}")))))) @@ -189,6 +190,7 @@ ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") ("class" "w-content")) (button + ("class" "primary") (text "{{ icon \"check\" }}")))) (div ("class" "card-nest") @@ -211,6 +213,7 @@ ("accept" "image/png,image/jpeg,image/avif,image/webp") ("class" "w-content")) (button + ("class" "primary") (text "{{ icon \"check\" }}"))) (span ("class" "fade") @@ -242,6 +245,7 @@ ("required" "") ("minlength" "18"))) (button + ("class" "primary") (text "{{ text \"communities:action.select\" }}"))))) (div ("class" "card flex flex-col gap-2 w-full") @@ -292,6 +296,7 @@ ("minlength" "2") ("maxlength" "32"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (text "{% for channel in channels %}") (div diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 8475223..156875e 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -102,23 +102,13 @@ ("class" "flush") ("style" "font-weight: 600") ("target" "_top") - (text "{% if user.permissions|has_banned -%}") - (del ("class" "fade") (text "{{ self::username(user=user) }}")) - (text "{% else %}") - (text "{{ self::username(user=user) }}") - (text "{%- endif %}")) + (text "{{ self::username(user=user) }}")) (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") (span ("title" "Verified") ("style" "color: var(--color-primary)") ("class" "flex items-center") (text "{{ icon \"badge-check\" }}")) - (text "{%- endif %} {% if user.permissions|has_staff_badge -%}") - (span - ("title" "Staff") - ("style" "color: var(--color-primary);") - ("class" "flex items-center") - (text "{{ icon \"shield-user\" }}")) (text "{%- endif %}")) (text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}") (div @@ -128,7 +118,7 @@ (div ("class" "card-nest post_outer:{{ post.id }} post_outer") ("is_repost" "{{ is_repost }}") - (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], asking_about=question[2], profile=owner) }} {% else %}") + (text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}") (div ("class" "card small") (a @@ -183,12 +173,6 @@ ("class" "flex items-center") ("style" "color: var(--color-primary)") (text "{{ icon \"square-asterisk\" }}")) - (text "{%- endif %} {% if post.context.full_unlist -%}") - (span - ("title" "Unlisted") - ("class" "flex items-center") - ("style" "color: var(--color-primary)") - (icon (text "eye-off"))) (text "{%- endif %} {% if post.stack -%}") (a ("title" "Posted to a stack you're in") @@ -237,7 +221,7 @@ ("hook" "long") (text "{{ post.title }}")) - (button ("title" "View post content") ("class" "small lowered") (icon (text "ellipsis")))) + (button ("class" "small lowered") (icon (text "ellipsis")))) (text "{% else %}") (text "{% if not post.context.content_warning -%}") (span @@ -331,13 +315,13 @@ ("class" "button camo small") ("target" "_blank") (text "{{ icon \"external-link\" }}")) + (text "{% if user -%}") (div ("class" "dropdown") (button ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -345,7 +329,6 @@ (b ("class" "title") (text "{{ text \"general:label.share\" }}")) - (text "{% if user -%}") (button ("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])") (text "{{ icon \"repeat-2\" }}") @@ -368,16 +351,7 @@ (span (text "BlueSky"))) (text "{%- endif %}") - (text "{% if owner.settings.enable_questions -%}") - (a - ("class" "button") - ("href" "/@{{ owner.username }}?asking_about={{ post.id }}") - (icon (text "reply")) - (span - (str (text "communities:label.ask_about_this")))) - (text "{%- endif %}") - (text "{%- endif %}") - (text "{% if user and user.id != post.owner -%}") + (text "{% if user.id != post.owner -%}") (b ("class" "title") (text "{{ text \"general:label.safety\" }}")) @@ -387,12 +361,12 @@ (text "{{ icon \"flag\" }}") (span (text "{{ text \"general:action.report\" }}"))) - (text "{%- endif %} {% if user and (user.id == post.owner) or is_helper or can_manage_post %}") + (text "{%- endif %} {% if (user.id == post.owner) or is_helper or can_manage_post %}") (b ("class" "title") (text "{{ text \"general:action.manage\" }}")) ; forge stuff - (text "{% if user and community and community.is_forge -%} {% if post.is_open -%}") + (text "{% if community and community.is_forge -%} {% if post.is_open -%}") (button ("class" "green") ("onclick" "trigger('me::update_open', ['{{ post.id }}', false])") @@ -408,7 +382,7 @@ (text "{{ text \"forge:action.reopen\" }}"))) (text "{%- endif %} {%- endif %}") ; owner stuff - (text "{% if user and user.id == post.owner -%}") + (text "{% if user.id == post.owner -%}") (a ("href" "/post/{{ post.id }}#/edit") (text "{{ icon \"pen\" }}") @@ -440,7 +414,8 @@ (text "{{ icon \"undo\" }}") (span (text "{{ text \"general:action.restore\" }}"))) - (text "{%- endif %} {%- endif %}")))))) + (text "{%- endif %} {%- endif %}"))) + (text "{%- endif %}")))) (text "{% if community and show_community and community.id != config.town_square or question %}")) (text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0 -%}") @@ -452,6 +427,7 @@ ("alt" "Image upload") ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])")) (text "{% endfor %}")) + (text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") (div ("class" "w-full card-nest") @@ -648,7 +624,7 @@ --{{ css }}: {{ color|color }} !important; }")) -(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, asking_about=false, show_community=true, secondary=false, profile=false) -%}") +(text "{%- endif %} {%- endmacro %} {% macro question(question, owner, show_community=true, secondary=false, profile=false) -%}") (div ("class" "card question {% if secondary -%}secondary{%- endif %} flex gap-2") (text "{% if owner.id == 0 or question.context.mask_owner -%}") @@ -718,10 +694,6 @@ (text "{{ question.content|markdown|safe }}")) ; question drawings (text "{{ self::post_media(upload_ids=question.drawings) }}") - ; asking about - (text "{% if asking_about -%}") - (text "{{ self::post(post=asking_about[1], owner=asking_about[0], secondary=not secondary, show_community=false) }}") - (text "{%- endif %}") ; anonymous user ip thing ; this is only shown if the post author is anonymous AND we are a helper (text "{% if is_helper and (owner.id == 0 or question.context.mask_owner) -%}") @@ -758,7 +730,6 @@ ("class" "no_p_margin") (text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}"))) (form - ("id" "create_question_form") ("class" "card flex flex-col gap-2") ("onsubmit" "create_question_from_form(event)") (div @@ -785,6 +756,7 @@ (div ("class" "flex gap-2") (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")) (text "{% if drawing_enabled -%}") @@ -828,7 +800,7 @@ }")) (text "{%- endif %}")) - (text "{% if not is_global and allow_anonymous and user -%}") + (text "{% if not is_global and allow_anonymous and not user -%}") (div ("class" "flex gap-2 items-center") (input @@ -844,15 +816,6 @@ (script (text "globalThis.gerald = null; - // asking about - globalThis.asking_about = new URLSearchParams(window.location.search).get(\"asking_about\") || \"\"; - - if (asking_about) { - document.getElementById(\"create_question_form\").innerHTML += - `
Asking about: ${asking_about} (cancel)`; - } - - // ... async function create_question_from_form(e) { e.preventDefault(); await trigger(\"atto::debounce\", [\"questions::create\"]); @@ -874,8 +837,7 @@ receiver: \"{{ receiver }}\", community: \"{{ community }}\", is_global: \"{{ is_global }}\" == \"true\", - mask_owner: (e.target.mask_owner || { checked:false }).checked, - asking_about, + mask_owner: (e.target.mask_owner || { checked:false }).checked }), ); @@ -904,7 +866,7 @@ (text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}") (div ("class" "card-nest") - (text "{{ self::question(question=question[0], owner=question[1], asking_about=false, show_community=show_community) }}") + (text "{{ self::question(question=question[0], owner=question[1], show_community=show_community) }}") (div ("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}") (div @@ -931,7 +893,6 @@ ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -1045,7 +1006,6 @@ ("class" "camo small") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -1148,6 +1108,16 @@ (text "{{ icon \"circle-user-round\" }}") (span (text "{{ text \"auth:link.my_profile\" }}"))) + (a + ("href" "/journals/0/0") + (icon (text "notebook")) + (str (text "general:link.journals"))) + (text "{% if config.lw_host -%}") + (button + ("onclick" "document.getElementById('littleweb').showModal()") + (icon (text "globe")) + (str (text "general:link.little_web"))) + (text "{%- endif %}") (text "{% if not user.settings.disable_achievements -%}") (a ("href" "/achievements") @@ -1274,7 +1244,6 @@ ("class" "camo small square") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -1458,9 +1427,7 @@ }); })();")) -(text "{%- endmacro %}") - -(text "{% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}") +(text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}") (div ("class" "card w-full supporter_ad") ("ui_ident" "supporter_ad") @@ -1480,9 +1447,8 @@ (text "{{ icon \"heart\" }}") (span (text "{{ text \"general:action.become_supporter\" }}"))))) -(text "{%- endif %} {%- endmacro %}") -(text "{% macro create_post_options() -%}") +(text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}") (div ("class" "flex gap-2 flex-wrap") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if not quoting -%} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {%- endif %} {%- endif %}") @@ -1499,7 +1465,6 @@ ("title" "More options") ("onclick" "document.getElementById('post_options_dialog').showModal()") ("type" "button") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (label @@ -1542,7 +1507,6 @@ is_nsfw: false, content_warning: \"\", tags: [], - full_unlist: false, }; window.BLANK_INITIAL_SETTINGS = JSON.stringify( @@ -1579,11 +1543,6 @@ // window.POST_INITIAL_SETTINGS.is_nsfw.toString(), // \"checkbox\", // ], - [ - [\"full_unlist\", \"Unlist from timelines\"], - window.POST_INITIAL_SETTINGS.full_unlist.toString(), - \"checkbox\", - ], [ [\"content_warning\", \"Content warning\"], window.POST_INITIAL_SETTINGS.content_warning, @@ -1800,8 +1759,8 @@ (span ("class" "notification chip") (text "{{ total }} votes")) (text "{% if not poll[2] -%}") (span - ("class" "notification chip flex items-center gap-1") - (text "Expires in") + ("class" "notification chip") + (text "Expires in ") (span ("class" "poll_date") ("data-created" "{{ poll[0].created }}") @@ -1877,6 +1836,7 @@ ("id" "join_or_leave") (text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}") (button + ("class" "primary") ("onclick" "join_community()") (text "{{ icon \"circle-plus\" }}") (span @@ -2090,7 +2050,6 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -2117,7 +2076,6 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -2209,7 +2167,6 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -2289,7 +2246,6 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "width: 32px") - ("title" "More options") (text "{{ icon \"ellipsis\" }}")) (div ("class" "inner") @@ -2367,6 +2323,10 @@ (text "Save infinite post drafts")) (li (text "Ability to search through all posts")) + (li + (text "Ability to create forges")) + (li + (text "Create more than 1 app")) (li (text "Create up to 10 stack blocks")) (li @@ -2390,16 +2350,18 @@ (sup (a ("href" "#footnote-1") (text "1")))) (text "{%- endif %}")) (a - ("href" "{{ config.stripe.payment_links.supporter }}?client_reference_id={{ user.id }}") + ("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}") ("class" "button") ("target" "_blank") - (text "Become a supporter ({{ config.stripe.price_texts.supporter }})")) + (text "Become a supporter ({{ config.stripe.supporter_price_text }})")) (span ("class" "fade") (text "Please use your") (b (text " real email ")) - (text "when completing payment. It is required to manage your billing settings.")) + (text "when + completing payment. It is required to manage + your billing settings.")) (text "{% if config.security.enable_invite_codes -%}") (span @@ -2408,46 +2370,3 @@ (b (text "1: ")) (text "After your account is at least 1 month old")) (text "{%- endif %}") (text "{%- endmacro %}") - -(text "{% macro get_developer_pass_button() -%}") -(p - (text "You currently do not hold a developer pass. With a developer pass, you'll get:")) -(ul - ("style" "margin-bottom: var(--pad-4)") - (li - (text "Increased app storage limit (500 KB->25 MB)")) - (li - (text "Ability to create forges")) - (li - (text "Ability to create more than 1 app")) - (li - (text "Developer pass profile badge"))) -(a - ("href" "{{ config.stripe.payment_links.dev_pass }}?client_reference_id={{ user.id }}") - ("class" "button") - ("target" "_blank") - (text "Continue ({{ config.stripe.price_texts.dev_pass }})")) -(span - ("class" "fade") - (text "Please use your") - (b - (text " real email ")) - (text "when completing payment. It is required to manage your billing settings. If you're already a supporter, please use the same email you used there.")) -(text "{%- endmacro %}") - -(text "{% macro developer_pass_ad(body) -%} {% if config.stripe and not has_developer_pass %}") -(div - ("class" "card w-full supporter_ad") - ("ui_ident" "supporter_ad") - ("onclick" "window.location.href = '/settings#/account/billing'") - (div - ("class" "card w-full flex flex-wrap items-center gap-2 justify-between") - (b - (text "{{ body }}")) - (a - ("href" "/settings#/account/billing") - ("class" "button small") - (icon (text "arrow-right")) - (span - (str (text "dialog:action.continue")))))) -(text "{%- endif %} {%- endmacro %}") diff --git a/crates/app/src/public/html/developer/app.lisp b/crates/app/src/public/html/developer/app.lisp index d19fb10..2850ef5 100644 --- a/crates/app/src/public/html/developer/app.lisp +++ b/crates/app/src/public/html/developer/app.lisp @@ -10,27 +10,11 @@ (div ("id" "manage_fields") ("class" "card lowered flex flex-col gap-2") - (div - ("class" "card-nest") - (div - ("class" "card small flex items-center gap-2") - (icon (text "database")) - (b (str (text "developer:label.data_usage")))) - (div - ("class" "card flex flex-col gap-2") - (p ("class" "fade") (text "App data keys are not included in this metric, only stored values count towards your limit.")) - (text "{% set percentage = (app.data_used / data_limit) * 100 %}") - (div ("class" "progress_bar") (div ("class" "poll_bar") ("style" "width: {{ percentage }}%"))) - (div - ("class" "w-full flex justify-between items-center") - (span (text "{{ app.data_used|filesizeformat }}")) - (span (text "{{ data_limit|filesizeformat }}"))))) (text "{% if is_helper -%}") (div ("class" "card-nest") (div - ("class" "card small flex items-center gap-2") - (icon (text "infinity")) + ("class" "card small") (b (str (text "developer:label.change_quota_status")))) (div ("class" "card") @@ -44,34 +28,11 @@ ("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") (div - ("class" "card small flex items-center gap-2") - (icon (text "pencil")) + ("class" "card small") (b (str (text "developer:label.change_title")))) (form ("class" "card flex flex-col gap-2") @@ -89,14 +50,14 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}"))))) (div ("class" "card-nest") (div - ("class" "card small flex items-center gap-2") - (icon (text "house")) + ("class" "card small") (b (str (text "developer:label.change_homepage")))) (form ("class" "card flex flex-col gap-2") @@ -114,14 +75,14 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}"))))) (div ("class" "card-nest") (div - ("class" "card small flex items-center gap-2") - (icon (text "goal")) + ("class" "card small") (b (str (text "developer:label.change_redirect")))) (form ("class" "card flex flex-col gap-2") @@ -139,14 +100,14 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}"))))) (div ("class" "card-nest") (div - ("class" "card small flex items-center gap-2") - (icon (text "telescope")) + ("class" "card small") (b (str (text "developer:label.manage_scopes")))) (form ("class" "card flex flex-col gap-2") @@ -179,22 +140,10 @@ (icon (text "external-link")) (text "Docs")))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span - (text "{{ text \"general:action.save\" }}"))))) - (div - ("class" "card-nest") - (div - ("class" "card small flex items-center gap-2") - (icon (text "rotate-ccw-key")) - (b (str (text "developer:label.secret_key")))) - (div - ("class" "card flex flex-col gap-2") - (p ("class" "fade") (text "Your app's API key can only be seen once, so don't lose it. Rolling the key will invalidate the old one.")) - (pre (code ("id" "new_key"))) - (button - ("onclick" "roll_key()") - (str (text "developer:label.roll_key")))))) + (text "{{ text \"general:action.save\" }}")))))) (div ("class" "card flex flex-col gap-2") (ul @@ -202,8 +151,7 @@ (li (b (text "Redirect URL: ")) (text "{{ app.redirect }}")) (li (b (text "Quota status: ")) (text "{{ app.quota_status }}")) (li (b (text "User grants: ")) (text "{{ app.grants }}")) - (li (b (text "Grant URL: ")) (text "{{ config.host }}/auth/connections_link/app/{{ app.id }}")) - (li (b (text "App ID (for SDK): ")) (text "{{ app.id }}"))) + (li (b (text "Grant URL: ")) (text "{{ config.host }}/auth/connections_link/app/{{ app.id }}"))) (a ("class" "button") @@ -254,26 +202,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(); @@ -395,31 +323,6 @@ }); }; - globalThis.roll_key = async () => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(\"/api/v1/apps/{{ app.id }}/roll\", { - method: \"POST\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - document.getElementById(\"new_key\").innerText = res.payload; - } - }); - }; - globalThis.delete_app = async () => { if ( !(await trigger(\"atto::confirm\", [ diff --git a/crates/app/src/public/html/developer/home.lisp b/crates/app/src/public/html/developer/home.lisp index 160181b..aefd55d 100644 --- a/crates/app/src/public/html/developer/home.lisp +++ b/crates/app/src/public/html/developer/home.lisp @@ -41,19 +41,23 @@ ("id" "homepage") ("placeholder" "homepage") ("required" "") - ("minlength" "2"))) + ("minlength" "2") + ("maxlength" "32"))) (div ("class" "flex flex-col gap-1") (label ("for" "title") - (text "{{ text \"developer:label.redirect\" }} (optional)")) + (text "{{ text \"developer:label.redirect\" }}")) (input ("type" "url") ("name" "redirect") ("id" "redirect") ("placeholder" "redirect URL") - ("minlength" "2"))) + ("required" "") + ("minlength" "2") + ("maxlength" "32"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) ; app listing @@ -122,7 +126,7 @@ body: JSON.stringify({ title: e.target.title.value, homepage: e.target.homepage.value, - redirect: e.target.redirect.value || \"\", + redirect: e.target.redirect.value, }), }) .then((res) => res.json()) diff --git a/crates/app/src/public/html/developer/link.lisp b/crates/app/src/public/html/developer/link.lisp index f50ad18..5d46c87 100644 --- a/crates/app/src/public/html/developer/link.lisp +++ b/crates/app/src/public/html/developer/link.lisp @@ -39,13 +39,6 @@ (str (text "dialog:action.cancel"))))))) (script (text "setTimeout(() => { - // {% if app.redirect|length == 0 %} - alert(\"App has an invalid redirect. Please contact the owner for help.\"); - window.close(); - return; - // {% endif %} - - // ... globalThis.authorize = async (event) => { if ( !(await trigger(\"atto::confirm\", [ @@ -83,7 +76,6 @@ const search = new URLSearchParams(window.location.search); search.append(\"verifier\", verifier); search.append(\"token\", res.payload); - search.append(\"uid\", \"{{ user.id }}\"); window.location.href = `{{ app.redirect|remove_script_tags|safe }}?${search.toString()}`; } diff --git a/crates/app/src/public/html/forge/home.lisp b/crates/app/src/public/html/forge/home.lisp index 3208a63..a83c545 100644 --- a/crates/app/src/public/html/forge/home.lisp +++ b/crates/app/src/public/html/forge/home.lisp @@ -6,7 +6,7 @@ (main ("class" "flex flex-col gap-2") ; create new - (text "{% if user.secondary_permissions|has_dev_pass -%}") + (text "{% if user.permissions|has_supporter -%}") (div ("class" "card-nest") (div @@ -30,9 +30,10 @@ ("minlength" "2") ("maxlength" "32"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (text "{% else %}") - (text "{{ components::developer_pass_ad(body=\"Get a developer pass to create forges!\") }}") + (text "{{ components::supporter_ad(body=\"Become a supporter to create forges!\") }}") (text "{%- endif %}") ; forge listing diff --git a/crates/app/src/public/html/journals/app.lisp b/crates/app/src/public/html/journals/app.lisp index 71fbd4d..255b2ec 100644 --- a/crates/app/src/public/html/journals/app.lisp +++ b/crates/app/src/public/html/journals/app.lisp @@ -253,6 +253,7 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}"))))))) @@ -378,6 +379,7 @@ ("name" "tags") ("id" "tags") ("placeholder" "tags") + ("required" "") ("minlength" "2") ("maxlength" "128") (text "{% for tag in note.tags -%} {{ tag }}, {% endfor %}")) diff --git a/crates/app/src/public/html/littleweb/browser.lisp b/crates/app/src/public/html/littleweb/browser.lisp index 95f35d8..76ac82b 100644 --- a/crates/app/src/public/html/littleweb/browser.lisp +++ b/crates/app/src/public/html/littleweb/browser.lisp @@ -32,7 +32,7 @@ (input ("type" "uri") ("class" "w-full") - ("true_value" "") + ("true_value" "{{ path }}") ("name" "uri") ("id" "uri")) @@ -47,7 +47,7 @@ ("exclude" "dropdown") ("style" "gap: var(--pad-1) !important") (text "{{ components::avatar(username=user.username, size=\"24px\") }}") - (icon_class (text "chevron-down") (text "dropdown_arrow"))) + (icon_class (text "chevron-down") (text "dropdown-arrow"))) (text "{{ components::user_menu() }}")) (text "{%- endif %}")) @@ -55,7 +55,7 @@ (iframe ("id" "browser_iframe") ("frameborder" "0") - ("src" "{% if path -%} {{ config.lw_host }}/api/v1/net/{{ path }}?s={{ session }} {%- endif %}")) + ("src" "{% if path -%} {{ config.lw_host }}/api/v1/net/{{ path }} {%- endif %}")) (style ("data-turbo-temporary" "true") @@ -91,7 +91,6 @@ position: fixed; width: calc(100dvw - (62px + var(--pad-2) * 2)) !important; left: var(--pad-2); - z-index: 2; } } @@ -102,7 +101,7 @@ height: var(--h); min-height: var(--h); max-height: var(--h); - font-size: 16px; + font-size: 14px; } #panel button:not(.inner *), @@ -116,23 +115,18 @@ }")) (script - (text "globalThis.SECRET_SESSION = \"{{ session }}\"; - function littleweb_navigate(uri) { + (text "function littleweb_navigate(uri) { if (!uri.includes(\".html\")) { uri = `${uri}/index.html`; } - // ... - console.log(\"navigate\", uri); - document.getElementById(\"browser_iframe\").src = `{{ config.lw_host|safe }}/api/v1/net/${uri}?s={{ session }}`; - - if (!uri.includes(\"atto://\")) { - document.getElementById(\"uri\").setAttribute(\"true_value\", `atto://${uri}`); - } else { - document.getElementById(\"uri\").setAttribute(\"true_value\", uri); + if (!uri.startsWith(\"atto://\")) { + uri = `atto://${uri}`; } - document.getElementById(\"uri\").value = uri.replace(\"atto://\", \"\").split(\"/\")[0]; + // ... + console.log(\"navigate\", uri); + document.getElementById(\"browser_iframe\").src = `{{ config.lw_host|safe }}/api/v1/net/${uri}`; } document.getElementById(\"browser_iframe\").addEventListener(\"load\", (e) => { @@ -157,14 +151,7 @@ if (data.event === \"change_url\") { const uri = new URL(data.target).pathname.slice(\"/api/v1/net/\".length); window.history.pushState(null, null, `/net/${uri.replace(\"atto://\", \"\")}`); - - if (!uri.includes(\"atto://\")) { - document.getElementById(\"uri\").setAttribute(\"true_value\", `atto://${uri}`); - } else { - document.getElementById(\"uri\").setAttribute(\"true_value\", uri); - } - - document.getElementById(\"uri\").value = uri.replace(\"atto://\", \"\").split(\"/\")[0]; + document.getElementById(\"uri\").setAttribute(\"true_value\", uri); } }); @@ -219,9 +206,6 @@ is_focused = false; }); - // navigate - if ({{ path|length }} > 0) { - littleweb_navigate(\"{{ path|safe }}\"); - }")) + document.getElementById(\"uri\").value = document.getElementById(\"uri\").getAttribute(\"true_value\").replace(\"atto://\", \"\").split(\"/\")[0]")) (text "{% endblock %}") diff --git a/crates/app/src/public/html/littleweb/domain.lisp b/crates/app/src/public/html/littleweb/domain.lisp index d4bc359..d8b01f1 100644 --- a/crates/app/src/public/html/littleweb/domain.lisp +++ b/crates/app/src/public/html/littleweb/domain.lisp @@ -72,7 +72,7 @@ ("class" "card hidden w-full lowered flex flex-col gap-2") ("onsubmit" "add_data_from_form(event)") (div - ("class" "flex gap-2 flex-collapse") + ("class" "flex gap-2") (div ("class" "flex w-full flex-col gap-1") (label @@ -119,48 +119,44 @@ (icon (text "check")) (str (text "general:action.save"))))) ; data - (div - ("class" "w-full") - ("style" "max-width: 100%; overflow: auto; min-height: 512px") - (table - ("class" "w-full") - (thead - (tr - (th (text "Name")) - (th (text "Type")) - (th (text "Value")) - (th (text "Actions")))) + (table + (thead + (tr + (th (text "Name")) + (th (text "Type")) + (th (text "Value")) + (th (text "Actions")))) - (tbody - (text "{% for item in domain.data -%}") - (tr - (td (text "{{ item[0] }}")) - (text "{% for k,v in item[1] -%}") - (td (text "{{ k }}")) - (td (text "{{ v }}")) - (text "{%- endfor %}") - (td - ("style" "overflow: auto") + (tbody + (text "{% for item in domain.data -%}") + (tr + (td (text "{{ item[0] }}")) + (text "{% for k,v in item[1] -%}") + (td (text "{{ k }}")) + (td (text "{{ v }}")) + (text "{%- endfor %}") + (td + ("style" "overflow: auto") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (icon (text "ellipsis"))) (div - ("class" "dropdown") + ("class" "inner") (button - ("class" "camo small") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - (icon (text "ellipsis"))) - (div - ("class" "inner") - (button - ("onclick" "rename_data('{{ item[0] }}')") - (icon (text "pencil")) - (str (text "littleweb:action.rename"))) + ("onclick" "rename_data('{{ item[0] }}')") + (icon (text "pencil")) + (str (text "littleweb:action.rename"))) - (button - ("class" "red") - ("onclick" "remove_data('{{ item[0] }}')") - (icon (text "trash")) - (str (text "general:action.delete"))))))) - (text "{%- endfor %}"))))))) + (button + ("class" "red") + ("onclick" "remove_data('{{ item[0] }}')") + (icon (text "trash")) + (str (text "general:action.delete"))))))) + (text "{%- endfor %}")))))) (script ("id" "domain_data") ("type" "application/json") (text "{{ domain.data|json_encode()|safe }}")) (script diff --git a/crates/app/src/public/html/littleweb/domains.lisp b/crates/app/src/public/html/littleweb/domains.lisp index c79ab3e..1a9b649 100644 --- a/crates/app/src/public/html/littleweb/domains.lisp +++ b/crates/app/src/public/html/littleweb/domains.lisp @@ -5,17 +5,6 @@ (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex-col gap-2") - - ; viewing other user's domains warning - (text "{% if profile.id != user.id -%}") - (div - ("class" "card w-full red flex gap-2 items-center") - (text "{{ icon \"skull\" }}") - (b - (text "Viewing other user's domains! Please be careful."))) - (text "{%- endif %}") - - ; ... (text "{% if user -%}") (div ("class" "pillmenu") @@ -59,6 +48,7 @@ (option ("value" "{{ tld }}") (text ".{{ tld|lower }}")) (text "{%- endfor %}"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")) (details diff --git a/crates/app/src/public/html/littleweb/service.lisp b/crates/app/src/public/html/littleweb/service.lisp index 7cd9597..b0b7ac9 100644 --- a/crates/app/src/public/html/littleweb/service.lisp +++ b/crates/app/src/public/html/littleweb/service.lisp @@ -14,24 +14,9 @@ (div ("class" "card-nest") (div - ("class" "card small flex flex-col gap-2") - (div - ("class" "flex w-full gap-2 justify-between") - (b - (text "{{ service.name }}")) - - (button - ("class" "small lowered") - ("title" "Help") - ("onclick" "document.getElementById('site_help').classList.toggle('hidden')") - (icon (text "circle-question-mark")))) - - (div - ("class" "card w-full lowered flex flex-col gap-2 hidden no_p_margin") - ("id" "site_help") - (p (text "Your site should include an \"index.html\" file in order to show content on its homepage.")) - (p (text "In the HTML editor, you can type `!` and use the provided suggestion to get an HTML boilerplate.")) - (p (text "After you've created a page, you can click \"Copy ID\" and go to manage a domain you own. On the domain management page, click \"Add\" and paste the ID you copied into the value field.")))) + ("class" "card small") + (b + (text "{{ service.name }}"))) (div ("class" "flex gap-2 flex-wrap card") @@ -87,57 +72,53 @@ ("class" "card flex flex-col gap-2") (text "{% if not file or file.children|length > 0 -%}") ; directory browser - (div - ("class" "w-full") - ("style" "max-width: 100%; overflow: auto; min-height: 512px") - (table - ("class" "w-full") - (thead - (tr - (th (text "Name")) - (th (text "Type")) - (th (text "Children")) - (th (text "Actions")))) + (table + (thead + (tr + (th (text "Name")) + (th (text "Type")) + (th (text "Children")) + (th (text "Actions")))) - (tbody - (text "{% for item in files %}") - (tr - (td - ("class" "flex gap-2 items-center") - (text "{% if item.children|length > 0 -%}") - (icon (text "folder")) - (text "{% else %}") - (icon (text "file")) - (text "{%- endif %}") + (tbody + (text "{% for item in files %}") + (tr + (td + ("class" "flex gap-2 items-center") + (text "{% if item.children|length > 0 -%}") + (icon (text "folder")) + (text "{% else %}") + (icon (text "file")) + (text "{%- endif %}") - (a - ("href" "?path={{ path }}/{{ item.name }}") - ("data-turbo" "false") - (text "{{ item.name }}"))) - (td (text "{{ item.mime }}")) - (td (text "{{ item.children|length }}")) - (td - ("style" "overflow: auto") + (a + ("href" "?path={{ path }}/{{ item.name }}") + ("data-turbo" "false") + (text "{{ item.name }}"))) + (td (text "{{ item.mime }}")) + (td (text "{{ item.children|length }}")) + (td + ("style" "overflow: auto") + (div + ("class" "dropdown") + (button + ("class" "camo small") + ("onclick" "trigger('atto::hooks::dropdown', [event])") + ("exclude" "dropdown") + (icon (text "ellipsis"))) (div - ("class" "dropdown") + ("class" "inner") (button - ("class" "camo small") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - (icon (text "ellipsis"))) - (div - ("class" "inner") - (button - ("onclick" "rename_file('{{ item.id }}')") - (icon (text "pencil")) - (str (text "littleweb:action.rename"))) + ("onclick" "rename_file('{{ item.id }}')") + (icon (text "pencil")) + (str (text "littleweb:action.rename"))) - (button - ("class" "red") - ("onclick" "remove_file('{{ item.id }}')") - (icon (text "trash")) - (str (text "general:action.delete"))))))) - (text "{% endfor %}")))) + (button + ("class" "red") + ("onclick" "remove_file('{{ item.id }}')") + (icon (text "trash")) + (str (text "general:action.delete"))))))) + (text "{% endfor %}"))) (text "{% else %}") ; file editor (div ("id" "editor_container") ("class" "w-full") ("style" "height: 600px")) @@ -338,7 +319,6 @@ (text "{% if file and file.mime != 'Plain' -%}") (script ("src" "https://unpkg.com/monaco-editor@0.52.2/min/vs/loader.js")) -(script ("src" "https://unpkg.com/emmet-monaco-es/dist/emmet-monaco.min.js")) (script ("id" "file_content") ("type" "text/plain") (text "{{ file.content|remove_script_tags|safe }}")) (script (text "require.config({ paths: { vs: \"https://unpkg.com/monaco-editor@0.52.2/min/vs\" } }); @@ -357,16 +337,10 @@ style.innerText = '@import \"https://unpkg.com/monaco-editor@0.52.2/min/vs/editor/editor.main.css\";'; shadow.appendChild(style); - emmetMonaco.emmetHTML(); - emmetMonaco.emmetCSS(); - globalThis.editor = monaco.editor.create(inner, { value: document.getElementById(\"file_content\").innerText.replaceAll(\"</script>\", \"\"), language: MIME_MODES[\"{{ file.mime }}\"], theme: \"vs-dark\", - suggest: { - snippetsPreventQuickSuggestions: false, - }, }); });")) (text "{%- endif %}") diff --git a/crates/app/src/public/html/littleweb/services.lisp b/crates/app/src/public/html/littleweb/services.lisp index 261b006..cca5af7 100644 --- a/crates/app/src/public/html/littleweb/services.lisp +++ b/crates/app/src/public/html/littleweb/services.lisp @@ -5,17 +5,6 @@ (text "{% endblock %} {% block body %} {{ macros::nav() }}") (main ("class" "flex flex-col gap-2") - - ; viewing other user's services warning - (text "{% if profile.id != user.id -%}") - (div - ("class" "card w-full red flex gap-2 items-center") - (text "{{ icon \"skull\" }}") - (b - (text "Viewing other user's sites! Please be careful."))) - (text "{%- endif %}") - - ; ... (text "{% if user -%}") (div ("class" "pillmenu") @@ -45,6 +34,7 @@ ("minlength" "2") ("maxlength" "32"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (text "{%- endif %}") (div @@ -72,10 +62,7 @@ (span ("class" "date") (text "{{ item.created }}")) - (text "; Updated ") - (span - ("class" "date") - (text "{{ item.revision }}")))) + (text "; {{ item.files|length }} files"))) (text "{% endfor %}")))) (script diff --git a/crates/app/src/public/html/macros.lisp b/crates/app/src/public/html/macros.lisp index fff3188..f9d8a1f 100644 --- a/crates/app/src/public/html/macros.lisp +++ b/crates/app/src/public/html/macros.lisp @@ -39,6 +39,12 @@ ("title" "Create post") (icon (text "square-pen"))) + (a + ("href" "/chats/0/0") + ("class" "button {% if selected == 'chats' -%}active{%- endif %}") + ("title" "Chats") + (icon (text "message-circle"))) + (a ("href" "/requests") ("class" "button {% if selected == 'requests' -%}active{%- endif %}") @@ -59,43 +65,6 @@ ("id" "notifications_span") (text "{{ user.notification_count }}"))) - (text "{% if user -%}") - (div - ("class" "dropdown") - (button - ("class" "flex-row {% if selected == 'chats' or selected == 'journals' -%}active{%- endif %}") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - ("title" "More services") - (icon (text "grip"))) - - (div - ("class" "inner") - (a - ("href" "/chats/0/0") - ("title" "Chats") - (icon (text "message-circle")) - (str (text "communities:label.chats"))) - (a - ("href" "/journals/0/0") - (icon (text "notebook")) - (str (text "general:link.journals"))) - (a - ("href" "/forges") - (icon (text "anvil")) - (str (text "forge:label.forges"))) - (a - ("href" "/developer") - (icon (text "code")) - (str (text "developer:label.apps"))) - (text "{% if config.lw_host -%}") - (button - ("onclick" "document.getElementById('littleweb').showModal()") - (icon (text "globe")) - (str (text "general:link.little_web"))) - (text "{%- endif %}"))) - (text "{%- endif %}") - (text "{% if not hide_user_menu -%}") (div ("class" "dropdown") @@ -104,9 +73,8 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("style" "gap: var(--pad-1) !important") - ("title" "Account options") (text "{{ components::avatar(username=user.username, size=\"24px\") }}") - (icon_class (text "chevron-down") (text "dropdown_arrow"))) + (icon_class (text "chevron-down") (text "dropdown-arrow"))) (text "{{ components::user_menu() }}")) (text "{%- endif %} {% else %}") @@ -116,7 +84,7 @@ ("class" "title") ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") - (icon_class (text "chevron-down") (text "dropdown_arrow"))) + (icon_class (text "chevron-down") (text "dropdown-arrow"))) (div ("class" "inner") @@ -363,17 +331,3 @@ (span (text "{{ text \"settings:tab.connections\" }}"))) (text "{%- endmacro %}") - -(text "{% macro seller_settings_nav_options() -%}") -(a - ("data-tab-button" "account") - ("class" "active") - ("href" "#/account") - (icon (text "smile")) - (span (str (text "settings:tab.account")))) -(a - ("data-tab-button" "products") - ("href" "#/products") - (icon (text "package")) - (span (str (text "marketplace:label.products")))) -(text "{%- endmacro %}") diff --git a/crates/app/src/public/html/marketplace/seller.lisp b/crates/app/src/public/html/marketplace/seller.lisp deleted file mode 100644 index 0efa4f0..0000000 --- a/crates/app/src/public/html/marketplace/seller.lisp +++ /dev/null @@ -1,79 +0,0 @@ -(text "{% extends \"root.html\" %} {% block head %}") -(title - (text "Seller settings - {{ config.name }}")) -(text "{% endblock %} {% block body %} {{ macros::nav() }}") -(main - ("class" "flex flex-col gap-2") - - ; nav - (div - ("class" "mobile_nav mobile") - ; primary nav - (div - ("class" "dropdown") - ("style" "width: max-content") - (button - ("class" "raised small") - ("onclick" "trigger('atto::hooks::dropdown', [event])") - ("exclude" "dropdown") - (icon (text "sliders-horizontal")) - (span ("class" "current_tab_text") (text "account"))) - (div - ("class" "inner left") - (text "{{ macros::seller_settings_nav_options() }}")))) - - ; nav desktop - (div - ("class" "desktop pillmenu") - (text "{{ macros::seller_settings_nav_options() }}")) - - ; ... - (div - ("class" "card w-full lowered flex flex-col gap-2") - ("data-tab" "account") - (div - ("class" "card-nest w-full") - (div - ("class" "card small flex items-center gap-2") - (div - ("class" "notification") - ("style" "width: 46px") - (icon (text "stripe"))) - - (b (str (text "marketplace:label.status")))) - - (div - ("class" "card") - (text "{% if user.seller_data.account_id -%}") - (text "{% if user.seller_data.completed_onboarding -%}") - ; completed onboarding + has stripe account linked - (button - ("onclick" "trigger('seller::login')") - (icon (text "arrow-right")) - (str (text "marketplace:action.open_seller_dashboard"))) - (text "{% else %}") - ; not completed onboarding - (p (text "You've not finished setting up your Stripe account.")) - (p (text "Please complete onboarding to accept payments.")) - - (button - ("onclick" "trigger('seller::onboarding')") - (icon (text "arrow-right")) - (str (text "marketplace:action.finsh_setting_up_account"))) - (text "{%- endif %}") - (text "{% else %}") - ; doesn't have a stripe account linked - (button - ("onclick" "trigger('seller::register')") - (icon (text "arrow-right")) - (str (text "marketplace:action.get_started"))) - (text "{%- endif %}")))) - - (div - ("class" "card w-full lowered hidden flex flex-col gap-2") - ("data-tab" "products") - (div - ("class" "card w-full flex flex-wrap gap-2") - ))) - -(text "{% endblock %}") diff --git a/crates/app/src/public/html/misc/achievements.lisp b/crates/app/src/public/html/misc/achievements.lisp index 93f895e..429c924 100644 --- a/crates/app/src/public/html/misc/achievements.lisp +++ b/crates/app/src/public/html/misc/achievements.lisp @@ -17,7 +17,7 @@ (p (text "You'll find out what each achievement is when you get it, so look around!")) (hr) (span (b (text "Your progress: ")) (text "{{ percentage|round(method=\"floor\", precision=2) }}%")) - (div ("class" "progress_bar") (div ("class" "poll_bar") ("style" "width: {{ percentage }}%"))))) + (div ("class" "poll_bar") ("style" "width: {{ percentage }}%")))) (div ("class" "card-nest") diff --git a/crates/app/src/public/html/misc/requests.lisp b/crates/app/src/public/html/misc/requests.lisp index f49b6f4..9ba68d2 100644 --- a/crates/app/src/public/html/misc/requests.lisp +++ b/crates/app/src/public/html/misc/requests.lisp @@ -62,15 +62,12 @@ ("class" "card-nest") (div ("class" "card small flex items-center gap-2") - (a - ("href" "/api/v1/auth/user/find/{{ request.id }}") - (text "{{ components::avatar(username=request.id, selector_type=\"id\") }}")) + (text "{{ icon \"user-plus\" }}") (span (text "{{ text \"requests:label.user_follow_request\" }}"))) (div ("class" "card flex flex-col gap-2") (span - ("class" "flex items-center gap-2") (text "{{ text \"requests:label.user_follow_request_message\" }}")) (div ("class" "card flex flex-wrap w-full secondary gap-2") @@ -95,7 +92,7 @@ (text "{%- endif %} {% endfor %} {% for question in questions %}") (div ("class" "card-nest") - (text "{{ components::question(question=question[0], owner=question[1], asking_about=question[2], profile=user) }}") + (text "{{ components::question(question=question[0], owner=question[1], profile=user) }}") (form ("class" "card flex flex-col gap-2") ("onsubmit" "answer_question_from_form(event, '{{ question[0].id }}')") @@ -132,6 +129,7 @@ (text "{{ text \"auth:action.ip_block\" }}"))) (button + ("class" "primary") (text "{{ text \"requests:label.answer\" }}"))))) (text "{% endfor %}"))) diff --git a/crates/app/src/public/html/mod/file_report.lisp b/crates/app/src/public/html/mod/file_report.lisp index 39f7669..39891a7 100644 --- a/crates/app/src/public/html/mod/file_report.lisp +++ b/crates/app/src/public/html/mod/file_report.lisp @@ -28,6 +28,7 @@ ("required" "") ("minlength" "16"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}"))))) (script diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 5a84aac..9fb5ebf 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -50,18 +50,6 @@ (span ("class" "notification") (text "{{ profile.request_count }}"))) - (a - ("href" "/services?id={{ profile.id }}") - ("class" "button lowered") - (icon (text "globe")) - (span - (text "Sites"))) - (a - ("href" "/domains?id={{ profile.id }}") - ("class" "button lowered") - (icon (text "globe")) - (span - (text "Domains"))) (button ("class" "red lowered") ("onclick" "delete_account(event)") @@ -84,7 +72,7 @@ const ui = await ns(\"ui\"); const element = document.getElementById(\"mod_options\"); - globalThis.profile_request = async (do_confirm, path, body) => { + async function profile_request(do_confirm, path, body) { if (do_confirm) { if ( !(await trigger(\"atto::confirm\", [ @@ -167,33 +155,6 @@ }); }; - globalThis.update_user_secondary_role = async (new_role) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this?\", - ])) - ) { - return; - } - - fetch(`/api/v1/auth/user/{{ profile.id }}/role/2`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - role: Number.parseInt(new_role), - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - ui.refresh_container(element, [\"actions\"]); setTimeout(() => { @@ -212,21 +173,11 @@ \"{{ profile.awaiting_purchase }}\", \"checkbox\", ], - [ - [\"is_deactivated\", \"Is deactivated\"], - \"{{ profile.is_deactivated }}\", - \"checkbox\", - ], [ [\"role\", \"Permission level\"], \"{{ profile.permissions }}\", \"input\", ], - [ - [\"secondary_role\", \"Secondary permission level\"], - \"{{ profile.secondary_permissions }}\", - \"input\", - ], ], null, { @@ -240,17 +191,9 @@ awaiting_purchase: value, }); }, - is_deactivated: (value) => { - profile_request(false, \"deactivated\", { - is_deactivated: value, - }); - }, role: (new_role) => { return update_user_role(new_role); }, - secondary_role: (new_role) => { - return update_user_secondary_role(new_role); - }, }, ); }, 100); @@ -283,32 +226,6 @@ ("class" "card lowered flex flex-wrap gap-2") (text "{{ components::user_plate(user=invite[0], show_menu=false) }}"))) (text "{%- endif %}") - (div - ("class" "card-nest w-full") - (div - ("class" "card small flex items-center justify-between gap-2") - (div - ("class" "flex items-center gap-2") - (icon (text "scale")) - (span - (str (text "mod_panel:label.ban_reason"))))) - (form - ("class" "card flex flex-col gap-2") - ("onsubmit" "event.preventDefault(); profile_request(false, 'ban_reason', { reason: event.target.reason.value || '' })") - (div - ("class" "flex flex-col gap-1") - (label - ("for" "title") - (str (text "mod_panel:label.ban_reason"))) - (textarea - ("type" "text") - ("name" "reason") - ("id" "reason") - ("placeholder" "ban reason") - ("minlength" "2") - (text "{{ profile.ban_reason|remove_script_tags|safe }}"))) - (button - (str (text "general:action.save"))))) (div ("class" "card-nest w-full") (div @@ -327,24 +244,6 @@ (div ("class" "card lowered flex flex-col gap-2") ("id" "permission_builder"))) - (div - ("class" "card-nest w-full") - (div - ("class" "card small flex items-center justify-between gap-2") - (div - ("class" "flex items-center gap-2") - (text "{{ icon \"blocks\" }}") - (span - (text "{{ text \"mod_panel:label.permissions_level_builder\" }}"))) - (button - ("class" "small lowered") - ("onclick" "update_user_secondary_role(Number.parseInt(document.getElementById('secondary_role').value))") - (text "{{ icon \"check\" }}") - (span - (text "{{ text \"general:action.save\" }}")))) - (div - ("class" "card lowered flex flex-col gap-2") - ("id" "secondary_permission_builder"))) (script (text "setTimeout(async () => { const get_permissions_html = await trigger( @@ -392,33 +291,6 @@ Number.parseInt(\"{{ profile.permissions }}\"), \"permission_builder\", ); - }, 250); - - setTimeout(async () => { - const get_permissions_html = await trigger( - \"ui::generate_permissions_ui\", - [ - { - // https://trisuaso.github.io/tetratto/tetratto/model/permissions/struct.SecondaryPermission.html - DEFAULT: 1 << 0, - ADMINISTRATOR: 1 << 1, - MANAGE_DOMAINS: 1 << 2, - MANAGE_SERVICES: 1 << 3, - MANAGE_PRODUCTS: 1 << 4, - DEVELOPER_PASS: 1 << 5, - MANAGE_LETTERS: 1 << 6, - }, - \"secondary_role\", - \"add_permission_to_secondary_role\", - \"remove_permission_to_secondary_role\", - ], - ); - - document.getElementById(\"secondary_permission_builder\").innerHTML = - get_permissions_html( - Number.parseInt(\"{{ profile.secondary_permissions }}\"), - \"secondary_permission_builder\", - ); }, 250);"))) (text "{% endblock %}") diff --git a/crates/app/src/public/html/mod/warnings.lisp b/crates/app/src/public/html/mod/warnings.lisp index 203fa3e..35c384e 100644 --- a/crates/app/src/public/html/mod/warnings.lisp +++ b/crates/app/src/public/html/mod/warnings.lisp @@ -37,6 +37,7 @@ ("minlength" "2") ("maxlength" "4096"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (div ("class" "card-nest") diff --git a/crates/app/src/public/html/post/post.lisp b/crates/app/src/public/html/post/post.lisp index 8013461..11a5156 100644 --- a/crates/app/src/public/html/post/post.lisp +++ b/crates/app/src/public/html/post/post.lisp @@ -71,6 +71,7 @@ ("name" "content") ("id" "content") ("placeholder" "content") + ("required" "") ("minlength" "2") ("maxlength" "4096"))) (div @@ -80,6 +81,7 @@ ("class" "flex gap-2") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}") (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}"))))) (text "{%- endif %}") (div @@ -123,6 +125,7 @@ (text "{{ icon \"settings\" }}") (span (text "{{ text \"communities:action.configure\" }}")))) + (text "{%- endif %}") (div ("class" "flex flex-col gap-2 hidden") ("data-tab" "configure") @@ -198,7 +201,7 @@ \"checkbox\", ], [ - [\"is_nsfw\", \"Mark as NSFW\"], + [\"is_nsfw\", \"Hide from public timelines\"], \"{{ community.context.is_nsfw }}\", \"checkbox\", ], @@ -207,11 +210,6 @@ settings.content_warning, \"textarea\", ], - [ - [\"full_unlist\", \"Unlist from timelines\"], - \"{{ user.settings.auto_full_unlist }}\", - \"checkbox\", - ], [ [\"tags\", \"Tags\"], settings.tags.join(\", \"), @@ -247,7 +245,6 @@ }, }); }, 250);"))) - (text "{%- endif %}") (text "{% if user and user.id == post.owner -%}") (div ("class" "card-nest w-full hidden") @@ -278,6 +275,7 @@ ("class" "flex gap-2") (text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}") (button + ("class" "primary") (text "{{ text \"general:action.save\" }}"))))) (script (text "async function edit_post_from_form(e) { diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index e10dec9..7962728 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -72,25 +72,19 @@ ("style" "color: var(--color-primary);") ("class" "flex items-center") (text "{{ icon \"badge-check\" }}")) - (text "{%- endif %} {% if profile.permissions|has_supporter -%}") + (text "{%- endif %} {% if profile.permissions|has_supporter -%}") (span ("title" "Supporter") ("style" "color: var(--color-primary);") ("class" "flex items-center") (text "{{ icon \"star\" }}")) - (text "{%- endif %} {% if profile.secondary_permissions|has_dev_pass -%}") - (span - ("title" "Developer pass") - ("style" "color: var(--color-primary);") - ("class" "flex items-center") - (text "{{ icon \"id-card-lanyard\" }}")) - (text "{%- endif %} {% if profile.permissions|has_staff_badge -%}") + (text "{%- endif %} {% if profile.permissions|has_staff_badge -%}") (span ("title" "Staff") ("style" "color: var(--color-primary);") ("class" "flex items-center") (text "{{ icon \"shield-user\" }}")) - (text "{%- endif %} {% if profile.permissions|has_banned -%}") + (text "{%- endif %} {% if profile.permissions|has_banned -%}") (span ("title" "Banned") ("style" "color: var(--color-primary);") @@ -107,7 +101,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 +117,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") @@ -233,7 +225,7 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("class" "lowered red") - (icon_class (text "chevron-down") (text "dropdown_arrow")) + (icon_class (text "chevron-down") (text "dropdown-arrow")) (str (text "auth:action.block"))) (div ("class" "inner left") @@ -298,7 +290,7 @@ ]); fetch( - \"/api/v1/auth/user/{{ profile.id }}/follow/toggle\", + \"/api/v1/auth/user/{{ profile.id }}/follow\", { method: \"POST\", }, diff --git a/crates/app/src/public/html/profile/blocked.lisp b/crates/app/src/public/html/profile/blocked.lisp index 660be0d..1a128fa 100644 --- a/crates/app/src/public/html/profile/blocked.lisp +++ b/crates/app/src/public/html/profile/blocked.lisp @@ -30,7 +30,7 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("class" "lowered red") - (icon_class (text "chevron-down") (text "dropdown_arrow")) + (icon_class (text "chevron-down") (text "dropdown-arrow")) (str (text "auth:action.block"))) (div ("class" "inner left") diff --git a/crates/app/src/public/html/profile/private.lisp b/crates/app/src/public/html/profile/private.lisp index 4654298..c5acd7d 100644 --- a/crates/app/src/public/html/profile/private.lisp +++ b/crates/app/src/public/html/profile/private.lisp @@ -20,11 +20,7 @@ (div ("class" "card flex flex-col gap-2") (span - ("class" "fade") (text "{{ text \"auth:label.private_profile_message\" }}")) - (span - ("class" "no_p_margin") - (text "{{ profile.settings.private_biography|markdown|safe }}")) (div ("class" "card w-full secondary flex gap-2") (text "{% if user -%} {% if not is_following -%}") @@ -35,7 +31,6 @@ (text "{{ icon \"user-plus\" }}") (span (text "{{ text \"auth:action.request_to_follow\" }}"))) - (text "{% if follow_requested -%}") (button ("onclick" "cancel_follow_user(event)") ("class" "lowered red{% if not follow_requested -%} hidden{%- endif %}") @@ -43,7 +38,7 @@ (text "{{ icon \"user-minus\" }}") (span (text "{{ text \"auth:action.cancel_follow_request\" }}"))) - (text "{%- endif %} {% else %}") + (text "{% else %}") (button ("onclick" "toggle_follow_user(event)") ("class" "lowered red") @@ -58,7 +53,7 @@ ("onclick" "trigger('atto::hooks::dropdown', [event])") ("exclude" "dropdown") ("class" "lowered red") - (icon_class (text "chevron-down") (text "dropdown_arrow")) + (icon_class (text "chevron-down") (text "dropdown-arrow")) (str (text "auth:action.block"))) (div ("class" "inner left") @@ -81,7 +76,7 @@ (script (text "globalThis.toggle_follow_user = async (e) => { await trigger(\"atto::debounce\", [\"users::follow\"]); - fetch(\"/api/v1/auth/user/{{ profile.id }}/follow/toggle\", { + fetch(\"/api/v1/auth/user/{{ profile.id }}/follow\", { method: \"POST\", }) .then((res) => res.json()) diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 64d3a30..b7f0947 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -35,87 +35,6 @@ (text "{{ macros::profile_settings_nav_options() }}")) ; ... - (div - ("class" "w-full flex flex-col gap-2 hidden") - ("data-tab" "presets") - (div - ("class" "card lowered flex flex-col gap-2") - (a - ("href" "#/account") - ("class" "button secondary") - (icon (text "arrow-left")) - (span - (str (text "general:action.back")))) - (div - ("class" "card-nest") - (div - ("class" "card flex items-center gap-2 small") - (icon (text "cooking-pot")) - (span - (str (text "settings:tab.presets")))) - (div - ("class" "card flex flex-col gap-2 secondary") - (p (text "Not sure where to start? Try some settings presets!")) - (details - ("class" "w-full accordion") - (summary - (icon (text "rss")) - (text "Microblogging")) - - (div - ("class" "inner flex flex-col gap-2") - (p ("class" "fade") (text "Focus on yourself and your communities.")) - (ul ("id" "preset_microblogging_ul")) - (button - ("onclick" "apply_preset(PRESET_MICROBLOGGING)") - (icon (text "settings")) - (str (text "general:action.apply"))))) - - (details - ("class" "w-full accordion") - (summary - (icon (text "message-circle-heart")) - (text "Q&A")) - - (div - ("class" "inner flex flex-col gap-2") - (p ("class" "fade") (text "Just like Neospring!")) - (ul ("id" "preset_questions_ul")) - (button - ("onclick" "apply_preset(PRESET_QUESTIONS)") - (icon (text "settings")) - (str (text "general:action.apply"))))) - - (details - ("class" "w-full accordion") - (summary - (icon (text "key")) - (text "Private")) - - (div - ("class" "inner flex flex-col gap-2") - (p ("class" "fade") (text "This preset allows you to keep your profile and posts hidden to people you aren't following.")) - (ul ("id" "preset_private_ul")) - (button - ("onclick" "apply_preset(PRESET_PRIVATE)") - (icon (text "settings")) - (str (text "general:action.apply"))))) - - (details - ("class" "w-full accordion") - (summary - (icon (text "eye-closed")) - (text "NSFW")) - - (div - ("class" "inner flex flex-col gap-2") - (p ("class" "fade") (text "NSFW content is allowed if it is hidden from main timelines. This preset will help you do that quickly.")) - (ul ("id" "preset_nsfw_ul")) - (button - ("onclick" "apply_preset(PRESET_NSFW)") - (icon (text "settings")) - (str (text "general:action.apply"))))))))) - (div ("class" "w-full flex flex-col gap-2") ("data-tab" "account") @@ -137,12 +56,6 @@ (text "{{ icon \"rss\" }}") (span (text "{{ text \"auth:label.following\" }}"))) - (a - ("data-tab-button" "account/followers") - ("href" "#/account/followers") - (text "{{ icon \"rss\" }}") - (span - (text "{{ text \"auth:label.followers\" }}"))) (a ("data-tab-button" "account/blocks") ("href" "#/account/blocks") @@ -221,16 +134,15 @@ ("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}") (text "All (questions)")) (text "{% for stack in stacks %}") - (text "") + (option + ("value" "{\\\"Stack\\\":\\\"{{ stack.id }}\\\"}") + ("selected" "{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}") + (text "{{ stack.name }} (stack)")) (text "{% endfor %}")) (span ("class" "fade") - (text "This represents the timeline the home button takes you to.")))) + (text "This represents the timeline the home button takes you + to.")))) (div ("class" "card-nest desktop") ("ui_ident" "notifications") @@ -276,6 +188,7 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}")))))) @@ -284,50 +197,30 @@ ("ui_ident" "delete_account") (div ("class" "card small flex items-center gap-2 red") - (icon (text "skull")) - (b (str (text "communities:label.danger_zone")))) - (div - ("class" "card lowered flex flex-col gap-2") - (details - ("class" "accordion") - (summary - ("class" "flex items-center gap-2") - (icon_class (text "chevron-down") (text "dropdown_arrow")) - (str (text "settings:label.deactivate_account"))) - (div - ("class" "inner flex flex-col gap-2") - (p (text "Deactivating your account will treat it as deleted, but all your data will be recoverable if you change your mind. This option is recommended over a full deletion.")) - (button - ("onclick" "deactivate_account()") - (icon (text "lock")) - (span - (str (text "settings:label.deactivate")))))) - (details - ("class" "accordion") - (summary - ("class" "flex items-center gap-2") - (icon_class (text "chevron-down") (text "dropdown_arrow")) - (str (text "settings:label.delete_account"))) - (form - ("class" "inner flex flex-col gap-2") - ("onsubmit" "delete_account(event)") - (div - ("class" "flex flex-col gap-1") - (label - ("for" "current_password") - (text "{{ text \"settings:label.current_password\" }}")) - (input - ("type" "password") - ("name" "current_password") - ("id" "current_password") - ("placeholder" "current_password") - ("required" "") - ("minlength" "6") - ("autocomplete" "off"))) - (button - (text "{{ icon \"trash\" }}") - (span - (text "{{ text \"general:action.delete\" }}"))))))) + (text "{{ icon \"skull\" }}") + (b + (text "{{ text \"settings:label.delete_account\" }}"))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "delete_account(event)") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "current_password") + (text "{{ text \"settings:label.current_password\" }}")) + (input + ("type" "password") + ("name" "current_password") + ("id" "current_password") + ("placeholder" "current_password") + ("required" "") + ("minlength" "6") + ("autocomplete" "off"))) + (button + ("class" "primary") + (text "{{ icon \"trash\" }}") + (span + (text "{{ text \"general:action.delete\" }}"))))) (button ("onclick" "save_settings()") ("id" "save_button") @@ -438,6 +331,7 @@ ("minlength" "6") ("autocomplete" "off"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}"))))))))) @@ -481,7 +375,7 @@ (text "{{ icon \"external-link\" }}") (span (text "{{ text \"requests:action.view_profile\" }}"))))) - (text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/following\") }}")))) + (text "{% endfor %}")))) (script (text "globalThis.toggle_follow_user = async (uid) => { await trigger(\"atto::debounce\", [\"users::follow\"]); @@ -497,62 +391,6 @@ ]); }); };"))) - (div - ("class" "w-full flex flex-col gap-2 hidden") - ("data-tab" "account/followers") - (div - ("class" "card lowered flex flex-col gap-2") - (a - ("href" "#/account") - ("class" "button secondary") - (text "{{ icon \"arrow-left\" }}") - (span - (text "{{ text \"general:action.back\" }}"))) - (div - ("class" "card-nest") - (div - ("class" "card flex items-center gap-2 small") - (text "{{ icon \"rss\" }}") - (span - (text "{{ text \"auth:label.followers\" }}"))) - (div - ("class" "card flex flex-col gap-2") - (text "{% for userfollow in followers %} {% set user = userfollow[1] %}") - (div - ("class" "card secondary flex flex-wrap gap-2 items-center justify-between") - (div - ("class" "flex gap-2") - (text "{{ components::avatar(username=user.username) }} {{ components::full_username(user=user) }}")) - (div - ("class" "flex gap-2") - (button - ("class" "lowered red small") - ("onclick" "force_unfollow_me('{{ user.id }}')") - (text "{{ icon \"user-minus\" }}") - (span - (str (text "stacks:label.remove")))) - (a - ("href" "/@{{ user.username }}") - ("class" "button lowered small") - (text "{{ icon \"external-link\" }}") - (span - (text "{{ text \"requests:action.view_profile\" }}"))))) - (text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/followers\") }}")))) - (script - (text "globalThis.force_unfollow_me = async (uid) => { - await trigger(\"atto::debounce\", [\"users::follow\"]); - - fetch(`/api/v1/auth/user/${uid}/force_unfollow_me`, { - method: \"POST\", - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - };"))) (div ("class" "w-full flex flex-col gap-2 hidden") ("data-tab" "account/blocks") @@ -654,51 +492,32 @@ (div ("class" "card flex flex-col gap-2 secondary") (text "{{ components::supporter_ad(body=\"Become a supporter to upload images directly to posts!\") }} {% for upload in uploads %}") - (details - ("class" "accordion w-full") - (summary - ("class" "card flex flex-wrap gap-2 items-center justify-between") - (div - ("class" "flex gap-2 items-center") - (icon_class (text "chevron-down") (text "dropdown_arrow")) - (b - (span - ("class" "date") - (text "{{ upload.created }}")) - (text " ({{ upload.what }})"))) - (div - ("class" "flex gap-2") - (button - ("class" "raised small") - ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])") - (text "{{ icon \"view\" }}") - (span - (text "{{ text \"general:action.view\" }}"))) - (button - ("class" "raised small red") - ("onclick" "remove_upload('{{ upload.id }}')") - (text "{{ icon \"x\" }}") - (span - (text "{{ text \"stacks:label.remove\" }}"))))) - + (div + ("class" "card flex flex-wrap gap-2 items-center justify-between") (div - ("class" "inner flex flex-col gap-2") - (form - ("class" "card lowered flex flex-col gap-2") - ("onsubmit" "update_upload_alt(event, '{{ upload.id }}')") - (div - ("class" "flex flex-col gap-1") - (label ("for" "alt_{{ upload.id }}") (b (str (text "settings:label.alt_text")))) - (textarea - ("id" "alt_{{ upload.id }}") - ("name" "alt") - ("class" "w-full") - ("placeholder" "Alternative text") - (text "{{ upload.alt|safe }}"))) - - (button - (icon (text "check")) - (str (text "general:action.save")))))) + ("class" "flex gap-2 items-center") + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])") + ("style" "cursor: pointer") + (text "{{ icon \"file-image\" }}") + (b + (span + ("class" "date") + (text "{{ upload.created }}")) + (text "({{ upload.what }})"))) + (div + ("class" "flex gap-2") + (button + ("class" "lowered small") + ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])") + (text "{{ icon \"view\" }}") + (span + (text "{{ text \"general:action.view\" }}"))) + (button + ("class" "lowered small red") + ("onclick" "remove_upload('{{ upload.id }}')") + (text "{{ icon \"x\" }}") + (span + (text "{{ text \"stacks:label.remove\" }}"))))) (text "{% endfor %} {{ components::pagination(page=page, items=uploads|length, key=\"#/account/uploads\") }}") (script (text "globalThis.remove_upload = async (id) => { @@ -720,26 +539,6 @@ res.message, ]); }); - }; - - globalThis.update_upload_alt = async (e, id) => { - e.preventDefault(); - fetch(`/api/v1/uploads/${id}/alt`, { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ - alt: e.target.alt.value, - }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); };")))))) (text "{% if config.security.enable_invite_codes -%}") @@ -844,29 +643,6 @@ (div ("class" "card flex flex-col gap-2 secondary") (text "{% if config.stripe -%}") - (text "{% if has_developer_pass or is_supporter -%}") - (div - ("class" "card-nest") - ("ui_ident" "supporter_card") - (div - ("class" "card small flex items-center gap-2") - (icon (text "credit-card")) - (b - (text "Manage billing"))) - (div - ("class" "card flex flex-col gap-2") - (p - (text "You currently have a subscription! You can manage your billing information below. ") - (b - (text "Please use your email address you supplied when paying to log into the billing portal.")) - (text " You can manage all of your active subscriptions through this page.")) - (a - ("href" "{{ config.stripe.billing_portal_url }}") - ("class" "button lowered") - ("target" "_blank") - (text "Manage billing")))) - (text "{%- endif %}") - (div ("class" "card-nest") ("ui_ident" "supporter_card") @@ -876,33 +652,28 @@ (b (text "Supporter status"))) (div - ("class" "card flex flex-col gap-2 no_p_margin") + ("class" "card flex flex-col gap-2") (text "{% if is_supporter -%}") (p (text "You ") - (b (text "are ")) - (text "a supporter! Thank you for all that you do.")) + (b + (text "are ")) + (text "a supporter! Thank you for all + that you do. You can manage your billing + information below.") + (b + (text "Please use your email address you supplied + when paying to login to the billing + portal."))) + (a + ("href" "{{ config.stripe.billing_portal_url }}") + ("class" "button lowered") + ("target" "_blank") + (text "Manage billing")) (text "{% else %}") (text "{{ components::become_supporter_button() }}") (text "{%- endif %}"))) - (div - ("class" "card-nest") - ("ui_ident" "supporter_card") - (div - ("class" "card small flex items-center gap-2") - (icon (text "id-card-lanyard")) - (b - (text "Developer pass status"))) - (div - ("class" "card flex flex-col gap-2 no_p_margin") - (text "{% if has_developer_pass -%}") - (p - (text "You currently have a developer pass!")) - (text "{% else %}") - (text "{{ components::get_developer_pass_button() }}") - (text "{%- endif %}"))) - (text "{% if user.was_purchased and user.invite_code == 0 -%}") (form ("class" "card w-full lowered flex flex-col gap-2") @@ -954,6 +725,7 @@ ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") ("class" "w-content")) (button + ("class" "primary") (text "{{ icon \"check\" }}"))) (span ("class" "fade") @@ -981,6 +753,7 @@ ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") ("class" "w-content")) (button + ("class" "primary") (text "{{ icon \"check\" }}"))) (span ("class" "fade") @@ -1006,23 +779,7 @@ (text "Responses"))) (span ("class" "fade") - (text "This represents the timeline that is shown on your profile by default.")))) - (div - ("class" "flex flex-col gap-2") - ("ui_ident" "show_presets") - (hr ("class" "margin")) - (div - ("class" "card-nest") - (div - ("class" "card small") - (b - (text "Not sure what to do?"))) - (div - ("class" "card no_p_margin") - (p - (text "Quickly set up your account with ") - (a ("href" "/settings#/presets") (text "settings presets")) - (text "!")))))) + (text "This represents the timeline that is shown on your profile by default."))))) (button ("onclick" "save_settings()") ("id" "save_button") @@ -1098,6 +855,7 @@ ("class" "card w-full flex flex-wrap gap-2") ("ui_ident" "import_export") (button + ("class" "primary") ("onclick" "import_theme_settings()") (text "{{ icon \"upload\" }}") (span @@ -1633,87 +1391,6 @@ }); } - globalThis.deactivate_account = async () => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you want to do this?\", - ])) - ) { - return; - } - - fetch(\"/api/v1/auth/user/{{ profile.id }}/deactivate\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ is_deactivated: true }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - }); - }; - - // presets - globalThis.apply_preset = async (preset) => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you would like to do this? This will change all listed settings to their listed values.\", - ])) - ) { - return; - } - - for (const x of preset) { - window.SETTING_SET_FUNCTIONS[0](x[0], x[1]) - } - - save_settings(); - } - - globalThis.render_preset_lis = (preset, id) => { - for (const x of preset) { - document.getElementById(id).innerHTML += `
  • ${x[0]}: ${x[1]}
  • `; - } - } - - globalThis.PRESET_MICROBLOGGING = [ - [\"default_timeline\", \"All\"], - [\"all_timeline_hide_answers\", true], - ]; - - globalThis.PRESET_QUESTIONS = [ - [\"default_timeline\", \"Following\"], - [\"auto_full_unlist\", true], - [\"enable_questions\", true], - [\"allow_anonymous_questions\", true], - [\"enable_drawings\", true], - [\"hide_extra_post_tabs\", true], - ]; - - globalThis.PRESET_PRIVATE = [ - [\"private_profile\", true], - [\"private_last_seen\", true], - [\"private_communities\", true], - [\"private_chats\", true], - [\"require_account\", true], - ]; - - globalThis.PRESET_NSFW = [ - [\"auto_unlist\", true], - [\"show_nsfw\", true], - ]; - - render_preset_lis(PRESET_MICROBLOGGING, \"preset_microblogging_ul\"); - render_preset_lis(PRESET_QUESTIONS, \"preset_questions_ul\"); - render_preset_lis(PRESET_PRIVATE, \"preset_private_ul\"); - render_preset_lis(PRESET_NSFW, \"preset_nsfw_ul\"); - - // ... const account_settings = document.getElementById(\"account_settings\"); const profile_settings = @@ -1733,7 +1410,6 @@ \"change_avatar\", \"change_banner\", \"default_profile_page\", - \"show_presets\", ]); ui.refresh_container(theme_settings, [ \"supporter_ad\", @@ -1756,15 +1432,6 @@ settings.biography, \"textarea\", ], - [ - [\"private_biography\", \"Private biography\"], - settings.private_biography, - \"textarea\", - { - embed_html: - 'This biography is only shown to users you are not following while your account is private.', - }, - ], [[\"status\", \"Status\"], settings.status, \"textarea\"], [ [\"warning\", \"Profile warning\"], @@ -1858,16 +1525,6 @@ \"{{ profile.settings.auto_unlist }}\", \"checkbox\", ], - [ - [\"auto_full_unlist\", \"Only publish my posts to my profile\"], - \"{{ profile.settings.auto_full_unlist }}\", - \"checkbox\", - ], - [ - [], - \"Your posts will still be visible in the \\\"Following\\\" timeline for users following you.\", - \"text\", - ], [ [\"all_timeline_hide_answers\", 'Hide posts answering questions from the \"All\" timeline'], \"{{ profile.settings.all_timeline_hide_answers }}\", @@ -1881,22 +1538,6 @@ \"{{ profile.settings.hide_associated_blocked_users }}\", \"checkbox\", ], - [ - [ - \"hide_from_social_lists\", - \"Hide my profile from social lists (followers/following)\", - ], - \"{{ 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/html/root.lisp b/crates/app/src/public/html/root.lisp index 5cf7da9..6730dd8 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -70,13 +70,9 @@ (str (text "general:label.account_banned"))) (div - ("class" "card flex flex-col gap-2 no_p_margin") - (str (text "general:label.account_banned_body")) - (hr) - (span ("class" "fade") (text "The following reason was provided by a moderator:")) - (div - ("class" "card lowered w-full") - (text "{{ user.ban_reason|markdown|safe }}")))))) + ("class" "card") + (str (text "general:label.account_banned_body")))))) + ; if we aren't banned, just show the page body (text "{% elif user and user.awaiting_purchase %}") ; account waiting for payment message @@ -141,55 +137,6 @@ } }); }")))))) - (text "{% elif user.is_deactivated -%}") - ; account deactivated message - (article - (main - (div - ("class" "card-nest") - (div - ("class" "card small flex items-center gap-2 red") - (icon (text "frown")) - (str (text "settings:label.account_deactivated"))) - - (div - ("class" "card flex flex-col gap-2 no_p_margin") - (p (text "You have deactivated your account. You can undo this with the button below if you'd like.")) - (hr) - (button - ("onclick" "activate_account()") - (icon (text "lock-open")) - (str (text "settings:label.activate_account"))))))) - - (script - (text "globalThis.activate_account = async () => { - if ( - !(await trigger(\"atto::confirm\", [ - \"Are you sure you want to do this?\", - ])) - ) { - return; - } - - fetch(\"/api/v1/auth/user/{{ user.id }}/deactivate\", { - method: \"POST\", - headers: { - \"Content-Type\": \"application/json\", - }, - body: JSON.stringify({ is_deactivated: false }), - }) - .then((res) => res.json()) - .then((res) => { - trigger(\"atto::toast\", [ - res.ok ? \"success\" : \"error\", - res.message, - ]); - - if (res.ok) { - window.location.reload(); - } - }); - };")) (text "{% else %}") ; page body (text "{% block body %}{% endblock %}") diff --git a/crates/app/src/public/html/stacks/list.lisp b/crates/app/src/public/html/stacks/list.lisp index 6381881..50246ef 100644 --- a/crates/app/src/public/html/stacks/list.lisp +++ b/crates/app/src/public/html/stacks/list.lisp @@ -29,6 +29,7 @@ ("minlength" "2") ("maxlength" "32"))) (button + ("class" "primary") (text "{{ text \"communities:action.create\" }}")))) (text "{%- endif %}") (div diff --git a/crates/app/src/public/html/stacks/manage.lisp b/crates/app/src/public/html/stacks/manage.lisp index ecd892c..450c027 100644 --- a/crates/app/src/public/html/stacks/manage.lisp +++ b/crates/app/src/public/html/stacks/manage.lisp @@ -114,6 +114,7 @@ ("required" "") ("minlength" "2"))) (button + ("class" "primary") (text "{{ icon \"check\" }}") (span (text "{{ text \"general:action.save\" }}")))))) diff --git a/crates/app/src/public/html/timelines/home.lisp b/crates/app/src/public/html/timelines/home.lisp index 65b3a60..5a5658b 100644 --- a/crates/app/src/public/html/timelines/home.lisp +++ b/crates/app/src/public/html/timelines/home.lisp @@ -24,18 +24,6 @@ (a ("href" "/communities/search") (text "searching for a community to join!"))))) - (div - ("class" "card-nest") - (div - ("class" "card small") - (b - (text "Need help getting started?"))) - (div - ("class" "card no_p_margin") - (p - (text "Quickly set up your account with ") - (a ("href" "/settings#/presets") (text "settings presets")) - (text "!")))) (text "{% else %}") (div ("class" "card w-full flex flex-col gap-2") diff --git a/crates/app/src/public/images/default-avatar.svg b/crates/app/src/public/images/default-avatar.svg index 2f92a92..00fa7ab 100644 --- a/crates/app/src/public/images/default-avatar.svg +++ b/crates/app/src/public/images/default-avatar.svg @@ -6,11 +6,4 @@ xmlns="http://www.w3.org/2000/svg" > - - - diff --git a/crates/app/src/public/images/vendor/stripe.svg b/crates/app/src/public/images/vendor/stripe.svg deleted file mode 100644 index 415271d..0000000 --- a/crates/app/src/public/images/vendor/stripe.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/crates/app/src/public/js/app_sdk.js b/crates/app/src/public/js/app_sdk.js deleted file mode 100644 index 7a6c834..0000000 --- a/crates/app/src/public/js/app_sdk.js +++ /dev/null @@ -1,338 +0,0 @@ -import { - JSONParse as json_parse, - JSONStringify as json_stringify, -} from "https://unpkg.com/json-with-bigint@3.4.4/json-with-bigint.js"; - -/// PKCE key generation. -export const PKCE = { - /// Create a verifier for [`PKCE::challenge`]. - verifier: async (length) => { - let text = ""; - const possible = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (let i = 0; i < length; i++) { - text += possible.charAt( - Math.floor(Math.random() * possible.length), - ); - } - - return text; - }, - /// Create the challenge needed to request a user token. - challenge: async (verifier) => { - const data = new TextEncoder().encode(verifier); - const digest = await window.crypto.subtle.digest("SHA-256", data); - return btoa( - String.fromCharCode.apply(null, [...new Uint8Array(digest)]), - ) - .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=+$/, ""); - }, -}; - -export default function tetratto({ - host = "https://tetratto.com", - api_key = null, - app_id = 0n, - user_token = null, - user_verifier = null, - user_id = 0n, -}) { - const GRANT_URL = `${host}/auth/connections_link/app/${app_id}`; - - function api_promise(res) { - return new Promise((resolve, reject) => { - if (res.ok) { - resolve(res.payload); - } else { - reject(res.message); - } - }); - } - - // app data - async function app() { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data/app`, { - method: "GET", - headers: { - "Atto-Secret-Key": api_key, - }, - }) - ).text(), - ), - ); - } - - 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."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data/query`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Atto-Secret-Key": api_key, - }, - body: json_stringify(body), - }) - ).text(), - ), - ); - } - - async function insert(key, value) { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Atto-Secret-Key": api_key, - }, - body: json_stringify({ - key, - value, - }), - }) - ).text(), - ), - ); - } - - async function update(id, value) { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data/${id}/value`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Atto-Secret-Key": api_key, - }, - body: json_stringify({ - value, - }), - }) - ).text(), - ), - ); - } - - async function rename(id, key) { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data/${id}/key`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Atto-Secret-Key": api_key, - }, - body: json_stringify({ - key, - }), - }) - ).text(), - ), - ); - } - - async function remove(id) { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data/${id}`, { - method: "DELETE", - headers: { - "Atto-Secret-Key": api_key, - }, - }) - ).text(), - ), - ); - } - - async function remove_query(body) { - if (!api_key) { - throw Error("No API key provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/app_data/query`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - "Atto-Secret-Key": api_key, - }, - body: json_stringify(body), - }) - ).text(), - ), - ); - } - - // user connection - /// Extract the verifier, token, and user ID from the URL. - function extract_verifier_token_uid() { - const search = new URLSearchParams(window.location.search); - return [ - search.get("verifier"), - search.get("token"), - BigInt(search.get("uid")), - ]; - } - - /// Accept a connection grant and store it in localStorage. - function localstorage_accept_connection() { - const [verifier, token, uid] = extract_verifier_token_uid(); - window.localStorage.setItem("atto:grant.verifier", verifier); - window.localStorage.setItem("atto:grant.token", token); - window.localStorage.setItem("atto:grant.user_id", uid); - } - - async function refresh_token() { - return api_promise( - json_parse( - await ( - await fetch( - `${host}/api/v1/auth/user/${user_id}/grants/${app_id}/refresh`, - { - method, - headers: { - "Content-Type": "application/json", - "X-Cookie": `Atto-Grant=${user_token}`, - }, - body: json_stringify({ - verifier: user_verifier, - }), - }, - ) - ).text(), - ), - ); - } - - async function request({ - route, - method = "POST", - content_type = "application/json", - body = {}, - }) { - if (!user_token) { - throw Error("No user token provided."); - } - - return api_promise( - json_parse( - await ( - await fetch(`${host}/api/v1/${route}`, { - method, - headers: { - "Content-Type": - method === "GET" ? null : content_type, - "X-Cookie": `Atto-Grant=${user_token}`, - }, - body: - method === "GET" - ? null - : content_type === "application/json" - ? json_stringify(body) - : body, - }) - ).text(), - ), - ); - } - - // ... - return { - user_id, - user_token, - user_verifier, - app_id, - api_key, - // app data - app, - check_ip, - query, - insert, - update, - rename, - remove, - remove_query, - // user connection - GRANT_URL, - extract_verifier_token_uid, - refresh_token, - localstorage_accept_connection, - request, - }; -} - -export function from_localstorage({ - host = "https://tetratto.com", - app_id = 0n, -}) { - const user_verifier = window.localStorage.getItem("atto:grant.verifier"); - const user_token = window.localStorage.getItem("atto:grant.token"); - const user_id = window.localStorage.getItem("atto:grant.user_id"); - - return tetratto({ - host, - app_id, - user_verifier, - user_id, - user_token, - }); -} diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 9c556cf..1b2a4db 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -156,7 +156,9 @@ media_theme_pref(); .replaceAll(" year ago", "y"); } - element.innerText = !pretty ? then.toLocaleDateString() : pretty; + element.innerText = + pretty === undefined ? then.toLocaleDateString() : pretty; + element.style.display = "inline-block"; } }); @@ -196,7 +198,9 @@ media_theme_pref(); .replaceAll(" year ago", "y") .replaceAll("Yesterday", "1d") || ""; - element.innerText = !pretty ? then.toLocaleDateString() : pretty; + element.innerText = + pretty === undefined ? then.toLocaleDateString() : pretty; + element.style.display = "inline-block"; } }); @@ -415,35 +419,33 @@ media_theme_pref(); }); self.define("hooks::long_text.init", (_) => { - setTimeout(() => { - for (const element of Array.from( - document.querySelectorAll("[hook=long]") || [], - )) { - const is_long = element.innerText.length >= 64 * 8; + for (const element of Array.from( + document.querySelectorAll("[hook=long]") || [], + )) { + const is_long = element.innerText.length >= 64 * 8; - if (!is_long) { - continue; - } - - element.classList.add("hook:long.hidden_text"); - - if (element.getAttribute("hook-arg") === "lowered") { - element.classList.add("hook:long.hidden_text+lowered"); - } - - const html = element.innerHTML; - const short = html.slice(0, 64 * 8); - element.innerHTML = `${short}...`; - - // event - const listener = () => { - self["hooks::long"](element, html); - element.removeEventListener("click", listener); - }; - - element.addEventListener("click", listener); + if (!is_long) { + continue; } - }, 150); + + element.classList.add("hook:long.hidden_text"); + + if (element.getAttribute("hook-arg") === "lowered") { + element.classList.add("hook:long.hidden_text+lowered"); + } + + const html = element.innerHTML; + const short = html.slice(0, 64 * 8); + element.innerHTML = `${short}...`; + + // event + const listener = () => { + self["hooks::long"](element, html); + element.removeEventListener("click", listener); + }; + + element.addEventListener("click", listener); + } }); self.define("hooks::alt", (_) => { @@ -689,7 +691,7 @@ media_theme_pref(); }); self.define("hooks::check_message_reactions", async ({ $ }) => { - const observer = await $.offload_work_to_client_when_in_view( + const observer = $.offload_work_to_client_when_in_view( async (element) => { const reactions = await ( await fetch( @@ -1067,13 +1069,7 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} // permissions ui self.define( "generate_permissions_ui", - ( - _, - permissions, - field_id = "role", - add_name = "add_permission_to_role", - remove_name = "remove_permission_from_role", - ) => { + (_, permissions, field_id = "role") => { function all_matching_permissions(role) { const matching = []; const not_matching = []; @@ -1103,7 +1099,7 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} function get_permissions_html(role, id) { const [matching, not_matching] = all_matching_permissions(role); - globalThis[remove_name] = (permission) => { + globalThis.remove_permission_from_role = (permission) => { matching.splice(matching.indexOf(permission), 1); not_matching.push(permission); @@ -1111,7 +1107,7 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} get_permissions_html(rebuild_role(matching), id); }; - globalThis[add_name] = (permission) => { + globalThis.add_permission_to_role = (permission) => { not_matching.splice(not_matching.indexOf(permission), 1); matching.push(permission); @@ -1124,14 +1120,14 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} for (const match of matching) { permissions_html += `
    ${match} ${permissions[match]} - +
    `; } for (const match of not_matching) { permissions_html += `
    ${match} ${permissions[match]} - +
    `; } @@ -1143,15 +1139,8 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} ); // lightbox - self.define("lightbox_open", async (_, src) => { + self.define("lightbox_open", (_, src) => { document.getElementById("lightbox_img").src = src; - - const data = await (await fetch(`${src}/data`)).json(); - document - .getElementById("lightbox_img") - .setAttribute("alt", data.payload.alt); - document.getElementById("lightbox_img").title = data.payload.alt; - document.getElementById("lightbox_img_a").href = src; document.getElementById("lightbox").classList.remove("hidden"); }); diff --git a/crates/app/src/public/js/layout_editor.js b/crates/app/src/public/js/layout_editor.js new file mode 100644 index 0000000..13d3d8b --- /dev/null +++ b/crates/app/src/public/js/layout_editor.js @@ -0,0 +1,762 @@ +/// Copy all the fields from one object to another. +function copy_fields(from, to) { + for (const field of Object.entries(from)) { + to[field[0]] = field[1]; + } + + return to; +} + +/// Simple template components. +const COMPONENT_TEMPLATES = { + EMPTY_COMPONENT: { component: "empty", options: {}, children: [] }, + FLEX_DEFAULT: { + component: "flex", + options: { + direction: "row", + gap: "2", + }, + children: [], + }, + FLEX_SIMPLE_ROW: { + component: "flex", + options: { + direction: "row", + gap: "2", + width: "full", + }, + children: [], + }, + FLEX_SIMPLE_COL: { + component: "flex", + options: { + direction: "col", + gap: "2", + width: "full", + }, + children: [], + }, + FLEX_MOBILE_COL: { + component: "flex", + options: { + collapse: "yes", + gap: "2", + width: "full", + }, + children: [], + }, + MARKDOWN_DEFAULT: { + component: "markdown", + options: { + text: "Hello, world!", + }, + }, + MARKDOWN_CARD: { + component: "markdown", + options: { + class: "card w-full", + text: "Hello, world!", + }, + }, +}; + +/// All available components with their label and JSON representation. +const COMPONENTS = [ + [ + "Markdown block", + COMPONENT_TEMPLATES.MARKDOWN_DEFAULT, + [["Card", COMPONENT_TEMPLATES.MARKDOWN_CARD]], + ], + [ + "Flex container", + COMPONENT_TEMPLATES.FLEX_DEFAULT, + [ + ["Simple rows", COMPONENT_TEMPLATES.FLEX_SIMPLE_ROW], + ["Simple columns", COMPONENT_TEMPLATES.FLEX_SIMPLE_COL], + ["Mobile columns", COMPONENT_TEMPLATES.FLEX_MOBILE_COL], + ], + ], + [ + "Profile tabs", + { + component: "tabs", + }, + ], + [ + "Profile feeds", + { + component: "feed", + }, + ], + [ + "Profile banner", + { + component: "banner", + }, + ], + [ + "Question box", + { + component: "ask", + }, + ], + [ + "Name & avatar", + { + component: "name", + }, + ], + [ + "About section", + { + component: "about", + }, + ], + [ + "Action buttons", + { + component: "actions", + }, + ], + [ + "CSS stylesheet", + { + component: "style", + options: { + data: "", + }, + }, + ], +]; + +// preload icons +trigger("app::icon", ["shapes"]); +trigger("app::icon", ["type"]); +trigger("app::icon", ["plus"]); +trigger("app::icon", ["move-up"]); +trigger("app::icon", ["move-down"]); +trigger("app::icon", ["trash"]); +trigger("app::icon", ["arrow-left"]); +trigger("app::icon", ["x"]); + +/// The location of an element as represented by array indexes. +class ElementPointer { + position = []; + + constructor(element) { + if (element) { + const pos = []; + + let target = element; + while (target.parentElement) { + const parent = target.parentElement; + + // push index + pos.push(Array.from(parent.children).indexOf(target) || 0); + + // update target + if (parent.id === "editor") { + break; + } + + target = parent; + } + + this.position = pos.reverse(); // indexes are added in reverse order because of how we traverse + } else { + this.position = []; + } + } + + get() { + return this.position; + } + + resolve(json, minus = 0) { + let out = json; + + if (this.position.length === 1) { + // this is the first element (this.position === [0]) + return out; + } + + const pos = this.position.slice(1, this.position.length); // the first one refers to the root element + + for (let i = 0; i < minus; i++) { + pos.pop(); + } + + for (const idx of pos) { + const child = ((out || { children: [] }).children || [])[idx]; + + if (!child) { + break; + } + + out = child; + } + + return out; + } +} + +/// The layout editor controller. +class LayoutEditor { + element; + json; + tree = ""; + current = { component: "empty" }; + pointer = new ElementPointer(); + + /// Create a new [`LayoutEditor`]. + constructor(element, json) { + this.element = element; + this.json = json; + + if (this.json.json) { + delete this.json.json; + } + + element.addEventListener("click", (e) => this.click(e, this)); + element.addEventListener("mouseover", (e) => { + e.stopImmediatePropagation(); + const ptr = new ElementPointer(e.target); + + if (document.getElementById("position")) { + document.getElementById( + "position", + ).parentElement.style.display = "flex"; + + document.getElementById("position").innerText = ptr + .get() + .join("."); + } + }); + + this.render(); + } + + /// Render layout. + render() { + fetch("/api/v0/auth/render_layout", { + method: "POST", + body: JSON.stringify({ + layout: this.json, + }), + headers: { + "Content-Type": "application/json", + }, + }) + .then((r) => r.json()) + .then((r) => { + this.element.innerHTML = r.block; + this.tree = r.tree; + + if (this.json.component !== "empty") { + // remove all "empty" components (if the root component isn't an empty) + for (const element of document.querySelectorAll( + '[data-component-name="empty"]', + )) { + element.remove(); + } + } + }); + } + + /// Editor clicked. + click(e, self) { + e.stopImmediatePropagation(); + trigger("app::hooks::dropdown.close"); + + const ptr = new ElementPointer(e.target); + self.current = ptr.resolve(self.json); + self.pointer = ptr; + + if (document.getElementById("current_position")) { + document.getElementById( + "current_position", + ).parentElement.style.display = "flex"; + + document.getElementById("current_position").innerText = ptr + .get() + .join("."); + } + + for (const element of document.querySelectorAll( + ".layout_editor_block.active", + )) { + element.classList.remove("active"); + } + + e.target.classList.add("active"); + self.screen("element"); + } + + /// Open sidebar. + open() { + document.getElementById("editor_sidebar").classList.add("open"); + document.getElementById("editor").style.transform = "scale(0.8)"; + } + + /// Close sidebar. + close() { + document.getElementById("editor_sidebar").style.animation = + "0.2s ease-in-out forwards to_left"; + + setTimeout(() => { + document.getElementById("editor_sidebar").classList.remove("open"); + document.getElementById("editor_sidebar").style.animation = + "0.2s ease-in-out forwards from_right"; + }, 250); + + document.getElementById("editor").style.transform = "scale(1)"; + } + + /// Render editor dialog. + screen(page = "element", data = {}) { + this.current.component = this.current.component.toLowerCase(); + + const sidebar = document.getElementById("editor_sidebar"); + sidebar.innerHTML = ""; + + // render page + if ( + page === "add" || + (page === "element" && this.current.component === "empty") + ) { + // add element + sidebar.appendChild( + (() => { + const heading = document.createElement("h3"); + heading.innerText = data.add_title || "Add component"; + return heading; + })(), + ); + + sidebar.appendChild(document.createElement("hr")); + + const container = document.createElement("div"); + container.className = "flex w-full gap-2 flex-wrap"; + + for (const component of data.components || COMPONENTS) { + container.appendChild( + (() => { + const button = document.createElement("button"); + button.classList.add("secondary"); + + trigger("app::icon", [ + data.icon || "shapes", + "icon", + ]).then((icon) => { + button.prepend(icon); + }); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = `${component[0]}${component[2] ? ` (${component[2].length + 1})` : ""}`; + return span; + })(), + ); + + button.addEventListener("click", () => { + if (component[2]) { + // render presets + return this.screen(page, { + back: ["add", {}], + add_title: "Select preset", + components: [ + ["Default", component[1]], + ...component[2], + ], + icon: "type", + }); + } + + // no presets + if ( + page === "element" && + this.current.component === "empty" + ) { + // replace with component + copy_fields(component[1], this.current); + } else { + // add component to children + this.current.children.push( + structuredClone(component[1]), + ); + } + + this.render(); + this.close(); + }); + + return button; + })(), + ); + } + + sidebar.appendChild(container); + } else if (page === "element") { + // edit element + const name = document.createElement("div"); + name.className = "flex flex-col gap-2"; + + name.appendChild( + (() => { + const heading = document.createElement("h3"); + heading.innerText = `Edit ${this.current.component}`; + return heading; + })(), + ); + + name.appendChild( + (() => { + const pos = document.createElement("div"); + pos.className = "notification w-content"; + pos.innerText = this.pointer.get().join("."); + return pos; + })(), + ); + + sidebar.appendChild(name); + sidebar.appendChild(document.createElement("hr")); + + // options + const options = document.createElement("div"); + options.className = "card flex flex-col gap-2 w-full"; + + const add_option = ( + label_text, + name, + valid = [], + input_element = "input", + ) => { + const card = document.createElement("details"); + card.className = "w-full"; + + const summary = document.createElement("summary"); + summary.className = "w-full"; + + const label = document.createElement("label"); + label.setAttribute("for", name); + label.className = "w-full"; + label.innerText = label_text; + label.style.cursor = "pointer"; + + label.addEventListener("click", () => { + // bubble to summary click + summary.click(); + }); + + const input_box = document.createElement("div"); + input_box.style.paddingLeft = "1rem"; + input_box.style.borderLeft = + "solid 2px var(--color-super-lowered)"; + + const input = document.createElement(input_element); + input.id = name; + input.setAttribute("name", name); + input.setAttribute("type", "text"); + + if (input_element === "input") { + input.setAttribute( + "value", + // biome-ignore lint/complexity/useOptionalChain: that's an unsafe change which would possibly break code + (this.current.options || {})[name] || "", + ); + } else { + // biome-ignore lint/complexity/useOptionalChain: that's an unsafe change which would possibly break code + input.innerHTML = (this.current.options || {})[name] || ""; + } + + // biome-ignore lint/complexity/useOptionalChain: that's an unsafe change which would possibly break code + if ((this.current.options || {})[name]) { + // open details if a value is set + card.setAttribute("open", ""); + } + + input.addEventListener("change", (e) => { + if ( + valid.length > 0 && + !valid.includes(e.target.value) && + e.target.value.length > 0 // anything can be set to empty + ) { + alert(`Must be one of: ${JSON.stringify(valid)}`); + return; + } + + if (!this.current.options) { + this.current.options = {}; + } + + this.current.options[name] = + e.target.value === "no" ? "" : e.target.value; + }); + + summary.appendChild(label); + card.appendChild(summary); + input_box.appendChild(input); + card.appendChild(input_box); + options.appendChild(card); + }; + + sidebar.appendChild(options); + + if (this.current.component === "flex") { + add_option("Gap", "gap", ["1", "2", "3", "4"]); + add_option("Direction", "direction", ["row", "col"]); + add_option("Do collapse", "collapse", ["yes", "no"]); + add_option("Width", "width", ["full", "content"]); + add_option("Class name", "class"); + add_option("Unique ID", "id"); + add_option("Style", "style", [], "textarea"); + } else if (this.current.component === "markdown") { + add_option("Content", "text", [], "textarea"); + add_option("Class name", "class"); + } else if (this.current.component === "divider") { + add_option("Class name", "class"); + } else if (this.current.component === "style") { + add_option("Style data", "data", [], "textarea"); + } else { + options.remove(); + } + + // action buttons + const buttons = document.createElement("div"); + buttons.className = "card w-full flex flex-wrap gap-2"; + + if (this.current.component === "flex") { + buttons.appendChild( + (() => { + const button = document.createElement("button"); + + trigger("app::icon", ["plus", "icon"]).then((icon) => { + button.prepend(icon); + }); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = "Add child"; + return span; + })(), + ); + + button.addEventListener("click", () => { + this.screen("add"); + }); + + return button; + })(), + ); + } + + buttons.appendChild( + (() => { + const button = document.createElement("button"); + + trigger("app::icon", ["move-up", "icon"]).then((icon) => { + button.prepend(icon); + }); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = "Move up"; + return span; + })(), + ); + + button.addEventListener("click", () => { + const idx = this.pointer.get().pop(); + const parent_ref = this.pointer.resolve( + this.json, + ).children; + + if (parent_ref[idx - 1] === undefined) { + alert("No space to move element."); + return; + } + + const clone = JSON.parse(JSON.stringify(this.current)); + const other_clone = JSON.parse( + JSON.stringify(parent_ref[idx - 1]), + ); + + copy_fields(clone, parent_ref[idx - 1]); // move here to here + copy_fields(other_clone, parent_ref[idx]); // move there to here + + this.close(); + this.render(); + }); + + return button; + })(), + ); + + buttons.appendChild( + (() => { + const button = document.createElement("button"); + + trigger("app::icon", ["move-down", "icon"]).then((icon) => { + button.prepend(icon); + }); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = "Move down"; + return span; + })(), + ); + + button.addEventListener("click", () => { + const idx = this.pointer.get().pop(); + const parent_ref = this.pointer.resolve( + this.json, + ).children; + + if (parent_ref[idx + 1] === undefined) { + alert("No space to move element."); + return; + } + + const clone = JSON.parse(JSON.stringify(this.current)); + const other_clone = JSON.parse( + JSON.stringify(parent_ref[idx + 1]), + ); + + copy_fields(clone, parent_ref[idx + 1]); // move here to here + copy_fields(other_clone, parent_ref[idx]); // move there to here + + this.close(); + this.render(); + }); + + return button; + })(), + ); + + buttons.appendChild( + (() => { + const button = document.createElement("button"); + button.classList.add("red"); + + trigger("app::icon", ["trash", "icon"]).then((icon) => { + button.prepend(icon); + }); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = "Delete"; + return span; + })(), + ); + + button.addEventListener("click", async () => { + if ( + !(await trigger("app::confirm", [ + "Are you sure you would like to do this?", + ])) + ) { + return; + } + + if (this.json === this.current) { + // this is the root element; replace with empty + copy_fields( + COMPONENT_TEMPLATES.EMPTY_COMPONENT, + this.current, + ); + } else { + // get parent + const idx = this.pointer.get().pop(); + const ref = this.pointer.resolve(this.json); + // remove element + ref.children.splice(idx, 1); + } + + this.render(); + this.close(); + }); + + return button; + })(), + ); + + sidebar.appendChild(buttons); + } else if (page === "tree") { + sidebar.innerHTML = this.tree; + } + + sidebar.appendChild(document.createElement("hr")); + + const buttons = document.createElement("div"); + buttons.className = "flex gap-2 flex-wrap"; + + if (data.back) { + buttons.appendChild( + (() => { + const button = document.createElement("button"); + button.className = "secondary"; + + trigger("app::icon", ["arrow-left", "icon"]).then( + (icon) => { + button.prepend(icon); + }, + ); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = "Back"; + return span; + })(), + ); + + button.addEventListener("click", () => { + this.screen(...data.back); + }); + + return button; + })(), + ); + } + + buttons.appendChild( + (() => { + const button = document.createElement("button"); + button.className = "red secondary"; + + trigger("app::icon", ["x", "icon"]).then((icon) => { + button.prepend(icon); + }); + + button.appendChild( + (() => { + const span = document.createElement("span"); + span.innerText = "Close"; + return span; + })(), + ); + + button.addEventListener("click", () => { + this.render(); + this.close(); + }); + + return button; + })(), + ); + + sidebar.appendChild(buttons); + + // ... + this.open(); + } +} + +define("ElementPointer", ElementPointer); +define("LayoutEditor", LayoutEditor); diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js index 99fda4e..4fd2150 100644 --- a/crates/app/src/public/js/me.js +++ b/crates/app/src/public/js/me.js @@ -193,13 +193,9 @@ like.classList.add("green"); like.querySelector("svg").classList.add("filled"); - if (dislike) { - dislike.classList.remove("red"); - } + dislike.classList.remove("red"); } else { - if (dislike) { - dislike.classList.add("red"); - } + dislike.classList.add("red"); like.classList.remove("green"); like.querySelector("svg").classList.remove("filled"); @@ -1205,60 +1201,3 @@ ]); }); })(); - -(() => { - const self = reg_ns("seller"); - - self.define("register", async () => { - await trigger("atto::debounce", ["seller::register"]); - - if ( - !(await trigger("atto::confirm", [ - "Are you sure you want to do this?", - ])) - ) { - return; - } - - const res = await ( - await fetch("/api/v1/service_hooks/stripe/seller/register", { - method: "POST", - }) - ).json(); - - trigger("atto::toast", [res.ok ? "success" : "error", res.message]); - self.onboarding(); - }); - - self.define("onboarding", async () => { - await trigger("atto::debounce", ["seller::onboarding"]); - - const res = await ( - await fetch("/api/v1/service_hooks/stripe/seller/onboarding", { - method: "POST", - }) - ).json(); - - trigger("atto::toast", [res.ok ? "success" : "error", res.message]); - - if (res.ok) { - window.location.href = res.payload; - } - }); - - self.define("login", async () => { - await trigger("atto::debounce", ["seller::login"]); - - const res = await ( - await fetch("/api/v1/service_hooks/stripe/seller/login", { - method: "POST", - }) - ).json(); - - trigger("atto::toast", [res.ok ? "success" : "error", res.message]); - - if (res.ok) { - window.location.href = res.payload; - } - }); -})(); diff --git a/crates/app/src/public/js/proto_links.js b/crates/app/src/public/js/proto_links.js index 9c8d9fd..ab5d938 100644 --- a/crates/app/src/public/js/proto_links.js +++ b/crates/app/src/public/js/proto_links.js @@ -31,9 +31,7 @@ function fix_atto_links() { if (TETRATTO_LINK_HANDLER_CTX === "embed") { // relative links for embeds - const path = window.location.pathname - .replace("atto://", "") - .slice("/api/v1/net/".length); + const path = window.location.pathname.slice("/api/v1/net/".length); function fix_element( selector = "a", @@ -45,28 +43,25 @@ function fix_atto_links() { continue; } - const p = new URL(y[property]).pathname.replace("atto://", ""); - let x = p.startsWith("/api/v1/net/") - ? p.replace("/api/v1/net/", "") - : p.startsWith("/") - ? `${path.split("/")[0]}${p}` - : p; + let x = new URL(y[property]).pathname; if (!x.includes(".html")) { x = `${x}/index.html`; } if (relative) { - y[property] = `atto://${x}`; + y[property] = + `atto://${path.replace("atto://", "").split("/")[0]}${x}`; } else { y[property] = - `/api/v1/net/${path.replace("atto://", "").split("/")[0]}${x}?s=${globalThis.SECRET_SESSION}`; + `/api/v1/net/atto://${path.replace("atto://", "").split("/")[0]}${x}`; } } } fix_element("a", "href", true); - fix_element("img", "src", false); + fix_element("link", "href", false); + fix_element("script", "src", false); // send message window.top.postMessage( @@ -113,11 +108,12 @@ function fix_atto_links() { } const href = structuredClone(anchor.href); + anchor.addEventListener("click", () => { if (TETRATTO_LINK_HANDLER_CTX === "net") { window.location.href = `/net/${href.replace("atto://", "")}`; } else { - window.location.href = `/api/v1/net/${href}?s=${globalThis.SECRET_SESSION}`; + window.location.href = `/api/v1/net/${href}`; } }); diff --git a/crates/app/src/routes/api/v1/app_data.rs b/crates/app/src/routes/api/v1/app_data.rs deleted file mode 100644 index b5fa212..0000000 --- a/crates/app/src/routes/api/v1/app_data.rs +++ /dev/null @@ -1,277 +0,0 @@ -use crate::{ - get_app_from_key, - routes::api::v1::{InsertAppData, QueryAppData, UpdateAppDataValue, UpdateAppDataKey}, - State, -}; -use axum::{extract::Path, http::HeaderMap, response::IntoResponse, Extension, Json}; -use tetratto_core::model::{ - apps::{AppData, AppDataQuery, AppDataQueryResult}, - ApiReturn, Error, -}; - -pub async fn get_app_request( - headers: HeaderMap, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(app), - }) -} - -pub async fn query_request( - headers: HeaderMap, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .query_app_data(AppDataQuery { - app: app.id, - query: req.query, - mode: req.mode, - }) - .await - { - Ok(x) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(x), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn create_request( - headers: HeaderMap, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - let owner = match data.get_user_by_id(app.owner).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - // check size - let new_size = app.data_used + req.value.len(); - if new_size > AppData::user_limit(&owner, &app) { - return Json(Error::AppHitStorageLimit.into()); - } - - // ... - if let Err(e) = data.add_app_data_used(app.id, req.value.len() as i32).await { - return Json(e.into()); - } - - match data - .create_app_data(AppData::new(app.id, req.key, req.value)) - .await - { - Ok(s) => Json(ApiReturn { - ok: true, - message: "Data inserted".to_string(), - payload: s.id.to_string(), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_key_request( - headers: HeaderMap, - Extension(data): Extension, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - let app_data = match data.get_app_data_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if app_data.app != app.id { - return Json(Error::NotAllowed.into()); - } - - match data.update_app_data_key(id, &req.key).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Data updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_value_request( - headers: HeaderMap, - Extension(data): Extension, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - let owner = match data.get_user_by_id(app.owner).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - let app_data = match data.get_app_data_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if app_data.app != app.id { - return Json(Error::NotAllowed.into()); - } - - // check size - 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) { - 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 - { - return Json(e.into()); - } - - match data.update_app_data_value(id, &req.value).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Data updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn delete_request( - headers: HeaderMap, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - let app_data = match data.get_app_data_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if app_data.app != app.id { - return Json(Error::NotAllowed.into()); - } - - // ... - if let Err(e) = data - .add_app_data_used(app.id, -(app_data.value.len() as i32)) - .await - { - return Json(e.into()); - } - - match data.delete_app_data(id).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Data deleted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn delete_query_request( - headers: HeaderMap, - Extension(data): Extension, - Json(req): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let app = match get_app_from_key!(data, headers) { - Some(x) => x, - None => return Json(Error::NotAllowed.into()), - }; - - // ... - let rows = match data - .query_app_data(AppDataQuery { - app: app.id, - query: req.query.clone(), - mode: req.mode.clone(), - }) - .await - { - Ok(x) => match x { - AppDataQueryResult::One(x) => vec![x], - AppDataQueryResult::Many(x) => x, - }, - Err(e) => return Json(e.into()), - }; - - let mut subtract_amount: usize = 0; - for row in &rows { - subtract_amount += row.value.len(); - } - drop(rows); - - if let Err(e) = data - .add_app_data_used(app.id, -(subtract_amount as i32)) - .await - { - return Json(e.into()); - } - - match data - .query_delete_app_data(AppDataQuery { - app: app.id, - query: req.query, - mode: req.mode, - }) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Data deleted".to_string(), - payload: (), - }), - Err(e) => 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..c4e2809 100644 --- a/crates/app/src/routes/api/v1/apps.rs +++ b/crates/app/src/routes/api/v1/apps.rs @@ -7,7 +7,7 @@ use crate::{ State, }; use axum::{Extension, Json, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ apps::{AppQuota, ThirdPartyApp}, oauth::{AuthGrant, PkceChallengeMethod}, @@ -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, @@ -268,34 +239,3 @@ pub async fn grant_request( Err(e) => Json(e.into()), } } - -pub async fn roll_api_key_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> 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()), - }; - - let app = match data.get_app_by_id(id).await { - Ok(x) => x, - Err(e) => return Json(e.into()), - }; - - if user.id != app.owner { - return Json(Error::NotAllowed.into()); - } - - let new_key = tetratto_shared::hash::random_id_salted_len(32); - match data.update_app_api_key(id, &new_key).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "App updated".to_string(), - payload: Some(new_key), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/auth/connections/last_fm.rs b/crates/app/src/routes/api/v1/auth/connections/last_fm.rs index be4176c..9740b5a 100644 --- a/crates/app/src/routes/api/v1/auth/connections/last_fm.rs +++ b/crates/app/src/routes/api/v1/auth/connections/last_fm.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use axum::{extract::Path, response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::{ database::connections::last_fm::LastFmConnection, model::{ diff --git a/crates/app/src/routes/api/v1/auth/connections/mod.rs b/crates/app/src/routes/api/v1/auth/connections/mod.rs index 8a98355..5cd9813 100644 --- a/crates/app/src/routes/api/v1/auth/connections/mod.rs +++ b/crates/app/src/routes/api/v1/auth/connections/mod.rs @@ -5,7 +5,7 @@ pub mod stripe; use std::collections::HashMap; use axum::{extract::Path, response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tetratto_core::model::{ auth::{ConnectionService, ExternalConnectionData}, diff --git a/crates/app/src/routes/api/v1/auth/connections/spotify.rs b/crates/app/src/routes/api/v1/auth/connections/spotify.rs index d83057e..8d0db30 100644 --- a/crates/app/src/routes/api/v1/auth/connections/spotify.rs +++ b/crates/app/src/routes/api/v1/auth/connections/spotify.rs @@ -1,5 +1,5 @@ use axum::{response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::{ database::connections::spotify::SpotifyConnection, model::{ diff --git a/crates/app/src/routes/api/v1/auth/connections/stripe.rs b/crates/app/src/routes/api/v1/auth/connections/stripe.rs index 2110924..e62a0e8 100644 --- a/crates/app/src/routes/api/v1/auth/connections/stripe.rs +++ b/crates/app/src/routes/api/v1/auth/connections/stripe.rs @@ -1,15 +1,14 @@ -use std::{str::FromStr, time::Duration}; +use std::time::Duration; use axum::{http::HeaderMap, response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; use tetratto_core::model::{ - auth::{Notification, User}, + auth::{User, Notification}, moderation::AuditLogEntry, - permissions::{FinePermission, SecondaryPermission}, + permissions::FinePermission, ApiReturn, Error, }; use stripe::{EventObject, EventType}; -use crate::{get_user_from_token, State}; +use crate::State; pub async fn stripe_webhook( Extension(data): Extension, @@ -18,10 +17,9 @@ pub async fn stripe_webhook( ) -> impl IntoResponse { let data = &(data.read().await).0; - let stripe_cnf = match data.0.0.stripe { - Some(ref c) => c, - None => return Json(Error::MiscError("Disabled".to_string()).into()), - }; + if data.0.0.stripe.is_none() { + return Json(Error::MiscError("Disabled".to_string()).into()); + } let sig = match headers.get("Stripe-Signature") { Some(s) => s, @@ -58,7 +56,7 @@ pub async fn stripe_webhook( Err(e) => return Json(e.into()), }; - tracing::info!("payment {} (stripe: {})", user.id, customer_id); + tracing::info!("subscribe {} (stripe: {})", user.id, customer_id); if let Err(e) = data .update_user_stripe_id(user.id, customer_id.as_str()) .await @@ -76,48 +74,6 @@ pub async fn stripe_webhook( }; let customer_id = invoice.customer.unwrap().id(); - let lines = invoice.lines.unwrap(); - - if lines.total_count.unwrap() > 1 { - if let Err(e) = data - .create_audit_log_entry(AuditLogEntry::new( - 0, - format!("too many invoice line items: stripe {customer_id}"), - )) - .await - { - return Json(e.into()); - } - - return Json(Error::MiscError("Too many line items".to_string()).into()); - } - - let item = match lines.data.get(0) { - Some(i) => i, - None => { - if let Err(e) = data - .create_audit_log_entry(AuditLogEntry::new( - 0, - format!("too few invoice line items: stripe {customer_id}"), - )) - .await - { - return Json(e.into()); - } - - return Json(Error::MiscError("Too few line items".to_string()).into()); - } - }; - - let product_id = item - .price - .as_ref() - .unwrap() - .product - .as_ref() - .unwrap() - .id() - .to_string(); // pull user and update role let mut retries: usize = 0; @@ -162,91 +118,45 @@ pub async fn stripe_webhook( } let user = user.unwrap(); + tracing::info!("found subscription user in {retries} tries"); - if product_id == stripe_cnf.product_ids.supporter { - // supporter - tracing::info!("found subscription user in {retries} tries"); + if user.permissions.check(FinePermission::SUPPORTER) { + return Json(ApiReturn { + ok: true, + message: "Already applied".to_string(), + payload: (), + }); + } - if user.permissions.check(FinePermission::SUPPORTER) { - return Json(ApiReturn { - ok: true, - message: "Already applied".to_string(), - payload: (), - }); - } + tracing::info!("invoice {} (stripe: {})", user.id, customer_id); + let new_user_permissions = user.permissions | FinePermission::SUPPORTER; - tracing::info!("invoice {} (stripe: {})", user.id, customer_id); - let new_user_permissions = user.permissions | FinePermission::SUPPORTER; + if let Err(e) = data + .update_user_role(user.id, new_user_permissions, user.clone(), true) + .await + { + return Json(e.into()); + } + if data.0.0.security.enable_invite_codes && user.awaiting_purchase { if let Err(e) = data - .update_user_role(user.id, new_user_permissions, user.clone(), true) + .update_user_awaiting_purchased_status(user.id, false, user.clone(), false) .await { return Json(e.into()); } + } - if data.0.0.security.enable_invite_codes && user.awaiting_purchase { - if let Err(e) = data - .update_user_awaiting_purchased_status(user.id, false, user.clone(), false) - .await - { - return Json(e.into()); - } - } - - if let Err(e) = data - .create_notification(Notification::new( - "Welcome new supporter!".to_string(), - "Thank you for your support! Your account has been updated with your new role." - .to_string(), - user.id, - )) - .await - { - return Json(e.into()); - } - } else if product_id == stripe_cnf.product_ids.dev_pass { - // dev pass - tracing::info!("found subscription user in {retries} tries"); - - if user - .secondary_permissions - .check(SecondaryPermission::DEVELOPER_PASS) - { - return Json(ApiReturn { - ok: true, - message: "Already applied".to_string(), - payload: (), - }); - } - - tracing::info!("invoice {} (stripe: {})", user.id, customer_id); - let new_user_permissions = - user.secondary_permissions | SecondaryPermission::DEVELOPER_PASS; - - if let Err(e) = data - .update_user_secondary_role(user.id, new_user_permissions, user.clone(), true) - .await - { - return Json(e.into()); - } - - if let Err(e) = data - .create_notification(Notification::new( - "Welcome new developer!".to_string(), - "Thank you for your support! Your account has been updated with your new role." - .to_string(), - user.id, - )) - .await - { - return Json(e.into()); - } - } else { - tracing::error!( - "received an invalid stripe product id, please check config.stripe.product_ids" - ); - return Json(Error::MiscError("Unknown product ID".to_string()).into()); + if let Err(e) = data + .create_notification(Notification::new( + "Welcome new supporter!".to_string(), + "Thank you for your support! Your account has been updated with your new role." + .to_string(), + user.id, + )) + .await + { + return Json(e.into()); } } EventType::CustomerSubscriptionDeleted => { @@ -257,72 +167,34 @@ pub async fn stripe_webhook( }; let customer_id = subscription.customer.id(); - let product_id = subscription - .items - .data - .get(0) - .as_ref() - .expect("cancelled nothing?") - .plan - .as_ref() - .expect("no subscription plan?") - .product - .as_ref() - .expect("plan with no product?") - .id() - .to_string(); let user = match data.get_user_by_stripe_id(customer_id.as_str()).await { Ok(ua) => ua, Err(e) => return Json(e.into()), }; - // handle each subscription item - if product_id == stripe_cnf.product_ids.supporter { - // supporter - tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id); - let new_user_permissions = user.permissions - FinePermission::SUPPORTER; + tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id); + let new_user_permissions = user.permissions - FinePermission::SUPPORTER; - if let Err(e) = data - .update_user_role(user.id, new_user_permissions, user.clone(), true) - .await - { - return Json(e.into()); - } - - if data.0.0.security.enable_invite_codes - && user.was_purchased - && user.invite_code == 0 - { - // user doesn't come from an invite code, and is a purchased account - // this means their account must be locked if they stop paying - if let Err(e) = data - .update_user_awaiting_purchased_status(user.id, true, user.clone(), false) - .await - { - return Json(e.into()); - } - } - } else if product_id == stripe_cnf.product_ids.dev_pass { - // dev pass - tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id); - let new_user_permissions = - user.secondary_permissions - SecondaryPermission::DEVELOPER_PASS; - - if let Err(e) = data - .update_user_secondary_role(user.id, new_user_permissions, user.clone(), true) - .await - { - return Json(e.into()); - } - } else { - tracing::error!( - "received an invalid stripe product id, please check config.stripe.product_ids" - ); - return Json(Error::MiscError("Unknown product ID".to_string()).into()); + if let Err(e) = data + .update_user_role(user.id, new_user_permissions, user.clone(), true) + .await + { + return Json(e.into()); + } + + if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0 + { + // user doesn't come from an invite code, and is a purchased account + // this means their account must be locked if they stop paying + if let Err(e) = data + .update_user_awaiting_purchased_status(user.id, true, user.clone(), false) + .await + { + return Json(e.into()); + } } - // send notification if let Err(e) = data .create_notification(Notification::new( "Sorry to see you go... :(".to_string(), @@ -337,119 +209,44 @@ pub async fn stripe_webhook( } EventType::InvoicePaymentFailed => { // payment failed - let invoice = match req.data.object { - EventObject::Invoice(i) => i, + let subscription = match req.data.object { + EventObject::Subscription(c) => c, _ => unreachable!("cannot be this"), }; - let customer_id = invoice.customer.expect("TETRATTO_STRIPE_NO_CUSTOMER").id(); - - let item = match invoice.lines.as_ref().expect("no line items?").data.get(0) { - Some(i) => i, - None => { - if let Err(e) = data - .create_audit_log_entry(AuditLogEntry::new( - 0, - format!("too few invoice line items: stripe {customer_id}"), - )) - .await - { - return Json(e.into()); - } - - return Json(Error::MiscError("Too few line items".to_string()).into()); - } - }; - - let product_id = item - .price - .as_ref() - .unwrap() - .product - .as_ref() - .unwrap() - .id() - .to_string(); + let customer_id = subscription.customer.id(); let user = match data.get_user_by_stripe_id(customer_id.as_str()).await { Ok(ua) => ua, Err(e) => return Json(e.into()), }; - // handle each subscription item - if product_id == stripe_cnf.product_ids.supporter { - // supporter - if !user.permissions.check(FinePermission::SUPPORTER) { - // the user isn't currently a supporter, there's no reason to send this notification - return Json(ApiReturn { - ok: true, - message: "Acceptable".to_string(), - payload: (), - }); - } + tracing::info!( + "unsubscribe (pay fail) {} (stripe: {})", + user.id, + customer_id + ); + let new_user_permissions = user.permissions - FinePermission::SUPPORTER; - tracing::info!( - "unsubscribe (pay fail) {} (stripe: {})", - user.id, - customer_id - ); - let new_user_permissions = user.permissions - FinePermission::SUPPORTER; - - if let Err(e) = data - .update_user_role(user.id, new_user_permissions, user.clone(), true) - .await - { - return Json(e.into()); - } - - if data.0.0.security.enable_invite_codes - && user.was_purchased - && user.invite_code == 0 - { - // user doesn't come from an invite code, and is a purchased account - // this means their account must be locked if they stop paying - if let Err(e) = data - .update_user_awaiting_purchased_status(user.id, true, user.clone(), false) - .await - { - return Json(e.into()); - } - } - } else if product_id == stripe_cnf.product_ids.dev_pass { - // dev pass - if !user - .secondary_permissions - .check(SecondaryPermission::DEVELOPER_PASS) - { - return Json(ApiReturn { - ok: true, - message: "Acceptable".to_string(), - payload: (), - }); - } - - tracing::info!( - "unsubscribe (pay fail) {} (stripe: {})", - user.id, - customer_id - ); - let new_user_permissions = - user.secondary_permissions - SecondaryPermission::DEVELOPER_PASS; - - if let Err(e) = data - .update_user_secondary_role(user.id, new_user_permissions, user.clone(), true) - .await - { - return Json(e.into()); - } - } else { - tracing::error!( - "received an invalid stripe product id, please check config.stripe.product_ids" - ); - return Json(Error::MiscError("Unknown product ID".to_string()).into()); + if let Err(e) = data + .update_user_role(user.id, new_user_permissions, user.clone(), true) + .await + { + return Json(e.into()); + } + + if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0 + { + // user doesn't come from an invite code, and is a purchased account + // this means their account must be locked if they stop paying + if let Err(e) = data + .update_user_awaiting_purchased_status(user.id, true, user.clone(), false) + .await + { + return Json(e.into()); + } } - // send notification if let Err(e) = data .create_notification(Notification::new( "It seems your recent payment has failed :(".to_string(), @@ -471,145 +268,3 @@ pub async fn stripe_webhook( payload: (), }) } - -pub async fn onboarding_account_link_request( - jar: CookieJar, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await); - let user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.seller_data.account_id.is_some() { - return Json(Error::NotAllowed.into()); - } - - let client = match data.3 { - Some(ref c) => c, - None => return Json(Error::Unknown.into()), - }; - - match stripe::AccountLink::create( - &client, - stripe::CreateAccountLink { - account: match user.seller_data.account_id { - Some(id) => stripe::AccountId::from_str(&id).unwrap(), - None => return Json(Error::NotAllowed.into()), - }, - type_: stripe::AccountLinkType::AccountOnboarding, - collect: None, - expand: &[], - refresh_url: Some(&format!( - "{}/auth/connections_link/seller/refresh", - data.0.0.0.host - )), - return_url: Some(&format!( - "{}/auth/connections_link/seller/return", - data.0.0.0.host - )), - collection_options: None, - }, - ) - .await - { - Ok(x) => Json(ApiReturn { - ok: true, - message: "Acceptable".to_string(), - payload: Some(x.url), - }), - Err(e) => Json(Error::MiscError(e.to_string()).into()), - } -} - -pub async fn create_seller_account_request( - jar: CookieJar, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await); - let mut user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.seller_data.account_id.is_some() { - return Json(Error::NotAllowed.into()); - } - - let client = match data.3 { - Some(ref c) => c, - None => return Json(Error::Unknown.into()), - }; - - let account = match stripe::Account::create( - &client, - stripe::CreateAccount { - type_: Some(stripe::AccountType::Express), - capabilities: Some(stripe::CreateAccountCapabilities { - card_payments: Some(stripe::CreateAccountCapabilitiesCardPayments { - requested: Some(true), - }), - transfers: Some(stripe::CreateAccountCapabilitiesTransfers { - requested: Some(true), - }), - ..Default::default() - }), - ..Default::default() - }, - ) - .await - { - Ok(a) => a, - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - }; - - user.seller_data.account_id = Some(account.id.to_string()); - match data - .0 - .update_user_seller_data(user.id, user.seller_data) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => return Json(e.into()), - } -} - -pub async fn login_link_request( - jar: CookieJar, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await); - let user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.seller_data.account_id.is_none() | !user.seller_data.completed_onboarding { - return Json(Error::NotAllowed.into()); - } - - let client = match data.3 { - Some(ref c) => c, - None => return Json(Error::Unknown.into()), - }; - - match stripe::LoginLink::create( - &client, - &stripe::AccountId::from_str(&user.seller_data.account_id.unwrap()).unwrap(), - &data.0.0.0.host, - ) - .await - { - Ok(x) => Json(ApiReturn { - ok: true, - message: "Acceptable".to_string(), - payload: Some(x.url), - }), - Err(e) => Json(Error::MiscError(e.to_string()).into()), - } -} diff --git a/crates/app/src/routes/api/v1/auth/images.rs b/crates/app/src/routes/api/v1/auth/images.rs index cbaf344..4619a80 100644 --- a/crates/app/src/routes/api/v1/auth/images.rs +++ b/crates/app/src/routes/api/v1/auth/images.rs @@ -4,7 +4,7 @@ use axum::{ extract::{Path, Query}, response::IntoResponse, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use pathbufd::{PathBufD, pathd}; use serde::Deserialize; use std::{ diff --git a/crates/app/src/routes/api/v1/auth/ipbans.rs b/crates/app/src/routes/api/v1/auth/ipbans.rs index a8eb856..8a71d25 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 crate::cookie::CookieJar; +use axum::{Extension, Json, extract::Path, response::IntoResponse}; +use axum_extra::extract::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/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs index dff259e..085844a 100644 --- a/crates/app/src/routes/api/v1/auth/mod.rs +++ b/crates/app/src/routes/api/v1/auth/mod.rs @@ -16,7 +16,7 @@ use axum::{ response::{IntoResponse, Redirect}, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tetratto_core::model::addr::RemoteAddr; use tetratto_shared::hash::hash; diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index aec31ef..8104c71 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -1,12 +1,11 @@ -use std::{str::FromStr, time::Duration}; +use std::time::Duration; use crate::{ get_user_from_token, model::{ApiReturn, Error}, routes::api::v1::{ AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken, - UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanReason, - UpdateUserInviteCode, UpdateUserIsDeactivated, UpdateUserIsVerified, UpdateUserPassword, - UpdateUserRole, UpdateUserUsername, + UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserInviteCode, + UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername, }, State, }; @@ -18,7 +17,7 @@ use axum::{ response::{IntoResponse, Redirect}, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use futures_util::{sink::SinkExt, stream::StreamExt}; use tetratto_core::{ cache::Cache, @@ -372,34 +371,6 @@ pub async fn update_user_awaiting_purchase_request( } } -/// Update the deactivated status of the given user. -/// -/// Does not support third-party grants. -pub async fn update_user_is_deactivated_request( - jar: CookieJar, - Path(id): Path, - Extension(data): Extension, - 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()), - }; - - match data - .update_user_is_deactivated(id, req.is_deactivated, user) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Deactivated status updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - /// Update the role of the given user. /// /// Does not support third-party grants. @@ -453,35 +424,6 @@ pub async fn update_user_secondary_role_request( } } -/// Update the ban reason of the given user. -/// -/// Does not support third-party grants. -pub async fn update_user_ban_reason_request( - jar: CookieJar, - Path(id): Path, - Extension(data): Extension, - 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_USERS) { - return Json(Error::NotAllowed.into()); - } - - match data.update_user_ban_reason(id, &req.reason).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "User updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - /// Update the current user's last seen value. pub async fn seen_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = &(data.read().await).0; @@ -509,8 +451,8 @@ pub async fn delete_user_request( Extension(data): Extension, Json(req): Json, ) -> impl IntoResponse { - let data = &(data.read().await); - let user = match get_user_from_token!(jar, data.0) { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -519,7 +461,6 @@ pub async fn delete_user_request( return Json(Error::NotAllowed.into()); } else if user.permissions.check(FinePermission::MANAGE_USERS) { if let Err(e) = data - .0 .create_audit_log_entry(AuditLogEntry::new( user.id, format!("invoked `delete_user` with x value `{id}`"), @@ -531,32 +472,14 @@ pub async fn delete_user_request( } match data - .0 .delete_user(id, &req.password, user.permissions.check_manager()) .await { - Ok(ua) => { - // delete stripe user - if let Some(stripe_id) = ua.seller_data.account_id - && let Some(ref client) = data.3 - { - if let Err(e) = stripe::Account::delete( - &client, - &stripe::AccountId::from_str(&stripe_id).unwrap(), - ) - .await - { - return Json(Error::MiscError(e.to_string()).into()); - } - } - - // ... - Json(ApiReturn { - ok: true, - message: "User deleted".to_string(), - payload: (), - }) - } + Ok(_) => Json(ApiReturn { + ok: true, + message: "User deleted".to_string(), + payload: (), + }), Err(e) => Json(e.into()), } } diff --git a/crates/app/src/routes/api/v1/auth/social.rs b/crates/app/src/routes/api/v1/auth/social.rs index 84e20c8..b80bd14 100644 --- a/crates/app/src/routes/api/v1/auth/social.rs +++ b/crates/app/src/routes/api/v1/auth/social.rs @@ -9,7 +9,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ addr::RemoteAddr, auth::{AchievementName, FollowResult, IpBlock, Notification, UserBlock, UserFollow}, @@ -17,7 +17,7 @@ use tetratto_core::model::{ }; /// Toggle following on the given user. -pub async fn toggle_follow_request( +pub async fn follow_request( jar: CookieJar, Path(id): Path, Extension(data): Extension, @@ -154,96 +154,6 @@ pub async fn accept_follow_request( } } -pub async fn follow_request( - jar: CookieJar, - Path(id): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if data - .get_userfollow_by_initiator_receiver(user.id, id) - .await - .is_ok() - { - return Json(Error::MiscError("Already following user".to_string()).into()); - } else { - match data - .create_userfollow(UserFollow::new(user.id, id), &user, false) - .await - { - Ok(r) => { - if r == FollowResult::Followed { - if let Err(e) = data - .create_notification(Notification::new( - "Somebody has followed you!".to_string(), - format!( - "You have been followed by [@{}](/api/v1/auth/user/find/{}).", - user.username, user.id - ), - id, - )) - .await - { - return Json(e.into()); - }; - - // award achievement - if let Err(e) = data - .add_achievement(&mut user, AchievementName::FollowUser.into(), true) - .await - { - return Json(e.into()); - } - - // ... - Json(ApiReturn { - ok: true, - message: "User followed".to_string(), - payload: (), - }) - } else { - Json(ApiReturn { - ok: true, - message: "Asked to follow user".to_string(), - payload: (), - }) - } - } - Err(e) => Json(e.into()), - } - } -} - -pub async fn force_unfollow_me_request( - jar: CookieJar, - Path(id): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if let Ok(userfollow) = data.get_userfollow_by_receiver_initiator(user.id, id).await { - match data.delete_userfollow(userfollow.id, &user, false).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "User is no longer following you".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } - } else { - return Json(Error::GeneralNotFound("user follow".to_string()).into()); - } -} - /// Toggle blocking on the given user. pub async fn block_request( jar: CookieJar, @@ -368,10 +278,7 @@ pub async fn followers_request( Ok(f) => Json(ApiReturn { ok: true, message: "Success".to_string(), - payload: match data - .fill_userfollows_with_initiator(f, &Some(user.clone()), id == user.id) - .await - { + payload: match data.fill_userfollows_with_initiator(f).await { Ok(f) => Some(data.userfollows_user_filter(&f)), Err(e) => return Json(e.into()), }, @@ -403,10 +310,7 @@ pub async fn following_request( Ok(f) => Json(ApiReturn { ok: true, message: "Success".to_string(), - payload: match data - .fill_userfollows_with_receiver(f, &Some(user.clone()), id == user.id) - .await - { + payload: match data.fill_userfollows_with_receiver(f).await { Ok(f) => Some(data.userfollows_user_filter(&f)), Err(e) => return Json(e.into()), }, diff --git a/crates/app/src/routes/api/v1/auth/user_warnings.rs b/crates/app/src/routes/api/v1/auth/user_warnings.rs index 3020ec6..321ab78 100644 --- a/crates/app/src/routes/api/v1/auth/user_warnings.rs +++ b/crates/app/src/routes/api/v1/auth/user_warnings.rs @@ -9,7 +9,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{auth::UserWarning, oauth, permissions::FinePermission}; /// Create a new user warning. diff --git a/crates/app/src/routes/api/v1/channels/channels.rs b/crates/app/src/routes/api/v1/channels/channels.rs index 2059a0f..e3ead5a 100644 --- a/crates/app/src/routes/api/v1/channels/channels.rs +++ b/crates/app/src/routes/api/v1/channels/channels.rs @@ -1,5 +1,5 @@ use axum::{Extension, Json, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{oauth, channels::Channel, ApiReturn, Error}; use crate::{ get_user_from_token, @@ -293,62 +293,3 @@ pub async fn get_request( Err(e) => Json(e.into()), } } - -pub async fn mute_channel_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageChannelMutes) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if user.channel_mutes.contains(&id) { - return Json(Error::MiscError("Channel already muted".to_string()).into()); - } - - user.channel_mutes.push(id); - match data - .update_user_channel_mutes(user.id, user.channel_mutes) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel muted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn unmute_channel_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageChannelMutes) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - let pos = match user.channel_mutes.iter().position(|x| *x == id) { - Some(x) => x, - None => return Json(Error::MiscError("Channel not muted".to_string()).into()), - }; - - user.channel_mutes.remove(pos); - match data - .update_user_channel_mutes(user.id, user.channel_mutes) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Channel muted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/channels/message_reactions.rs b/crates/app/src/routes/api/v1/channels/message_reactions.rs index 5f5c79c..b9ccb53 100644 --- a/crates/app/src/routes/api/v1/channels/message_reactions.rs +++ b/crates/app/src/routes/api/v1/channels/message_reactions.rs @@ -1,6 +1,6 @@ use crate::{get_user_from_token, routes::api::v1::CreateMessageReaction, State}; use axum::{Extension, Json, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{channels::MessageReaction, oauth, ApiReturn, Error}; pub async fn get_request( diff --git a/crates/app/src/routes/api/v1/channels/messages.rs b/crates/app/src/routes/api/v1/channels/messages.rs index 92a5c48..e88138e 100644 --- a/crates/app/src/routes/api/v1/channels/messages.rs +++ b/crates/app/src/routes/api/v1/channels/messages.rs @@ -7,7 +7,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::{ cache::{Cache, redis::Commands}, model::{ diff --git a/crates/app/src/routes/api/v1/communities/communities.rs b/crates/app/src/routes/api/v1/communities/communities.rs index a0793b1..539cc08 100644 --- a/crates/app/src/routes/api/v1/communities/communities.rs +++ b/crates/app/src/routes/api/v1/communities/communities.rs @@ -3,7 +3,7 @@ use axum::{ extract::Path, response::{IntoResponse, Redirect}, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ auth::Notification, communities::{Community, CommunityMembership}, diff --git a/crates/app/src/routes/api/v1/communities/drafts.rs b/crates/app/src/routes/api/v1/communities/drafts.rs index 559e4b3..75f0948 100644 --- a/crates/app/src/routes/api/v1/communities/drafts.rs +++ b/crates/app/src/routes/api/v1/communities/drafts.rs @@ -3,7 +3,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{auth::AchievementName, communities::PostDraft, oauth, ApiReturn, Error}; use crate::{ get_user_from_token, diff --git a/crates/app/src/routes/api/v1/communities/emojis.rs b/crates/app/src/routes/api/v1/communities/emojis.rs index 84fadc0..1db4c0c 100644 --- a/crates/app/src/routes/api/v1/communities/emojis.rs +++ b/crates/app/src/routes/api/v1/communities/emojis.rs @@ -7,7 +7,7 @@ use crate::{ State, }; use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ oauth, uploads::{CustomEmoji, MediaType, MediaUpload}, @@ -17,8 +17,6 @@ use tetratto_core::model::{ /// Expand a unicode emoji into its Gemoji shortcode. pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse { match emoji.as_str() { - // matches `CustomEmoji::replace` - "💯" => "100".to_string(), "👍" => "thumbs_up".to_string(), "👎" => "thumbs_down".to_string(), _ => match emojis::get(&emoji) { diff --git a/crates/app/src/routes/api/v1/communities/images.rs b/crates/app/src/routes/api/v1/communities/images.rs index 9f32ef3..3ddee00 100644 --- a/crates/app/src/routes/api/v1/communities/images.rs +++ b/crates/app/src/routes/api/v1/communities/images.rs @@ -1,5 +1,5 @@ use axum::{Extension, Json, body::Body, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use pathbufd::{PathBufD, pathd}; use std::fs::exists; use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission, oauth}; diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index 13729b3..d6554ff 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -4,7 +4,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ addr::RemoteAddr, auth::AchievementName, @@ -152,11 +152,10 @@ pub async fn create_request( } // ... - let uploads = props.uploads.clone(); - match data.create_post(props).await { + match data.create_post(props.clone()).await { Ok(id) => { // write to uploads - for (i, upload_id) in uploads.iter().enumerate() { + for (i, upload_id) in props.uploads.iter().enumerate() { let image = match images.get(i) { Some(img) => img, None => { @@ -724,7 +723,7 @@ pub async fn from_communities_request( }; match data - .get_posts_from_user_communities(user.id, 12, props.page, &user) + .get_posts_from_user_communities(user.id, 12, props.page) .await { Ok(posts) => { diff --git a/crates/app/src/routes/api/v1/communities/questions.rs b/crates/app/src/routes/api/v1/communities/questions.rs index de6cbb2..1d1a7ba 100644 --- a/crates/app/src/routes/api/v1/communities/questions.rs +++ b/crates/app/src/routes/api/v1/communities/questions.rs @@ -4,7 +4,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ addr::RemoteAddr, auth::{AchievementName, IpBlock}, @@ -96,13 +96,6 @@ pub async fn create_request( props.context.mask_owner = true; } - if !req.asking_about.is_empty() && !req.is_global { - props.context.asking_about = match req.asking_about.parse::() { - Ok(x) => Some(x), - Err(e) => return Json(Error::MiscError(e.to_string()).into()), - } - } - match data .create_question(props, drawings.iter().map(|x| x.to_vec()).collect()) .await diff --git a/crates/app/src/routes/api/v1/domains.rs b/crates/app/src/routes/api/v1/domains.rs index 1e57049..8cfd9dc 100644 --- a/crates/app/src/routes/api/v1/domains.rs +++ b/crates/app/src/routes/api/v1/domains.rs @@ -3,21 +3,12 @@ use crate::{ routes::api::v1::{CreateDomain, UpdateDomainData}, State, }; -use axum::{ - extract::{Path, Query}, - http::StatusCode, - response::IntoResponse, - Extension, Json, -}; -use crate::cookie::CookieJar; +use axum::{extract::Path, response::IntoResponse, http::StatusCode, Extension, Json}; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ - auth::AchievementName, littleweb::{Domain, ServiceFsMime}, - oauth, - permissions::FinePermission, - ApiReturn, Error, + oauth, ApiReturn, Error, }; -use serde::Deserialize; pub async fn get_request( Path(id): Path, @@ -57,20 +48,11 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateDomains) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateDomains) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; - // award achievement - if let Err(e) = data - .add_achievement(&mut user, AchievementName::CreateDomain.into(), true) - .await - { - return Json(e.into()); - } - - // ... match data .create_domain(Domain::new(req.name, req.tld, user.id)) .await @@ -127,38 +109,13 @@ pub async fn delete_request( } } -#[derive(Deserialize)] -pub struct GetFileQuery { - #[serde(default, alias = "s")] - pub session: String, -} - pub async fn get_file_request( - Path(mut addr): Path, + Path(addr): Path, Extension(data): Extension, - Query(props): Query, ) -> impl IntoResponse { - if !addr.starts_with("atto://") { - addr = format!("atto://{addr}"); - } - - // ... let data = &(data.read().await).0; - let user = get_user_from_token!(--browser_session = props.session, data); let (subdomain, domain, tld, path) = Domain::from_str(&addr); - if path.starts_with("$") && user.is_none() { - return Err((StatusCode::BAD_REQUEST, Error::NotAllowed.to_string())); - } else if let Some(ref ua) = user - && path.starts_with("$paid") - && !ua.permissions.check(FinePermission::SUPPORTER) - { - return Err(( - StatusCode::BAD_REQUEST, - Error::RequiresSupporter.to_string(), - )); - } - // resolve domain let domain = match data.get_domain_by_name_tld(&domain, &tld).await { Ok(x) => x, @@ -188,28 +145,16 @@ pub async fn get_file_request( Some((f, _)) => Ok(( [("Content-Type".to_string(), f.mime.to_string())], if f.mime == ServiceFsMime::Html { - f.content - .replace( - "", - &format!( - "", - data.0.0.host, props.session - ), - ) - .replace( - ".js\"", - &format!(".js?r={}&s={}\"", service.revision, props.session), - ) - .replace( - ".css\"", - &format!(".css?r={}&s={}\"", service.revision, props.session), - ) + f.content.replace( + "", + &format!( + "", + data.0.0.host + ), + ) } else { f.content - } - .replace("atto://", "/api/v1/net/"), + }, )), None => { return Err(( diff --git a/crates/app/src/routes/api/v1/journals.rs b/crates/app/src/routes/api/v1/journals.rs index d018903..0b1b394 100644 --- a/crates/app/src/routes/api/v1/journals.rs +++ b/crates/app/src/routes/api/v1/journals.rs @@ -3,7 +3,7 @@ use axum::{ extract::{Json, Path}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_shared::snow::Snowflake; use crate::{ get_user_from_token, diff --git a/crates/app/src/routes/api/v1/layouts.rs b/crates/app/src/routes/api/v1/layouts.rs new file mode 100644 index 0000000..b86bfd2 --- /dev/null +++ b/crates/app/src/routes/api/v1/layouts.rs @@ -0,0 +1,175 @@ +use crate::{ + get_user_from_token, + routes::{ + api::v1::{CreateLayout, UpdateLayoutName, UpdateLayoutPages, UpdateLayoutPrivacy}, + }, + State, +}; +use axum::{extract::Path, response::IntoResponse, Extension, Json}; +use axum_extra::extract::CookieJar; +use tetratto_core::{ + model::{ + layouts::{Layout, LayoutPrivacy}, + oauth, + permissions::FinePermission, + ApiReturn, Error, + }, +}; + +pub async fn get_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadStacks) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let layout = match data.get_layout_by_id(id).await { + Ok(x) => x, + Err(e) => return Json(e.into()), + }; + + if layout.privacy == LayoutPrivacy::Public + && user.id != layout.owner + && !user.permissions.check(FinePermission::MANAGE_USERS) + { + return Json(Error::NotAllowed.into()); + } + + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(layout), + }) +} + +pub async fn list_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadLayouts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_layouts_by_user(user.id).await { + Ok(x) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(x), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn create_request( + jar: CookieJar, + Extension(data): Extension, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateLayouts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data + .create_layout(Layout::new(req.name, user.id, req.replaces)) + .await + { + Ok(s) => Json(ApiReturn { + ok: true, + message: "Layout created".to_string(), + payload: s.id.to_string(), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn update_name_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, oauth::AppScope::UserManageLayouts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.update_layout_title(id, &user, &req.name).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Layout updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn update_privacy_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, oauth::AppScope::UserManageLayouts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.update_layout_privacy(id, &user, req.privacy).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Layout updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn update_pages_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, oauth::AppScope::UserManageLayouts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.update_layout_pages(id, &user, req.pages).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Layout updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + +pub async fn delete_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageLayouts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.delete_layout(id, &user).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Layout deleted".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 8a5d95a..506e74f 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -1,13 +1,12 @@ -pub mod app_data; pub mod apps; pub mod auth; pub mod channels; pub mod communities; pub mod domains; pub mod journals; +pub mod layouts; pub mod notes; pub mod notifications; -pub mod products; pub mod reactions; pub mod reports; pub mod requests; @@ -22,7 +21,7 @@ use axum::{ }; use serde::Deserialize; use tetratto_core::model::{ - apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota, DeveloperPassStorageQuota}, + apps::AppQuota, auth::AchievementName, communities::{ CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, @@ -30,10 +29,10 @@ use tetratto_core::model::{ }, communities_permissions::CommunityPermission, journals::JournalPrivacyPermission, + layouts::{CustomizablePage, LayoutPage, LayoutPrivacy}, littleweb::{DomainData, DomainTld, ServiceFsEntry}, oauth::AppScope, permissions::{FinePermission, SecondaryPermission}, - products::{ProductPrice, ProductType}, reactions::AssetType, stacks::{StackMode, StackPrivacy, StackSort}, }; @@ -286,10 +285,6 @@ pub fn routes() -> Router { .route("/auth/user/{id}/avatar", get(auth::images::avatar_request)) .route("/auth/user/{id}/banner", get(auth::images::banner_request)) .route("/auth/user/{id}/follow", post(auth::social::follow_request)) - .route( - "/auth/user/{id}/follow/toggle", - post(auth::social::toggle_follow_request), - ) .route( "/auth/user/{id}/follow/cancel", post(auth::social::cancel_follow_request), @@ -298,10 +293,6 @@ pub fn routes() -> Router { "/auth/user/{id}/follow/accept", post(auth::social::accept_follow_request), ) - .route( - "/auth/user/{id}/force_unfollow_me", - post(auth::social::force_unfollow_me_request), - ) .route("/auth/user/{id}/block", post(auth::social::block_request)) .route( "/auth/user/{id}/block_ip", @@ -323,10 +314,6 @@ pub fn routes() -> Router { "/auth/user/{id}/role/2", post(auth::profile::update_user_secondary_role_request), ) - .route( - "/auth/user/{id}/ban_reason", - post(auth::profile::update_user_ban_reason_request), - ) .route( "/auth/user/{id}", delete(auth::profile::delete_user_request), @@ -351,10 +338,6 @@ pub fn routes() -> Router { "/auth/user/{id}/awaiting_purchase", post(auth::profile::update_user_awaiting_purchase_request), ) - .route( - "/auth/user/{id}/deactivate", - post(auth::profile::update_user_is_deactivated_request), - ) .route( "/auth/user/{id}/totp", post(auth::profile::enable_totp_request), @@ -424,7 +407,6 @@ pub fn routes() -> Router { ) // apps .route("/apps", post(apps::create_request)) - .route("/apps/{id}", delete(apps::delete_request)) .route("/apps/{id}/title", post(apps::update_title_request)) .route("/apps/{id}/homepage", post(apps::update_homepage_request)) .route("/apps/{id}/redirect", post(apps::update_redirect_request)) @@ -432,21 +414,9 @@ 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}", delete(apps::delete_request)) .route("/apps/{id}/grant", post(apps::grant_request)) - .route("/apps/{id}/roll", post(apps::roll_api_key_request)) - // app data - .route("/app_data", post(app_data::create_request)) - .route("/app_data/app", get(app_data::get_app_request)) - .route("/app_data/{id}", delete(app_data::delete_request)) - .route("/app_data/{id}/key", post(app_data::update_key_request)) - .route("/app_data/{id}/value", post(app_data::update_value_request)) - .route("/app_data/query", post(app_data::query_request)) - .route("/app_data/query", delete(app_data::delete_query_request)) // warnings .route("/warnings/{id}", get(auth::user_warnings::get_request)) .route("/warnings/{id}", post(auth::user_warnings::create_request)) @@ -495,7 +465,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 @@ -545,18 +514,6 @@ pub fn routes() -> Router { "/service_hooks/stripe", post(auth::connections::stripe::stripe_webhook), ) - .route( - "/service_hooks/stripe/seller/register", - post(auth::connections::stripe::create_seller_account_request), - ) - .route( - "/service_hooks/stripe/seller/onboarding", - post(auth::connections::stripe::onboarding_account_link_request), - ) - .route( - "/service_hooks/stripe/seller/login", - post(auth::connections::stripe::login_link_request), - ) // channels .route("/channels", post(channels::channels::create_request)) .route( @@ -580,14 +537,6 @@ pub fn routes() -> Router { "/channels/{id}/kick", post(channels::channels::kick_member_request), ) - .route( - "/channels/{id}/mute", - post(channels::channels::mute_channel_request), - ) - .route( - "/channels/{id}/mute", - delete(channels::channels::unmute_channel_request), - ) .route("/channels/{id}", get(channels::channels::get_request)) .route( "/channels/community/{id}", @@ -676,8 +625,17 @@ pub fn routes() -> Router { // uploads .route("/uploads/{id}", get(uploads::get_request)) .route("/uploads/{id}", delete(uploads::delete_request)) - .route("/uploads/{id}/data", get(uploads::get_json_request)) - .route("/uploads/{id}/alt", post(uploads::update_alt_request)) + // layouts + .route("/layouts", get(layouts::list_request)) + .route("/layouts", post(layouts::create_request)) + .route("/layouts/{id}", get(layouts::get_request)) + .route("/layouts/{id}", delete(layouts::delete_request)) + .route("/layouts/{id}/title", post(layouts::update_name_request)) + .route( + "/layouts/{id}/privacy", + post(layouts::update_privacy_request), + ) + .route("/layouts/{id}/pages", post(layouts::update_pages_request)) // services .route("/services", get(services::list_request)) .route("/services", post(services::create_request)) @@ -695,17 +653,6 @@ pub fn routes() -> Router { .route("/domains/{id}", get(domains::get_request)) .route("/domains/{id}", delete(domains::delete_request)) .route("/domains/{id}/data", post(domains::update_data_request)) - // products - .route("/products", get(products::list_request)) - .route("/products", post(products::create_request)) - .route("/products/{id}", get(products::get_request)) - .route("/products/{id}", delete(products::delete_request)) - .route("/products/{id}/name", post(products::update_name_request)) - .route( - "/products/{id}/description", - post(products::update_description_request), - ) - .route("/products/{id}/price", post(products::update_price_request)) } pub fn lw_routes() -> Router { @@ -846,11 +793,6 @@ pub struct UpdateUserAwaitingPurchase { pub awaiting_purchase: bool, } -#[derive(Deserialize)] -pub struct UpdateUserIsDeactivated { - pub is_deactivated: bool, -} - #[derive(Deserialize)] pub struct UpdateNotificationRead { pub read: bool, @@ -876,11 +818,6 @@ pub struct UpdateSecondaryUserRole { pub role: SecondaryPermission, } -#[derive(Deserialize)] -pub struct UpdateUserBanReason { - pub reason: String, -} - #[derive(Deserialize)] pub struct UpdateUserInviteCode { pub invite_code: String, @@ -916,8 +853,6 @@ pub struct CreateQuestion { pub community: String, #[serde(default)] pub mask_owner: bool, - #[serde(default)] - pub asking_about: String, } #[derive(Deserialize)] @@ -1012,7 +947,6 @@ pub struct UpdatePostIsOpen { pub struct CreateApp { pub title: String, pub homepage: String, - #[serde(default)] pub redirect: String, } @@ -1036,11 +970,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, @@ -1126,6 +1055,27 @@ pub struct AwardAchievement { pub name: AchievementName, } +#[derive(Deserialize)] +pub struct CreateLayout { + pub name: String, + pub replaces: CustomizablePage, +} + +#[derive(Deserialize)] +pub struct UpdateLayoutName { + pub name: String, +} + +#[derive(Deserialize)] +pub struct UpdateLayoutPrivacy { + pub privacy: LayoutPrivacy, +} + +#[derive(Deserialize)] +pub struct UpdateLayoutPages { + pub pages: Vec, +} + #[derive(Deserialize)] pub struct CreateService { pub name: String, @@ -1158,53 +1108,3 @@ pub struct CreateDomain { pub struct UpdateDomainData { pub data: Vec<(String, DomainData)>, } - -#[derive(Deserialize)] -pub struct CreateProduct { - pub name: String, - pub description: String, - pub product_type: ProductType, - pub price: ProductPrice, -} - -#[derive(Deserialize)] -pub struct UpdateProductName { - pub name: String, -} - -#[derive(Deserialize)] -pub struct UpdateProductDescription { - pub description: String, -} - -#[derive(Deserialize)] -pub struct UpdateProductPrice { - pub price: ProductPrice, -} - -#[derive(Deserialize)] -pub struct UpdateUploadAlt { - pub alt: String, -} - -#[derive(Deserialize)] -pub struct UpdateAppDataKey { - pub key: String, -} - -#[derive(Deserialize)] -pub struct UpdateAppDataValue { - pub value: String, -} - -#[derive(Deserialize)] -pub struct InsertAppData { - pub key: String, - pub value: String, -} - -#[derive(Deserialize)] -pub struct QueryAppData { - pub query: AppDataSelectQuery, - pub mode: AppDataSelectMode, -} diff --git a/crates/app/src/routes/api/v1/notes.rs b/crates/app/src/routes/api/v1/notes.rs index 979dbf7..6b274ff 100644 --- a/crates/app/src/routes/api/v1/notes.rs +++ b/crates/app/src/routes/api/v1/notes.rs @@ -3,7 +3,7 @@ use axum::{ extract::{Json, Path}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_shared::unix_epoch_timestamp; use crate::{ get_user_from_token, @@ -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/api/v1/notifications.rs b/crates/app/src/routes/api/v1/notifications.rs index de683ae..06b2397 100644 --- a/crates/app/src/routes/api/v1/notifications.rs +++ b/crates/app/src/routes/api/v1/notifications.rs @@ -5,7 +5,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{oauth, ApiReturn, Error}; pub async fn delete_request( diff --git a/crates/app/src/routes/api/v1/products.rs b/crates/app/src/routes/api/v1/products.rs deleted file mode 100644 index 4d53814..0000000 --- a/crates/app/src/routes/api/v1/products.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::{ - get_user_from_token, - image::{save_webp_buffer, JsonMultipart}, - routes::{ - api::v1::{ - communities::posts::MAXIMUM_FILE_SIZE, CreateProduct, UpdateProductDescription, - UpdateProductName, UpdateProductPrice, - }, - pages::PaginatedQuery, - }, - State, -}; -use axum::{ - extract::{Path, Query}, - response::IntoResponse, - Extension, Json, -}; -use crate::cookie::CookieJar; -use tetratto_core::model::{ - oauth, - products::Product, - uploads::{MediaType, MediaUpload}, - ApiReturn, Error, -}; - -pub async fn get_request( - Path(id): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - match data.get_product_by_id(id).await { - Ok(x) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(x), - }), - Err(e) => return Json(e.into()), - } -} - -pub async fn list_request( - jar: CookieJar, - Extension(data): Extension, - Query(props): Query, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.get_products_by_user(user.id, 12, props.page).await { - Ok(x) => Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(x), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn create_request( - jar: CookieJar, - Extension(data): Extension, - JsonMultipart(uploads, req): JsonMultipart, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - if uploads.len() > 4 { - return Json( - Error::MiscError("Too many uploads. Please use a maximum of 4".to_string()).into(), - ); - } - - let mut product = Product::new( - user.id, - req.name, - req.description, - req.price, - req.product_type, - ); - - // check sizes - for img in &uploads { - if img.len() > MAXIMUM_FILE_SIZE { - return Json(Error::FileTooLarge.into()); - } - } - - // create uploads - for _ in 0..uploads.len() { - product.uploads.push( - match data - .create_upload(MediaUpload::new(MediaType::Webp, product.owner)) - .await - { - Ok(u) => u.id, - Err(e) => return Json(e.into()), - }, - ); - } - - let product_uploads = product.uploads.clone(); - match data.create_product(product).await { - Ok(x) => { - // store uploads - for (i, upload_id) in product_uploads.iter().enumerate() { - let image = match uploads.get(i) { - Some(img) => img, - None => { - if let Err(e) = data.delete_upload(*upload_id).await { - return Json(e.into()); - } - - continue; - } - }; - - let upload = match data.get_upload_by_id(*upload_id).await { - Ok(u) => u, - Err(e) => return Json(e.into()), - }; - - if let Err(e) = - save_webp_buffer(&upload.path(&data.0.0).to_string(), image.to_vec(), None) - { - return Json(Error::MiscError(e.to_string()).into()); - } - } - - // ... - Json(ApiReturn { - ok: true, - message: "Product created".to_string(), - payload: x.id.to_string(), - }) - } - Err(e) => Json(e.into()), - } -} - -pub async fn update_name_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, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.update_product_name(id, &user, &req.name).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_description_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, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data - .update_product_description(id, &user, &req.description) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn update_price_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, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.update_product_price(id, &user, req.price).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} - -pub async fn delete_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageProducts) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.delete_product(id, &user).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Product deleted".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/reactions.rs b/crates/app/src/routes/api/v1/reactions.rs index b8589e4..261a48d 100644 --- a/crates/app/src/routes/api/v1/reactions.rs +++ b/crates/app/src/routes/api/v1/reactions.rs @@ -5,7 +5,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{addr::RemoteAddr, oauth, reactions::Reaction, ApiReturn, Error}; pub async fn get_request( diff --git a/crates/app/src/routes/api/v1/reports.rs b/crates/app/src/routes/api/v1/reports.rs index 8509896..459b8a9 100644 --- a/crates/app/src/routes/api/v1/reports.rs +++ b/crates/app/src/routes/api/v1/reports.rs @@ -1,7 +1,7 @@ use super::CreateReport; use crate::{State, get_user_from_token}; use axum::{Extension, Json, extract::Path, response::IntoResponse}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ApiReturn, Error, moderation::Report}; pub async fn create_request( diff --git a/crates/app/src/routes/api/v1/requests.rs b/crates/app/src/routes/api/v1/requests.rs index 90236cc..0169b72 100644 --- a/crates/app/src/routes/api/v1/requests.rs +++ b/crates/app/src/routes/api/v1/requests.rs @@ -4,7 +4,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{oauth, ApiReturn, Error}; pub async fn delete_request( diff --git a/crates/app/src/routes/api/v1/services.rs b/crates/app/src/routes/api/v1/services.rs index 556924a..252fe5a 100644 --- a/crates/app/src/routes/api/v1/services.rs +++ b/crates/app/src/routes/api/v1/services.rs @@ -6,9 +6,8 @@ use crate::{ State, }; use axum::{extract::Path, response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; -use tetratto_core::model::{auth::AchievementName, littleweb::Service, oauth, ApiReturn, Error}; -use tetratto_shared::unix_epoch_timestamp; +use axum_extra::extract::CookieJar; +use tetratto_core::model::{littleweb::Service, oauth, ApiReturn, Error}; pub async fn get_request( Path(id): Path, @@ -48,20 +47,11 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let mut user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateServices) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateServices) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; - // award achievement - if let Err(e) = data - .add_achievement(&mut user, AchievementName::CreateSite.into(), true) - .await - { - return Json(e.into()); - } - - // ... match data.create_service(Service::new(req.name, user.id)).await { Ok(x) => Json(ApiReturn { ok: true, @@ -157,17 +147,11 @@ pub async fn update_content_request( // ... match data.update_service_files(id, &user, service.files).await { - Ok(_) => match data - .update_service_revision(id, unix_epoch_timestamp() as i64) - .await - { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Service updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - }, + Ok(_) => Json(ApiReturn { + ok: true, + message: "Service updated".to_string(), + payload: (), + }), Err(e) => Json(e.into()), } } diff --git a/crates/app/src/routes/api/v1/stacks.rs b/crates/app/src/routes/api/v1/stacks.rs index e46cfdc..1fe5c87 100644 --- a/crates/app/src/routes/api/v1/stacks.rs +++ b/crates/app/src/routes/api/v1/stacks.rs @@ -4,7 +4,7 @@ use axum::{ response::IntoResponse, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::{ model::{ oauth, diff --git a/crates/app/src/routes/api/v1/uploads.rs b/crates/app/src/routes/api/v1/uploads.rs index a1d11f8..0e7d6ab 100644 --- a/crates/app/src/routes/api/v1/uploads.rs +++ b/crates/app/src/routes/api/v1/uploads.rs @@ -1,8 +1,8 @@ use std::fs::exists; use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json}; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use pathbufd::PathBufD; -use crate::{get_user_from_token, routes::api::v1::UpdateUploadAlt, State}; +use crate::{get_user_from_token, State}; use super::auth::images::read_image; use tetratto_core::model::{carp::CarpGraph, oauth, uploads::MediaType, ApiReturn, Error}; @@ -52,24 +52,6 @@ pub async fn get_request( Ok(([("Content-Type", upload.what.mime())], Body::from(bytes))) } -pub async fn get_json_request( - Path(id): Path, - Extension(data): Extension, -) -> impl IntoResponse { - let data = &(data.read().await).0; - - let upload = match data.get_upload_by_id(id).await { - Ok(u) => u, - Err(e) => return Json(e.into()), - }; - - Json(ApiReturn { - ok: true, - message: "Success".to_string(), - payload: Some(upload), - }) -} - pub async fn delete_request( jar: CookieJar, Extension(data): Extension, @@ -90,25 +72,3 @@ pub async fn delete_request( Err(e) => Json(e.into()), } } - -pub async fn update_alt_request( - jar: CookieJar, - Extension(data): Extension, - Path(id): Path, - Json(props): Json, -) -> impl IntoResponse { - let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageUploads) { - Some(ua) => ua, - None => return Json(Error::NotAllowed.into()), - }; - - match data.update_upload_alt(id, &user, &props.alt).await { - Ok(_) => Json(ApiReturn { - ok: true, - message: "Upload updated".to_string(), - payload: (), - }), - Err(e) => Json(e.into()), - } -} diff --git a/crates/app/src/routes/api/v1/util.rs b/crates/app/src/routes/api/v1/util.rs index 501f0d9..8714968 100644 --- a/crates/app/src/routes/api/v1/util.rs +++ b/crates/app/src/routes/api/v1/util.rs @@ -7,7 +7,7 @@ use axum::{ response::IntoResponse, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use pathbufd::PathBufD; use serde::Deserialize; use tetratto_core::model::permissions::FinePermission; diff --git a/crates/app/src/routes/assets.rs b/crates/app/src/routes/assets.rs index f18ede0..2aa1bc5 100644 --- a/crates/app/src/routes/assets.rs +++ b/crates/app/src/routes/assets.rs @@ -19,5 +19,5 @@ serve_asset!(atto_js_request: ATTO_JS("text/javascript")); serve_asset!(me_js_request: ME_JS("text/javascript")); serve_asset!(streams_js_request: STREAMS_JS("text/javascript")); serve_asset!(carp_js_request: CARP_JS("text/javascript")); +serve_asset!(layout_editor_js_request: LAYOUT_EDITOR_JS("text/javascript")); serve_asset!(proto_links_request: PROTO_LINKS_JS("text/javascript")); -serve_asset!(app_sdk_request: APP_SDK_JS("text/javascript")); diff --git a/crates/app/src/routes/mod.rs b/crates/app/src/routes/mod.rs index cde54f5..0872632 100644 --- a/crates/app/src/routes/mod.rs +++ b/crates/app/src/routes/mod.rs @@ -20,8 +20,11 @@ pub fn routes(config: &Config) -> Router { .route("/js/me.js", get(assets::me_js_request)) .route("/js/streams.js", get(assets::streams_js_request)) .route("/js/carp.js", get(assets::carp_js_request)) + .route( + "/js/layout_editor.js", + get(assets::layout_editor_js_request), + ) .route("/js/proto_links.js", get(assets::proto_links_request)) - .route("/js/app_sdk.js", get(assets::app_sdk_request)) .nest_service( "/public", get_service(tower_http::services::ServeDir::new(&config.dirs.assets)), diff --git a/crates/app/src/routes/pages/auth.rs b/crates/app/src/routes/pages/auth.rs index a675e6a..e9f1699 100644 --- a/crates/app/src/routes/pages/auth.rs +++ b/crates/app/src/routes/pages/auth.rs @@ -4,7 +4,7 @@ use axum::{ response::{Html, IntoResponse}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{Error, auth::ConnectionService}; use super::render_error; diff --git a/crates/app/src/routes/pages/chats.rs b/crates/app/src/routes/pages/chats.rs index 65ff437..e6ef791 100644 --- a/crates/app/src/routes/pages/chats.rs +++ b/crates/app/src/routes/pages/chats.rs @@ -5,7 +5,7 @@ use axum::{ response::{Html, IntoResponse, Redirect}, Extension, Json, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ channels::Message, communities_permissions::CommunityPermission, permissions::FinePermission, Error, diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 6f5524f..30d2ce0 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use super::{render_error, PaginatedQuery, RepostsQuery, SearchedQuery}; use crate::{ assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, State, @@ -10,7 +8,7 @@ use axum::{ response::{Html, IntoResponse}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tera::Context; use tetratto_core::model::{ @@ -124,20 +122,12 @@ macro_rules! community_context_bools { ) } else { false - } || if let Some(ref ua) = $user { - ua.permissions.check(tetratto_core::model::permissions::FinePermission::MANAGE_POSTS) - } else { - false }; let can_manage_community = if let Some(ref membership) = membership { membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_COMMUNITY) } else { false - } || if let Some(ref ua) = $user { - ua.permissions.check(tetratto_core::model::permissions::FinePermission::MANAGE_COMMUNITIES) - } else { - false }; let can_manage_roles = if let Some(ref membership) = membership { @@ -808,11 +798,7 @@ pub async fn post_request( let (_, reposting) = data.0.get_post_reposting(&post, &ignore_users, &user).await; // check question - let question = match data - .0 - .get_post_question(&post, &ignore_users, &mut HashMap::new()) - .await - { + let question = match data.0.get_post_question(&post, &ignore_users).await { Ok(q) => q, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }; @@ -932,11 +918,7 @@ pub async fn reposts_request( let reposting = data.0.get_post_reposting(&post, &ignore_users, &user).await; // check question - let question = match data - .0 - .get_post_question(&post, &ignore_users, &mut HashMap::new()) - .await - { + let question = match data.0.get_post_question(&post, &ignore_users).await { Ok(q) => q, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }; @@ -1087,11 +1069,7 @@ pub async fn likes_request( .await; // check question - let question = match data - .0 - .get_post_question(&post, &ignore_users, &mut HashMap::new()) - .await - { + let question = match data.0.get_post_question(&post, &ignore_users).await { Ok(q) => q, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; diff --git a/crates/app/src/routes/pages/developer.rs b/crates/app/src/routes/pages/developer.rs index a9e5f92..76d94fe 100644 --- a/crates/app/src/routes/pages/developer.rs +++ b/crates/app/src/routes/pages/developer.rs @@ -5,8 +5,8 @@ use axum::{ extract::Path, Extension, }; -use crate::cookie::CookieJar; -use tetratto_core::model::{apps::AppData, permissions::FinePermission, Error}; +use axum_extra::extract::CookieJar; +use tetratto_core::model::{permissions::FinePermission, Error}; /// `/developer` pub async fn home_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { @@ -62,13 +62,9 @@ pub async fn app_request( )); } - let data_limit = AppData::user_limit(&user, &app); - let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - context.insert("app", &app); - context.insert("data_limit", &data_limit); // return Ok(Html(data.1.render("developer/app.html", &context).unwrap())) diff --git a/crates/app/src/routes/pages/forge.rs b/crates/app/src/routes/pages/forge.rs index c09c04f..be1769c 100644 --- a/crates/app/src/routes/pages/forge.rs +++ b/crates/app/src/routes/pages/forge.rs @@ -8,7 +8,7 @@ use axum::{ response::{Html, IntoResponse}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{communities::Community, Error}; /// `/forges` diff --git a/crates/app/src/routes/pages/journals.rs b/crates/app/src/routes/pages/journals.rs index 8ac0a05..cdfba32 100644 --- a/crates/app/src/routes/pages/journals.rs +++ b/crates/app/src/routes/pages/journals.rs @@ -3,7 +3,7 @@ use axum::{ response::{Html, IntoResponse, Redirect}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use crate::{ assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, @@ -365,7 +365,7 @@ pub async fn global_view_request( Ok(( [( "content-security-policy", - "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' blob: *; frame-ancestors *", + "default-src 'self' *.spotify.com musicbrainz.org; img-src * data:; media-src *; font-src *; style-src 'unsafe-inline' 'self' *; script-src 'self' 'unsafe-inline' *; worker-src * blob:; object-src 'self' *; upgrade-insecure-requests; connect-src * localhost; frame-src 'self' *; frame-ancestors *", )], Html(data.1.render("journals/app.html", &context).unwrap()), )) diff --git a/crates/app/src/routes/pages/littleweb.rs b/crates/app/src/routes/pages/littleweb.rs index 18233ff..9dc5907 100644 --- a/crates/app/src/routes/pages/littleweb.rs +++ b/crates/app/src/routes/pages/littleweb.rs @@ -1,23 +1,18 @@ use super::render_error; -use crate::{ - assets::initial_context, get_lang, get_user_from_token, - routes::pages::misc::NotificationsProps, State, -}; +use crate::{assets::initial_context, get_lang, get_user_from_token, State}; use axum::{ response::{Html, IntoResponse}, extract::{Query, Path}, Extension, }; -use crate::cookie::CookieJar; -use tetratto_core::model::{littleweb::TLDS_VEC, permissions::SecondaryPermission, Error}; +use axum_extra::extract::CookieJar; +use tetratto_core::model::{littleweb::TLDS_VEC, Error}; use serde::Deserialize; -use tetratto_shared::hash::salt; /// `/services` pub async fn services_request( jar: CookieJar, Extension(data): Extension, - Query(props): Query, ) -> impl IntoResponse { let data = data.read().await; let user = match get_user_from_token!(jar, data.0) { @@ -29,26 +24,7 @@ pub async fn services_request( } }; - let profile = if props.id != 0 { - match data.0.get_user_by_id(props.id).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)), - } - } else { - user.clone() - }; - - if profile.id != user.id - && !user - .secondary_permissions - .check(SecondaryPermission::MANAGE_SERVICES) - { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - - let list = match data.0.get_services_by_user(profile.id).await { + let list = match data.0.get_services_by_user(user.id).await { Ok(x) => x, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; @@ -56,7 +32,6 @@ pub async fn services_request( let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; context.insert("list", &list); - context.insert("profile", &profile); // return Ok(Html( @@ -68,7 +43,6 @@ pub async fn services_request( pub async fn domains_request( jar: CookieJar, Extension(data): Extension, - Query(props): Query, ) -> impl IntoResponse { let data = data.read().await; let user = match get_user_from_token!(jar, data.0) { @@ -80,26 +54,7 @@ pub async fn domains_request( } }; - let profile = if props.id != 0 { - match data.0.get_user_by_id(props.id).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)), - } - } else { - user.clone() - }; - - if profile.id != user.id - && !user - .secondary_permissions - .check(SecondaryPermission::MANAGE_DOMAINS) - { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - - let list = match data.0.get_domains_by_user(profile.id).await { + let list = match data.0.get_domains_by_user(user.id).await { Ok(x) => x, Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; @@ -109,7 +64,6 @@ pub async fn domains_request( context.insert("list", &list); context.insert("tlds", &*TLDS_VEC); - context.insert("profile", &profile); // return Ok(Html( @@ -145,11 +99,7 @@ pub async fn service_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; - if user.id != service.owner - && !user - .secondary_permissions - .check(SecondaryPermission::MANAGE_SERVICES) - { + if user.id != service.owner { return Err(Html( render_error(Error::NotAllowed, &jar, &data, &None).await, )); @@ -203,11 +153,7 @@ pub async fn domain_request( Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), }; - if user.id != domain.owner - && !user - .secondary_permissions - .check(SecondaryPermission::MANAGE_DOMAINS) - { + if user.id != domain.owner { return Err(Html( render_error(Error::NotAllowed, &jar, &data, &None).await, )); @@ -231,26 +177,12 @@ pub async fn browser_home_request( let data = data.read().await; let user = get_user_from_token!(jar, data.0); - // update session - let session = salt(); - - if let Some(ref ua) = user { - if let Err(e) = data.0.update_user_browser_session(ua.id, &session).await { - return Err(Html(render_error(e.into(), &jar, &data, &None).await)); - } - } - - // ... let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &user).await; - context.insert("path", &""); - context.insert("session", &session); // return - Ok(Html( - data.1.render("littleweb/browser.html", &context).unwrap(), - )) + Html(data.1.render("littleweb/browser.html", &context).unwrap()) } /// `/net/{uri}` @@ -270,24 +202,10 @@ pub async fn browser_request( uri = format!("atto://{uri}"); } - // update session - let session = salt(); - - if let Some(ref ua) = user { - if let Err(e) = data.0.update_user_browser_session(ua.id, &session).await { - return Err(Html(render_error(e.into(), &jar, &data, &None).await)); - } - } - - // ... let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0.0, lang, &user).await; - - context.insert("session", &session); - context.insert("path", &uri.replace("atto://", "")); + context.insert("path", &uri); // return - Ok(Html( - data.1.render("littleweb/browser.html", &context).unwrap(), - )) + Html(data.1.render("littleweb/browser.html", &context).unwrap()) } diff --git a/crates/app/src/routes/pages/marketplace.rs b/crates/app/src/routes/pages/marketplace.rs deleted file mode 100644 index 8d5a3be..0000000 --- a/crates/app/src/routes/pages/marketplace.rs +++ /dev/null @@ -1,107 +0,0 @@ -use super::render_error; -use crate::{ - assets::initial_context, get_lang, get_user_from_token, State, routes::pages::PaginatedQuery, -}; -use axum::{ - extract::Query, - response::{Html, IntoResponse}, - Extension, -}; -use crate::cookie::CookieJar; -use tetratto_core::model::Error; - -/// `/settings/seller` -pub async fn seller_settings_request( - jar: CookieJar, - Extension(data): Extension, - Query(props): Query, -) -> impl IntoResponse { - let data = data.read().await; - let user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - }; - - let products = match data.0.get_products_by_user(user.id, 12, props.page).await { - Ok(x) => x, - Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)), - }; - - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - - context.insert("list", &products); - context.insert("page", &props.page); - - // return - Ok(Html( - data.1.render("marketplace/seller.html", &context).unwrap(), - )) -} - -pub async fn connection_return_request( - jar: CookieJar, - Extension(data): Extension, -) -> impl IntoResponse { - let data = data.read().await; - let mut user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - }; - - // update user - user.seller_data.completed_onboarding = true; - if let Err(e) = data - .0 - .update_user_seller_data(user.id, user.seller_data.clone()) - .await - { - return Err(Html(render_error(e, &jar, &data, &None).await)); - } - - // ... - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - context.insert("connection_type", "return"); - - // return - Ok(Html( - data.1 - .render("auth/seller_connection.html", &context) - .unwrap(), - )) -} - -pub async fn connection_reload_request( - jar: CookieJar, - Extension(data): Extension, -) -> impl IntoResponse { - let data = data.read().await; - let user = match get_user_from_token!(jar, data.0) { - Some(ua) => ua, - None => { - return Err(Html( - render_error(Error::NotAllowed, &jar, &data, &None).await, - )); - } - }; - - let lang = get_lang!(jar, data.0); - let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; - context.insert("connection_type", "reload"); - - // return - Ok(Html( - data.1 - .render("auth/seller_connection.html", &context) - .unwrap(), - )) -} diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 7ee2f72..e65f4b5 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -7,7 +7,7 @@ use axum::{ response::{Html, IntoResponse}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tetratto_core::model::{ auth::{AchievementName, DefaultTimelineChoice, ACHIEVEMENTS}, @@ -58,7 +58,7 @@ pub async fn index_request( let list = match data .0 - .get_posts_from_user_communities(user.id, 12, req.page, &user) + .get_posts_from_user_communities(user.id, 12, req.page) .await { Ok(l) => match data @@ -725,7 +725,7 @@ pub async fn swiss_army_timeline_request( DefaultTimelineChoice::MyCommunities => { if let Some(ref ua) = user { data.0 - .get_posts_from_user_communities(ua.id, 12, req.page, ua) + .get_posts_from_user_communities(ua.id, 12, req.page) .await } else { return Err(Html( diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index 2f3c9d5..6ce6318 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -5,7 +5,6 @@ pub mod developer; pub mod forge; pub mod journals; pub mod littleweb; -pub mod marketplace; pub mod misc; pub mod mod_panel; pub mod profile; @@ -15,13 +14,14 @@ use axum::{ routing::{get, post}, Router, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tetratto_core::{ + DataManager, model::{Error, auth::User}, }; -use crate::{assets::initial_context, get_lang, InnerState}; +use crate::{assets::initial_context, get_lang}; pub fn routes() -> Router { Router::new() @@ -77,14 +77,6 @@ pub fn routes() -> Router { "/auth/connections_link/app/{id}", get(developer::connection_callback_request), ) - .route( - "/auth/connections_link/seller/reload", - get(marketplace::connection_reload_request), - ) - .route( - "/auth/connections_link/seller/return", - get(marketplace::connection_return_request), - ) // profile .route("/settings", get(profile::settings_request)) .route("/@{username}", get(profile::posts_request)) @@ -155,11 +147,6 @@ pub fn routes() -> Router { .route("/domains/{id}", get(littleweb::domain_request)) .route("/net", get(littleweb::browser_home_request)) .route("/net/{*uri}", get(littleweb::browser_request)) - // marketplace - .route( - "/settings/seller", - get(marketplace::seller_settings_request), - ) } pub fn lw_routes() -> Router { @@ -169,7 +156,7 @@ pub fn lw_routes() -> Router { pub async fn render_error( e: Error, jar: &CookieJar, - data: &InnerState, + data: &(DataManager, tera::Tera, reqwest::Client), user: &Option, ) -> String { let lang = get_lang!(jar, data.0); diff --git a/crates/app/src/routes/pages/mod_panel.rs b/crates/app/src/routes/pages/mod_panel.rs index 2b82cf1..7a9b6f7 100644 --- a/crates/app/src/routes/pages/mod_panel.rs +++ b/crates/app/src/routes/pages/mod_panel.rs @@ -5,7 +5,7 @@ use axum::{ response::{Html, IntoResponse}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tetratto_core::{ cache::Cache, diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index 3f6274b..186d291 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -8,7 +8,7 @@ use axum::{ extract::{Path, Query}, response::{Html, IntoResponse}, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use serde::Deserialize; use tera::Context; use tetratto_core::model::{ @@ -63,31 +63,13 @@ pub async fn settings_request( } }; - let followers = match data - .0 - .fill_userfollows_with_initiator( - data.0 - .get_userfollows_by_receiver(profile.id, 12, req.page) - .await - .unwrap_or(Vec::new()), - &None, - false, - ) - .await - { - Ok(r) => r, - Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)), - }; - let following = match data .0 .fill_userfollows_with_receiver( data.0 - .get_userfollows_by_initiator(profile.id, 12, req.page) + .get_userfollows_by_initiator_all(profile.id) .await .unwrap_or(Vec::new()), - &None, - false, ) .await { @@ -154,7 +136,6 @@ pub async fn settings_request( context.insert("page", &req.page); context.insert("uploads", &uploads); context.insert("stacks", &stacks); - context.insert("followers", &followers); context.insert("following", &following); context.insert("blocks", &blocks); context.insert("stackblocks", &stackblocks); @@ -731,28 +712,13 @@ 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 .get_userfollows_by_initiator(other_user.id, 12, props.page) .await { - Ok(l) => match data.0.fill_userfollows_with_receiver(l, &user, true).await { + Ok(l) => match data.0.fill_userfollows_with_receiver(l).await { Ok(l) => l, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }, @@ -841,28 +807,13 @@ 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 .get_userfollows_by_receiver(other_user.id, 12, props.page) .await { - Ok(l) => match data.0.fill_userfollows_with_initiator(l, &user, true).await { + Ok(l) => match data.0.fill_userfollows_with_initiator(l).await { Ok(l) => l, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), }, diff --git a/crates/app/src/routes/pages/stacks.rs b/crates/app/src/routes/pages/stacks.rs index f4ee986..e8285e9 100644 --- a/crates/app/src/routes/pages/stacks.rs +++ b/crates/app/src/routes/pages/stacks.rs @@ -3,7 +3,7 @@ use axum::{ response::{Html, IntoResponse}, Extension, }; -use crate::cookie::CookieJar; +use axum_extra::extract::CookieJar; use tetratto_core::model::{ auth::User, permissions::FinePermission, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index bd7ac03..ffbd2c2 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,38 +1,26 @@ [package] name = "tetratto-core" -description = "The core behind Tetratto" -version = "12.0.2" +version = "11.0.0" edition = "2024" -authors.workspace = true -repository.workspace = true -license.workspace = true -homepage.workspace = true - -[features] -database = ["dep:oiseau", "dep:base64", "dep:base16ct", "dep:async-recursion", "dep:md-5"] -types = ["dep:totp-rs", "dep:paste", "dep:bitflags"] -sdk = ["types", "dep:reqwest"] -default = ["database", "types", "sdk"] [dependencies] pathbufd = "0.1.4" serde = { version = "1.0.219", features = ["derive"] } -toml = "0.9.2" -tetratto-shared = { version = "12.0.6", 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 } -reqwest = { version = "0.12.22", features = ["json", "multipart"], optional = true } -bitflags = { version = "2.9.1", optional = true } -async-recursion = { version = "1.1.1", optional = true } -md-5 = { version = "0.10.6", optional = true } -base16ct = { version = "0.2.0", features = ["alloc"], optional = true } -base64 = { version = "0.22.1", optional = true } +toml = "0.8.23" +tetratto-shared = { path = "../shared" } +tetratto-l10n = { path = "../l10n" } +serde_json = "1.0.140" +totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] } +reqwest = { version = "0.12.20", features = ["json"] } +bitflags = "2.9.1" +async-recursion = "1.1.1" +md-5 = "0.10.6" +base16ct = { version = "0.2.0", features = ["alloc"] } +base64 = "0.22.1" emojis = "0.7.0" regex = "1.11.1" oiseau = { version = "0.1.2", default-features = false, features = [ "postgres", "redis", -], optional = true } -paste = { version = "1.0.15", optional = true } -tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } +] } +paste = "1.0.15" diff --git a/crates/core/examples/sdk_db.rs b/crates/core/examples/sdk_db.rs deleted file mode 100644 index becdca1..0000000 --- a/crates/core/examples/sdk_db.rs +++ /dev/null @@ -1,65 +0,0 @@ -extern crate tetratto_core; -use tetratto_core::{ - model::apps::{AppDataSelectMode, AppDataSelectQuery, AppDataQueryResult}, - sdk::{DataClient, SimplifiedQuery}, -}; -use std::env::var; - -// mirror of https://trisua.com/t/tetratto/src/branch/master/example/app_sdk_test.js ... but in rust -#[tokio::main] -pub async fn main() { - let client = DataClient::new( - Some("http://localhost:4118".to_string()), - var("APP_API_KEY").unwrap(), - ); - - println!("data used: {}", client.get_app().await.unwrap().data_used); - - // record insert - client - .insert("rust_test".to_string(), "Hello, world!".to_string()) - .await - .unwrap(); - println!("record created"); - println!("data used: {}", client.get_app().await.unwrap().data_used); - - // testing record query then delete - let record = match client - .query(&SimplifiedQuery { - query: AppDataSelectQuery::KeyIs("rust_test".to_string()), - mode: AppDataSelectMode::One(0), - }) - .await - .unwrap() - { - AppDataQueryResult::One(x) => x, - AppDataQueryResult::Many(_) => unreachable!(), - }; - - println!("{:?}", record); - - client - .update(record.id, "Hello, world! 1".to_string()) - .await - .unwrap(); - println!("record updated"); - println!("data used: {}", client.get_app().await.unwrap().data_used); - - let record = match client - .query(&SimplifiedQuery { - query: AppDataSelectQuery::KeyIs("rust_test".to_string()), - mode: AppDataSelectMode::One(0), - }) - .await - .unwrap() - { - AppDataQueryResult::One(x) => x, - AppDataQueryResult::Many(_) => unreachable!(), - }; - - println!("{:?}", record); - - client.remove(record.id).await.unwrap(); - println!("record deleted"); - println!("data used: {}", client.get_app().await.unwrap().data_used); -} diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs index e1637b1..309c851 100644 --- a/crates/core/src/config.rs +++ b/crates/core/src/config.rs @@ -173,15 +173,13 @@ pub struct ConnectionsConfig { /// - Use testing card numbers: #[derive(Clone, Serialize, Deserialize, Debug, Default)] pub struct StripeConfig { - /// Your Stripe API secret. - pub secret: String, - /// Payment links from the Stripe dashboard. + /// Payment link from the Stripe dashboard. /// /// 1. Create a product and set the price for your membership /// 2. Set the product price to a recurring subscription /// 3. Create a payment link for the new product /// 4. The payment link pasted into this config field should NOT include a query string - pub payment_links: StripePaymentLinks, + pub payment_link: String, /// To apply benefits to user accounts, you should then go into the Stripe developer /// "workbench" and create a new webhook. The webhook needs the scopes: /// `invoice.payment_succeeded`, `customer.subscription.deleted`, `checkout.session.completed`. @@ -194,30 +192,8 @@ pub struct StripeConfig { /// /// pub billing_portal_url: String, - /// The text representation of prices. (like `$4 USD`) - pub price_texts: StripePriceTexts, - /// Product IDs from the Stripe dashboard. - /// - /// These are checked when we receive a webhook to ensure we provide the correct product. - pub product_ids: StripeProductIds, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Default)] -pub struct StripePriceTexts { - pub supporter: String, - pub dev_pass: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Default)] -pub struct StripePaymentLinks { - pub supporter: String, - pub dev_pass: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Default)] -pub struct StripeProductIds { - pub supporter: String, - pub dev_pass: String, + /// The text representation of the price of supporter. (like `$4 USD`) + pub supporter_price_text: String, } /// Manuals config (search help, etc) diff --git a/crates/core/src/database/app_data.rs b/crates/core/src/database/app_data.rs deleted file mode 100644 index 9aeafc1..0000000 --- a/crates/core/src/database/app_data.rs +++ /dev/null @@ -1,189 +0,0 @@ -use oiseau::cache::Cache; -use crate::model::apps::{AppDataQuery, AppDataQueryResult, AppDataSelectMode}; -use crate::model::{apps::AppData, permissions::FinePermission, Error, Result}; -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; - -impl DataManager { - /// Get a [`AppData`] from an SQL row. - pub(crate) fn get_app_data_from_row(x: &PostgresRow) -> AppData { - AppData { - id: get!(x->0(i64)) as usize, - app: get!(x->1(i64)) as usize, - key: get!(x->2(String)), - value: get!(x->3(String)), - } - } - - auto_method!(get_app_data_by_id(usize as i64)@get_app_data_from_row -> "SELECT * FROM app_data WHERE id = $1" --name="app_data" --returns=AppData --cache-key-tmpl="atto.app_data:{}"); - - /// Get all app_data by app. - /// - /// # Arguments - /// * `id` - the ID of the app to fetch app_data for - pub async fn get_app_data_by_app(&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 app_data WHERE app = $1 ORDER BY created DESC", - &[&(id as i64)], - |x| { Self::get_app_data_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("app_data".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all app_data by the given query. - pub async fn query_app_data(&self, query: AppDataQuery) -> Result { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let query_str = query.to_string().replace("%q%", &query.query.selector()); - - let res = match query.mode { - AppDataSelectMode::One(_) => AppDataQueryResult::One( - match query_row!(&conn, &query_str, params![&query.query.to_string()], |x| { - Ok(Self::get_app_data_from_row(x)) - }) { - Ok(x) => x, - Err(_) => return Err(Error::GeneralNotFound("app_data".to_string())), - }, - ), - AppDataSelectMode::Many(_, _) => AppDataQueryResult::Many( - match query_rows!(&conn, &query_str, params![&query.query.to_string()], |x| { - Self::get_app_data_from_row(x) - }) { - Ok(x) => x, - Err(_) => return Err(Error::GeneralNotFound("app_data".to_string())), - }, - ), - AppDataSelectMode::ManyJson(_, _, _) => AppDataQueryResult::Many( - match query_rows!(&conn, &query_str, params![&query.query.to_string()], |x| { - Self::get_app_data_from_row(x) - }) { - Ok(x) => x, - Err(_) => return Err(Error::GeneralNotFound("app_data".to_string())), - }, - ), - }; - - Ok(res) - } - - /// Delete all app_data matched by the given query. - pub async fn query_delete_app_data(&self, query: AppDataQuery) -> Result<()> { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let query_str = query - .to_string() - .replace("%q%", &query.query.selector()) - .replace("SELECT * FROM", "SELECT id FROM"); - - if let Err(e) = execute!( - &conn, - &format!("DELETE FROM app_data WHERE id IN ({query_str})"), - params![&query.query.to_string()] - ) { - return Err(Error::MiscError(e.to_string())); - } - - Ok(()) - } - - const MAXIMUM_FREE_APP_DATA: usize = 5; - const MAXIMUM_DATA_SIZE: usize = 205_000; - - /// Create a new app_data in the database. - /// - /// # Arguments - /// * `data` - a mock [`AppData`] object to insert - pub async fn create_app_data(&self, data: AppData) -> Result { - let app = self.get_app_by_id(data.app).await?; - - // check values - if data.key.len() < 1 { - return Err(Error::DataTooShort("key".to_string())); - } else if data.key.len() > 128 { - return Err(Error::DataTooLong("key".to_string())); - } - - if data.value.len() < 1 { - return Err(Error::DataTooShort("value".to_string())); - } else if data.value.len() > Self::MAXIMUM_DATA_SIZE { - return Err(Error::DataTooLong("value".to_string())); - } - - // check number of app_data - let owner = self.get_user_by_id(app.owner).await?; - - if !owner.permissions.check(FinePermission::SUPPORTER) { - let app_data = self - .get_table_row_count_where("app_data", &format!("app = {}", data.app)) - .await? as usize; - - if app_data >= Self::MAXIMUM_FREE_APP_DATA { - return Err(Error::MiscError( - "You already have the maximum number of app_data you can have".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 app_data VALUES ($1, $2, $3, $4)", - params![ - &(data.id as i64), - &(data.app as i64), - &data.key, - &data.value - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(data) - } - - pub async fn delete_app_data(&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 = execute!(&conn, "DELETE FROM app_data WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.0.1.remove(format!("atto.app_data:{}", id)).await; - Ok(()) - } - - auto_method!(update_app_data_key(&str) -> "UPDATE app_data SET k = $1 WHERE id = $2" --cache-key-tmpl="atto.app_data:{}"); - auto_method!(update_app_data_value(&str) -> "UPDATE app_data SET v = $1 WHERE id = $2" --cache-key-tmpl="atto.app_data:{}"); -} diff --git a/crates/core/src/database/apps.rs b/crates/core/src/database/apps.rs index 72334a8..f24b427 100644 --- a/crates/core/src/database/apps.rs +++ b/crates/core/src/database/apps.rs @@ -1,13 +1,16 @@ use oiseau::cache::Cache; use crate::model::{ - apps::{AppQuota, ThirdPartyApp, DeveloperPassStorageQuota}, + apps::{AppQuota, ThirdPartyApp}, auth::User, oauth::AppScope, - permissions::{FinePermission, SecondaryPermission}, + permissions::FinePermission, Error, Result, }; use crate::{auto_method, DataManager}; -use oiseau::{PostgresRow, execute, get, query_row, query_rows, params}; + +use oiseau::PostgresRow; + +use oiseau::{execute, get, query_rows, params}; impl DataManager { /// Get a [`ThirdPartyApp`] from an SQL row. @@ -23,14 +26,10 @@ impl DataManager { banned: get!(x->7(i32)) as i8 == 1, grants: get!(x->8(i32)) as usize, 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(), } } auto_method!(get_app_by_id(usize as i64)@get_app_from_row -> "SELECT * FROM apps WHERE id = $1" --name="app" --returns=ThirdPartyApp --cache-key-tmpl="atto.app:{}"); - auto_method!(get_app_by_api_key(&str)@get_app_from_row -> "SELECT * FROM apps WHERE api_key = $1" --name="app" --returns=ThirdPartyApp --cache-key-tmpl="atto.app_k:{}"); /// Get all apps by user. /// @@ -73,15 +72,10 @@ impl DataManager { // check number of apps let owner = self.get_user_by_id(data.owner).await?; - if !owner - .secondary_permissions - .check(SecondaryPermission::DEVELOPER_PASS) - { - let apps = self - .get_table_row_count_where("apps", &format!("owner = {}", owner.id)) - .await? as usize; + if !owner.permissions.check(FinePermission::SUPPORTER) { + let apps = self.get_apps_by_owner(data.owner).await?; - if apps >= Self::MAXIMUM_FREE_APPS { + if apps.len() >= Self::MAXIMUM_FREE_APPS { return Err(Error::MiscError( "You already have the maximum number of apps you can have".to_string(), )); @@ -96,7 +90,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)", params![ &(data.id as i64), &(data.created as i64), @@ -108,9 +102,6 @@ impl DataManager { &{ if data.banned { 1 } else { 0 } }, &(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(), ] ); @@ -141,39 +132,16 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } - self.cache_clear_app(&app).await; - - // remove data - let res = execute!( - &conn, - "DELETE FROM app_data WHERE app = $1", - &[&(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // ... + self.0.1.remove(format!("atto.app:{}", id)).await; Ok(()) } - pub async fn cache_clear_app(&self, app: &ThirdPartyApp) { - self.0.1.remove(format!("atto.app:{}", app.id)).await; - self.0.1.remove(format!("atto.app_k:{}", app.api_key)).await; - } + auto_method!(update_app_title(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.app:{}"); + auto_method!(update_app_homepage(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET homepage = $1 WHERE id = $2" --cache-key-tmpl="atto.app:{}"); + auto_method!(update_app_redirect(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET redirect = $1 WHERE id = $2" --cache-key-tmpl="atto.app:{}"); + auto_method!(update_app_quota_status(AppQuota) -> "UPDATE apps SET quota_status = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.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="atto.app:{}"); - auto_method!(update_app_title(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET title = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); - auto_method!(update_app_homepage(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET homepage = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); - auto_method!(update_app_redirect(&str)@get_app_by_id:FinePermission::MANAGE_APPS; -> "UPDATE apps SET redirect = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_app); - 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); - - auto_method!(incr_app_grants()@get_app_by_id -> "UPDATE apps SET grants = grants + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_app --incr); - auto_method!(decr_app_grants()@get_app_by_id -> "UPDATE apps SET grants = grants - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_app --decr=grants); + auto_method!(incr_app_grants() -> "UPDATE apps SET grants = grants + 1 WHERE id = $1" --cache-key-tmpl="atto.app:{}" --incr); + auto_method!(decr_app_grants()@get_app_by_id -> "UPDATE apps SET grants = grants - 1 WHERE id = $1" --cache-key-tmpl="atto.app:{}" --decr=grants); } diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 4ced643..fbf229b 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -1,8 +1,7 @@ use super::common::NAME_REGEX; use oiseau::cache::Cache; use crate::model::auth::{ - Achievement, AchievementName, AchievementRarity, Notification, StripeSellerData, - UserConnections, ACHIEVEMENTS, + Achievement, AchievementName, AchievementRarity, Notification, UserConnections, ACHIEVEMENTS, }; use crate::model::moderation::AuditLogEntry; use crate::model::oauth::AuthGrant; @@ -100,21 +99,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(), @@ -124,18 +116,12 @@ impl DataManager { achievements: serde_json::from_str(&get!(x->23(String)).to_string()).unwrap(), awaiting_purchase: get!(x->24(i32)) as i8 == 1, was_purchased: get!(x->25(i32)) as i8 == 1, - browser_session: get!(x->26(String)), - seller_data: serde_json::from_str(&get!(x->27(String)).to_string()).unwrap(), - ban_reason: get!(x->28(String)), - channel_mutes: serde_json::from_str(&get!(x->29(String)).to_string()).unwrap(), - is_deactivated: get!(x->30(i32)) as i8 == 1, } } auto_method!(get_user_by_id(usize as i64)@get_user_from_row -> "SELECT * FROM users WHERE id = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}"); auto_method!(get_user_by_username(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User --cache-key-tmpl="atto.user:{}"); auto_method!(get_user_by_username_no_cache(&str)@get_user_from_row -> "SELECT * FROM users WHERE username = $1" --name="user" --returns=User); - auto_method!(get_user_by_browser_session(&str)@get_user_from_row -> "SELECT * FROM users WHERE browser_session = $1" --name="user" --returns=User); /// Get a user given just their ID. Returns the void user if the user doesn't exist. /// @@ -204,8 +190,8 @@ impl DataManager { let res = query_row!( &conn, - "SELECT * FROM users WHERE grants LIKE $1", - &[&format!("%\"token\":\"{token}\"%")], + "SELECT * FROM users WHERE (SELECT jsonb_array_elements(grants::jsonb) @> ('{\"token\":\"' || $1 || '\"}')::jsonb)", + &[&token], |x| Ok(Self::get_user_from_row(x)) ); @@ -285,7 +271,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)", params![ &(data.id as i64), &(data.created as i64), @@ -313,11 +299,6 @@ impl DataManager { &serde_json::to_string(&data.achievements).unwrap(), &if data.awaiting_purchase { 1_i32 } else { 0_i32 }, &if data.was_purchased { 1_i32 } else { 0_i32 }, - &data.browser_session, - &serde_json::to_string(&data.seller_data).unwrap(), - &data.ban_reason, - &serde_json::to_string(&data.channel_mutes).unwrap(), - &if data.is_deactivated { 1_i32 } else { 0_i32 }, ] ); @@ -334,7 +315,7 @@ impl DataManager { /// * `id` - the ID of the user /// * `password` - the current password of the user /// * `force` - if we should delete even if the given password is incorrect - pub async fn delete_user(&self, id: usize, password: &str, force: bool) -> Result { + pub async fn delete_user(&self, id: usize, password: &str, force: bool) -> Result<()> { let user = self.get_user_by_id(id).await?; if (hash_salted(password.to_string(), user.salt.clone()) != user.password) && !force { @@ -544,11 +525,6 @@ impl DataManager { self.delete_userfollow(follow.id, &user, true).await?; } - // delete apps - for app in self.get_apps_by_owner(id).await? { - self.delete_app(app.id, &user).await?; - } - // remove images let avatar = PathBufD::current().extend(&[ self.0.0.dirs.media.as_str(), @@ -599,7 +575,7 @@ impl DataManager { } // ... - Ok(user) + Ok(()) } pub async fn update_user_verified_status(&self, id: usize, x: bool, user: User) -> Result<()> { @@ -640,44 +616,6 @@ impl DataManager { Ok(()) } - pub async fn update_user_is_deactivated(&self, id: usize, x: bool, user: User) -> Result<()> { - if id != user.id && !user.permissions.check(FinePermission::MANAGE_USERS) { - return Err(Error::NotAllowed); - } - - let other_user = self.get_user_by_id(id).await?; - - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = execute!( - &conn, - "UPDATE users SET is_deactivated = $1 WHERE id = $2", - params![&{ if x { 1 } else { 0 } }, &(id as i64)] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - self.cache_clear_user(&other_user).await; - - // create audit log entry - self.create_audit_log_entry(AuditLogEntry::new( - user.id, - format!( - "invoked `update_user_is_deactivated` with x value `{}` and y value `{}`", - other_user.id, x - ), - )) - .await?; - - // ... - Ok(()) - } - pub async fn update_user_password( &self, id: usize, @@ -1055,10 +993,6 @@ impl DataManager { auto_method!(update_user_associated(Vec)@get_user_by_id -> "UPDATE users SET associated = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_achievements(Vec)@get_user_by_id -> "UPDATE users SET achievements = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(update_user_invite_code(i64)@get_user_by_id -> "UPDATE users SET invite_code = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); - auto_method!(update_user_browser_session(&str)@get_user_by_id -> "UPDATE users SET browser_session = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); - auto_method!(update_user_seller_data(StripeSellerData)@get_user_by_id -> "UPDATE users SET seller_data = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); - auto_method!(update_user_ban_reason(&str)@get_user_by_id -> "UPDATE users SET ban_reason = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); - auto_method!(update_user_channel_mutes(Vec)@get_user_by_id -> "UPDATE users SET channel_mutes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/database/channels.rs b/crates/core/src/database/channels.rs index c1e9938..ee42d4b 100644 --- a/crates/core/src/database/channels.rs +++ b/crates/core/src/database/channels.rs @@ -5,6 +5,7 @@ use crate::model::{ communities_permissions::CommunityPermission, channels::Channel, }; use crate::{auto_method, DataManager}; + use oiseau::{PostgresRow, execute, get, query_row, query_rows, params}; impl DataManager { diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs index 5e10783..969b014 100644 --- a/crates/core/src/database/common.rs +++ b/crates/core/src/database/common.rs @@ -40,15 +40,9 @@ impl DataManager { execute!(&conn, common::CREATE_TABLE_NOTES).unwrap(); execute!(&conn, common::CREATE_TABLE_MESSAGE_REACTIONS).unwrap(); execute!(&conn, common::CREATE_TABLE_INVITE_CODES).unwrap(); + execute!(&conn, common::CREATE_TABLE_LAYOUTS).unwrap(); execute!(&conn, common::CREATE_TABLE_DOMAINS).unwrap(); 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(); - } self.0 .1 @@ -81,26 +75,6 @@ impl DataManager { Ok(res.unwrap()) } - - pub async fn get_table_row_count_where(&self, table: &str, r#where: &str) -> Result { - let conn = match self.0.connect().await { - Ok(c) => c, - Err(e) => return Err(Error::DatabaseConnection(e.to_string())), - }; - - let res = query_row!( - &conn, - &format!("SELECT COUNT(*)::int FROM {} WHERE {}", table, r#where), - params![], - |x| Ok(x.get::(0)) - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(res.unwrap()) - } } #[macro_export] diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index df107e9..fa7c234 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -3,7 +3,6 @@ use super::common::NAME_REGEX; use oiseau::cache::Cache; use crate::model::communities::{CommunityContext, CommunityJoinAccess, CommunityMembership}; use crate::model::communities_permissions::CommunityPermission; -use crate::model::permissions::SecondaryPermission; use crate::model::{ Error, Result, auth::User, @@ -256,11 +255,7 @@ impl DataManager { // check is_forge // only supporters can CREATE forge communities... anybody can contribute to them - if data.is_forge - && !owner - .secondary_permissions - .check(SecondaryPermission::DEVELOPER_PASS) - { + if data.is_forge && !owner.permissions.check(FinePermission::SUPPORTER) { return Err(Error::RequiresSupporter); } diff --git a/crates/core/src/database/domains.rs b/crates/core/src/database/domains.rs index 737bd5f..672de1c 100644 --- a/crates/core/src/database/domains.rs +++ b/crates/core/src/database/domains.rs @@ -74,7 +74,7 @@ impl DataManager { Ok(res.unwrap()) } - const MAXIMUM_FREE_DOMAINS: usize = 10; + const MAXIMUM_FREE_DOMAINS: usize = 5; /// Create a new domain in the database. /// diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs index bccbfb9..efa3eae 100644 --- a/crates/core/src/database/drivers/common.rs +++ b/crates/core/src/database/drivers/common.rs @@ -1,4 +1,3 @@ -pub const VERSION_MIGRATIONS: &str = include_str!("./sql/version_migrations.sql"); pub const CREATE_TABLE_USERS: &str = include_str!("./sql/create_users.sql"); pub const CREATE_TABLE_COMMUNITIES: &str = include_str!("./sql/create_communities.sql"); pub const CREATE_TABLE_POSTS: &str = include_str!("./sql/create_posts.sql"); @@ -28,8 +27,6 @@ pub const CREATE_TABLE_JOURNALS: &str = include_str!("./sql/create_journals.sql" pub const CREATE_TABLE_NOTES: &str = include_str!("./sql/create_notes.sql"); pub const CREATE_TABLE_MESSAGE_REACTIONS: &str = include_str!("./sql/create_message_reactions.sql"); pub const CREATE_TABLE_INVITE_CODES: &str = include_str!("./sql/create_invite_codes.sql"); +pub const CREATE_TABLE_LAYOUTS: &str = include_str!("./sql/create_layouts.sql"); 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_app_data.sql b/crates/core/src/database/drivers/sql/create_app_data.sql deleted file mode 100644 index 64cdd3f..0000000 --- a/crates/core/src/database/drivers/sql/create_app_data.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS app_data ( - id BIGINT NOT NULL PRIMARY KEY, - app BIGINT NOT NULL, - k TEXT NOT NULL, - v TEXT NOT NULL -) diff --git a/crates/core/src/database/drivers/sql/create_apps.sql b/crates/core/src/database/drivers/sql/create_apps.sql index 70f2b8d..575ce5c 100644 --- a/crates/core/src/database/drivers/sql/create_apps.sql +++ b/crates/core/src/database/drivers/sql/create_apps.sql @@ -8,7 +8,5 @@ CREATE TABLE IF NOT EXISTS apps ( quota_status TEXT NOT NULL, 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 + scopes TEXT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/create_layouts.sql b/crates/core/src/database/drivers/sql/create_layouts.sql new file mode 100644 index 0000000..3f28c0a --- /dev/null +++ b/crates/core/src/database/drivers/sql/create_layouts.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS layouts ( + id BIGINT NOT NULL PRIMARY KEY, + created BIGINT NOT NULL, + owner BIGINT NOT NULL, + title TEXT NOT NULL, + privacy TEXT NOT NULL, + pages TEXT NOT NULL, + replaces TEXT NOT NULL +) 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/create_products.sql b/crates/core/src/database/drivers/sql/create_products.sql deleted file mode 100644 index 4a972aa..0000000 --- a/crates/core/src/database/drivers/sql/create_products.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE IF NOT EXISTS products ( - id BIGINT NOT NULL PRIMARY KEY, - created BIGINT NOT NULL, - owner BIGINT NOT NULL, - name TEXT NOT NULL, - description TEXT NOT NULL, - likes INT NOT NULL, - dislikes INT NOT NULL, - product_type TEXT NOT NULL, - price TEXT NOT NULL, - uploads TEXT NOT NULL -) diff --git a/crates/core/src/database/drivers/sql/create_services.sql b/crates/core/src/database/drivers/sql/create_services.sql index ecb04d6..78277b5 100644 --- a/crates/core/src/database/drivers/sql/create_services.sql +++ b/crates/core/src/database/drivers/sql/create_services.sql @@ -3,6 +3,5 @@ CREATE TABLE IF NOT EXISTS services ( created BIGINT NOT NULL, owner BIGINT NOT NULL, name TEXT NOT NULL, - files TEXT NOT NULL, - revision BIGINT NOT NULL + files TEXT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/create_uploads.sql b/crates/core/src/database/drivers/sql/create_uploads.sql index 57d4037..a563080 100644 --- a/crates/core/src/database/drivers/sql/create_uploads.sql +++ b/crates/core/src/database/drivers/sql/create_uploads.sql @@ -2,6 +2,5 @@ CREATE TABLE IF NOT EXISTS uploads ( id BIGINT NOT NULL PRIMARY KEY, created BIGINT NOT NULL, owner BIGINT NOT NULL, - what TEXT NOT NULL, - alt TEXT NOT NULL + what TEXT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 6a939e5..3257a2d 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -20,14 +20,8 @@ CREATE TABLE IF NOT EXISTS users ( stripe_id TEXT NOT NULL, grants TEXT NOT NULL, associated TEXT NOT NULL, - invite_code TEXT NOT NULL, secondary_permissions INT NOT NULL, achievements TEXT NOT NULL, awaiting_purchase INT NOT NULL, - was_purchased INT NOT NULL, - browser_session TEXT NOT NULL, - seller_data TEXT NOT NULL, - ban_reason TEXT NOT NULL, - channel_mutes TEXT NOT NULL, - is_deactivated INT NOT NULL + was_purchased INT NOT NULL ) diff --git a/crates/core/src/database/drivers/sql/version_migrations.sql b/crates/core/src/database/drivers/sql/version_migrations.sql deleted file mode 100644 index c101e7d..0000000 --- a/crates/core/src/database/drivers/sql/version_migrations.sql +++ /dev/null @@ -1,15 +0,0 @@ --- users channel_mutes -ALTER TABLE users -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/layouts.rs b/crates/core/src/database/layouts.rs new file mode 100644 index 0000000..052a733 --- /dev/null +++ b/crates/core/src/database/layouts.rs @@ -0,0 +1,117 @@ +use crate::model::{ + auth::User, + layouts::{Layout, LayoutPage, LayoutPrivacy}, + permissions::FinePermission, + Error, Result, +}; +use crate::{auto_method, DataManager}; +use oiseau::{PostgresRow, execute, get, query_rows, params, cache::Cache}; + +impl DataManager { + /// Get a [`Layout`] from an SQL row. + pub(crate) fn get_layout_from_row(x: &PostgresRow) -> Layout { + Layout { + id: get!(x->0(i64)) as usize, + created: get!(x->1(i64)) as usize, + owner: get!(x->2(i64)) as usize, + title: get!(x->3(String)), + privacy: serde_json::from_str(&get!(x->4(String))).unwrap(), + pages: serde_json::from_str(&get!(x->5(String))).unwrap(), + replaces: serde_json::from_str(&get!(x->6(String))).unwrap(), + } + } + + auto_method!(get_layout_by_id(usize as i64)@get_layout_from_row -> "SELECT * FROM layouts WHERE id = $1" --name="layout" --returns=Layout --cache-key-tmpl="atto.layout:{}"); + + /// Get all layouts by user. + /// + /// # Arguments + /// * `id` - the ID of the user to fetch layouts for + pub async fn get_layouts_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 layouts WHERE owner = $1 ORDER BY created DESC", + &[&(id as i64)], + |x| { Self::get_layout_from_row(x) } + ); + + if res.is_err() { + return Err(Error::GeneralNotFound("layout".to_string())); + } + + Ok(res.unwrap()) + } + + /// Create a new layout in the database. + /// + /// # Arguments + /// * `data` - a mock [`Layout`] object to insert + pub async fn create_layout(&self, data: Layout) -> Result { + // check values + if data.title.len() < 2 { + return Err(Error::DataTooShort("title".to_string())); + } else if data.title.len() > 32 { + return Err(Error::DataTooLong("title".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 layouts VALUES ($1, $2, $3, $4, $5, $6, $7)", + params![ + &(data.id as i64), + &(data.created as i64), + &(data.owner as i64), + &data.title, + &serde_json::to_string(&data.privacy).unwrap(), + &serde_json::to_string(&data.pages).unwrap(), + &serde_json::to_string(&data.replaces).unwrap(), + ] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + Ok(data) + } + + pub async fn delete_layout(&self, id: usize, user: &User) -> Result<()> { + let layout = self.get_layout_by_id(id).await?; + + // check user permission + if user.id != layout.owner && !user.permissions.check(FinePermission::MANAGE_STACKS) { + 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 layouts WHERE id = $1", &[&(id as i64)]); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + // ... + self.0.1.remove(format!("atto.layout:{}", id)).await; + Ok(()) + } + + auto_method!(update_layout_title(&str)@get_layout_by_id:FinePermission::MANAGE_USERS; -> "UPDATE layouts SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.layout:{}"); + auto_method!(update_layout_privacy(LayoutPrivacy)@get_layout_by_id:FinePermission::MANAGE_USERS; -> "UPDATE layouts SET privacy = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.layout:{}"); + auto_method!(update_layout_pages(Vec)@get_layout_by_id:FinePermission::MANAGE_USERS; -> "UPDATE layouts SET pages = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.layout:{}"); +} 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/messages.rs b/crates/core/src/database/messages.rs index 3acb2ee..64157f0 100644 --- a/crates/core/src/database/messages.rs +++ b/crates/core/src/database/messages.rs @@ -190,11 +190,6 @@ impl DataManager { continue; } - let user = self.get_user_by_id(member).await?; - if user.channel_mutes.contains(&channel.id) { - continue; - } - let mut notif = Notification::new( "You've received a new message!".to_string(), format!( diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs index 218bcd6..1009797 100644 --- a/crates/core/src/database/mod.rs +++ b/crates/core/src/database/mod.rs @@ -1,4 +1,3 @@ -pub mod app_data; mod apps; mod audit_log; mod auth; @@ -14,7 +13,7 @@ mod invite_codes; mod ipbans; mod ipblocks; mod journals; -mod letters; +mod layouts; mod memberships; mod message_reactions; mod messages; @@ -23,7 +22,6 @@ mod notifications; mod polls; mod pollvotes; mod posts; -mod products; mod questions; mod reactions; mod reports; diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 0891fed..becb780 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -22,11 +22,10 @@ pub type FullPost = ( User, Community, Option<(User, Post)>, - Option<(Question, User, Option<(User, Post)>)>, + Option<(Question, User)>, Option<(Poll, bool, bool)>, Option, ); -pub type FullQuestion = (Question, User, Option<(User, Post)>); macro_rules! private_post_replying { ($post:ident, $replying_posts:ident, $ua1:ident, $data:ident) => { @@ -225,14 +224,8 @@ impl DataManager { &self, post: &Post, ignore_users: &[usize], - seen_questions: &mut HashMap, - ) -> Result> { + ) -> Result> { if post.context.answering != 0 { - if let Some(q) = seen_questions.get(&post.context.answering) { - return Ok(Some(q.to_owned())); - } - - // ... let question = self.get_question_by_id(post.context.answering).await?; if ignore_users.contains(&question.owner) { @@ -245,11 +238,7 @@ impl DataManager { self.get_user_by_id_with_void(question.owner).await? }; - let asking_about = self.get_question_asking_about(&question).await?; - let full_question = (question, user, asking_about); - - seen_questions.insert(post.context.answering, full_question.to_owned()); - Ok(Some(full_question)) + Ok(Some((question, user))) } else { Ok(None) } @@ -333,7 +322,7 @@ impl DataManager { Post, User, Option<(User, Post)>, - Option<(Question, User, Option<(User, Post)>)>, + Option<(Question, User)>, Option<(Poll, bool, bool)>, Option, )>, @@ -343,7 +332,6 @@ impl DataManager { let mut users: HashMap = HashMap::new(); let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); let mut seen_stacks: HashMap = HashMap::new(); - let mut seen_questions: HashMap = HashMap::new(); let mut replying_posts: HashMap = HashMap::new(); for post in posts { @@ -385,8 +373,7 @@ impl DataManager { post.clone(), ua.clone(), reposting, - self.get_post_question(&post, ignore_users, &mut seen_questions) - .await?, + self.get_post_question(&post, ignore_users).await?, self.get_post_poll(&post, user).await?, stack, )); @@ -397,9 +384,7 @@ impl DataManager { continue; } - if (ua.permissions.check_banned() - | ignore_users.contains(&owner) - | ua.is_deactivated) + if ua.permissions.check_banned() | ignore_users.contains(&owner) && !ua.permissions.check(FinePermission::MANAGE_POSTS) { continue; @@ -469,8 +454,7 @@ impl DataManager { post.clone(), ua, reposting, - self.get_post_question(&post, ignore_users, &mut seen_questions) - .await?, + self.get_post_question(&post, ignore_users).await?, self.get_post_poll(&post, user).await?, stack, )); @@ -493,7 +477,6 @@ impl DataManager { let mut seen_before: HashMap<(usize, usize), (User, Community)> = HashMap::new(); let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new(); let mut seen_stacks: HashMap = HashMap::new(); - let mut seen_questions: HashMap = HashMap::new(); let mut replying_posts: HashMap = HashMap::new(); let mut memberships: HashMap = HashMap::new(); @@ -561,8 +544,7 @@ impl DataManager { ua.clone(), community.to_owned(), reposting, - self.get_post_question(&post, ignore_users, &mut seen_questions) - .await?, + self.get_post_question(&post, ignore_users).await?, self.get_post_poll(&post, user).await?, stack, )); @@ -661,8 +643,7 @@ impl DataManager { ua, community, reposting, - self.get_post_question(&post, ignore_users, &mut seen_questions) - .await?, + self.get_post_question(&post, ignore_users).await?, self.get_post_poll(&post, user).await?, stack, )); @@ -735,12 +716,8 @@ impl DataManager { } // question - if let Some((_, ref mut x, ref mut y)) = post.4 { + if let Some((_, ref mut x)) = post.4 { x.clean(); - - if y.is_some() { - y.as_mut().unwrap().0.clean(); - } } // ... @@ -1475,14 +1452,6 @@ impl DataManager { false }; - // check if we should hide nsfw posts - let mut hide_nsfw: bool = true; - - if let Some(ua) = as_user { - hide_nsfw = !ua.settings.show_nsfw; - } - - // ... let conn = match self.0.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), @@ -1491,17 +1460,12 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE replying_to = 0{}{}{} AND NOT context LIKE '%\"full_unlist\":true%' ORDER BY created DESC LIMIT $1 OFFSET $2", + "SELECT * FROM posts WHERE replying_to = 0{} AND NOT context LIKE '%\"is_nsfw\":true%'{} ORDER BY created DESC LIMIT $1 OFFSET $2", if before_time > 0 { format!(" AND created < {before_time}") } else { String::new() }, - if hide_nsfw { - " AND NOT context LIKE '%\"is_nsfw\":true%'" - } else { - "" - }, if hide_answers { " AND context::jsonb->>'answering' = '0'" } else { @@ -1530,7 +1494,6 @@ impl DataManager { id: usize, batch: usize, page: usize, - user: &User, ) -> Result> { let memberships = self.get_memberships_by_owner(id).await?; let mut memberships = memberships.iter(); @@ -1545,9 +1508,6 @@ impl DataManager { query_string.push_str(&format!(" OR community = {}", membership.community)); } - // check if we should hide nsfw posts - let hide_nsfw: bool = !user.settings.show_nsfw; - // ... let conn = match self.0.connect().await { Ok(c) => c, @@ -1557,13 +1517,8 @@ impl DataManager { let res = query_rows!( &conn, &format!( - "SELECT * FROM posts WHERE (community = {} {query_string}){} AND NOT context LIKE '%\"full_unlist\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", - first.community, - if hide_nsfw { - " AND NOT context LIKE '%\"is_nsfw\":true%'" - } else { - "" - }, + "SELECT * FROM posts WHERE (community = {} {query_string}) AND NOT context LIKE '%\"is_nsfw\":true%' AND replying_to = 0 AND is_deleted = 0 ORDER BY created DESC LIMIT $1 OFFSET $2", + first.community ), &[&(batch as i64), &((page * batch) as i64)], |x| { Self::get_post_from_row(x) } @@ -2002,10 +1957,6 @@ impl DataManager { data.context.is_nsfw = true; } - if owner.settings.auto_full_unlist { - data.context.full_unlist = true; - } - // ... let conn = match self.0.connect().await { Ok(c) => c, @@ -2406,10 +2357,6 @@ impl DataManager { x.is_nsfw = true; } - if user.settings.auto_full_unlist { - x.full_unlist = true; - } - // ... let conn = match self.0.connect().await { Ok(c) => c, diff --git a/crates/core/src/database/products.rs b/crates/core/src/database/products.rs deleted file mode 100644 index 0eab9aa..0000000 --- a/crates/core/src/database/products.rs +++ /dev/null @@ -1,175 +0,0 @@ -use crate::model::{ - auth::User, - permissions::{FinePermission, SecondaryPermission}, - products::{Product, ProductPrice}, - Error, Result, -}; -use crate::{auto_method, DataManager}; -use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow}; - -impl DataManager { - /// Get a [`Product`] from an SQL row. - pub(crate) fn get_product_from_row(x: &PostgresRow) -> Product { - Product { - id: get!(x->0(i64)) as usize, - created: get!(x->1(i64)) as usize, - owner: get!(x->2(i64)) as usize, - name: get!(x->3(String)), - description: get!(x->4(String)), - likes: get!(x->5(i32)) as isize, - dislikes: get!(x->6(i32)) as isize, - product_type: serde_json::from_str(&get!(x->7(String))).unwrap(), - price: serde_json::from_str(&get!(x->8(String))).unwrap(), - uploads: serde_json::from_str(&get!(x->9(String))).unwrap(), - } - } - - auto_method!(get_product_by_id(usize as i64)@get_product_from_row -> "SELECT * FROM products WHERE id = $1" --name="product" --returns=Product --cache-key-tmpl="atto.product:{}"); - - /// Get all products by user. - /// - /// # Arguments - /// * `id` - the ID of the user to fetch products for - /// * `batch` - /// * `page` - pub async fn get_products_by_user( - &self, - id: usize, - batch: usize, - page: 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 products WHERE owner = $1 ORDER BY created DESC LIMIT {} OFFSET {}", - &[&(id as i64), &(batch as i64), &((page * batch) as i64)], - |x| { Self::get_product_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("product".to_string())); - } - - Ok(res.unwrap()) - } - - /// Get all products by user. - /// - /// # Arguments - /// * `id` - the ID of the user to fetch products for - pub async fn get_products_by_user_all(&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 products WHERE owner = $1 ORDER BY created DESC", - &[&(id as i64)], - |x| { Self::get_product_from_row(x) } - ); - - if res.is_err() { - return Err(Error::GeneralNotFound("product".to_string())); - } - - Ok(res.unwrap()) - } - - const MAXIMUM_FREE_PRODUCTS: usize = 15; - - /// Create a new product in the database. - /// - /// # Arguments - /// * `data` - a mock [`Product`] object to insert - pub async fn create_product(&self, data: Product) -> Result { - // check values - if data.name.len() < 2 { - return Err(Error::DataTooShort("name".to_string())); - } else if data.name.len() > 128 { - return Err(Error::DataTooLong("name".to_string())); - } - - // check number of products - let owner = self.get_user_by_id(data.owner).await?; - - if !owner.permissions.check(FinePermission::SUPPORTER) { - let products = self - .get_table_row_count_where("products", &format!("owner = {}", owner.id)) - .await? as usize; - - if products >= Self::MAXIMUM_FREE_PRODUCTS { - return Err(Error::MiscError( - "You already have the maximum number of products you can have".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 products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)", - params![ - &(data.id as i64), - &(data.created as i64), - &(data.owner as i64), - &data.name, - &data.description, - &0_i32, - &0_i32, - &serde_json::to_string(&data.product_type).unwrap(), - &serde_json::to_string(&data.price).unwrap(), - &serde_json::to_string(&data.uploads).unwrap(), - ] - ); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - Ok(data) - } - - pub async fn delete_product(&self, id: usize, user: &User) -> Result<()> { - let product = self.get_product_by_id(id).await?; - - // check user permission - if user.id != product.owner - && !user - .secondary_permissions - .check(SecondaryPermission::MANAGE_PRODUCTS) - { - 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 products WHERE id = $1", &[&(id as i64)]); - - if let Err(e) = res { - return Err(Error::DatabaseError(e.to_string())); - } - - // ... - self.0.1.remove(format!("atto.product:{}", id)).await; - Ok(()) - } - - auto_method!(update_product_name(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); - auto_method!(update_product_description(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET description = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}"); - auto_method!(update_product_price(ProductPrice)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET price = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}"); -} diff --git a/crates/core/src/database/questions.rs b/crates/core/src/database/questions.rs index 3703d4f..1cee527 100644 --- a/crates/core/src/database/questions.rs +++ b/crates/core/src/database/questions.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use oiseau::cache::Cache; use tetratto_shared::unix_epoch_timestamp; use crate::model::addr::RemoteAddr; -use crate::model::communities::Post; use crate::model::communities_permissions::CommunityPermission; use crate::model::uploads::{MediaType, MediaUpload}; use crate::model::{ @@ -39,30 +38,13 @@ impl DataManager { auto_method!(get_question_by_id()@get_question_from_row -> "SELECT * FROM questions WHERE id = $1" --name="question" --returns=Question --cache-key-tmpl="atto.question:{}"); - /// Get the post a given question is asking about. - pub async fn get_question_asking_about( - &self, - question: &Question, - ) -> Result> { - Ok(if let Some(id) = question.context.asking_about { - let post = match self.get_post_by_id(id).await { - Ok(x) => x, - Err(_) => return Ok(None), - }; - - Some((self.get_user_by_id(post.owner).await?, post)) - } else { - None - }) - } - /// Fill the given vector of questions with their owner as well. pub async fn fill_questions( &self, questions: Vec, ignore_users: &[usize], - ) -> Result)>> { - let mut out: Vec<(Question, User, Option<(User, Post)>)> = Vec::new(); + ) -> Result> { + let mut out: Vec<(Question, User)> = Vec::new(); let mut seen_users: HashMap = HashMap::new(); for question in questions { @@ -71,8 +53,7 @@ impl DataManager { } if let Some(ua) = seen_users.get(&question.owner) { - let asking_about = self.get_question_asking_about(&question).await?; - out.push((question, ua.to_owned(), asking_about)); + out.push((question, ua.to_owned())); } else { let user = if question.owner == 0 { User::anonymous() @@ -81,9 +62,7 @@ impl DataManager { }; seen_users.insert(question.owner, user.clone()); - - let asking_about = self.get_question_asking_about(&question).await?; - out.push((question, user, asking_about)); + out.push((question, user)); } } @@ -93,17 +72,12 @@ impl DataManager { /// Filter to update questions to clean their owner for public APIs. pub fn questions_owner_filter( &self, - questions: &Vec<(Question, User, Option<(User, Post)>)>, - ) -> Vec<(Question, User, Option<(User, Post)>)> { - let mut out: Vec<(Question, User, Option<(User, Post)>)> = Vec::new(); + questions: &Vec<(Question, User)>, + ) -> Vec<(Question, User)> { + let mut out: Vec<(Question, User)> = Vec::new(); for mut question in questions.clone() { question.1.clean(); - - if question.2.is_some() { - question.2.as_mut().unwrap().0.clean(); - } - out.push(question); } @@ -387,8 +361,23 @@ impl DataManager { // inherit nsfw status data.context.is_nsfw = community.context.is_nsfw; } else { - // this should be unreachable - return Err(Error::Unknown); + let receiver = self.get_user_by_id(data.receiver).await?; + + if !receiver.settings.enable_questions { + return Err(Error::QuestionsDisabled); + } + + // check for ip block + if self + .get_ipblock_by_initiator_receiver( + receiver.id, + &RemoteAddr::from(data.ip.as_str()), + ) + .await + .is_ok() + { + return Err(Error::NotAllowed); + } } } else { // single @@ -406,18 +395,6 @@ impl DataManager { return Err(Error::DrawingsDisabled); } - // 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); - } - } - // check for ip block if self .get_ipblock_by_initiator_receiver(receiver.id, &RemoteAddr::from(data.ip.as_str())) @@ -428,22 +405,6 @@ impl DataManager { } } - // check asking_about - if let Some(id) = data.context.asking_about { - let post = self.get_post_by_id(id).await?; - let owner = self.get_user_by_id(post.owner).await?; - - if post.stack != 0 { - return Err(Error::MiscError( - "Cannot ask about posts in a circle".to_string(), - )); - } else if owner.settings.private_profile { - return Err(Error::MiscError( - "Cannot ask about posts from a private user".to_string(), - )); - } - } - // create uploads if drawings.len() > 2 { return Err(Error::MiscError( diff --git a/crates/core/src/database/services.rs b/crates/core/src/database/services.rs index adc9bc6..adadf7e 100644 --- a/crates/core/src/database/services.rs +++ b/crates/core/src/database/services.rs @@ -16,7 +16,6 @@ impl DataManager { owner: get!(x->2(i64)) as usize, name: get!(x->3(String)), files: serde_json::from_str(&get!(x->4(String))).unwrap(), - revision: get!(x->5(i64)) as usize, } } @@ -46,7 +45,7 @@ impl DataManager { Ok(res.unwrap()) } - const MAXIMUM_FREE_SERVICES: usize = 10; + const MAXIMUM_FREE_SERVICES: usize = 5; /// Create a new service in the database. /// @@ -81,14 +80,13 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO services VALUES ($1, $2, $3, $4, $5, $6)", + "INSERT INTO services VALUES ($1, $2, $3, $4, $5)", params![ &(data.id as i64), &(data.created as i64), &(data.owner as i64), &data.name, &serde_json::to_string(&data.files).unwrap(), - &(data.created as i64) ] ); @@ -130,5 +128,4 @@ impl DataManager { auto_method!(update_service_name(&str)@get_service_by_id:FinePermission::MANAGE_USERS; -> "UPDATE services SET name = $1 WHERE id = $2" --cache-key-tmpl="atto.service:{}"); auto_method!(update_service_files(Vec)@get_service_by_id:FinePermission::MANAGE_USERS; -> "UPDATE services SET files = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.service:{}"); - auto_method!(update_service_revision(i64) -> "UPDATE services SET revision = $1 WHERE id = $2" --cache-key-tmpl="atto.service:{}"); } diff --git a/crates/core/src/database/stacks.rs b/crates/core/src/database/stacks.rs index cea2be9..6a64b53 100644 --- a/crates/core/src/database/stacks.rs +++ b/crates/core/src/database/stacks.rs @@ -165,11 +165,9 @@ impl DataManager { let owner = self.get_user_by_id(data.owner).await?; if !owner.permissions.check(FinePermission::SUPPORTER) { - let stacks = self - .get_table_row_count_where("stacks", &format!("owner = {}", owner.id)) - .await? as usize; + let stacks = self.get_stacks_by_user(data.owner).await?; - if stacks >= Self::MAXIMUM_FREE_STACKS { + if stacks.len() >= Self::MAXIMUM_FREE_STACKS { return Err(Error::MiscError( "You already have the maximum number of stacks you can have".to_string(), )); diff --git a/crates/core/src/database/uploads.rs b/crates/core/src/database/uploads.rs index f669c53..e3b2cb5 100644 --- a/crates/core/src/database/uploads.rs +++ b/crates/core/src/database/uploads.rs @@ -16,11 +16,10 @@ impl DataManager { created: get!(x->1(i64)) as usize, owner: get!(x->2(i64)) as usize, what: serde_json::from_str(&get!(x->3(String))).unwrap(), - alt: get!(x->4(String)), } } - auto_method!(get_upload_by_id(usize as i64)@get_upload_from_row -> "SELECT * FROM uploads WHERE id = $1" --name="upload" --returns=MediaUpload --cache-key-tmpl="atto.upload:{}"); + auto_method!(get_upload_by_id(usize as i64)@get_upload_from_row -> "SELECT * FROM uploads WHERE id = $1" --name="upload" --returns=MediaUpload --cache-key-tmpl="atto.uploads:{}"); /// Get all uploads (paginated). /// @@ -114,13 +113,12 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO uploads VALUES ($1, $2, $3, $4, $5)", + "INSERT INTO uploads VALUES ($1, $2, $3, $4)", params![ &(data.id as i64), &(data.created as i64), &(data.owner as i64), &serde_json::to_string(&data.what).unwrap().as_str(), - &data.alt, ] ); @@ -189,6 +187,4 @@ impl DataManager { self.0.1.remove(format!("atto.upload:{}", id)).await; Ok(()) } - - auto_method!(update_upload_alt(&str)@get_upload_by_id:FinePermission::MANAGE_UPLOADS; -> "UPDATE uploads SET alt = $1 WHERE id = $2" --cache-key-tmpl="atto.upload:{}"); } diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs index 4b22835..5428f67 100644 --- a/crates/core/src/database/userfollows.rs +++ b/crates/core/src/database/userfollows.rs @@ -195,29 +195,18 @@ impl DataManager { pub async fn fill_userfollows_with_receiver( &self, userfollows: Vec, - as_user: &Option, - do_check: bool, ) -> Result> { let mut out: Vec<(UserFollow, User)> = Vec::new(); for userfollow in userfollows { let receiver = userfollow.receiver; - let user = match self.get_user_by_id(receiver).await { - Ok(u) => u, - Err(_) => continue, - }; - - if user.settings.hide_from_social_lists && do_check { - if let Some(ua) = as_user { - if !ua.permissions.check(FinePermission::MANAGE_USERS) { - continue; - } - } else { - continue; - } - } - - out.push((userfollow, user)); + out.push(( + userfollow, + match self.get_user_by_id(receiver).await { + Ok(u) => u, + Err(_) => continue, + }, + )); } Ok(out) @@ -227,29 +216,18 @@ impl DataManager { pub async fn fill_userfollows_with_initiator( &self, userfollows: Vec, - as_user: &Option, - do_check: bool, ) -> Result> { let mut out: Vec<(UserFollow, User)> = Vec::new(); for userfollow in userfollows { let initiator = userfollow.initiator; - let user = match self.get_user_by_id(initiator).await { - Ok(u) => u, - Err(_) => continue, - }; - - if user.settings.hide_from_social_lists && do_check { - if let Some(ua) = as_user { - if !ua.permissions.check(FinePermission::MANAGE_USERS) { - continue; - } - } else { - continue; - } - } - - out.push((userfollow, user)); + out.push(( + userfollow, + match self.get_user_by_id(initiator).await { + Ok(u) => u, + Err(_) => continue, + }, + )); } Ok(out) @@ -400,13 +378,9 @@ impl DataManager { // decr counts (if we aren't deleting the user OR the user id isn't the deleted user id) if !is_deleting_user | (follow.initiator != user.id) { - if self - .decr_user_following_count(follow.initiator) + self.decr_user_following_count(follow.initiator) .await - .is_err() - { - println!("ERR_TETRATTO_DECR_FOLLOWS: could not decr initiator follow count") - } + .unwrap(); } if !is_deleting_user | (follow.receiver != user.id) { 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. @@ -96,7 +62,7 @@ pub struct ThirdPartyApp { /// if the verifier doesn't match, it won't pass the challenge. /// /// Requests to API endpoints using your grant token should be sent with a - /// cookie (in the `Cookie` or `X-Cookie` header) named `Atto-Grant`. This cookie should + /// cookie (in the `Cookie` header) named `Atto-Grant`. This cookie should /// contain the token you received from either the initial connection, /// or a token refresh. pub redirect: String, @@ -115,12 +81,6 @@ pub struct ThirdPartyApp { /// /// Your app should handle informing users when scopes change. pub scopes: Vec, - /// The app's secret API key (for app_data access). - 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,135 +93,10 @@ 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(), } } } - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AppData { - pub id: usize, - pub app: usize, - pub key: String, - pub value: String, -} - -impl AppData { - /// Create a new [`AppData`]. - pub fn new(app: usize, key: String, value: String) -> Self { - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - app, - key, - value, - } - } - - /// Get the data limit of a given user. - pub fn user_limit(user: &User, app: &ThirdPartyApp) -> usize { - if user - .secondary_permissions - .check(SecondaryPermission::DEVELOPER_PASS) - { - if app.storage_capacity != DeveloperPassStorageQuota::Tier1 { - app.storage_capacity.limit() - } else { - PASS_DATA_LIMIT - } - } else { - FREE_DATA_LIMIT - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum AppDataSelectQuery { - KeyIs(String), - KeyLike(String), - ValueLike(String), - LikeJson(String, String), -} - -impl Display for AppDataSelectQuery { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&match self { - Self::KeyIs(k) => k.to_owned(), - Self::KeyLike(k) => k.to_owned(), - Self::ValueLike(v) => v.to_owned(), - Self::LikeJson(k, v) => format!("%\"{k}\":\"{v}\"%"), - }) - } -} - -impl AppDataSelectQuery { - pub fn selector(&self) -> String { - match self { - AppDataSelectQuery::KeyIs(_) => format!("k = $1"), - AppDataSelectQuery::KeyLike(_) => format!("k LIKE $1"), - AppDataSelectQuery::ValueLike(_) => format!("v LIKE $1"), - AppDataSelectQuery::LikeJson(_, _) => format!("v LIKE $1"), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum AppDataSelectMode { - /// Select a single row (with offset). - One(usize), - /// Select multiple rows at once. - /// - /// `(limit, offset)` - Many(usize, usize), - /// Select multiple rows at once. - /// - /// `(order by top level key, limit, offset)` - ManyJson(String, usize, usize), -} - -impl Display for AppDataSelectMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&match self { - Self::One(offset) => format!("LIMIT 1 OFFSET {offset}"), - Self::Many(limit, offset) => { - format!( - "ORDER BY k DESC LIMIT {} OFFSET {offset}", - if *limit > 24 { 24 } else { *limit } - ) - } - Self::ManyJson(order_by_top_level_key, limit, offset) => { - format!( - "ORDER BY v::jsonb->>'{order_by_top_level_key}' DESC LIMIT {} OFFSET {offset}", - if *limit > 24 { 24 } else { *limit } - ) - } - }) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AppDataQuery { - pub app: usize, - pub query: AppDataSelectQuery, - pub mode: AppDataSelectMode, -} - -impl Display for AppDataQuery { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!( - "SELECT * FROM app_data WHERE app = {} AND %q% {}", - self.app, self.mode - )) - } -} - -#[derive(Serialize, Deserialize)] -pub enum AppDataQueryResult { - One(AppData), - Many(Vec), -} diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 2e45b73..2b47562 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}, @@ -69,29 +70,6 @@ pub struct User { /// used an invite code. #[serde(default)] pub was_purchased: bool, - /// This value is updated for every **new** littleweb browser session. - /// - /// This means the user can only have one of these sessions open at once - /// (unless this token is stored somewhere with a way to say we already have one, - /// but this does not happen yet). - /// - /// Without this token, the user can still use the browser, they just cannot - /// view pages which require authentication (all `$` routes). - #[serde(default)] - pub browser_session: String, - /// Stripe connected account information (for Tetratto marketplace). - #[serde(default)] - pub seller_data: StripeSellerData, - /// The reason the user was banned. - #[serde(default)] - pub ban_reason: String, - /// IDs of channels the user has muted. - #[serde(default)] - pub channel_mutes: Vec, - /// If the user is deactivated. Deactivated users act almost like deleted - /// users, but their data is not wiped. - #[serde(default)] - pub is_deactivated: bool, } pub type UserConnections = @@ -324,31 +302,6 @@ pub struct UserSettings { /// Which tab is shown by default on the user's profile. #[serde(default)] pub default_profile_tab: DefaultProfileTabChoice, - /// If the user is hidden from followers/following tabs. - /// - /// The user will still impact the followers/following numbers, but will not - /// be shown in the UI (or API). - #[serde(default)] - pub hide_from_social_lists: bool, - /// Automatically hide your posts from all timelines except your profile - /// and the following timeline. - #[serde(default)] - pub auto_full_unlist: bool, - /// 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)] -pub struct StripeSellerData { - #[serde(default)] - pub account_id: Option, - #[serde(default)] - pub completed_onboarding: bool, } fn mime_avif() -> String { @@ -394,11 +347,6 @@ impl User { achievements: Vec::new(), awaiting_purchase: false, was_purchased: false, - browser_session: String::new(), - seller_data: StripeSellerData::default(), - ban_reason: String::new(), - channel_mutes: Vec::new(), - is_deactivated: false, } } @@ -573,7 +521,7 @@ pub struct ExternalConnectionData { } /// The total number of achievements needed to 100% Tetratto! -pub const ACHIEVEMENTS: usize = 36; +pub const ACHIEVEMENTS: usize = 34; /// "self-serve" achievements can be granted by the user through the API. pub const SELF_SERVE_ACHIEVEMENTS: &[AchievementName] = &[ AchievementName::OpenReference, @@ -619,8 +567,6 @@ pub enum AchievementName { GetAllOtherAchievements, AcceptProfileWarning, OpenSessionSettings, - CreateSite, - CreateDomain, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -667,8 +613,6 @@ impl AchievementName { Self::GetAllOtherAchievements => "The final performance", Self::AcceptProfileWarning => "I accept the risks!", Self::OpenSessionSettings => "Am I alone in here?", - Self::CreateSite => "Littlewebmaster", - Self::CreateDomain => "LittleDNS", } } @@ -708,8 +652,6 @@ impl AchievementName { Self::GetAllOtherAchievements => "Get every other achievement.", Self::AcceptProfileWarning => "Accept a profile warning.", Self::OpenSessionSettings => "Open your session settings.", - Self::CreateSite => "Create a site.", - Self::CreateDomain => "Create a domain.", } } @@ -751,8 +693,6 @@ impl AchievementName { Self::GetAllOtherAchievements => Rare, Self::AcceptProfileWarning => Common, Self::OpenSessionSettings => Common, - Self::CreateSite => Common, - Self::CreateDomain => Common, } } } diff --git a/crates/core/src/model/communities.rs b/crates/core/src/model/communities.rs index 14f640f..4df9795 100644 --- a/crates/core/src/model/communities.rs +++ b/crates/core/src/model/communities.rs @@ -190,8 +190,6 @@ pub struct PostContext { pub content_warning: String, #[serde(default)] pub tags: Vec, - #[serde(default)] - pub full_unlist: bool, } fn default_comments_enabled() -> bool { @@ -220,7 +218,6 @@ impl Default for PostContext { reactions_enabled: default_reactions_enabled(), content_warning: String::new(), tags: Vec::new(), - full_unlist: false, } } } @@ -387,9 +384,6 @@ pub struct QuestionContext { /// If the owner is shown as anonymous in the UI. #[serde(default)] pub mask_owner: bool, - /// The POST this question is asking about. - #[serde(default)] - pub asking_about: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/crates/core/src/model/layouts.rs b/crates/core/src/model/layouts.rs new file mode 100644 index 0000000..a9d60a4 --- /dev/null +++ b/crates/core/src/model/layouts.rs @@ -0,0 +1,403 @@ +use std::{collections::HashMap, fmt::Display}; +use serde::{Deserialize, Serialize}; +use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; +use crate::model::auth::DefaultTimelineChoice; + +/// Each different page which can be customized. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum CustomizablePage { + Home, + All, + Popular, +} + +/// Layouts allow you to customize almost every page in the Tetratto UI through +/// simple blocks. +#[derive(Serialize, Deserialize)] +pub struct Layout { + pub id: usize, + pub created: usize, + pub owner: usize, + pub title: String, + pub privacy: LayoutPrivacy, + pub pages: Vec, + pub replaces: CustomizablePage, +} + +impl Layout { + /// Create a new [`Layout`]. + pub fn new(title: String, owner: usize, replaces: CustomizablePage) -> Self { + Self { + id: Snowflake::new().to_string().parse::().unwrap(), + created: unix_epoch_timestamp(), + owner, + title, + privacy: LayoutPrivacy::Public, + pages: Vec::new(), + replaces, + } + } +} + +/// The privacy of the layout, which controls who has the ability to view it. +#[derive(Serialize, Deserialize, PartialEq, Eq)] +pub enum LayoutPrivacy { + Public, + Private, +} + +impl Display for Layout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut out = String::new(); + + for (i, page) in self.pages.iter().enumerate() { + let mut x = page.to_string(); + + if i == 0 { + x = x.replace("%?%", ""); + } else { + x = x.replace("%?%", "hidden"); + } + + out.push_str(&x); + } + + f.write_str(&out) + } +} + +/// Layouts are able to contain subpages within them. +/// +/// Each layout is only allowed 2 subpages pages, meaning one main page and one extra. +#[derive(Serialize, Deserialize)] +pub struct LayoutPage { + pub name: String, + pub blocks: Vec, + pub css: String, + pub js: String, +} + +impl Display for LayoutPage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!( + "
    {}
    ", + { + let mut out = String::new(); + + for block in &self.blocks { + out.push_str(&block.to_string()); + } + + out + }, + self.css, + self.js + )) + } +} + +/// Blocks are the basis of each layout page. They are simple and composable. +#[derive(Serialize, Deserialize)] +pub struct LayoutBlock { + pub r#type: BlockType, + pub children: Vec, +} + +impl LayoutBlock { + pub fn render_children(&self) -> String { + let mut out = String::new(); + + for child in &self.children { + out.push_str(&child.to_string()); + } + + out + } +} + +impl Display for LayoutBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut out = String::new(); + + // head + out.push_str(&match self.r#type { + BlockType::Block(ref x) => format!("<{} {}>", x.element, x), + BlockType::Flexible(ref x) => format!("<{} {}>", x.element, x), + BlockType::Markdown(ref x) => format!("<{} {}>", x.element, x), + BlockType::Timeline(ref x) => format!("<{} {}>", x.element, x), + }); + + // body + out.push_str(&match self.r#type { + BlockType::Block(_) => self.render_children(), + BlockType::Flexible(_) => self.render_children(), + BlockType::Markdown(ref x) => x.sub_options.content.to_string(), + BlockType::Timeline(ref x) => { + format!( + "
    ", + x.sub_options.timeline + ) + } + }); + + // tail + out.push_str(&self.r#type.unwrap_cloned().element.tail()); + + // ... + f.write_str(&out) + } +} + +/// Each different type of block has different attributes associated with it. +#[derive(Serialize, Deserialize)] +pub enum BlockType { + Block(GeneralBlockOptions), + Flexible(GeneralBlockOptions), + Markdown(GeneralBlockOptions), + Timeline(GeneralBlockOptions), +} + +impl BlockType { + pub fn unwrap(self) -> GeneralBlockOptions> { + match self { + Self::Block(x) => x.boxed(), + Self::Flexible(x) => x.boxed(), + Self::Markdown(x) => x.boxed(), + Self::Timeline(x) => x.boxed(), + } + } + + pub fn unwrap_cloned(&self) -> GeneralBlockOptions> { + match self { + Self::Block(x) => x.boxed_cloned::(), + Self::Flexible(x) => x.boxed_cloned::(), + Self::Markdown(x) => x.boxed_cloned::(), + Self::Timeline(x) => x.boxed_cloned::(), + } + } +} + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum HtmlElement { + Div, + Span, + Italics, + Bold, + Heading1, + Heading2, + Heading3, + Heading4, + Heading5, + Heading6, + Image, +} + +impl HtmlElement { + pub fn tail(&self) -> String { + match self { + Self::Image => String::new(), + _ => format!(""), + } + } +} + +impl Display for HtmlElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Div => "div", + Self::Span => "span", + Self::Italics => "i", + Self::Bold => "b", + Self::Heading1 => "h1", + Self::Heading2 => "h2", + Self::Heading3 => "h3", + Self::Heading4 => "h4", + Self::Heading5 => "h5", + Self::Heading6 => "h6", + Self::Image => "img", + }) + } +} + +/// This trait is used to provide cloning capabilities to structs which DO implement +/// clone, but we aren't allowed to tell the compiler that they implement clone +/// (through a trait bound), as Clone is not dyn compatible. +/// +/// Implementations for this trait should really just take reference to another +/// value (T), then just run `.to_owned()` on it. This means T and F (Self) MUST +/// be the same type. +pub trait RefFrom { + fn ref_from(value: &T) -> Self; +} + +#[derive(Serialize, Deserialize)] +pub struct GeneralBlockOptions +where + T: Display, +{ + pub element: HtmlElement, + pub class_list: String, + pub id: String, + pub attributes: HashMap, + pub sub_options: T, +} + +impl GeneralBlockOptions { + pub fn boxed(self) -> GeneralBlockOptions> { + GeneralBlockOptions { + element: self.element, + class_list: self.class_list, + id: self.id, + attributes: self.attributes, + sub_options: Box::new(self.sub_options), + } + } + + pub fn boxed_cloned + 'static>( + &self, + ) -> GeneralBlockOptions> { + let x: F = F::ref_from(&self.sub_options); + GeneralBlockOptions { + element: self.element.clone(), + class_list: self.class_list.clone(), + id: self.id.clone(), + attributes: self.attributes.clone(), + sub_options: Box::new(x), + } + } +} + +impl Display for GeneralBlockOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!( + "class=\"{} {}\" {} id={} {}", + self.class_list, + self.sub_options.to_string(), + { + let mut attrs = String::new(); + + for (k, v) in &self.attributes { + attrs.push_str(&format!("{k}=\"{v}\"")); + } + + attrs + }, + self.id, + if self.element == HtmlElement::Image { + "/" + } else { + "" + } + )) + } +} +#[derive(Clone, Serialize, Deserialize)] +pub struct EmptyBlockOptions; + +impl Display for EmptyBlockOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + +impl RefFrom for EmptyBlockOptions { + fn ref_from(value: &EmptyBlockOptions) -> Self { + value.to_owned() + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct FlexibleBlockOptions { + pub gap: FlexibleBlockGap, + pub direction: FlexibleBlockDirection, + pub wrap: bool, + pub collapse: bool, +} + +impl Display for FlexibleBlockOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!( + "flex {} {} {} {}", + self.gap, + self.direction, + if self.wrap { "flex-wrap" } else { "" }, + if self.collapse { "flex-collapse" } else { "" } + )) + } +} + +impl RefFrom for FlexibleBlockOptions { + fn ref_from(value: &FlexibleBlockOptions) -> Self { + value.to_owned() + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum FlexibleBlockGap { + Tight, + Comfortable, + Spacious, + Large, +} + +impl Display for FlexibleBlockGap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Tight => "gap-1", + Self::Comfortable => "gap-2", + Self::Spacious => "gap-3", + Self::Large => "gap-4", + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub enum FlexibleBlockDirection { + Row, + Column, +} + +impl Display for FlexibleBlockDirection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Row => "flex-row", + Self::Column => "flex-col", + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct MarkdownBlockOptions { + pub content: String, +} + +impl Display for MarkdownBlockOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("") + } +} + +impl RefFrom for MarkdownBlockOptions { + fn ref_from(value: &MarkdownBlockOptions) -> Self { + value.to_owned() + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct TimelineBlockOptions { + pub timeline: DefaultTimelineChoice, +} + +impl Display for TimelineBlockOptions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("w-full flex flex-col gap-2\" ui_ident=\"io_data_load") + } +} + +impl RefFrom for TimelineBlockOptions { + fn ref_from(value: &TimelineBlockOptions) -> Self { + value.to_owned() + } +} diff --git a/crates/core/src/model/littleweb.rs b/crates/core/src/model/littleweb.rs index f06154d..479a444 100644 --- a/crates/core/src/model/littleweb.rs +++ b/crates/core/src/model/littleweb.rs @@ -11,7 +11,6 @@ pub struct Service { pub owner: usize, pub name: String, pub files: Vec, - pub revision: usize, } impl Service { @@ -23,7 +22,6 @@ impl Service { owner, name, files: Vec::new(), - revision: unix_epoch_timestamp(), } } @@ -195,8 +193,7 @@ macro_rules! define_domain_tlds { define_domain_tlds!( Bunny, Tet, Cool, Qwerty, Boy, Girl, Them, Quack, Bark, Meow, Silly, Wow, Neko, Yay, Lol, Love, - Fun, Gay, City, Woah, Clown, Apple, Yaoi, Yuri, World, Wav, Zero, Evil, Dragon, Yum, Site, All, - Me, Bug, Slop, Retro, Eye, Neo, Spring, Nurse, Pony + Fun, Gay, City, Woah, Clown, Apple, Yaoi, Yuri, World, Wav, Zero, Evil, Dragon, Yum, Site ); #[derive(Debug, Clone, Serialize, Deserialize)] 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..e825340 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -6,12 +6,11 @@ pub mod channels; pub mod communities; pub mod communities_permissions; pub mod journals; +pub mod layouts; pub mod littleweb; -pub mod mail; pub mod moderation; pub mod oauth; pub mod permissions; -pub mod products; pub mod reactions; pub mod requests; pub mod socket; @@ -52,7 +51,6 @@ pub enum Error { QuestionsDisabled, RequiresSupporter, DrawingsDisabled, - AppHitStorageLimit, Unknown, } @@ -77,7 +75,6 @@ impl Display for Error { Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(), Self::RequiresSupporter => "Only site supporters can do this".to_string(), Self::DrawingsDisabled => "You are not allowed to submit drawings there".to_string(), - Self::AppHitStorageLimit => "This app has already hit its storage limit, or will do so if this data is processed.".to_string(), _ => format!("An unknown error as occurred: ({:?})", self), }) } diff --git a/crates/core/src/model/oauth.rs b/crates/core/src/model/oauth.rs index aa0e00a..07a23c3 100644 --- a/crates/core/src/model/oauth.rs +++ b/crates/core/src/model/oauth.rs @@ -74,8 +74,6 @@ pub enum AppScope { UserReadDomains, /// Read the user's services. UserReadServices, - /// Read the user's products. - UserReadProducts, /// Create posts as the user. UserCreatePosts, /// Create messages as the user. @@ -100,8 +98,6 @@ pub enum AppScope { UserCreateDomains, /// Create services on behalf of the user. UserCreateServices, - /// Create products on behalf of the user. - UserCreateProducts, /// Delete posts owned by the user. UserDeletePosts, /// Delete messages owned by the user. @@ -142,10 +138,6 @@ pub enum AppScope { UserManageDomains, /// Manage the user's services. UserManageServices, - /// Manage the user's products. - UserManageProducts, - /// Manage the user's channel mutes. - UserManageChannelMutes, /// Edit posts created by the user. UserEditPosts, /// Edit drafts created by the user. diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index 61ebb61..55cf9cc 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -176,9 +176,6 @@ bitflags! { const ADMINISTRATOR = 1 << 1; const MANAGE_DOMAINS = 1 << 2; 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/model/products.rs b/crates/core/src/model/products.rs deleted file mode 100644 index 2b90ca5..0000000 --- a/crates/core/src/model/products.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::fmt::Display; - -use serde::{Serialize, Deserialize}; -use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Product { - pub id: usize, - pub created: usize, - pub owner: usize, - pub name: String, - pub description: String, - pub likes: isize, - pub dislikes: isize, - pub product_type: ProductType, - pub price: ProductPrice, - /// Optional uploads to accompany the product title and description. Maximum of 4. - pub uploads: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum ProductType { - /// Text + images. - Data, - /// When a commission product is purchased, the creator will receive a request - /// prompting them to respond with text + images. - /// - /// This is the only product type which does not immediately return data to the - /// customer, as seller input is required. - /// - /// If the request is deleted, the purchase should be immediately refunded. - /// - /// Commissions are paid beforehand to prevent theft. This means it is vital - /// that refunds are enforced. - Commission, -} - -/// A currency. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Currency { - USD, - EUR, - GBP, -} - -impl Display for Currency { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - Currency::USD => "$", - Currency::EUR => "€", - Currency::GBP => "£", - }) - } -} - -/// Price in USD. `(dollars, cents)`. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ProductPrice(u64, u64, Currency); - -impl Display for ProductPrice { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&format!("{}{}.{}", self.2, self.0, self.1)) - } -} - -impl Product { - /// Create a new [`Product`]. - pub fn new( - owner: usize, - name: String, - description: String, - price: ProductPrice, - r#type: ProductType, - ) -> Self { - Self { - id: Snowflake::new().to_string().parse::().unwrap(), - created: unix_epoch_timestamp(), - owner, - name, - description, - likes: 0, - dislikes: 0, - product_type: r#type, - price, - uploads: Vec::new(), - } - } -} diff --git a/crates/core/src/model/uploads.rs b/crates/core/src/model/uploads.rs index bed6dad..35165c6 100644 --- a/crates/core/src/model/uploads.rs +++ b/crates/core/src/model/uploads.rs @@ -44,7 +44,6 @@ pub struct MediaUpload { pub created: usize, pub owner: usize, pub what: MediaType, - pub alt: String, } impl MediaUpload { @@ -55,7 +54,6 @@ impl MediaUpload { created: unix_epoch_timestamp(), owner, what, - alt: String::new(), } } @@ -131,14 +129,9 @@ impl CustomEmoji { if emoji.1 == 0 { out = out.replace( &emoji.0, - match emoji.2.as_str() { - "100" => "💯", - "thumbs_up" => "👍", - "thumbs_down" => "👎", - _ => match emojis::get_by_shortcode(&emoji.2) { - Some(e) => e.as_str(), - None => &emoji.0, - }, + match emojis::get_by_shortcode(&emoji.2) { + Some(e) => e.as_str(), + None => &emoji.0, }, ); } else { diff --git a/crates/core/src/sdk.rs b/crates/core/src/sdk.rs deleted file mode 100644 index 0e5add6..0000000 --- a/crates/core/src/sdk.rs +++ /dev/null @@ -1,348 +0,0 @@ -use crate::model::{ - apps::{ - AppDataQuery, AppDataQueryResult, AppDataSelectMode, AppDataSelectQuery, ThirdPartyApp, - }, - ApiReturn, Error, Result, -}; -use reqwest::{ - multipart::{Form, Part}, - Client as HttpClient, -}; -pub use reqwest::Method; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; - -macro_rules! api_return_ok { - ($ret:ty, $res:ident) => { - match $res.json::>().await { - Ok(x) => { - if x.ok { - Ok(x.payload) - } else { - Err(Error::MiscError(x.message)) - } - } - Err(e) => Err(Error::MiscError(e.to_string())), - } - }; -} - -/// A simplified app data query which matches what the API endpoint actually requires. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SimplifiedQuery { - pub query: AppDataSelectQuery, - pub mode: AppDataSelectMode, -} - -/// The data client is used to access an app's data storage capabilities. -#[derive(Debug, Clone)] -pub struct DataClient { - /// The HTTP client associated with this client. - pub http: HttpClient, - /// The app's API key. You can retrieve this from the web dashboard. - pub api_key: String, - /// The origin of the Tetratto server. When creating with [`DataClient::new`], - /// you can provide `None` to use `https://tetratto.com`. - pub host: String, -} - -impl DataClient { - /// Create a new [`DataClient`]. - pub fn new(host: Option, api_key: String) -> Self { - Self { - http: HttpClient::new(), - api_key, - host: host.unwrap_or("https://tetratto.com".to_string()), - } - } - - /// Get the current app using the provided API key. - /// - /// # Usage - /// ```rust - /// let client = DataClient::new("https://tetratto.com".to_string(), "...".to_string()); - /// let app = client.get_app().await.expect("failed to get app"); - /// ``` - pub async fn get_app(&self) -> Result { - match self - .http - .get(format!("{}/api/v1/app_data/app", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .send() - .await - { - Ok(x) => api_return_ok!(ThirdPartyApp, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// 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 - .http - .post(format!("{}/api/v1/app_data/query", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .json(&query) - .send() - .await - { - Ok(x) => api_return_ok!(AppDataQueryResult, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Insert a key, value pair into the app's data. - pub async fn insert(&self, key: String, value: String) -> Result { - match self - .http - .post(format!("{}/api/v1/app_data", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .json(&serde_json::Value::Object({ - let mut map = serde_json::Map::new(); - map.insert("key".to_string(), serde_json::Value::String(key)); - map.insert("value".to_string(), serde_json::Value::String(value)); - map - })) - .send() - .await - { - Ok(x) => api_return_ok!(String, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Update a record's value given its ID and the new value. - pub async fn update(&self, id: usize, value: String) -> Result<()> { - match self - .http - .post(format!("{}/api/v1/app_data/{id}/value", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .json(&serde_json::Value::Object({ - let mut map = serde_json::Map::new(); - map.insert("value".to_string(), serde_json::Value::String(value)); - map - })) - .send() - .await - { - Ok(x) => api_return_ok!((), x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Update a record's key given its ID and the new key. - pub async fn rename(&self, id: usize, key: String) -> Result<()> { - match self - .http - .post(format!("{}/api/v1/app_data/{id}/key", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .json(&serde_json::Value::Object({ - let mut map = serde_json::Map::new(); - map.insert("key".to_string(), serde_json::Value::String(key)); - map - })) - .send() - .await - { - Ok(x) => api_return_ok!((), x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Delete a row from the app's data by its `id`. - pub async fn remove(&self, id: usize) -> Result<()> { - match self - .http - .delete(format!("{}/api/v1/app_data/{id}", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .send() - .await - { - Ok(x) => api_return_ok!((), x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Delete row(s) from the app's data by a query. - pub async fn remove_query(&self, query: &AppDataQuery) -> Result<()> { - match self - .http - .delete(format!("{}/api/v1/app_data/query", self.host)) - .header("Atto-Secret-Key", &self.api_key) - .json(&query) - .send() - .await - { - Ok(x) => api_return_ok!((), x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } -} - -/// The state of the [`ApiClient`]. -#[derive(Debug, Clone, Default)] -pub struct ApiClientState { - /// The token you received from an app grant request. - pub user_token: String, - /// The verifier you received from an app grant request. - pub user_verifier: String, - /// The ID of the user this client is connecting to. - pub user_id: usize, - /// The ID of the app that is being used for user grants. - /// - /// You can get this from the web dashboard. - pub app_id: usize, -} - -/// The API client is used to manage authentication flow and send requests on behalf of a user. -/// -/// This client assumes you already have the required information for the given user. -/// If you don't, try using the JS SDK to extract this information. -#[derive(Debug, Clone)] -pub struct ApiClient { - /// The HTTP client associated with this client. - pub http: HttpClient, - /// The general state of the client. Will be updated whenever you refresh the user's token. - pub state: ApiClientState, - /// The origin of the Tetratto server. When creating with [`ApiClient::new`], - /// you can provide `None` to use `https://tetratto.com`. - pub host: String, -} - -impl ApiClient { - /// Create a new [`ApiClient`]. - pub fn new(host: Option, state: ApiClientState) -> Self { - Self { - http: HttpClient::new(), - state, - host: host.unwrap_or("https://tetratto.com".to_string()), - } - } - - /// Refresh the client's user_token. - pub async fn refresh_token(&mut self) -> Result { - match self - .http - .post(format!( - "{}/api/v1/auth/user/{}/grants/{}/refresh", - self.host, self.state.user_id, self.state.app_id - )) - .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) - .json(&serde_json::Value::Object({ - let mut map = serde_json::Map::new(); - map.insert( - "verifier".to_string(), - serde_json::Value::String(self.state.user_verifier.to_owned()), - ); - map - })) - .send() - .await - { - Ok(x) => { - let ret = api_return_ok!(String, x)?; - self.state.user_token = ret.clone(); - Ok(ret) - } - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - - /// Send a simple JSON request to the given endpoint. - pub async fn request( - &self, - route: String, - method: Method, - body: Option<&B>, - ) -> Result> - where - T: Serialize + DeserializeOwned, - B: Serialize + ?Sized, - { - if let Some(body) = body { - match self - .http - .request(method, format!("{}/api/v1/auth/{route}", self.host)) - .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) - .json(&body) - .send() - .await - { - Ok(x) => api_return_ok!(ApiReturn, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } else { - match self - .http - .request(method, format!("{}/api/v1/auth/{route}", self.host)) - .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) - .send() - .await - { - Ok(x) => api_return_ok!(ApiReturn, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } - } - - /// Send a JSON request with attachments to the given endpoint. - /// - /// This type of request is only required for routes which use JsonMultipart, - /// such as `POST /api/v1/posts` (`create_post`). - /// - /// Method is locked to `POST` for this type of request. - pub async fn request_attachments( - &self, - route: String, - attachments: Vec>, - body: &B, - ) -> Result> - where - T: Serialize + DeserializeOwned, - B: Serialize + ?Sized, - { - let mut multipart_body = Form::new(); - - // add attachments - for v in attachments.clone() { - // the file name doesn't matter - multipart_body = multipart_body.part(String::new(), Part::bytes(v)); - } - - drop(attachments); - - // add json - multipart_body = multipart_body.part( - String::new(), - Part::text(serde_json::to_string(body).unwrap()), - ); - - // ... - match self - .http - .post(format!("{}/api/v1/auth/{route}", self.host)) - .header("X-Cookie", &format!("Atto-Grant={}", self.state.user_token)) - .multipart(multipart_body) - .send() - .await - { - Ok(x) => api_return_ok!(ApiReturn, x), - Err(e) => Err(Error::MiscError(e.to_string())), - } - } -} diff --git a/crates/l10n/Cargo.toml b/crates/l10n/Cargo.toml index 5993ffc..9544981 100644 --- a/crates/l10n/Cargo.toml +++ b/crates/l10n/Cargo.toml @@ -1,14 +1,12 @@ [package] name = "tetratto-l10n" -description = "Localization for Tetratto" -version = "12.0.0" +version = "11.0.0" edition = "2024" authors.workspace = true repository.workspace = true license.workspace = true -homepage.workspace = true [dependencies] pathbufd = "0.1.4" serde = { version = "1.0.219", features = ["derive"] } -toml = "0.9.2" +toml = "0.8.23" diff --git a/crates/shared/Cargo.toml b/crates/shared/Cargo.toml index a866bd1..633984b 100644 --- a/crates/shared/Cargo.toml +++ b/crates/shared/Cargo.toml @@ -1,20 +1,18 @@ [package] name = "tetratto-shared" -description = "Shared stuff for Tetratto" -version = "12.0.6" +version = "11.0.0" edition = "2024" authors.workspace = true repository.workspace = true license.workspace = true [dependencies] -ammonia = "4.1.1" +ammonia = "4.1.0" 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"] } +serde = "1.0.219" sha2 = "0.10.9" snowflaked = "1.0.3" uuid = { version = "1.17.0", features = ["v4"] } diff --git a/crates/shared/src/hash.rs b/crates/shared/src/hash.rs index a267bc4..f346861 100644 --- a/crates/shared/src/hash.rs +++ b/crates/shared/src/hash.rs @@ -33,18 +33,6 @@ pub fn salt() -> String { .collect() } -pub fn salt_len(len: usize) -> String { - rng() - .sample_iter(&Alphanumeric) - .take(len) - .map(char::from) - .collect() -} - pub fn random_id() -> String { hash(uuid()) } - -pub fn random_id_salted_len(len: usize) -> String { - hash(uuid() + &salt_len(len)) -} 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("