Compare commits

..

No commits in common. "master" and "11.0.0" have entirely different histories.

163 changed files with 2368 additions and 6299 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
/target /target
debug/ debug/
.dev

184
Cargo.lock generated
View file

@ -34,9 +34,9 @@ checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1"
[[package]] [[package]]
name = "ammonia" name = "ammonia"
version = "4.1.1" version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" checksum = "3ada2ee439075a3e70b6992fce18ac4e407cd05aea9ca3f75d2c0b0c20bbb364"
dependencies = [ dependencies = [
"cssparser", "cssparser",
"html5ever", "html5ever",
@ -337,6 +337,12 @@ dependencies = [
"tokio-postgres", "tokio-postgres",
] ]
[[package]]
name = "bberry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee0ee2ee1f1a6094d77ba1bf5402f8a8d66e77f6353aff728e37249b2e77458"
[[package]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.2" version = "0.10.2"
@ -955,15 +961,6 @@ dependencies = [
"version_check", "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]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.1.16" version = "0.1.16"
@ -1127,11 +1124,12 @@ dependencies = [
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.35.0" version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" checksum = "953cbbe631aae7fc0a112702ad5d3aaf09da38beaf45ea84610d6e1c358f569c"
dependencies = [ dependencies = [
"log", "log",
"mac",
"markup5ever", "markup5ever",
"match_token", "match_token",
] ]
@ -1578,17 +1576,6 @@ dependencies = [
"syn 2.0.101", "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]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.11.0" version = "2.11.0"
@ -1753,10 +1740,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]] [[package]]
name = "markup5ever" name = "markdown"
version = "0.35.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"log", "log",
"tendril", "tendril",
@ -1765,9 +1761,9 @@ dependencies = [
[[package]] [[package]]
name = "match_token" name = "match_token"
version = "0.35.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1875,12 +1871,6 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "nanoneo"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1495d19c5bed5372c613d7b4a38e8093b357f4405ce38ba1de2d6586e5c892"
[[package]] [[package]]
name = "native-tls" name = "native-tls"
version = "0.2.14" version = "0.2.14"
@ -2355,25 +2345,6 @@ dependencies = [
"syn 2.0.101", "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]] [[package]]
name = "qoi" name = "qoi"
version = "0.4.1" version = "0.4.1"
@ -2668,9 +2639,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.22" version = "0.12.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@ -2688,7 +2659,6 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"mime_guess",
"native-tls", "native-tls",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
@ -2904,9 +2874,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.141" version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -2955,15 +2925,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -3230,7 +3191,7 @@ dependencies = [
"cfg-expr", "cfg-expr",
"heck", "heck",
"pkg-config", "pkg-config",
"toml 0.8.23", "toml",
"version-compare", "version-compare",
] ]
@ -3288,20 +3249,19 @@ dependencies = [
[[package]] [[package]]
name = "tetratto" name = "tetratto"
version = "12.0.0" version = "11.0.0"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"async-stripe", "async-stripe",
"axum", "axum",
"axum-extra", "axum-extra",
"bberry",
"cf-turnstile", "cf-turnstile",
"contrasted", "contrasted",
"cookie",
"emojis", "emojis",
"futures-util", "futures-util",
"image", "image",
"mime_guess", "mime_guess",
"nanoneo",
"pathbufd", "pathbufd",
"regex", "regex",
"reqwest", "reqwest",
@ -3320,7 +3280,7 @@ dependencies = [
[[package]] [[package]]
name = "tetratto-core" name = "tetratto-core"
version = "12.0.2" version = "11.0.0"
dependencies = [ dependencies = [
"async-recursion", "async-recursion",
"base16ct", "base16ct",
@ -3337,30 +3297,28 @@ dependencies = [
"serde_json", "serde_json",
"tetratto-l10n", "tetratto-l10n",
"tetratto-shared", "tetratto-shared",
"tokio", "toml",
"toml 0.9.2",
"totp-rs", "totp-rs",
] ]
[[package]] [[package]]
name = "tetratto-l10n" name = "tetratto-l10n"
version = "12.0.0" version = "11.0.0"
dependencies = [ dependencies = [
"pathbufd", "pathbufd",
"serde", "serde",
"toml 0.9.2", "toml",
] ]
[[package]] [[package]]
name = "tetratto-shared" name = "tetratto-shared"
version = "12.0.6" version = "11.0.0"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"chrono", "chrono",
"hex_fmt", "hex_fmt",
"pulldown-cmark", "markdown",
"rand 0.9.1", "rand 0.9.1",
"regex",
"serde", "serde",
"sha2", "sha2",
"snowflaked", "snowflaked",
@ -3486,18 +3444,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.46.1" version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
"io-uring",
"libc", "libc",
"mio", "mio",
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"slab",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -3592,26 +3548,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned 0.6.9", "serde_spanned",
"toml_datetime 0.6.11", "toml_datetime",
"toml_edit", "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]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.11" version = "0.6.11"
@ -3621,15 +3562,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.27" version = "0.22.27"
@ -3638,25 +3570,17 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
"serde_spanned 0.6.9", "serde_spanned",
"toml_datetime 0.6.11", "toml_datetime",
"toml_write",
"winnow", "winnow",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_write"
version = "1.0.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]] [[package]]
name = "totp-rs" name = "totp-rs"
@ -3891,6 +3815,12 @@ version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-id"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.18" version = "1.0.18"
@ -3912,12 +3842,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
[[package]]
name = "unicode-width"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"

View file

@ -4,7 +4,6 @@ members = ["crates/app", "crates/shared", "crates/core", "crates/l10n"]
package.authors = ["trisuaso"] package.authors = ["trisuaso"]
package.repository = "https://trisua.com/t/tetratto" package.repository = "https://trisua.com/t/tetratto"
package.license = "AGPL-3.0-or-later" package.license = "AGPL-3.0-or-later"
package.homepage = "https://tetratto.com"
[profile.dev] [profile.dev]
incremental = true incremental = true

View file

@ -1,11 +1,7 @@
[package] [package]
name = "tetratto" name = "tetratto"
version = "12.0.0" version = "11.0.0"
edition = "2024" edition = "2024"
authors.workspace = true
repository.workspace = true
license.workspace = true
homepage.workspace = true
[dependencies] [dependencies]
pathbufd = "0.1.4" pathbufd = "0.1.4"
@ -20,16 +16,17 @@ tower-http = { version = "0.6.6", features = [
"set-header", "set-header",
] } ] }
axum = { version = "0.8.4", features = ["macros", "ws"] } 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"] } axum-extra = { version = "0.10.1", features = ["cookie", "multipart"] }
ammonia = "4.1.1" ammonia = "4.1.0"
tetratto-shared = { path = "../shared" } tetratto-shared = { path = "../shared" }
tetratto-core = { path = "../core" } tetratto-core = { path = "../core" }
tetratto-l10n = { path = "../l10n" } tetratto-l10n = { path = "../l10n" }
image = "0.25.6" image = "0.25.6"
reqwest = { version = "0.12.22", features = ["json", "stream"] } reqwest = { version = "0.12.20", features = ["json", "stream"] }
regex = "1.11.1" regex = "1.11.1"
serde_json = "1.0.141" serde_json = "1.0.140"
mime_guess = "2.0.5" mime_guess = "2.0.5"
cf-turnstile = "0.2.0" cf-turnstile = "0.2.0"
contrasted = "0.1.3" contrasted = "0.1.3"
@ -40,9 +37,7 @@ async-stripe = { version = "0.41.0", features = [
"webhook-events", "webhook-events",
"billing", "billing",
"runtime-tokio-hyper", "runtime-tokio-hyper",
"connect",
] } ] }
emojis = "0.7.0" emojis = "0.7.0"
webp = "0.3.0" webp = "0.3.0"
nanoneo = "0.2.0" bberry = "0.2.0"
cookie = "0.18.1"

View file

@ -1,17 +1,21 @@
use nanoneo::{ use bberry::{
core::element::{Element, Render}, core::element::{Element, Render},
text, read_param, text, read_param,
}; };
use pathbufd::PathBufD; use pathbufd::PathBufD;
use regex::Regex; 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 tera::Context;
use tetratto_core::{ use tetratto_core::{
config::Config, config::Config,
html::{pull_icons, ICONS},
model::{ model::{
auth::{DefaultTimelineChoice, User}, auth::{DefaultTimelineChoice, User},
permissions::{FinePermission, SecondaryPermission}, permissions::FinePermission,
}, },
}; };
use tetratto_l10n::LangFile; 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 ME_JS: &str = include_str!("./public/js/me.js");
pub const STREAMS_JS: &str = include_str!("./public/js/streams.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 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 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 // html
pub const BODY: &str = include_str!("./public/html/body.lisp"); 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_LOGIN: &str = include_str!("./public/html/auth/login.lisp");
pub const AUTH_REGISTER: &str = include_str!("./public/html/auth/register.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_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_BASE: &str = include_str!("./public/html/profile/base.lisp");
pub const PROFILE_POSTS: &str = include_str!("./public/html/profile/posts.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_DOMAIN: &str = include_str!("./public/html/littleweb/domain.lisp");
pub const LITTLEWEB_BROWSER: &str = include_str!("./public/html/littleweb/browser.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 // langs
pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); 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_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_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 const TETRATTO_BUNNY: &[u8] = include_bytes!("./public/images/tetratto_bunny.webp");
pub(crate) static HTML_FOOTER: LazyLock<RwLock<String>> = pub(crate) static HTML_FOOTER: LazyLock<RwLock<String>> =
LazyLock::new(|| RwLock::new(String::new())); LazyLock::new(|| RwLock::new(String::new()));
/// A container for all loaded icons.
pub(crate) static ICONS: LazyLock<RwLock<HashMap<String, String>>> =
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 { macro_rules! vendor_icon {
($name:literal, $icon:ident, $icons_dir:expr) => {{ ($name:literal, $icon:ident, $icons_dir:expr) => {{
let writer = &mut ICONS.write().await; let writer = &mut ICONS.write().await;
@ -205,7 +237,7 @@ pub(crate) async fn replace_in_html(
input.to_string() input.to_string()
} else { } else {
let start = SystemTime::now(); let start = SystemTime::now();
let parsed = nanoneo::parse(input); let parsed = bberry::parse(input);
println!("parsed lisp in {}μs", start.elapsed().unwrap().as_micros()); println!("parsed lisp in {}μs", start.elapsed().unwrap().as_micros());
if let Some(plugins) = plugins { 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); input = input.replace(cap.get(0).unwrap().as_str(), &replace_with);
} }
// icons // icon (with class)
input = pull_icons(input, &config.dirs.icons).await; 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(
"<svg",
&format!("<svg class=\"icon {}\"", cap.get(4).unwrap().as_str()),
);
input = input.replace(
&format!(
"{{{{ icon \"{cap_str}\" c({}) }}}}",
cap.get(4).unwrap().as_str()
),
&icon_text,
);
}
// icon (without class)
let icon_without_class = Regex::new("(\\{\\{)\\s*(icon)\\s*(.*?)\\s*(\\}\\})").unwrap();
for cap in icon_without_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("<svg", "<svg class=\"icon\"");
input = input.replace(&format!("{{{{ icon \"{cap_str}\" }}}}",), &icon_text);
}
// return // return
input = input.replacen("<!-- html_footer_goes_here -->", &format!("{reader}"), 1); input = input.replacen("<!-- html_footer_goes_here -->", &format!("{reader}"), 1);
@ -264,7 +344,6 @@ pub(crate) fn lisp_plugins() -> HashMap<String, Box<dyn FnMut(Element) -> Elemen
pub(crate) async fn write_assets(config: &Config) -> PathBufD { pub(crate) async fn write_assets(config: &Config) -> PathBufD {
vendor_icon!("spotify", VENDOR_SPOTIFY_ICON, config.dirs.icons); vendor_icon!("spotify", VENDOR_SPOTIFY_ICON, config.dirs.icons);
vendor_icon!("last_fm", VENDOR_LAST_FM_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); 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/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/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/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/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); 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/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->"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 html_path
} }
@ -432,11 +508,6 @@ pub(crate) async fn initial_context(
"is_supporter", "is_supporter",
&ua.permissions.check(FinePermission::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()); ctx.insert("home", &ua.settings.default_timeline.relative_url());
} else { } else {
ctx.insert("is_helper", &false); ctx.insert("is_helper", &false);

View file

@ -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,
}
/// <https://docs.rs/axum-extra/latest/src/axum_extra/extract/cookie/mod.rs.html#92-101>
impl<S> FromRequestParts<S> for CookieJar
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self::from_headers(&parts.headers))
}
}
fn cookies_from_request(
header: String,
headers: &HeaderMap,
) -> impl Iterator<Item = Cookie<'static>> + '_ {
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 {
/// <https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.CookieJar.html#method.from_headers>
///
/// 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 }
}
/// <https://docs.rs/axum-extra/latest/axum_extra/extract/cookie/struct.CookieJar.html#method.get>
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
self.jar.get(name)
}
}

View file

@ -32,7 +32,6 @@ version = "1.0.0"
"general:action.copy_link" = "Copy link" "general:action.copy_link" = "Copy link"
"general:action.copy_id" = "Copy ID" "general:action.copy_id" = "Copy ID"
"general:action.post" = "Post" "general:action.post" = "Post"
"general:action.apply" = "Apply"
"general:label.account" = "Account" "general:label.account" = "Account"
"general:label.safety" = "Safety" "general:label.safety" = "Safety"
"general:label.share" = "Share" "general:label.share" = "Share"
@ -131,7 +130,6 @@ version = "1.0.0"
"communities:label.edit_content" = "Edit content" "communities:label.edit_content" = "Edit content"
"communities:label.repost" = "Repost" "communities:label.repost" = "Repost"
"communities:label.quote_post" = "Quote post" "communities:label.quote_post" = "Quote post"
"communities:label.ask_about_this" = "Ask about this"
"communities:label.search_results" = "Search results" "communities:label.search_results" = "Search results"
"communities:label.query" = "Query" "communities:label.query" = "Query"
"communities:label.join_new" = "Join new" "communities:label.join_new" = "Join new"
@ -163,7 +161,6 @@ version = "1.0.0"
"settings:tab.sessions" = "Sessions" "settings:tab.sessions" = "Sessions"
"settings:tab.connections" = "Connections" "settings:tab.connections" = "Connections"
"settings:tab.images" = "Images" "settings:tab.images" = "Images"
"settings:tab.presets" = "Presets"
"settings:label.change_password" = "Change password" "settings:label.change_password" = "Change password"
"settings:label.current_password" = "Current password" "settings:label.current_password" = "Current password"
"settings:label.delete_account" = "Delete account" "settings:label.delete_account" = "Delete account"
@ -183,11 +180,6 @@ version = "1.0.0"
"settings:label.ips" = "IPs" "settings:label.ips" = "IPs"
"settings:label.generate_invites" = "Generate invites" "settings:label.generate_invites" = "Generate invites"
"settings:label.add_to_stack" = "Add to stack" "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.security" = "Security"
"settings:tab.blocks" = "Blocks" "settings:tab.blocks" = "Blocks"
"settings:tab.billing" = "Billing" "settings:tab.billing" = "Billing"
@ -202,7 +194,6 @@ version = "1.0.0"
"mod_panel:label.associations" = "Associations" "mod_panel:label.associations" = "Associations"
"mod_panel:label.invited_by" = "Invited by" "mod_panel:label.invited_by" = "Invited by"
"mod_panel:label.send_debug_payload" = "Send debug payload" "mod_panel:label.send_debug_payload" = "Send debug payload"
"mod_panel:label.ban_reason" = "Ban reason"
"mod_panel:action.send" = "Send" "mod_panel:action.send" = "Send"
"requests:label.requests" = "Requests" "requests:label.requests" = "Requests"
@ -226,8 +217,6 @@ version = "1.0.0"
"chats:action.add_someone" = "Add someone" "chats:action.add_someone" = "Add someone"
"chats:action.kick_member" = "Kick member" "chats:action.kick_member" = "Kick member"
"chats:action.mention_user" = "Mention user" "chats:action.mention_user" = "Mention user"
"chats:action.mute" = "Mute"
"chats:action.unmute" = "Unmute"
"stacks:link.stacks" = "Stacks" "stacks:link.stacks" = "Stacks"
"stacks:label.my_stacks" = "My stacks" "stacks:label.my_stacks" = "My stacks"
@ -240,7 +229,6 @@ version = "1.0.0"
"stacks:label.block_all" = "Block all" "stacks:label.block_all" = "Block all"
"stacks:label.unblock_all" = "Unblock all" "stacks:label.unblock_all" = "Unblock all"
"forge:label.forges" = "Forges"
"forge:label.my_forges" = "My forges" "forge:label.my_forges" = "My forges"
"forge:label.create_new" = "Create new forge" "forge:label.create_new" = "Create new forge"
"forge:tab.info" = "Info" "forge:tab.info" = "Info"
@ -249,7 +237,6 @@ version = "1.0.0"
"forge:action.close" = "Close" "forge:action.close" = "Close"
"developer:label.for_developers" = "for Developers" "developer:label.for_developers" = "for Developers"
"developer:label.apps" = "Apps"
"developer:label.my_apps" = "My apps" "developer:label.my_apps" = "My apps"
"developer:label.create_new" = "Create new app" "developer:label.create_new" = "Create new app"
"developer:label.homepage" = "Homepage" "developer:label.homepage" = "Homepage"
@ -258,13 +245,9 @@ version = "1.0.0"
"developer:label.change_homepage" = "Change homepage" "developer:label.change_homepage" = "Change homepage"
"developer:label.change_redirect" = "Change redirect URL" "developer:label.change_redirect" = "Change redirect URL"
"developer:label.change_quota_status" = "Change quota status" "developer:label.change_quota_status" = "Change quota status"
"developer:label.change_storage_capacity" = "Change storage capacity"
"developer:label.manage_scopes" = "Manage scopes" "developer:label.manage_scopes" = "Manage scopes"
"developer:label.scopes" = "Scopes" "developer:label.scopes" = "Scopes"
"developer:label.guides_and_help" = "Guides & help" "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.delete" = "Delete app"
"developer:action.authorize" = "Authorize" "developer:action.authorize" = "Authorize"
@ -302,9 +285,3 @@ version = "1.0.0"
"littleweb:action.edit_site_name" = "Edit site name" "littleweb:action.edit_site_name" = "Edit site name"
"littleweb:action.rename" = "Rename" "littleweb:action.rename" = "Rename"
"littleweb:action.add" = "Add" "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"

View file

@ -87,10 +87,7 @@ macro_rules! get_user_from_token {
{ {
Ok(ua) => { Ok(ua) => {
if ua.permissions.check_banned() { if ua.permissions.check_banned() {
let mut banned_user = tetratto_core::model::auth::User::banned(); Some(tetratto_core::model::auth::User::banned())
banned_user.ban_reason = ua.ban_reason;
Some(banned_user)
} else { } else {
Some(ua) Some(ua)
} }
@ -112,7 +109,7 @@ macro_rules! get_user_from_token {
Ok((grant, ua)) => { Ok((grant, ua)) => {
if grant.scopes.contains(&$grant_scope) { if grant.scopes.contains(&$grant_scope) {
if ua.permissions.check_banned() { if ua.permissions.check_banned() {
None Some(tetratto_core::model::auth::User::banned())
} else { } else {
Some(ua) Some(ua)
} }
@ -143,20 +140,6 @@ macro_rules! get_user_from_token {
None 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] #[macro_export]
@ -192,27 +175,6 @@ macro_rules! user_banned {
#[macro_export] #[macro_export]
macro_rules! check_user_blocked_or_private { macro_rules! check_user_blocked_or_private {
($user:expr, $other_user:ident, $data:ident, $jar:ident) => { ($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 // check require_account
if $user.is_none() && $other_user.settings.require_account { if $user.is_none() && $other_user.settings.require_account {
return Err(Html( return Err(Html(
@ -440,17 +402,3 @@ macro_rules! ignore_users_gen {
.concat() .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
}
};
}

View file

@ -2,18 +2,13 @@
#![doc(html_favicon_url = "/public/favicon.svg")] #![doc(html_favicon_url = "/public/favicon.svg")]
#![doc(html_logo_url = "/public/tetratto_bunny.webp")] #![doc(html_logo_url = "/public/tetratto_bunny.webp")]
mod assets; mod assets;
mod cookie;
mod image; mod image;
mod macros; mod macros;
mod routes; mod routes;
mod sanitize; mod sanitize;
use assets::{init_dirs, write_assets}; use assets::{init_dirs, write_assets};
use stripe::Client as StripeClient; use tetratto_core::model::{permissions::FinePermission, uploads::CustomEmoji};
use tetratto_core::model::{
permissions::{FinePermission, SecondaryPermission},
uploads::CustomEmoji,
};
pub use tetratto_core::*; pub use tetratto_core::*;
use axum::{ use axum::{
@ -32,17 +27,15 @@ use tracing::{Level, info};
use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc}; use std::{collections::HashMap, env::var, net::SocketAddr, process::exit, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
pub(crate) type InnerState = (DataManager, Tera, Client, Option<StripeClient>); pub(crate) type State = Arc<RwLock<(DataManager, Tera, Client)>>;
pub(crate) type State = Arc<RwLock<InnerState>>;
fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> { fn render_markdown(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(tetratto_shared::markdown::render_markdown( Ok(
&CustomEmoji::replace(value.as_str().unwrap()), tetratto_shared::markdown::render_markdown(&CustomEmoji::replace(value.as_str().unwrap()))
true, .replace("\\@", "@")
.replace("%5C@", "@")
.into(),
) )
.replace("\\@", "@")
.replace("%5C@", "@")
.into())
} }
fn render_emojis(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> { fn render_emojis(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
@ -60,15 +53,6 @@ fn check_supporter(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Va
.into()) .into())
} }
fn check_dev_pass(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(
SecondaryPermission::from_bits(value.as_u64().unwrap() as u32)
.unwrap()
.check(SecondaryPermission::DEVELOPER_PASS)
.into(),
)
}
fn check_staff_badge(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> { fn check_staff_badge(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(FinePermission::from_bits(value.as_u64().unwrap() as u32) Ok(FinePermission::from_bits(value.as_u64().unwrap() as u32)
.unwrap() .unwrap()
@ -123,7 +107,6 @@ async fn main() {
tera.register_filter("markdown", render_markdown); tera.register_filter("markdown", render_markdown);
tera.register_filter("color", color_escape); tera.register_filter("color", color_escape);
tera.register_filter("has_supporter", check_supporter); 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_staff_badge", check_staff_badge);
tera.register_filter("has_banned", check_banned); tera.register_filter("has_banned", check_banned);
tera.register_filter("remove_script_tags", remove_script_tags); tera.register_filter("remove_script_tags", remove_script_tags);
@ -132,13 +115,6 @@ async fn main() {
let client = Client::new(); let client = Client::new();
let mut app = Router::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 // add correct routes
if var("LITTLEWEB").is_ok() { if var("LITTLEWEB").is_ok() {
app = app.merge(routes::lw_routes()); app = app.merge(routes::lw_routes());
@ -147,18 +123,13 @@ async fn main() {
.merge(routes::routes(&config)) .merge(routes::routes(&config))
.layer(SetResponseHeaderLayer::if_not_present( .layer(SetResponseHeaderLayer::if_not_present(
HeaderName::from_static("content-security-policy"), 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 // add junk
app = app app = app
.layer(Extension(Arc::new(RwLock::new(( .layer(Extension(Arc::new(RwLock::new((database, tera, client)))))
database,
tera,
client,
stripe_client,
)))))
.layer(axum::extract::DefaultBodyLimit::max( .layer(axum::extract::DefaultBodyLimit::max(
var("BODY_LIMIT") var("BODY_LIMIT")
.unwrap_or("8388608".to_string()) .unwrap_or("8388608".to_string())

View file

@ -404,7 +404,7 @@ select:focus {
.poll_bar { .poll_bar {
background-color: var(--color-primary); background-color: var(--color-primary);
border-radius: var(--radius); border-radius: var(--radius);
height: 24px; height: 25px;
} }
.poll_option { .poll_option {
@ -413,22 +413,6 @@ select:focus {
overflow-wrap: anywhere; 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"] { input[type="checkbox"] {
--color: #c9b1bc; --color: #c9b1bc;
appearance: none; appearance: none;
@ -599,9 +583,6 @@ input[type="checkbox"]:checked {
border-radius: 6px; border-radius: 6px;
height: max-content; height: max-content;
font-weight: 600; font-weight: 600;
display: flex;
justify-content: center;
align-items: center;
} }
.notification.tr { .notification.tr {
@ -616,11 +597,6 @@ input[type="checkbox"]:checked {
padding: 0; padding: 0;
} }
.notification:not(.chip) .icon {
width: 100%;
height: 100%;
}
/* chip */ /* chip */
.chip { .chip {
background: var(--color-primary); background: var(--color-primary);
@ -955,7 +931,7 @@ dialog::backdrop {
transition: transform 0.15s; transition: transform 0.15s;
} }
.dropdown:has(.inner.open) .dropdown_arrow { .dropdown:has(.inner.open) .dropdown-arrow {
transform: rotateZ(180deg); transform: rotateZ(180deg);
} }
@ -1135,7 +1111,7 @@ details[open] > summary {
margin-bottom: var(--pad-1); margin-bottom: var(--pad-1);
} }
details[open]:not(.accordion) > summary::after { details[open] > summary::after {
top: 0; top: 0;
left: 0; left: 0;
width: 5px; width: 5px;
@ -1158,7 +1134,8 @@ details.accordion {
} }
details.accordion summary { details.accordion summary {
background: var(--color-lowered); background: var(--background);
border: solid 1px var(--color-super-lowered);
border-radius: var(--radius); border-radius: var(--radius);
padding: var(--pad-3) var(--pad-4); padding: var(--pad-3) var(--pad-4);
margin: 0; margin: 0;
@ -1166,15 +1143,11 @@ details.accordion summary {
user-select: none; user-select: none;
} }
details.accordion summary:hover { details.accordion summary .icon {
background: var(--color-super-lowered);
}
details.accordion summary .icon.dropdown_arrow {
transition: transform 0.15s; transition: transform 0.15s;
} }
details.accordion[open] summary .icon.dropdown_arrow { details.accordion[open] summary .icon {
transform: rotateZ(180deg); transform: rotateZ(180deg);
} }
@ -1184,11 +1157,13 @@ details.accordion[open] summary {
} }
details.accordion .inner { details.accordion .inner {
background: var(--color-raised); background: var(--background);
padding: var(--pad-3) var(--pad-4); padding: var(--pad-3) var(--pad-4);
border-radius: var(--radius); border-radius: var(--radius);
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
border: solid 1px var(--color-super-lowered);
border-top: none;
} }
/* codemirror */ /* codemirror */

View file

@ -118,7 +118,7 @@
("class" "hidden lowered card w-full no_p_margin") ("class" "hidden lowered card w-full no_p_margin")
("ui_ident" "purchase_help") ("ui_ident" "purchase_help")
(b (text "What does \"Purchase account\" mean?")) (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."))) (p (text "Alternatively, you can provide an invite code to create your account for free.")))
(text "{%- endif %}") (text "{%- endif %}")
(button (button

View file

@ -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 =
`<b>Account updated.</b> You can now close this tab.`;
}, 1000);"))
(text "{%- endif %} {% endblock %}")

View file

@ -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) => { globalThis.update_channel_title = async (id) => {
await trigger(\"atto::debounce\", [\"channels::update_title\"]); await trigger(\"atto::debounce\", [\"channels::update_title\"]);
const title = await trigger(\"atto::prompt\", [\"New channel title:\"]); const title = await trigger(\"atto::prompt\", [\"New channel title:\"]);

View file

@ -31,22 +31,6 @@
(text "{{ icon \"user-plus\" }}") (text "{{ icon \"user-plus\" }}")
(span (span
(text "{{ text \"chats:action.add_someone\" }}"))) (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 %}") (text "{%- endif %}")
(button (button
("class" "lowered small") ("class" "lowered small")

View file

@ -29,6 +29,7 @@
("minlength" "2") ("minlength" "2")
("maxlength" "32"))) ("maxlength" "32")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (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 %}") (text "{% if list|length >= 4 -%} {{ components::supporter_ad(body=\"Become a supporter to create up to 10 communities!\") }} {%- endif %} {%- endif %}")
(div (div

View file

@ -39,6 +39,7 @@
("class" "flex gap-2") ("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 %}") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
(button (button
("class" "primary")
(text "{{ text \"requests:label.answer\" }}"))))) (text "{{ text \"requests:label.answer\" }}")))))
(text "{%- endif %}") (text "{%- endif %}")
(div (div

View file

@ -28,6 +28,7 @@
("maxlength" "32") ("maxlength" "32")
("value" "{{ text }}"))) ("value" "{{ text }}")))
(button (button
("class" "primary")
(text "{{ text \"dialog:action.continue\" }}")))) (text "{{ text \"dialog:action.continue\" }}"))))
(div (div
("class" "card-nest") ("class" "card-nest")

View file

@ -135,6 +135,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}")))))) (text "{{ text \"general:action.save\" }}"))))))
@ -189,6 +190,7 @@
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
("class" "w-content")) ("class" "w-content"))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}")))) (text "{{ icon \"check\" }}"))))
(div (div
("class" "card-nest") ("class" "card-nest")
@ -211,6 +213,7 @@
("accept" "image/png,image/jpeg,image/avif,image/webp") ("accept" "image/png,image/jpeg,image/avif,image/webp")
("class" "w-content")) ("class" "w-content"))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}"))) (text "{{ icon \"check\" }}")))
(span (span
("class" "fade") ("class" "fade")
@ -242,6 +245,7 @@
("required" "") ("required" "")
("minlength" "18"))) ("minlength" "18")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.select\" }}"))))) (text "{{ text \"communities:action.select\" }}")))))
(div (div
("class" "card flex flex-col gap-2 w-full") ("class" "card flex flex-col gap-2 w-full")
@ -292,6 +296,7 @@
("minlength" "2") ("minlength" "2")
("maxlength" "32"))) ("maxlength" "32")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (text "{{ text \"communities:action.create\" }}"))))
(text "{% for channel in channels %}") (text "{% for channel in channels %}")
(div (div

View file

@ -102,23 +102,13 @@
("class" "flush") ("class" "flush")
("style" "font-weight: 600") ("style" "font-weight: 600")
("target" "_top") ("target" "_top")
(text "{% if user.permissions|has_banned -%}") (text "{{ self::username(user=user) }}"))
(del ("class" "fade") (text "{{ self::username(user=user) }}"))
(text "{% else %}")
(text "{{ self::username(user=user) }}")
(text "{%- endif %}"))
(text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}")
(span (span
("title" "Verified") ("title" "Verified")
("style" "color: var(--color-primary)") ("style" "color: var(--color-primary)")
("class" "flex items-center") ("class" "flex items-center")
(text "{{ icon \"badge-check\" }}")) (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 %}"))
(text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}") (text "{%- endif %} {%- endmacro %} {% macro repost(repost, post, owner, secondary=false, community=false, show_community=true, can_manage_post=false) -%}")
(div (div
@ -128,7 +118,7 @@
(div (div
("class" "card-nest post_outer:{{ post.id }} post_outer") ("class" "card-nest post_outer:{{ post.id }} post_outer")
("is_repost" "{{ is_repost }}") ("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 (div
("class" "card small") ("class" "card small")
(a (a
@ -183,12 +173,6 @@
("class" "flex items-center") ("class" "flex items-center")
("style" "color: var(--color-primary)") ("style" "color: var(--color-primary)")
(text "{{ icon \"square-asterisk\" }}")) (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 -%}") (text "{%- endif %} {% if post.stack -%}")
(a (a
("title" "Posted to a stack you're in") ("title" "Posted to a stack you're in")
@ -237,7 +221,7 @@
("hook" "long") ("hook" "long")
(text "{{ post.title }}")) (text "{{ post.title }}"))
(button ("title" "View post content") ("class" "small lowered") (icon (text "ellipsis")))) (button ("class" "small lowered") (icon (text "ellipsis"))))
(text "{% else %}") (text "{% else %}")
(text "{% if not post.context.content_warning -%}") (text "{% if not post.context.content_warning -%}")
(span (span
@ -331,13 +315,13 @@
("class" "button camo small") ("class" "button camo small")
("target" "_blank") ("target" "_blank")
(text "{{ icon \"external-link\" }}")) (text "{{ icon \"external-link\" }}"))
(text "{% if user -%}")
(div (div
("class" "dropdown") ("class" "dropdown")
(button (button
("class" "camo small") ("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -345,7 +329,6 @@
(b (b
("class" "title") ("class" "title")
(text "{{ text \"general:label.share\" }}")) (text "{{ text \"general:label.share\" }}"))
(text "{% if user -%}")
(button (button
("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])") ("onclick" "trigger('me::repost', ['{{ post.id }}', '', '{{ config.town_square }}', true])")
(text "{{ icon \"repeat-2\" }}") (text "{{ icon \"repeat-2\" }}")
@ -368,16 +351,7 @@
(span (span
(text "BlueSky"))) (text "BlueSky")))
(text "{%- endif %}") (text "{%- endif %}")
(text "{% if owner.settings.enable_questions -%}") (text "{% if user.id != post.owner -%}")
(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 -%}")
(b (b
("class" "title") ("class" "title")
(text "{{ text \"general:label.safety\" }}")) (text "{{ text \"general:label.safety\" }}"))
@ -387,12 +361,12 @@
(text "{{ icon \"flag\" }}") (text "{{ icon \"flag\" }}")
(span (span
(text "{{ text \"general:action.report\" }}"))) (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 (b
("class" "title") ("class" "title")
(text "{{ text \"general:action.manage\" }}")) (text "{{ text \"general:action.manage\" }}"))
; forge stuff ; 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 (button
("class" "green") ("class" "green")
("onclick" "trigger('me::update_open', ['{{ post.id }}', false])") ("onclick" "trigger('me::update_open', ['{{ post.id }}', false])")
@ -408,7 +382,7 @@
(text "{{ text \"forge:action.reopen\" }}"))) (text "{{ text \"forge:action.reopen\" }}")))
(text "{%- endif %} {%- endif %}") (text "{%- endif %} {%- endif %}")
; owner stuff ; owner stuff
(text "{% if user and user.id == post.owner -%}") (text "{% if user.id == post.owner -%}")
(a (a
("href" "/post/{{ post.id }}#/edit") ("href" "/post/{{ post.id }}#/edit")
(text "{{ icon \"pen\" }}") (text "{{ icon \"pen\" }}")
@ -440,7 +414,8 @@
(text "{{ icon \"undo\" }}") (text "{{ icon \"undo\" }}")
(span (span
(text "{{ text \"general:action.restore\" }}"))) (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 "{% 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 -%}") (text "{%- endif %} {%- endmacro %} {% macro post_media(upload_ids) -%} {% if upload_ids|length > 0 -%}")
@ -452,6 +427,7 @@
("alt" "Image upload") ("alt" "Image upload")
("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])")) ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload }}'])"))
(text "{% endfor %}")) (text "{% endfor %}"))
(text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}") (text "{%- endif %} {%- endmacro %} {% macro notification(notification) -%}")
(div (div
("class" "w-full card-nest") ("class" "w-full card-nest")
@ -648,7 +624,7 @@
--{{ css }}: {{ color|color }} !important; --{{ 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 (div
("class" "card question {% if secondary -%}secondary{%- endif %} flex gap-2") ("class" "card question {% if secondary -%}secondary{%- endif %} flex gap-2")
(text "{% if owner.id == 0 or question.context.mask_owner -%}") (text "{% if owner.id == 0 or question.context.mask_owner -%}")
@ -718,10 +694,6 @@
(text "{{ question.content|markdown|safe }}")) (text "{{ question.content|markdown|safe }}"))
; question drawings ; question drawings
(text "{{ self::post_media(upload_ids=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 ; anonymous user ip thing
; this is only shown if the post author is anonymous AND we are a helper ; 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) -%}") (text "{% if is_helper and (owner.id == 0 or question.context.mask_owner) -%}")
@ -758,7 +730,6 @@
("class" "no_p_margin") ("class" "no_p_margin")
(text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}"))) (text "{% if header -%} {{ header|markdown|safe }} {% else %} {{ text \"requests:label.ask_question\" }} {%- endif %}")))
(form (form
("id" "create_question_form")
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
("onsubmit" "create_question_from_form(event)") ("onsubmit" "create_question_from_form(event)")
(div (div
@ -785,6 +756,7 @@
(div (div
("class" "flex gap-2") ("class" "flex gap-2")
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")) (text "{{ text \"communities:action.create\" }}"))
(text "{% if drawing_enabled -%}") (text "{% if drawing_enabled -%}")
@ -828,7 +800,7 @@
}")) }"))
(text "{%- endif %}")) (text "{%- endif %}"))
(text "{% if not is_global and allow_anonymous and user -%}") (text "{% if not is_global and allow_anonymous and not user -%}")
(div (div
("class" "flex gap-2 items-center") ("class" "flex gap-2 items-center")
(input (input
@ -844,15 +816,6 @@
(script (script
(text "globalThis.gerald = null; (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 +=
`<hr /><span class=\"fade\">Asking about: <a href=\"/post/${asking_about}\" target=\"_blank\">${asking_about}</a> <a href=\"?\" class=\"red\">(cancel)</a></span>`;
}
// ...
async function create_question_from_form(e) { async function create_question_from_form(e) {
e.preventDefault(); e.preventDefault();
await trigger(\"atto::debounce\", [\"questions::create\"]); await trigger(\"atto::debounce\", [\"questions::create\"]);
@ -874,8 +837,7 @@
receiver: \"{{ receiver }}\", receiver: \"{{ receiver }}\",
community: \"{{ community }}\", community: \"{{ community }}\",
is_global: \"{{ is_global }}\" == \"true\", is_global: \"{{ is_global }}\" == \"true\",
mask_owner: (e.target.mask_owner || { checked:false }).checked, mask_owner: (e.target.mask_owner || { checked:false }).checked
asking_about,
}), }),
); );
@ -904,7 +866,7 @@
(text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}") (text "{%- endmacro %} {% macro global_question(question, can_manage_questions=false, secondary=false, show_community=true) -%}")
(div (div
("class" "card-nest") ("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 (div
("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}") ("class" "small card flex justify-between flex-wrap gap-2{% if secondary -%} secondary{%- endif %}")
(div (div
@ -931,7 +893,6 @@
("class" "camo small") ("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -1045,7 +1006,6 @@
("class" "camo small") ("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -1148,6 +1108,16 @@
(text "{{ icon \"circle-user-round\" }}") (text "{{ icon \"circle-user-round\" }}")
(span (span
(text "{{ text \"auth:link.my_profile\" }}"))) (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 -%}") (text "{% if not user.settings.disable_achievements -%}")
(a (a
("href" "/achievements") ("href" "/achievements")
@ -1274,7 +1244,6 @@
("class" "camo small square") ("class" "camo small square")
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -1458,9 +1427,7 @@
}); });
})();")) })();"))
(text "{%- endmacro %}") (text "{%- endmacro %} {% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}")
(text "{% macro supporter_ad(body=\"\") -%} {% if config.stripe and not is_supporter %}")
(div (div
("class" "card w-full supporter_ad") ("class" "card w-full supporter_ad")
("ui_ident" "supporter_ad") ("ui_ident" "supporter_ad")
@ -1480,9 +1447,8 @@
(text "{{ icon \"heart\" }}") (text "{{ icon \"heart\" }}")
(span (span
(text "{{ text \"general:action.become_supporter\" }}"))))) (text "{{ text \"general:action.become_supporter\" }}")))))
(text "{%- endif %} {%- endmacro %}")
(text "{% macro create_post_options() -%}") (text "{%- endif %} {%- endmacro %} {% macro create_post_options() -%}")
(div (div
("class" "flex gap-2 flex-wrap") ("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 %}") (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") ("title" "More options")
("onclick" "document.getElementById('post_options_dialog').showModal()") ("onclick" "document.getElementById('post_options_dialog').showModal()")
("type" "button") ("type" "button")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(label (label
@ -1542,7 +1507,6 @@
is_nsfw: false, is_nsfw: false,
content_warning: \"\", content_warning: \"\",
tags: [], tags: [],
full_unlist: false,
}; };
window.BLANK_INITIAL_SETTINGS = JSON.stringify( window.BLANK_INITIAL_SETTINGS = JSON.stringify(
@ -1579,11 +1543,6 @@
// window.POST_INITIAL_SETTINGS.is_nsfw.toString(), // window.POST_INITIAL_SETTINGS.is_nsfw.toString(),
// \"checkbox\", // \"checkbox\",
// ], // ],
[
[\"full_unlist\", \"Unlist from timelines\"],
window.POST_INITIAL_SETTINGS.full_unlist.toString(),
\"checkbox\",
],
[ [
[\"content_warning\", \"Content warning\"], [\"content_warning\", \"Content warning\"],
window.POST_INITIAL_SETTINGS.content_warning, window.POST_INITIAL_SETTINGS.content_warning,
@ -1800,8 +1759,8 @@
(span ("class" "notification chip") (text "{{ total }} votes")) (span ("class" "notification chip") (text "{{ total }} votes"))
(text "{% if not poll[2] -%}") (text "{% if not poll[2] -%}")
(span (span
("class" "notification chip flex items-center gap-1") ("class" "notification chip")
(text "Expires in") (text "Expires in ")
(span (span
("class" "poll_date") ("class" "poll_date")
("data-created" "{{ poll[0].created }}") ("data-created" "{{ poll[0].created }}")
@ -1877,6 +1836,7 @@
("id" "join_or_leave") ("id" "join_or_leave")
(text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}") (text "{% if not is_owner -%} {% if not is_joined -%} {% if not is_pending %}")
(button (button
("class" "primary")
("onclick" "join_community()") ("onclick" "join_community()")
(text "{{ icon \"circle-plus\" }}") (text "{{ icon \"circle-plus\" }}")
(span (span
@ -2090,7 +2050,6 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("style" "width: 32px") ("style" "width: 32px")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -2117,7 +2076,6 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("style" "width: 32px") ("style" "width: 32px")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -2209,7 +2167,6 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("style" "width: 32px") ("style" "width: 32px")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -2289,7 +2246,6 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("style" "width: 32px") ("style" "width: 32px")
("title" "More options")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner") ("class" "inner")
@ -2367,6 +2323,10 @@
(text "Save infinite post drafts")) (text "Save infinite post drafts"))
(li (li
(text "Ability to search through all posts")) (text "Ability to search through all posts"))
(li
(text "Ability to create forges"))
(li
(text "Create more than 1 app"))
(li (li
(text "Create up to 10 stack blocks")) (text "Create up to 10 stack blocks"))
(li (li
@ -2390,16 +2350,18 @@
(sup (a ("href" "#footnote-1") (text "1")))) (sup (a ("href" "#footnote-1") (text "1"))))
(text "{%- endif %}")) (text "{%- endif %}"))
(a (a
("href" "{{ config.stripe.payment_links.supporter }}?client_reference_id={{ user.id }}") ("href" "{{ config.stripe.payment_link }}?client_reference_id={{ user.id }}")
("class" "button") ("class" "button")
("target" "_blank") ("target" "_blank")
(text "Become a supporter ({{ config.stripe.price_texts.supporter }})")) (text "Become a supporter ({{ config.stripe.supporter_price_text }})"))
(span (span
("class" "fade") ("class" "fade")
(text "Please use your") (text "Please use your")
(b (b
(text " real email ")) (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 -%}") (text "{% if config.security.enable_invite_codes -%}")
(span (span
@ -2408,46 +2370,3 @@
(b (text "1: ")) (text "After your account is at least 1 month old")) (b (text "1: ")) (text "After your account is at least 1 month old"))
(text "{%- endif %}") (text "{%- endif %}")
(text "{%- endmacro %}") (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 %}")

View file

@ -10,27 +10,11 @@
(div (div
("id" "manage_fields") ("id" "manage_fields")
("class" "card lowered flex flex-col gap-2") ("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 -%}") (text "{% if is_helper -%}")
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex items-center gap-2") ("class" "card small")
(icon (text "infinity"))
(b (str (text "developer:label.change_quota_status")))) (b (str (text "developer:label.change_quota_status"))))
(div (div
("class" "card") ("class" "card")
@ -44,34 +28,11 @@
("value" "Unlimited") ("value" "Unlimited")
("selected" "{% if app.quota_status == 'Unlimited' -%}true{% else %}false{%- endif %}") ("selected" "{% if app.quota_status == 'Unlimited' -%}true{% else %}false{%- endif %}")
(text "Unlimited"))))) (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 %}") (text "{%- endif %}")
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex items-center gap-2") ("class" "card small")
(icon (text "pencil"))
(b (str (text "developer:label.change_title")))) (b (str (text "developer:label.change_title"))))
(form (form
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
@ -89,14 +50,14 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex items-center gap-2") ("class" "card small")
(icon (text "house"))
(b (str (text "developer:label.change_homepage")))) (b (str (text "developer:label.change_homepage"))))
(form (form
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
@ -114,14 +75,14 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex items-center gap-2") ("class" "card small")
(icon (text "goal"))
(b (str (text "developer:label.change_redirect")))) (b (str (text "developer:label.change_redirect"))))
(form (form
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
@ -139,14 +100,14 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex items-center gap-2") ("class" "card small")
(icon (text "telescope"))
(b (str (text "developer:label.manage_scopes")))) (b (str (text "developer:label.manage_scopes"))))
(form (form
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
@ -179,22 +140,10 @@
(icon (text "external-link")) (text "Docs")))) (icon (text "external-link")) (text "Docs"))))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}"))))) (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"))))))
(div (div
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
(ul (ul
@ -202,8 +151,7 @@
(li (b (text "Redirect URL: ")) (text "{{ app.redirect }}")) (li (b (text "Redirect URL: ")) (text "{{ app.redirect }}"))
(li (b (text "Quota status: ")) (text "{{ app.quota_status }}")) (li (b (text "Quota status: ")) (text "{{ app.quota_status }}"))
(li (b (text "User grants: ")) (text "{{ app.grants }}")) (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 "Grant URL: ")) (text "{{ config.host }}/auth/connections_link/app/{{ app.id }}")))
(li (b (text "App ID (for SDK): ")) (text "{{ app.id }}")))
(a (a
("class" "button") ("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) => { globalThis.change_title = async (e) => {
e.preventDefault(); 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 () => { globalThis.delete_app = async () => {
if ( if (
!(await trigger(\"atto::confirm\", [ !(await trigger(\"atto::confirm\", [

View file

@ -41,19 +41,23 @@
("id" "homepage") ("id" "homepage")
("placeholder" "homepage") ("placeholder" "homepage")
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")
("maxlength" "32")))
(div (div
("class" "flex flex-col gap-1") ("class" "flex flex-col gap-1")
(label (label
("for" "title") ("for" "title")
(text "{{ text \"developer:label.redirect\" }} (optional)")) (text "{{ text \"developer:label.redirect\" }}"))
(input (input
("type" "url") ("type" "url")
("name" "redirect") ("name" "redirect")
("id" "redirect") ("id" "redirect")
("placeholder" "redirect URL") ("placeholder" "redirect URL")
("minlength" "2"))) ("required" "")
("minlength" "2")
("maxlength" "32")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (text "{{ text \"communities:action.create\" }}"))))
; app listing ; app listing
@ -122,7 +126,7 @@
body: JSON.stringify({ body: JSON.stringify({
title: e.target.title.value, title: e.target.title.value,
homepage: e.target.homepage.value, homepage: e.target.homepage.value,
redirect: e.target.redirect.value || \"\", redirect: e.target.redirect.value,
}), }),
}) })
.then((res) => res.json()) .then((res) => res.json())

View file

@ -39,13 +39,6 @@
(str (text "dialog:action.cancel"))))))) (str (text "dialog:action.cancel")))))))
(script (script
(text "setTimeout(() => { (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) => { globalThis.authorize = async (event) => {
if ( if (
!(await trigger(\"atto::confirm\", [ !(await trigger(\"atto::confirm\", [
@ -83,7 +76,6 @@
const search = new URLSearchParams(window.location.search); const search = new URLSearchParams(window.location.search);
search.append(\"verifier\", verifier); search.append(\"verifier\", verifier);
search.append(\"token\", res.payload); search.append(\"token\", res.payload);
search.append(\"uid\", \"{{ user.id }}\");
window.location.href = `{{ app.redirect|remove_script_tags|safe }}?${search.toString()}`; window.location.href = `{{ app.redirect|remove_script_tags|safe }}?${search.toString()}`;
} }

View file

@ -6,7 +6,7 @@
(main (main
("class" "flex flex-col gap-2") ("class" "flex flex-col gap-2")
; create new ; create new
(text "{% if user.secondary_permissions|has_dev_pass -%}") (text "{% if user.permissions|has_supporter -%}")
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
@ -30,9 +30,10 @@
("minlength" "2") ("minlength" "2")
("maxlength" "32"))) ("maxlength" "32")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (text "{{ text \"communities:action.create\" }}"))))
(text "{% else %}") (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 %}") (text "{%- endif %}")
; forge listing ; forge listing

View file

@ -253,6 +253,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}"))))))) (text "{{ text \"general:action.save\" }}")))))))
@ -378,6 +379,7 @@
("name" "tags") ("name" "tags")
("id" "tags") ("id" "tags")
("placeholder" "tags") ("placeholder" "tags")
("required" "")
("minlength" "2") ("minlength" "2")
("maxlength" "128") ("maxlength" "128")
(text "{% for tag in note.tags -%} {{ tag }}, {% endfor %}")) (text "{% for tag in note.tags -%} {{ tag }}, {% endfor %}"))

View file

@ -32,7 +32,7 @@
(input (input
("type" "uri") ("type" "uri")
("class" "w-full") ("class" "w-full")
("true_value" "") ("true_value" "{{ path }}")
("name" "uri") ("name" "uri")
("id" "uri")) ("id" "uri"))
@ -47,7 +47,7 @@
("exclude" "dropdown") ("exclude" "dropdown")
("style" "gap: var(--pad-1) !important") ("style" "gap: var(--pad-1) !important")
(text "{{ components::avatar(username=user.username, size=\"24px\") }}") (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 "{{ components::user_menu() }}"))
(text "{%- endif %}")) (text "{%- endif %}"))
@ -55,7 +55,7 @@
(iframe (iframe
("id" "browser_iframe") ("id" "browser_iframe")
("frameborder" "0") ("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 (style
("data-turbo-temporary" "true") ("data-turbo-temporary" "true")
@ -91,7 +91,6 @@
position: fixed; position: fixed;
width: calc(100dvw - (62px + var(--pad-2) * 2)) !important; width: calc(100dvw - (62px + var(--pad-2) * 2)) !important;
left: var(--pad-2); left: var(--pad-2);
z-index: 2;
} }
} }
@ -102,7 +101,7 @@
height: var(--h); height: var(--h);
min-height: var(--h); min-height: var(--h);
max-height: var(--h); max-height: var(--h);
font-size: 16px; font-size: 14px;
} }
#panel button:not(.inner *), #panel button:not(.inner *),
@ -116,23 +115,18 @@
}")) }"))
(script (script
(text "globalThis.SECRET_SESSION = \"{{ session }}\"; (text "function littleweb_navigate(uri) {
function littleweb_navigate(uri) {
if (!uri.includes(\".html\")) { if (!uri.includes(\".html\")) {
uri = `${uri}/index.html`; uri = `${uri}/index.html`;
} }
// ... if (!uri.startsWith(\"atto://\")) {
console.log(\"navigate\", uri); uri = `atto://${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);
} }
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) => { document.getElementById(\"browser_iframe\").addEventListener(\"load\", (e) => {
@ -157,14 +151,7 @@
if (data.event === \"change_url\") { if (data.event === \"change_url\") {
const uri = new URL(data.target).pathname.slice(\"/api/v1/net/\".length); const uri = new URL(data.target).pathname.slice(\"/api/v1/net/\".length);
window.history.pushState(null, null, `/net/${uri.replace(\"atto://\", \"\")}`); window.history.pushState(null, null, `/net/${uri.replace(\"atto://\", \"\")}`);
document.getElementById(\"uri\").setAttribute(\"true_value\", uri);
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];
} }
}); });
@ -219,9 +206,6 @@
is_focused = false; is_focused = false;
}); });
// navigate document.getElementById(\"uri\").value = document.getElementById(\"uri\").getAttribute(\"true_value\").replace(\"atto://\", \"\").split(\"/\")[0]"))
if ({{ path|length }} > 0) {
littleweb_navigate(\"{{ path|safe }}\");
}"))
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -72,7 +72,7 @@
("class" "card hidden w-full lowered flex flex-col gap-2") ("class" "card hidden w-full lowered flex flex-col gap-2")
("onsubmit" "add_data_from_form(event)") ("onsubmit" "add_data_from_form(event)")
(div (div
("class" "flex gap-2 flex-collapse") ("class" "flex gap-2")
(div (div
("class" "flex w-full flex-col gap-1") ("class" "flex w-full flex-col gap-1")
(label (label
@ -119,48 +119,44 @@
(icon (text "check")) (icon (text "check"))
(str (text "general:action.save"))))) (str (text "general:action.save")))))
; data ; data
(div (table
("class" "w-full") (thead
("style" "max-width: 100%; overflow: auto; min-height: 512px") (tr
(table (th (text "Name"))
("class" "w-full") (th (text "Type"))
(thead (th (text "Value"))
(tr (th (text "Actions"))))
(th (text "Name"))
(th (text "Type"))
(th (text "Value"))
(th (text "Actions"))))
(tbody (tbody
(text "{% for item in domain.data -%}") (text "{% for item in domain.data -%}")
(tr (tr
(td (text "{{ item[0] }}")) (td (text "{{ item[0] }}"))
(text "{% for k,v in item[1] -%}") (text "{% for k,v in item[1] -%}")
(td (text "{{ k }}")) (td (text "{{ k }}"))
(td (text "{{ v }}")) (td (text "{{ v }}"))
(text "{%- endfor %}") (text "{%- endfor %}")
(td (td
("style" "overflow: auto") ("style" "overflow: auto")
(div
("class" "dropdown")
(button
("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
(icon (text "ellipsis")))
(div (div
("class" "dropdown") ("class" "inner")
(button (button
("class" "camo small") ("onclick" "rename_data('{{ item[0] }}')")
("onclick" "trigger('atto::hooks::dropdown', [event])") (icon (text "pencil"))
("exclude" "dropdown") (str (text "littleweb:action.rename")))
(icon (text "ellipsis")))
(div
("class" "inner")
(button
("onclick" "rename_data('{{ item[0] }}')")
(icon (text "pencil"))
(str (text "littleweb:action.rename")))
(button (button
("class" "red") ("class" "red")
("onclick" "remove_data('{{ item[0] }}')") ("onclick" "remove_data('{{ item[0] }}')")
(icon (text "trash")) (icon (text "trash"))
(str (text "general:action.delete"))))))) (str (text "general:action.delete")))))))
(text "{%- endfor %}"))))))) (text "{%- endfor %}"))))))
(script ("id" "domain_data") ("type" "application/json") (text "{{ domain.data|json_encode()|safe }}")) (script ("id" "domain_data") ("type" "application/json") (text "{{ domain.data|json_encode()|safe }}"))
(script (script

View file

@ -5,17 +5,6 @@
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex-col gap-2") ("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 -%}") (text "{% if user -%}")
(div (div
("class" "pillmenu") ("class" "pillmenu")
@ -59,6 +48,7 @@
(option ("value" "{{ tld }}") (text ".{{ tld|lower }}")) (option ("value" "{{ tld }}") (text ".{{ tld|lower }}"))
(text "{%- endfor %}"))) (text "{%- endfor %}")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")) (text "{{ text \"communities:action.create\" }}"))
(details (details

View file

@ -14,24 +14,9 @@
(div (div
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex flex-col gap-2") ("class" "card small")
(div (b
("class" "flex w-full gap-2 justify-between") (text "{{ service.name }}")))
(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."))))
(div (div
("class" "flex gap-2 flex-wrap card") ("class" "flex gap-2 flex-wrap card")
@ -87,57 +72,53 @@
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
(text "{% if not file or file.children|length > 0 -%}") (text "{% if not file or file.children|length > 0 -%}")
; directory browser ; directory browser
(div (table
("class" "w-full") (thead
("style" "max-width: 100%; overflow: auto; min-height: 512px") (tr
(table (th (text "Name"))
("class" "w-full") (th (text "Type"))
(thead (th (text "Children"))
(tr (th (text "Actions"))))
(th (text "Name"))
(th (text "Type"))
(th (text "Children"))
(th (text "Actions"))))
(tbody (tbody
(text "{% for item in files %}") (text "{% for item in files %}")
(tr (tr
(td (td
("class" "flex gap-2 items-center") ("class" "flex gap-2 items-center")
(text "{% if item.children|length > 0 -%}") (text "{% if item.children|length > 0 -%}")
(icon (text "folder")) (icon (text "folder"))
(text "{% else %}") (text "{% else %}")
(icon (text "file")) (icon (text "file"))
(text "{%- endif %}") (text "{%- endif %}")
(a (a
("href" "?path={{ path }}/{{ item.name }}") ("href" "?path={{ path }}/{{ item.name }}")
("data-turbo" "false") ("data-turbo" "false")
(text "{{ item.name }}"))) (text "{{ item.name }}")))
(td (text "{{ item.mime }}")) (td (text "{{ item.mime }}"))
(td (text "{{ item.children|length }}")) (td (text "{{ item.children|length }}"))
(td (td
("style" "overflow: auto") ("style" "overflow: auto")
(div
("class" "dropdown")
(button
("class" "camo small")
("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown")
(icon (text "ellipsis")))
(div (div
("class" "dropdown") ("class" "inner")
(button (button
("class" "camo small") ("onclick" "rename_file('{{ item.id }}')")
("onclick" "trigger('atto::hooks::dropdown', [event])") (icon (text "pencil"))
("exclude" "dropdown") (str (text "littleweb:action.rename")))
(icon (text "ellipsis")))
(div
("class" "inner")
(button
("onclick" "rename_file('{{ item.id }}')")
(icon (text "pencil"))
(str (text "littleweb:action.rename")))
(button (button
("class" "red") ("class" "red")
("onclick" "remove_file('{{ item.id }}')") ("onclick" "remove_file('{{ item.id }}')")
(icon (text "trash")) (icon (text "trash"))
(str (text "general:action.delete"))))))) (str (text "general:action.delete")))))))
(text "{% endfor %}")))) (text "{% endfor %}")))
(text "{% else %}") (text "{% else %}")
; file editor ; file editor
(div ("id" "editor_container") ("class" "w-full") ("style" "height: 600px")) (div ("id" "editor_container") ("class" "w-full") ("style" "height: 600px"))
@ -338,7 +319,6 @@
(text "{% if file and file.mime != 'Plain' -%}") (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/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 ("id" "file_content") ("type" "text/plain") (text "{{ file.content|remove_script_tags|safe }}"))
(script (script
(text "require.config({ paths: { vs: \"https://unpkg.com/monaco-editor@0.52.2/min/vs\" } }); (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\";'; style.innerText = '@import \"https://unpkg.com/monaco-editor@0.52.2/min/vs/editor/editor.main.css\";';
shadow.appendChild(style); shadow.appendChild(style);
emmetMonaco.emmetHTML();
emmetMonaco.emmetCSS();
globalThis.editor = monaco.editor.create(inner, { globalThis.editor = monaco.editor.create(inner, {
value: document.getElementById(\"file_content\").innerText.replaceAll(\"&lt;/script&gt;\", \"</script\" + \">\"), value: document.getElementById(\"file_content\").innerText.replaceAll(\"&lt;/script&gt;\", \"</script\" + \">\"),
language: MIME_MODES[\"{{ file.mime }}\"], language: MIME_MODES[\"{{ file.mime }}\"],
theme: \"vs-dark\", theme: \"vs-dark\",
suggest: {
snippetsPreventQuickSuggestions: false,
},
}); });
});")) });"))
(text "{%- endif %}") (text "{%- endif %}")

View file

@ -5,17 +5,6 @@
(text "{% endblock %} {% block body %} {{ macros::nav() }}") (text "{% endblock %} {% block body %} {{ macros::nav() }}")
(main (main
("class" "flex flex-col gap-2") ("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 -%}") (text "{% if user -%}")
(div (div
("class" "pillmenu") ("class" "pillmenu")
@ -45,6 +34,7 @@
("minlength" "2") ("minlength" "2")
("maxlength" "32"))) ("maxlength" "32")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (text "{{ text \"communities:action.create\" }}"))))
(text "{%- endif %}") (text "{%- endif %}")
(div (div
@ -72,10 +62,7 @@
(span (span
("class" "date") ("class" "date")
(text "{{ item.created }}")) (text "{{ item.created }}"))
(text "; Updated ") (text "; {{ item.files|length }} files")))
(span
("class" "date")
(text "{{ item.revision }}"))))
(text "{% endfor %}")))) (text "{% endfor %}"))))
(script (script

View file

@ -39,6 +39,12 @@
("title" "Create post") ("title" "Create post")
(icon (text "square-pen"))) (icon (text "square-pen")))
(a
("href" "/chats/0/0")
("class" "button {% if selected == 'chats' -%}active{%- endif %}")
("title" "Chats")
(icon (text "message-circle")))
(a (a
("href" "/requests") ("href" "/requests")
("class" "button {% if selected == 'requests' -%}active{%- endif %}") ("class" "button {% if selected == 'requests' -%}active{%- endif %}")
@ -59,43 +65,6 @@
("id" "notifications_span") ("id" "notifications_span")
(text "{{ user.notification_count }}"))) (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 -%}") (text "{% if not hide_user_menu -%}")
(div (div
("class" "dropdown") ("class" "dropdown")
@ -104,9 +73,8 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("style" "gap: var(--pad-1) !important") ("style" "gap: var(--pad-1) !important")
("title" "Account options")
(text "{{ components::avatar(username=user.username, size=\"24px\") }}") (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 "{{ components::user_menu() }}"))
(text "{%- endif %} {% else %}") (text "{%- endif %} {% else %}")
@ -116,7 +84,7 @@
("class" "title") ("class" "title")
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
(icon_class (text "chevron-down") (text "dropdown_arrow"))) (icon_class (text "chevron-down") (text "dropdown-arrow")))
(div (div
("class" "inner") ("class" "inner")
@ -363,17 +331,3 @@
(span (span
(text "{{ text \"settings:tab.connections\" }}"))) (text "{{ text \"settings:tab.connections\" }}")))
(text "{%- endmacro %}") (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 %}")

View file

@ -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 %}")

View file

@ -17,7 +17,7 @@
(p (text "You'll find out what each achievement is when you get it, so look around!")) (p (text "You'll find out what each achievement is when you get it, so look around!"))
(hr) (hr)
(span (b (text "Your progress: ")) (text "{{ percentage|round(method=\"floor\", precision=2) }}%")) (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 (div
("class" "card-nest") ("class" "card-nest")

View file

@ -62,15 +62,12 @@
("class" "card-nest") ("class" "card-nest")
(div (div
("class" "card small flex items-center gap-2") ("class" "card small flex items-center gap-2")
(a (text "{{ icon \"user-plus\" }}")
("href" "/api/v1/auth/user/find/{{ request.id }}")
(text "{{ components::avatar(username=request.id, selector_type=\"id\") }}"))
(span (span
(text "{{ text \"requests:label.user_follow_request\" }}"))) (text "{{ text \"requests:label.user_follow_request\" }}")))
(div (div
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
(span (span
("class" "flex items-center gap-2")
(text "{{ text \"requests:label.user_follow_request_message\" }}")) (text "{{ text \"requests:label.user_follow_request_message\" }}"))
(div (div
("class" "card flex flex-wrap w-full secondary gap-2") ("class" "card flex flex-wrap w-full secondary gap-2")
@ -95,7 +92,7 @@
(text "{%- endif %} {% endfor %} {% for question in questions %}") (text "{%- endif %} {% endfor %} {% for question in questions %}")
(div (div
("class" "card-nest") ("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 (form
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
("onsubmit" "answer_question_from_form(event, '{{ question[0].id }}')") ("onsubmit" "answer_question_from_form(event, '{{ question[0].id }}')")
@ -132,6 +129,7 @@
(text "{{ text \"auth:action.ip_block\" }}"))) (text "{{ text \"auth:action.ip_block\" }}")))
(button (button
("class" "primary")
(text "{{ text \"requests:label.answer\" }}"))))) (text "{{ text \"requests:label.answer\" }}")))))
(text "{% endfor %}"))) (text "{% endfor %}")))

View file

@ -28,6 +28,7 @@
("required" "") ("required" "")
("minlength" "16"))) ("minlength" "16")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))) (text "{{ text \"communities:action.create\" }}")))))
(script (script

View file

@ -50,18 +50,6 @@
(span (span
("class" "notification") ("class" "notification")
(text "{{ profile.request_count }}"))) (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 (button
("class" "red lowered") ("class" "red lowered")
("onclick" "delete_account(event)") ("onclick" "delete_account(event)")
@ -84,7 +72,7 @@
const ui = await ns(\"ui\"); const ui = await ns(\"ui\");
const element = document.getElementById(\"mod_options\"); 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 (do_confirm) {
if ( if (
!(await trigger(\"atto::confirm\", [ !(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\"]); ui.refresh_container(element, [\"actions\"]);
setTimeout(() => { setTimeout(() => {
@ -212,21 +173,11 @@
\"{{ profile.awaiting_purchase }}\", \"{{ profile.awaiting_purchase }}\",
\"checkbox\", \"checkbox\",
], ],
[
[\"is_deactivated\", \"Is deactivated\"],
\"{{ profile.is_deactivated }}\",
\"checkbox\",
],
[ [
[\"role\", \"Permission level\"], [\"role\", \"Permission level\"],
\"{{ profile.permissions }}\", \"{{ profile.permissions }}\",
\"input\", \"input\",
], ],
[
[\"secondary_role\", \"Secondary permission level\"],
\"{{ profile.secondary_permissions }}\",
\"input\",
],
], ],
null, null,
{ {
@ -240,17 +191,9 @@
awaiting_purchase: value, awaiting_purchase: value,
}); });
}, },
is_deactivated: (value) => {
profile_request(false, \"deactivated\", {
is_deactivated: value,
});
},
role: (new_role) => { role: (new_role) => {
return update_user_role(new_role); return update_user_role(new_role);
}, },
secondary_role: (new_role) => {
return update_user_secondary_role(new_role);
},
}, },
); );
}, 100); }, 100);
@ -283,32 +226,6 @@
("class" "card lowered flex flex-wrap gap-2") ("class" "card lowered flex flex-wrap gap-2")
(text "{{ components::user_plate(user=invite[0], show_menu=false) }}"))) (text "{{ components::user_plate(user=invite[0], show_menu=false) }}")))
(text "{%- endif %}") (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 (div
("class" "card-nest w-full") ("class" "card-nest w-full")
(div (div
@ -327,24 +244,6 @@
(div (div
("class" "card lowered flex flex-col gap-2") ("class" "card lowered flex flex-col gap-2")
("id" "permission_builder"))) ("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 (script
(text "setTimeout(async () => { (text "setTimeout(async () => {
const get_permissions_html = await trigger( const get_permissions_html = await trigger(
@ -392,33 +291,6 @@
Number.parseInt(\"{{ profile.permissions }}\"), Number.parseInt(\"{{ profile.permissions }}\"),
\"permission_builder\", \"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);"))) }, 250);")))
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -37,6 +37,7 @@
("minlength" "2") ("minlength" "2")
("maxlength" "4096"))) ("maxlength" "4096")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (text "{{ text \"communities:action.create\" }}"))))
(div (div
("class" "card-nest") ("class" "card-nest")

View file

@ -71,6 +71,7 @@
("name" "content") ("name" "content")
("id" "content") ("id" "content")
("placeholder" "content") ("placeholder" "content")
("required" "")
("minlength" "2") ("minlength" "2")
("maxlength" "4096"))) ("maxlength" "4096")))
(div (div
@ -80,6 +81,7 @@
("class" "flex gap-2") ("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 %}") (text "{{ components::emoji_picker(element_id=\"content\", render_dialog=true) }} {% if is_supporter -%} {{ components::file_picker(files_list_id=\"files_list\") }} {% endif %}")
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}"))))) (text "{{ text \"communities:action.create\" }}")))))
(text "{%- endif %}") (text "{%- endif %}")
(div (div
@ -123,6 +125,7 @@
(text "{{ icon \"settings\" }}") (text "{{ icon \"settings\" }}")
(span (span
(text "{{ text \"communities:action.configure\" }}")))) (text "{{ text \"communities:action.configure\" }}"))))
(text "{%- endif %}")
(div (div
("class" "flex flex-col gap-2 hidden") ("class" "flex flex-col gap-2 hidden")
("data-tab" "configure") ("data-tab" "configure")
@ -198,7 +201,7 @@
\"checkbox\", \"checkbox\",
], ],
[ [
[\"is_nsfw\", \"Mark as NSFW\"], [\"is_nsfw\", \"Hide from public timelines\"],
\"{{ community.context.is_nsfw }}\", \"{{ community.context.is_nsfw }}\",
\"checkbox\", \"checkbox\",
], ],
@ -207,11 +210,6 @@
settings.content_warning, settings.content_warning,
\"textarea\", \"textarea\",
], ],
[
[\"full_unlist\", \"Unlist from timelines\"],
\"{{ user.settings.auto_full_unlist }}\",
\"checkbox\",
],
[ [
[\"tags\", \"Tags\"], [\"tags\", \"Tags\"],
settings.tags.join(\", \"), settings.tags.join(\", \"),
@ -247,7 +245,6 @@
}, },
}); });
}, 250);"))) }, 250);")))
(text "{%- endif %}")
(text "{% if user and user.id == post.owner -%}") (text "{% if user and user.id == post.owner -%}")
(div (div
("class" "card-nest w-full hidden") ("class" "card-nest w-full hidden")
@ -278,6 +275,7 @@
("class" "flex gap-2") ("class" "flex gap-2")
(text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}") (text "{{ components::emoji_picker(element_id=\"new_content\", render_dialog=false) }}")
(button (button
("class" "primary")
(text "{{ text \"general:action.save\" }}"))))) (text "{{ text \"general:action.save\" }}")))))
(script (script
(text "async function edit_post_from_form(e) { (text "async function edit_post_from_form(e) {

View file

@ -72,25 +72,19 @@
("style" "color: var(--color-primary);") ("style" "color: var(--color-primary);")
("class" "flex items-center") ("class" "flex items-center")
(text "{{ icon \"badge-check\" }}")) (text "{{ icon \"badge-check\" }}"))
(text "{%- endif %} {% if profile.permissions|has_supporter -%}") (text "{%- endif %} {% if profile.permissions|has_supporter -%}")
(span (span
("title" "Supporter") ("title" "Supporter")
("style" "color: var(--color-primary);") ("style" "color: var(--color-primary);")
("class" "flex items-center") ("class" "flex items-center")
(text "{{ icon \"star\" }}")) (text "{{ icon \"star\" }}"))
(text "{%- endif %} {% if profile.secondary_permissions|has_dev_pass -%}") (text "{%- endif %} {% if profile.permissions|has_staff_badge -%}")
(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 -%}")
(span (span
("title" "Staff") ("title" "Staff")
("style" "color: var(--color-primary);") ("style" "color: var(--color-primary);")
("class" "flex items-center") ("class" "flex items-center")
(text "{{ icon \"shield-user\" }}")) (text "{{ icon \"shield-user\" }}"))
(text "{%- endif %} {% if profile.permissions|has_banned -%}") (text "{%- endif %} {% if profile.permissions|has_banned -%}")
(span (span
("title" "Banned") ("title" "Banned")
("style" "color: var(--color-primary);") ("style" "color: var(--color-primary);")
@ -107,7 +101,6 @@
(p (p
(text "{{ profile.settings.status }}")) (text "{{ profile.settings.status }}"))
(text "{%- endif %}") (text "{%- endif %}")
(text "{% if not profile.settings.hide_social_follows or (user and user.id == profile.id) -%}")
(div (div
("class" "w-full flex") ("class" "w-full flex")
(a (a
@ -124,7 +117,6 @@
(text "{{ profile.following_count }}")) (text "{{ profile.following_count }}"))
(span (span
(text "{{ text \"auth:label.following\" }}")))) (text "{{ text \"auth:label.following\" }}"))))
(text "{%- endif %}")
(text "{% if is_following_you -%}") (text "{% if is_following_you -%}")
(b (b
("class" "notification chip w-content flex items-center gap-2") ("class" "notification chip w-content flex items-center gap-2")
@ -233,7 +225,7 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("class" "lowered red") ("class" "lowered red")
(icon_class (text "chevron-down") (text "dropdown_arrow")) (icon_class (text "chevron-down") (text "dropdown-arrow"))
(str (text "auth:action.block"))) (str (text "auth:action.block")))
(div (div
("class" "inner left") ("class" "inner left")
@ -298,7 +290,7 @@
]); ]);
fetch( fetch(
\"/api/v1/auth/user/{{ profile.id }}/follow/toggle\", \"/api/v1/auth/user/{{ profile.id }}/follow\",
{ {
method: \"POST\", method: \"POST\",
}, },

View file

@ -30,7 +30,7 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("class" "lowered red") ("class" "lowered red")
(icon_class (text "chevron-down") (text "dropdown_arrow")) (icon_class (text "chevron-down") (text "dropdown-arrow"))
(str (text "auth:action.block"))) (str (text "auth:action.block")))
(div (div
("class" "inner left") ("class" "inner left")

View file

@ -20,11 +20,7 @@
(div (div
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
(span (span
("class" "fade")
(text "{{ text \"auth:label.private_profile_message\" }}")) (text "{{ text \"auth:label.private_profile_message\" }}"))
(span
("class" "no_p_margin")
(text "{{ profile.settings.private_biography|markdown|safe }}"))
(div (div
("class" "card w-full secondary flex gap-2") ("class" "card w-full secondary flex gap-2")
(text "{% if user -%} {% if not is_following -%}") (text "{% if user -%} {% if not is_following -%}")
@ -35,7 +31,6 @@
(text "{{ icon \"user-plus\" }}") (text "{{ icon \"user-plus\" }}")
(span (span
(text "{{ text \"auth:action.request_to_follow\" }}"))) (text "{{ text \"auth:action.request_to_follow\" }}")))
(text "{% if follow_requested -%}")
(button (button
("onclick" "cancel_follow_user(event)") ("onclick" "cancel_follow_user(event)")
("class" "lowered red{% if not follow_requested -%} hidden{%- endif %}") ("class" "lowered red{% if not follow_requested -%} hidden{%- endif %}")
@ -43,7 +38,7 @@
(text "{{ icon \"user-minus\" }}") (text "{{ icon \"user-minus\" }}")
(span (span
(text "{{ text \"auth:action.cancel_follow_request\" }}"))) (text "{{ text \"auth:action.cancel_follow_request\" }}")))
(text "{%- endif %} {% else %}") (text "{% else %}")
(button (button
("onclick" "toggle_follow_user(event)") ("onclick" "toggle_follow_user(event)")
("class" "lowered red") ("class" "lowered red")
@ -58,7 +53,7 @@
("onclick" "trigger('atto::hooks::dropdown', [event])") ("onclick" "trigger('atto::hooks::dropdown', [event])")
("exclude" "dropdown") ("exclude" "dropdown")
("class" "lowered red") ("class" "lowered red")
(icon_class (text "chevron-down") (text "dropdown_arrow")) (icon_class (text "chevron-down") (text "dropdown-arrow"))
(str (text "auth:action.block"))) (str (text "auth:action.block")))
(div (div
("class" "inner left") ("class" "inner left")
@ -81,7 +76,7 @@
(script (script
(text "globalThis.toggle_follow_user = async (e) => { (text "globalThis.toggle_follow_user = async (e) => {
await trigger(\"atto::debounce\", [\"users::follow\"]); 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\", method: \"POST\",
}) })
.then((res) => res.json()) .then((res) => res.json())

View file

@ -35,87 +35,6 @@
(text "{{ macros::profile_settings_nav_options() }}")) (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 (div
("class" "w-full flex flex-col gap-2") ("class" "w-full flex flex-col gap-2")
("data-tab" "account") ("data-tab" "account")
@ -137,12 +56,6 @@
(text "{{ icon \"rss\" }}") (text "{{ icon \"rss\" }}")
(span (span
(text "{{ text \"auth:label.following\" }}"))) (text "{{ text \"auth:label.following\" }}")))
(a
("data-tab-button" "account/followers")
("href" "#/account/followers")
(text "{{ icon \"rss\" }}")
(span
(text "{{ text \"auth:label.followers\" }}")))
(a (a
("data-tab-button" "account/blocks") ("data-tab-button" "account/blocks")
("href" "#/account/blocks") ("href" "#/account/blocks")
@ -221,16 +134,15 @@
("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}") ("selected" "{% if home == '/all/questions' -%}true{% else %}false{%- endif %}")
(text "All (questions)")) (text "All (questions)"))
(text "{% for stack in stacks %}") (text "{% for stack in stacks %}")
(text "<option (option
value='{\"Stack\":\"{{ stack.id }}\"}' ("value" "{\\\"Stack\\\":\\\"{{ stack.id }}\\\"}")
selected=\"{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}\" ("selected" "{% if home is ending_with(stack.id|as_str) -%}true{% else %}false{%- endif %}")
> (text "{{ stack.name }} (stack)"))
{{ stack.name }} (stack)
</option>")
(text "{% endfor %}")) (text "{% endfor %}"))
(span (span
("class" "fade") ("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 (div
("class" "card-nest desktop") ("class" "card-nest desktop")
("ui_ident" "notifications") ("ui_ident" "notifications")
@ -276,6 +188,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}")))))) (text "{{ text \"general:action.save\" }}"))))))
@ -284,50 +197,30 @@
("ui_ident" "delete_account") ("ui_ident" "delete_account")
(div (div
("class" "card small flex items-center gap-2 red") ("class" "card small flex items-center gap-2 red")
(icon (text "skull")) (text "{{ icon \"skull\" }}")
(b (str (text "communities:label.danger_zone")))) (b
(div (text "{{ text \"settings:label.delete_account\" }}")))
("class" "card lowered flex flex-col gap-2") (form
(details ("class" "card flex flex-col gap-2")
("class" "accordion") ("onsubmit" "delete_account(event)")
(summary (div
("class" "flex items-center gap-2") ("class" "flex flex-col gap-1")
(icon_class (text "chevron-down") (text "dropdown_arrow")) (label
(str (text "settings:label.deactivate_account"))) ("for" "current_password")
(div (text "{{ text \"settings:label.current_password\" }}"))
("class" "inner flex flex-col gap-2") (input
(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.")) ("type" "password")
(button ("name" "current_password")
("onclick" "deactivate_account()") ("id" "current_password")
(icon (text "lock")) ("placeholder" "current_password")
(span ("required" "")
(str (text "settings:label.deactivate")))))) ("minlength" "6")
(details ("autocomplete" "off")))
("class" "accordion") (button
(summary ("class" "primary")
("class" "flex items-center gap-2") (text "{{ icon \"trash\" }}")
(icon_class (text "chevron-down") (text "dropdown_arrow")) (span
(str (text "settings:label.delete_account"))) (text "{{ text \"general:action.delete\" }}")))))
(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\" }}")))))))
(button (button
("onclick" "save_settings()") ("onclick" "save_settings()")
("id" "save_button") ("id" "save_button")
@ -438,6 +331,7 @@
("minlength" "6") ("minlength" "6")
("autocomplete" "off"))) ("autocomplete" "off")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}"))))))))) (text "{{ text \"general:action.save\" }}")))))))))
@ -481,7 +375,7 @@
(text "{{ icon \"external-link\" }}") (text "{{ icon \"external-link\" }}")
(span (span
(text "{{ text \"requests:action.view_profile\" }}"))))) (text "{{ text \"requests:action.view_profile\" }}")))))
(text "{% endfor %} {{ components::pagination(page=page, items=following|length, key=\"#/account/following\") }}")))) (text "{% endfor %}"))))
(script (script
(text "globalThis.toggle_follow_user = async (uid) => { (text "globalThis.toggle_follow_user = async (uid) => {
await trigger(\"atto::debounce\", [\"users::follow\"]); 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 (div
("class" "w-full flex flex-col gap-2 hidden") ("class" "w-full flex flex-col gap-2 hidden")
("data-tab" "account/blocks") ("data-tab" "account/blocks")
@ -654,51 +492,32 @@
(div (div
("class" "card flex flex-col gap-2 secondary") ("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 %}") (text "{{ components::supporter_ad(body=\"Become a supporter to upload images directly to posts!\") }} {% for upload in uploads %}")
(details (div
("class" "accordion w-full") ("class" "card flex flex-wrap gap-2 items-center justify-between")
(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 (div
("class" "inner flex flex-col gap-2") ("class" "flex gap-2 items-center")
(form ("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])")
("class" "card lowered flex flex-col gap-2") ("style" "cursor: pointer")
("onsubmit" "update_upload_alt(event, '{{ upload.id }}')") (text "{{ icon \"file-image\" }}")
(div (b
("class" "flex flex-col gap-1") (span
(label ("for" "alt_{{ upload.id }}") (b (str (text "settings:label.alt_text")))) ("class" "date")
(textarea (text "{{ upload.created }}"))
("id" "alt_{{ upload.id }}") (text "({{ upload.what }})")))
("name" "alt") (div
("class" "w-full") ("class" "flex gap-2")
("placeholder" "Alternative text") (button
(text "{{ upload.alt|safe }}"))) ("class" "lowered small")
("onclick" "trigger('ui::lightbox_open', ['/api/v1/uploads/{{ upload.id }}'])")
(button (text "{{ icon \"view\" }}")
(icon (text "check")) (span
(str (text "general:action.save")))))) (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\") }}") (text "{% endfor %} {{ components::pagination(page=page, items=uploads|length, key=\"#/account/uploads\") }}")
(script (script
(text "globalThis.remove_upload = async (id) => { (text "globalThis.remove_upload = async (id) => {
@ -720,26 +539,6 @@
res.message, 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 -%}") (text "{% if config.security.enable_invite_codes -%}")
@ -844,29 +643,6 @@
(div (div
("class" "card flex flex-col gap-2 secondary") ("class" "card flex flex-col gap-2 secondary")
(text "{% if config.stripe -%}") (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 (div
("class" "card-nest") ("class" "card-nest")
("ui_ident" "supporter_card") ("ui_ident" "supporter_card")
@ -876,33 +652,28 @@
(b (b
(text "Supporter status"))) (text "Supporter status")))
(div (div
("class" "card flex flex-col gap-2 no_p_margin") ("class" "card flex flex-col gap-2")
(text "{% if is_supporter -%}") (text "{% if is_supporter -%}")
(p (p
(text "You ") (text "You ")
(b (text "are ")) (b
(text "a supporter! Thank you for all that you do.")) (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 "{% else %}")
(text "{{ components::become_supporter_button() }}") (text "{{ components::become_supporter_button() }}")
(text "{%- endif %}"))) (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 -%}") (text "{% if user.was_purchased and user.invite_code == 0 -%}")
(form (form
("class" "card w-full lowered flex flex-col gap-2") ("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") ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
("class" "w-content")) ("class" "w-content"))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}"))) (text "{{ icon \"check\" }}")))
(span (span
("class" "fade") ("class" "fade")
@ -981,6 +753,7 @@
("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif") ("accept" "image/png,image/jpeg,image/avif,image/webp,image/gif")
("class" "w-content")) ("class" "w-content"))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}"))) (text "{{ icon \"check\" }}")))
(span (span
("class" "fade") ("class" "fade")
@ -1006,23 +779,7 @@
(text "Responses"))) (text "Responses")))
(span (span
("class" "fade") ("class" "fade")
(text "This represents the timeline that is shown on your profile by default.")))) (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 "!"))))))
(button (button
("onclick" "save_settings()") ("onclick" "save_settings()")
("id" "save_button") ("id" "save_button")
@ -1098,6 +855,7 @@
("class" "card w-full flex flex-wrap gap-2") ("class" "card w-full flex flex-wrap gap-2")
("ui_ident" "import_export") ("ui_ident" "import_export")
(button (button
("class" "primary")
("onclick" "import_theme_settings()") ("onclick" "import_theme_settings()")
(text "{{ icon \"upload\" }}") (text "{{ icon \"upload\" }}")
(span (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 += `<li><b>${x[0]}:</b> ${x[1]}</li>`;
}
}
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 = const account_settings =
document.getElementById(\"account_settings\"); document.getElementById(\"account_settings\");
const profile_settings = const profile_settings =
@ -1733,7 +1410,6 @@
\"change_avatar\", \"change_avatar\",
\"change_banner\", \"change_banner\",
\"default_profile_page\", \"default_profile_page\",
\"show_presets\",
]); ]);
ui.refresh_container(theme_settings, [ ui.refresh_container(theme_settings, [
\"supporter_ad\", \"supporter_ad\",
@ -1756,15 +1432,6 @@
settings.biography, settings.biography,
\"textarea\", \"textarea\",
], ],
[
[\"private_biography\", \"Private biography\"],
settings.private_biography,
\"textarea\",
{
embed_html:
'<span class=\"fade\">This biography is only shown to users you are not following while your account is private.</span>',
},
],
[[\"status\", \"Status\"], settings.status, \"textarea\"], [[\"status\", \"Status\"], settings.status, \"textarea\"],
[ [
[\"warning\", \"Profile warning\"], [\"warning\", \"Profile warning\"],
@ -1858,16 +1525,6 @@
\"{{ profile.settings.auto_unlist }}\", \"{{ profile.settings.auto_unlist }}\",
\"checkbox\", \"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'], [\"all_timeline_hide_answers\", 'Hide posts answering questions from the \"All\" timeline'],
\"{{ profile.settings.all_timeline_hide_answers }}\", \"{{ profile.settings.all_timeline_hide_answers }}\",
@ -1881,22 +1538,6 @@
\"{{ profile.settings.hide_associated_blocked_users }}\", \"{{ profile.settings.hide_associated_blocked_users }}\",
\"checkbox\", \"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\"], [[], \"Questions\", \"title\"],
[ [
[ [

View file

@ -70,13 +70,9 @@
(str (text "general:label.account_banned"))) (str (text "general:label.account_banned")))
(div (div
("class" "card flex flex-col gap-2 no_p_margin") ("class" "card")
(str (text "general:label.account_banned_body")) (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 }}"))))))
; if we aren't banned, just show the page body ; if we aren't banned, just show the page body
(text "{% elif user and user.awaiting_purchase %}") (text "{% elif user and user.awaiting_purchase %}")
; account waiting for payment message ; 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 %}") (text "{% else %}")
; page body ; page body
(text "{% block body %}{% endblock %}") (text "{% block body %}{% endblock %}")

View file

@ -29,6 +29,7 @@
("minlength" "2") ("minlength" "2")
("maxlength" "32"))) ("maxlength" "32")))
(button (button
("class" "primary")
(text "{{ text \"communities:action.create\" }}")))) (text "{{ text \"communities:action.create\" }}"))))
(text "{%- endif %}") (text "{%- endif %}")
(div (div

View file

@ -114,6 +114,7 @@
("required" "") ("required" "")
("minlength" "2"))) ("minlength" "2")))
(button (button
("class" "primary")
(text "{{ icon \"check\" }}") (text "{{ icon \"check\" }}")
(span (span
(text "{{ text \"general:action.save\" }}")))))) (text "{{ text \"general:action.save\" }}"))))))

View file

@ -24,18 +24,6 @@
(a (a
("href" "/communities/search") ("href" "/communities/search")
(text "searching for a community to join!"))))) (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 %}") (text "{% else %}")
(div (div
("class" "card w-full flex flex-col gap-2") ("class" "card w-full flex flex-col gap-2")

View file

@ -6,11 +6,4 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<rect width="460" height="460" fill="#E793B9" /> <rect width="460" height="460" fill="#E793B9" />
<ellipse cx="125" cy="205" rx="23" ry="24" fill="#FFBFDD" />
<circle cx="334" cy="205" r="24" fill="#FFBFDD" />
<path
d="M281.204 235.5C284.405 235.5 287.05 238.115 286.488 241.266C285.823 244.997 284.514 248.655 282.585 252.147C279.67 257.424 275.398 262.22 270.012 266.259C264.626 270.298 258.233 273.503 251.196 275.689C244.159 277.875 236.617 279 229 279C221.383 279 213.841 277.875 206.804 275.689C199.767 273.503 193.374 270.298 187.988 266.259C182.602 262.22 178.33 257.424 175.415 252.147C173.486 248.655 172.177 244.997 171.512 241.266C170.95 238.115 173.595 235.5 176.796 235.5V235.5C179.998 235.5 182.533 238.125 183.23 241.25C183.809 243.841 184.779 246.381 186.125 248.819C188.458 253.042 191.876 256.879 196.185 260.111C200.495 263.343 205.61 265.907 211.241 267.656C216.871 269.405 222.906 270.305 229 270.305C235.094 270.305 241.129 269.405 246.759 267.656C252.39 265.907 257.505 263.343 261.815 260.111C266.124 256.879 269.542 253.042 271.875 248.819C273.221 246.381 274.191 243.841 274.77 241.25C275.467 238.125 278.002 235.5 281.204 235.5V235.5Z"
fill="#FFBFDD"
fill-opacity="0.984314"
/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 181 B

Before After
Before After

View file

@ -1 +0,0 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 468 222.5" style="enable-background:new 0 0 468 222.5" xml:space="preserve"><style>.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635bff}</style><path class="st0" d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -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,
});
}

View file

@ -156,7 +156,9 @@ media_theme_pref();
.replaceAll(" year ago", "y"); .replaceAll(" year ago", "y");
} }
element.innerText = !pretty ? then.toLocaleDateString() : pretty; element.innerText =
pretty === undefined ? then.toLocaleDateString() : pretty;
element.style.display = "inline-block"; element.style.display = "inline-block";
} }
}); });
@ -196,7 +198,9 @@ media_theme_pref();
.replaceAll(" year ago", "y") .replaceAll(" year ago", "y")
.replaceAll("Yesterday", "1d") || ""; .replaceAll("Yesterday", "1d") || "";
element.innerText = !pretty ? then.toLocaleDateString() : pretty; element.innerText =
pretty === undefined ? then.toLocaleDateString() : pretty;
element.style.display = "inline-block"; element.style.display = "inline-block";
} }
}); });
@ -415,35 +419,33 @@ media_theme_pref();
}); });
self.define("hooks::long_text.init", (_) => { self.define("hooks::long_text.init", (_) => {
setTimeout(() => { for (const element of Array.from(
for (const element of Array.from( document.querySelectorAll("[hook=long]") || [],
document.querySelectorAll("[hook=long]") || [], )) {
)) { const is_long = element.innerText.length >= 64 * 8;
const is_long = element.innerText.length >= 64 * 8;
if (!is_long) { if (!is_long) {
continue; 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);
} }
}, 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", (_) => { self.define("hooks::alt", (_) => {
@ -689,7 +691,7 @@ media_theme_pref();
}); });
self.define("hooks::check_message_reactions", async ({ $ }) => { 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) => { async (element) => {
const reactions = await ( const reactions = await (
await fetch( await fetch(
@ -1067,13 +1069,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
// permissions ui // permissions ui
self.define( self.define(
"generate_permissions_ui", "generate_permissions_ui",
( (_, permissions, field_id = "role") => {
_,
permissions,
field_id = "role",
add_name = "add_permission_to_role",
remove_name = "remove_permission_from_role",
) => {
function all_matching_permissions(role) { function all_matching_permissions(role) {
const matching = []; const matching = [];
const not_matching = []; const not_matching = [];
@ -1103,7 +1099,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
function get_permissions_html(role, id) { function get_permissions_html(role, id) {
const [matching, not_matching] = all_matching_permissions(role); const [matching, not_matching] = all_matching_permissions(role);
globalThis[remove_name] = (permission) => { globalThis.remove_permission_from_role = (permission) => {
matching.splice(matching.indexOf(permission), 1); matching.splice(matching.indexOf(permission), 1);
not_matching.push(permission); not_matching.push(permission);
@ -1111,7 +1107,7 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
get_permissions_html(rebuild_role(matching), id); 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); not_matching.splice(not_matching.indexOf(permission), 1);
matching.push(permission); matching.push(permission);
@ -1124,14 +1120,14 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
for (const match of matching) { for (const match of matching) {
permissions_html += `<div class="card w-full secondary flex justify-between gap-2"> permissions_html += `<div class="card w-full secondary flex justify-between gap-2">
<span>${match} <code>${permissions[match]}</code></span> <span>${match} <code>${permissions[match]}</code></span>
<button class="red lowered" onclick="${remove_name}('${match}')">Remove</button> <button class="red lowered" onclick="remove_permission_from_role('${match}')">Remove</button>
</div>`; </div>`;
} }
for (const match of not_matching) { for (const match of not_matching) {
permissions_html += `<div class="card w-full secondary flex justify-between gap-2"> permissions_html += `<div class="card w-full secondary flex justify-between gap-2">
<span>${match} <code>${permissions[match]}</code></span> <span>${match} <code>${permissions[match]}</code></span>
<button class="green lowered" onclick="${add_name}('${match}')">Add</button> <button class="green lowered" onclick="add_permission_to_role('${match}')">Add</button>
</div>`; </div>`;
} }
@ -1143,15 +1139,8 @@ ${option.input_element_type === "textarea" ? `${option.value}</textarea>` : ""}
); );
// lightbox // lightbox
self.define("lightbox_open", async (_, src) => { self.define("lightbox_open", (_, src) => {
document.getElementById("lightbox_img").src = 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_img_a").href = src;
document.getElementById("lightbox").classList.remove("hidden"); document.getElementById("lightbox").classList.remove("hidden");
}); });

View file

@ -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);

View file

@ -193,13 +193,9 @@
like.classList.add("green"); like.classList.add("green");
like.querySelector("svg").classList.add("filled"); like.querySelector("svg").classList.add("filled");
if (dislike) { dislike.classList.remove("red");
dislike.classList.remove("red");
}
} else { } else {
if (dislike) { dislike.classList.add("red");
dislike.classList.add("red");
}
like.classList.remove("green"); like.classList.remove("green");
like.querySelector("svg").classList.remove("filled"); 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;
}
});
})();

View file

@ -31,9 +31,7 @@ function fix_atto_links() {
if (TETRATTO_LINK_HANDLER_CTX === "embed") { if (TETRATTO_LINK_HANDLER_CTX === "embed") {
// relative links for embeds // relative links for embeds
const path = window.location.pathname const path = window.location.pathname.slice("/api/v1/net/".length);
.replace("atto://", "")
.slice("/api/v1/net/".length);
function fix_element( function fix_element(
selector = "a", selector = "a",
@ -45,28 +43,25 @@ function fix_atto_links() {
continue; continue;
} }
const p = new URL(y[property]).pathname.replace("atto://", ""); let x = new URL(y[property]).pathname;
let x = p.startsWith("/api/v1/net/")
? p.replace("/api/v1/net/", "")
: p.startsWith("/")
? `${path.split("/")[0]}${p}`
: p;
if (!x.includes(".html")) { if (!x.includes(".html")) {
x = `${x}/index.html`; x = `${x}/index.html`;
} }
if (relative) { if (relative) {
y[property] = `atto://${x}`; y[property] =
`atto://${path.replace("atto://", "").split("/")[0]}${x}`;
} else { } else {
y[property] = 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("a", "href", true);
fix_element("img", "src", false); fix_element("link", "href", false);
fix_element("script", "src", false);
// send message // send message
window.top.postMessage( window.top.postMessage(
@ -113,11 +108,12 @@ function fix_atto_links() {
} }
const href = structuredClone(anchor.href); const href = structuredClone(anchor.href);
anchor.addEventListener("click", () => { anchor.addEventListener("click", () => {
if (TETRATTO_LINK_HANDLER_CTX === "net") { if (TETRATTO_LINK_HANDLER_CTX === "net") {
window.location.href = `/net/${href.replace("atto://", "")}`; window.location.href = `/net/${href.replace("atto://", "")}`;
} else { } else {
window.location.href = `/api/v1/net/${href}?s=${globalThis.SECRET_SESSION}`; window.location.href = `/api/v1/net/${href}`;
} }
}); });

View file

@ -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<State>,
) -> 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<State>,
Json(req): Json<QueryAppData>,
) -> 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<State>,
Json(req): Json<InsertAppData>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateAppDataKey>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateAppDataValue>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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<State>,
Json(req): Json<QueryAppData>,
) -> 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()),
}
}

View file

@ -7,7 +7,7 @@ use crate::{
State, State,
}; };
use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum::{Extension, Json, extract::Path, response::IntoResponse};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
apps::{AppQuota, ThirdPartyApp}, apps::{AppQuota, ThirdPartyApp},
oauth::{AuthGrant, PkceChallengeMethod}, oauth::{AuthGrant, PkceChallengeMethod},
@ -15,7 +15,7 @@ use tetratto_core::model::{
ApiReturn, Error, ApiReturn, Error,
}; };
use tetratto_shared::{hash::random_id, unix_epoch_timestamp}; use tetratto_shared::{hash::random_id, unix_epoch_timestamp};
use super::{CreateApp, UpdateAppStorageCapacity}; use super::CreateApp;
pub async fn create_request( pub async fn create_request(
jar: CookieJar, 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateAppStorageCapacity>,
) -> 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( pub async fn update_scopes_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
@ -268,34 +239,3 @@ pub async fn grant_request(
Err(e) => Json(e.into()), Err(e) => Json(e.into()),
} }
} }
pub async fn roll_api_key_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
) -> 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()),
}
}

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use axum::{extract::Path, response::IntoResponse, Extension, Json}; use axum::{extract::Path, response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::{ use tetratto_core::{
database::connections::last_fm::LastFmConnection, database::connections::last_fm::LastFmConnection,
model::{ model::{

View file

@ -5,7 +5,7 @@ pub mod stripe;
use std::collections::HashMap; use std::collections::HashMap;
use axum::{extract::Path, response::IntoResponse, Extension, Json}; use axum::{extract::Path, response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::{ use tetratto_core::model::{
auth::{ConnectionService, ExternalConnectionData}, auth::{ConnectionService, ExternalConnectionData},

View file

@ -1,5 +1,5 @@
use axum::{response::IntoResponse, Extension, Json}; use axum::{response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::{ use tetratto_core::{
database::connections::spotify::SpotifyConnection, database::connections::spotify::SpotifyConnection,
model::{ model::{

View file

@ -1,15 +1,14 @@
use std::{str::FromStr, time::Duration}; use std::time::Duration;
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json}; use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
auth::{Notification, User}, auth::{User, Notification},
moderation::AuditLogEntry, moderation::AuditLogEntry,
permissions::{FinePermission, SecondaryPermission}, permissions::FinePermission,
ApiReturn, Error, ApiReturn, Error,
}; };
use stripe::{EventObject, EventType}; use stripe::{EventObject, EventType};
use crate::{get_user_from_token, State}; use crate::State;
pub async fn stripe_webhook( pub async fn stripe_webhook(
Extension(data): Extension<State>, Extension(data): Extension<State>,
@ -18,10 +17,9 @@ pub async fn stripe_webhook(
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = &(data.read().await).0; let data = &(data.read().await).0;
let stripe_cnf = match data.0.0.stripe { if data.0.0.stripe.is_none() {
Some(ref c) => c, return Json(Error::MiscError("Disabled".to_string()).into());
None => return Json(Error::MiscError("Disabled".to_string()).into()), }
};
let sig = match headers.get("Stripe-Signature") { let sig = match headers.get("Stripe-Signature") {
Some(s) => s, Some(s) => s,
@ -58,7 +56,7 @@ pub async fn stripe_webhook(
Err(e) => return Json(e.into()), 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 if let Err(e) = data
.update_user_stripe_id(user.id, customer_id.as_str()) .update_user_stripe_id(user.id, customer_id.as_str())
.await .await
@ -76,48 +74,6 @@ pub async fn stripe_webhook(
}; };
let customer_id = invoice.customer.unwrap().id(); 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 // pull user and update role
let mut retries: usize = 0; let mut retries: usize = 0;
@ -162,91 +118,45 @@ pub async fn stripe_webhook(
} }
let user = user.unwrap(); let user = user.unwrap();
tracing::info!("found subscription user in {retries} tries");
if product_id == stripe_cnf.product_ids.supporter { if user.permissions.check(FinePermission::SUPPORTER) {
// supporter return Json(ApiReturn {
tracing::info!("found subscription user in {retries} tries"); ok: true,
message: "Already applied".to_string(),
payload: (),
});
}
if user.permissions.check(FinePermission::SUPPORTER) { tracing::info!("invoice {} (stripe: {})", user.id, customer_id);
return Json(ApiReturn { let new_user_permissions = user.permissions | FinePermission::SUPPORTER;
ok: true,
message: "Already applied".to_string(),
payload: (),
});
}
tracing::info!("invoice {} (stripe: {})", user.id, customer_id); if let Err(e) = data
let new_user_permissions = user.permissions | FinePermission::SUPPORTER; .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 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 .await
{ {
return Json(e.into()); return Json(e.into());
} }
}
if data.0.0.security.enable_invite_codes && user.awaiting_purchase { if let Err(e) = data
if let Err(e) = data .create_notification(Notification::new(
.update_user_awaiting_purchased_status(user.id, false, user.clone(), false) "Welcome new supporter!".to_string(),
.await "Thank you for your support! Your account has been updated with your new role."
{ .to_string(),
return Json(e.into()); user.id,
} ))
} .await
{
if let Err(e) = data return Json(e.into());
.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());
} }
} }
EventType::CustomerSubscriptionDeleted => { EventType::CustomerSubscriptionDeleted => {
@ -257,72 +167,34 @@ pub async fn stripe_webhook(
}; };
let customer_id = subscription.customer.id(); 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 { let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
Ok(ua) => ua, Ok(ua) => ua,
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
}; };
// handle each subscription item tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id);
if product_id == stripe_cnf.product_ids.supporter { let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
// supporter
tracing::info!("unsubscribe {} (stripe: {})", user.id, customer_id);
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
if let Err(e) = data if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true) .update_user_role(user.id, new_user_permissions, user.clone(), true)
.await .await
{ {
return Json(e.into()); return Json(e.into());
} }
if data.0.0.security.enable_invite_codes if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0
&& 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
// user doesn't come from an invite code, and is a purchased account if let Err(e) = data
// this means their account must be locked if they stop paying .update_user_awaiting_purchased_status(user.id, true, user.clone(), false)
if let Err(e) = data .await
.update_user_awaiting_purchased_status(user.id, true, user.clone(), false) {
.await return Json(e.into());
{ }
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());
} }
// send notification
if let Err(e) = data if let Err(e) = data
.create_notification(Notification::new( .create_notification(Notification::new(
"Sorry to see you go... :(".to_string(), "Sorry to see you go... :(".to_string(),
@ -337,119 +209,44 @@ pub async fn stripe_webhook(
} }
EventType::InvoicePaymentFailed => { EventType::InvoicePaymentFailed => {
// payment failed // payment failed
let invoice = match req.data.object { let subscription = match req.data.object {
EventObject::Invoice(i) => i, EventObject::Subscription(c) => c,
_ => unreachable!("cannot be this"), _ => unreachable!("cannot be this"),
}; };
let customer_id = invoice.customer.expect("TETRATTO_STRIPE_NO_CUSTOMER").id(); let customer_id = subscription.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 user = match data.get_user_by_stripe_id(customer_id.as_str()).await { let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
Ok(ua) => ua, Ok(ua) => ua,
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
}; };
// handle each subscription item tracing::info!(
if product_id == stripe_cnf.product_ids.supporter { "unsubscribe (pay fail) {} (stripe: {})",
// supporter user.id,
if !user.permissions.check(FinePermission::SUPPORTER) { customer_id
// the user isn't currently a supporter, there's no reason to send this notification );
return Json(ApiReturn { let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
ok: true,
message: "Acceptable".to_string(),
payload: (),
});
}
tracing::info!( if let Err(e) = data
"unsubscribe (pay fail) {} (stripe: {})", .update_user_role(user.id, new_user_permissions, user.clone(), true)
user.id, .await
customer_id {
); return Json(e.into());
let new_user_permissions = user.permissions - FinePermission::SUPPORTER; }
if let Err(e) = data if data.0.0.security.enable_invite_codes && user.was_purchased && user.invite_code == 0
.update_user_role(user.id, new_user_permissions, user.clone(), true) {
.await // user doesn't come from an invite code, and is a purchased account
{ // this means their account must be locked if they stop paying
return Json(e.into()); if let Err(e) = data
} .update_user_awaiting_purchased_status(user.id, true, user.clone(), false)
.await
if data.0.0.security.enable_invite_codes {
&& user.was_purchased return Json(e.into());
&& 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());
} }
// send notification
if let Err(e) = data if let Err(e) = data
.create_notification(Notification::new( .create_notification(Notification::new(
"It seems your recent payment has failed :(".to_string(), "It seems your recent payment has failed :(".to_string(),
@ -471,145 +268,3 @@ pub async fn stripe_webhook(
payload: (), payload: (),
}) })
} }
pub async fn onboarding_account_link_request(
jar: CookieJar,
Extension(data): Extension<State>,
) -> 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<State>,
) -> 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<State>,
) -> 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()),
}
}

View file

@ -4,7 +4,7 @@ use axum::{
extract::{Path, Query}, extract::{Path, Query},
response::IntoResponse, response::IntoResponse,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use pathbufd::{PathBufD, pathd}; use pathbufd::{PathBufD, pathd};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{

View file

@ -1,34 +1,12 @@
use crate::{ use crate::{
get_app_from_key, get_user_from_token, State, get_user_from_token,
model::{ApiReturn, Error}, model::{ApiReturn, Error},
routes::api::v1::CreateIpBan, routes::api::v1::CreateIpBan,
State,
}; };
use axum::{extract::Path, http::HeaderMap, response::IntoResponse, Extension, Json}; use axum::{Extension, Json, extract::Path, response::IntoResponse};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{addr::RemoteAddr, auth::IpBan, permissions::FinePermission}; 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<String>,
Extension(data): Extension<State>,
) -> 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. /// Create a new IP ban.
pub async fn create_request( pub async fn create_request(
jar: CookieJar, jar: CookieJar,

View file

@ -16,7 +16,7 @@ use axum::{
response::{IntoResponse, Redirect}, response::{IntoResponse, Redirect},
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::addr::RemoteAddr; use tetratto_core::model::addr::RemoteAddr;
use tetratto_shared::hash::hash; use tetratto_shared::hash::hash;

View file

@ -1,12 +1,11 @@
use std::{str::FromStr, time::Duration}; use std::time::Duration;
use crate::{ use crate::{
get_user_from_token, get_user_from_token,
model::{ApiReturn, Error}, model::{ApiReturn, Error},
routes::api::v1::{ routes::api::v1::{
AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken, AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken,
UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanReason, UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserInviteCode,
UpdateUserInviteCode, UpdateUserIsDeactivated, UpdateUserIsVerified, UpdateUserPassword, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername,
UpdateUserRole, UpdateUserUsername,
}, },
State, State,
}; };
@ -18,7 +17,7 @@ use axum::{
response::{IntoResponse, Redirect}, response::{IntoResponse, Redirect},
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use futures_util::{sink::SinkExt, stream::StreamExt}; use futures_util::{sink::SinkExt, stream::StreamExt};
use tetratto_core::{ use tetratto_core::{
cache::Cache, 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<usize>,
Extension(data): Extension<State>,
Json(req): Json<UpdateUserIsDeactivated>,
) -> 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. /// Update the role of the given user.
/// ///
/// Does not support third-party grants. /// 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<usize>,
Extension(data): Extension<State>,
Json(req): Json<UpdateUserBanReason>,
) -> 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. /// Update the current user's last seen value.
pub async fn seen_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse { pub async fn seen_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0; let data = &(data.read().await).0;
@ -509,8 +451,8 @@ pub async fn delete_user_request(
Extension(data): Extension<State>, Extension(data): Extension<State>,
Json(req): Json<DeleteUser>, Json(req): Json<DeleteUser>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = &(data.read().await); let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data.0) { let user = match get_user_from_token!(jar, data) {
Some(ua) => ua, Some(ua) => ua,
None => return Json(Error::NotAllowed.into()), None => return Json(Error::NotAllowed.into()),
}; };
@ -519,7 +461,6 @@ pub async fn delete_user_request(
return Json(Error::NotAllowed.into()); return Json(Error::NotAllowed.into());
} else if user.permissions.check(FinePermission::MANAGE_USERS) { } else if user.permissions.check(FinePermission::MANAGE_USERS) {
if let Err(e) = data if let Err(e) = data
.0
.create_audit_log_entry(AuditLogEntry::new( .create_audit_log_entry(AuditLogEntry::new(
user.id, user.id,
format!("invoked `delete_user` with x value `{id}`"), format!("invoked `delete_user` with x value `{id}`"),
@ -531,32 +472,14 @@ pub async fn delete_user_request(
} }
match data match data
.0
.delete_user(id, &req.password, user.permissions.check_manager()) .delete_user(id, &req.password, user.permissions.check_manager())
.await .await
{ {
Ok(ua) => { Ok(_) => Json(ApiReturn {
// delete stripe user ok: true,
if let Some(stripe_id) = ua.seller_data.account_id message: "User deleted".to_string(),
&& let Some(ref client) = data.3 payload: (),
{ }),
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: (),
})
}
Err(e) => Json(e.into()), Err(e) => Json(e.into()),
} }
} }

View file

@ -9,7 +9,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
addr::RemoteAddr, addr::RemoteAddr,
auth::{AchievementName, FollowResult, IpBlock, Notification, UserBlock, UserFollow}, auth::{AchievementName, FollowResult, IpBlock, Notification, UserBlock, UserFollow},
@ -17,7 +17,7 @@ use tetratto_core::model::{
}; };
/// Toggle following on the given user. /// Toggle following on the given user.
pub async fn toggle_follow_request( pub async fn follow_request(
jar: CookieJar, jar: CookieJar,
Path(id): Path<usize>, Path(id): Path<usize>,
Extension(data): Extension<State>, Extension(data): Extension<State>,
@ -154,96 +154,6 @@ pub async fn accept_follow_request(
} }
} }
pub async fn follow_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> 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<usize>,
Extension(data): Extension<State>,
) -> 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. /// Toggle blocking on the given user.
pub async fn block_request( pub async fn block_request(
jar: CookieJar, jar: CookieJar,
@ -368,10 +278,7 @@ pub async fn followers_request(
Ok(f) => Json(ApiReturn { Ok(f) => Json(ApiReturn {
ok: true, ok: true,
message: "Success".to_string(), message: "Success".to_string(),
payload: match data payload: match data.fill_userfollows_with_initiator(f).await {
.fill_userfollows_with_initiator(f, &Some(user.clone()), id == user.id)
.await
{
Ok(f) => Some(data.userfollows_user_filter(&f)), Ok(f) => Some(data.userfollows_user_filter(&f)),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
}, },
@ -403,10 +310,7 @@ pub async fn following_request(
Ok(f) => Json(ApiReturn { Ok(f) => Json(ApiReturn {
ok: true, ok: true,
message: "Success".to_string(), message: "Success".to_string(),
payload: match data payload: match data.fill_userfollows_with_receiver(f).await {
.fill_userfollows_with_receiver(f, &Some(user.clone()), id == user.id)
.await
{
Ok(f) => Some(data.userfollows_user_filter(&f)), Ok(f) => Some(data.userfollows_user_filter(&f)),
Err(e) => return Json(e.into()), Err(e) => return Json(e.into()),
}, },

View file

@ -9,7 +9,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{auth::UserWarning, oauth, permissions::FinePermission}; use tetratto_core::model::{auth::UserWarning, oauth, permissions::FinePermission};
/// Create a new user warning. /// Create a new user warning.

View file

@ -1,5 +1,5 @@
use axum::{Extension, Json, extract::Path, response::IntoResponse}; 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 tetratto_core::model::{oauth, channels::Channel, ApiReturn, Error};
use crate::{ use crate::{
get_user_from_token, get_user_from_token,
@ -293,62 +293,3 @@ pub async fn get_request(
Err(e) => Json(e.into()), Err(e) => Json(e.into()),
} }
} }
pub async fn mute_channel_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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()),
}
}

View file

@ -1,6 +1,6 @@
use crate::{get_user_from_token, routes::api::v1::CreateMessageReaction, State}; use crate::{get_user_from_token, routes::api::v1::CreateMessageReaction, State};
use axum::{Extension, Json, extract::Path, response::IntoResponse}; 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}; use tetratto_core::model::{channels::MessageReaction, oauth, ApiReturn, Error};
pub async fn get_request( pub async fn get_request(

View file

@ -7,7 +7,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::{ use tetratto_core::{
cache::{Cache, redis::Commands}, cache::{Cache, redis::Commands},
model::{ model::{

View file

@ -3,7 +3,7 @@ use axum::{
extract::Path, extract::Path,
response::{IntoResponse, Redirect}, response::{IntoResponse, Redirect},
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
auth::Notification, auth::Notification,
communities::{Community, CommunityMembership}, communities::{Community, CommunityMembership},

View file

@ -3,7 +3,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{auth::AchievementName, communities::PostDraft, oauth, ApiReturn, Error}; use tetratto_core::model::{auth::AchievementName, communities::PostDraft, oauth, ApiReturn, Error};
use crate::{ use crate::{
get_user_from_token, get_user_from_token,

View file

@ -7,7 +7,7 @@ use crate::{
State, State,
}; };
use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json}; use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
oauth, oauth,
uploads::{CustomEmoji, MediaType, MediaUpload}, uploads::{CustomEmoji, MediaType, MediaUpload},
@ -17,8 +17,6 @@ use tetratto_core::model::{
/// Expand a unicode emoji into its Gemoji shortcode. /// Expand a unicode emoji into its Gemoji shortcode.
pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse { pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
match emoji.as_str() { match emoji.as_str() {
// matches `CustomEmoji::replace`
"💯" => "100".to_string(),
"👍" => "thumbs_up".to_string(), "👍" => "thumbs_up".to_string(),
"👎" => "thumbs_down".to_string(), "👎" => "thumbs_down".to_string(),
_ => match emojis::get(&emoji) { _ => match emojis::get(&emoji) {

View file

@ -1,5 +1,5 @@
use axum::{Extension, Json, body::Body, extract::Path, response::IntoResponse}; use axum::{Extension, Json, body::Body, extract::Path, response::IntoResponse};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use pathbufd::{PathBufD, pathd}; use pathbufd::{PathBufD, pathd};
use std::fs::exists; use std::fs::exists;
use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission, oauth}; use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission, oauth};

View file

@ -4,7 +4,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
addr::RemoteAddr, addr::RemoteAddr,
auth::AchievementName, auth::AchievementName,
@ -152,11 +152,10 @@ pub async fn create_request(
} }
// ... // ...
let uploads = props.uploads.clone(); match data.create_post(props.clone()).await {
match data.create_post(props).await {
Ok(id) => { Ok(id) => {
// write to uploads // 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) { let image = match images.get(i) {
Some(img) => img, Some(img) => img,
None => { None => {
@ -724,7 +723,7 @@ pub async fn from_communities_request(
}; };
match data match data
.get_posts_from_user_communities(user.id, 12, props.page, &user) .get_posts_from_user_communities(user.id, 12, props.page)
.await .await
{ {
Ok(posts) => { Ok(posts) => {

View file

@ -4,7 +4,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
addr::RemoteAddr, addr::RemoteAddr,
auth::{AchievementName, IpBlock}, auth::{AchievementName, IpBlock},
@ -96,13 +96,6 @@ pub async fn create_request(
props.context.mask_owner = true; props.context.mask_owner = true;
} }
if !req.asking_about.is_empty() && !req.is_global {
props.context.asking_about = match req.asking_about.parse::<usize>() {
Ok(x) => Some(x),
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
}
}
match data match data
.create_question(props, drawings.iter().map(|x| x.to_vec()).collect()) .create_question(props, drawings.iter().map(|x| x.to_vec()).collect())
.await .await

View file

@ -3,21 +3,12 @@ use crate::{
routes::api::v1::{CreateDomain, UpdateDomainData}, routes::api::v1::{CreateDomain, UpdateDomainData},
State, State,
}; };
use axum::{ use axum::{extract::Path, response::IntoResponse, http::StatusCode, Extension, Json};
extract::{Path, Query}, use axum_extra::extract::CookieJar;
http::StatusCode,
response::IntoResponse,
Extension, Json,
};
use crate::cookie::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
auth::AchievementName,
littleweb::{Domain, ServiceFsMime}, littleweb::{Domain, ServiceFsMime},
oauth, oauth, ApiReturn, Error,
permissions::FinePermission,
ApiReturn, Error,
}; };
use serde::Deserialize;
pub async fn get_request( pub async fn get_request(
Path(id): Path<usize>, Path(id): Path<usize>,
@ -57,20 +48,11 @@ pub async fn create_request(
Json(req): Json<CreateDomain>, Json(req): Json<CreateDomain>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = &(data.read().await).0; 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, Some(ua) => ua,
None => return Json(Error::NotAllowed.into()), 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 match data
.create_domain(Domain::new(req.name, req.tld, user.id)) .create_domain(Domain::new(req.name, req.tld, user.id))
.await .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( pub async fn get_file_request(
Path(mut addr): Path<String>, Path(addr): Path<String>,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Query(props): Query<GetFileQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
if !addr.starts_with("atto://") {
addr = format!("atto://{addr}");
}
// ...
let data = &(data.read().await).0; 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); 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 // resolve domain
let domain = match data.get_domain_by_name_tld(&domain, &tld).await { let domain = match data.get_domain_by_name_tld(&domain, &tld).await {
Ok(x) => x, Ok(x) => x,
@ -188,28 +145,16 @@ pub async fn get_file_request(
Some((f, _)) => Ok(( Some((f, _)) => Ok((
[("Content-Type".to_string(), f.mime.to_string())], [("Content-Type".to_string(), f.mime.to_string())],
if f.mime == ServiceFsMime::Html { if f.mime == ServiceFsMime::Html {
f.content f.content.replace(
.replace( "</body>",
"</body>", &format!(
&format!( "<script src=\"{}/js/proto_links.js\" defer></script></body>",
"<script src=\"{}/js/proto_links.js\" defer></script><script> data.0.0.host
globalThis.SECRET_SESSION = \"{}\"; ),
</script></body>", )
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),
)
} else { } else {
f.content f.content
} },
.replace("atto://", "/api/v1/net/"),
)), )),
None => { None => {
return Err(( return Err((

View file

@ -3,7 +3,7 @@ use axum::{
extract::{Json, Path}, extract::{Json, Path},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_shared::snow::Snowflake; use tetratto_shared::snow::Snowflake;
use crate::{ use crate::{
get_user_from_token, get_user_from_token,

View file

@ -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<usize>,
Extension(data): Extension<State>,
) -> 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<State>) -> 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<State>,
Json(req): Json<CreateLayout>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateLayoutName>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateLayoutPrivacy>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateLayoutPages>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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()),
}
}

View file

@ -1,13 +1,12 @@
pub mod app_data;
pub mod apps; pub mod apps;
pub mod auth; pub mod auth;
pub mod channels; pub mod channels;
pub mod communities; pub mod communities;
pub mod domains; pub mod domains;
pub mod journals; pub mod journals;
pub mod layouts;
pub mod notes; pub mod notes;
pub mod notifications; pub mod notifications;
pub mod products;
pub mod reactions; pub mod reactions;
pub mod reports; pub mod reports;
pub mod requests; pub mod requests;
@ -22,7 +21,7 @@ use axum::{
}; };
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::{ use tetratto_core::model::{
apps::{AppDataSelectMode, AppDataSelectQuery, AppQuota, DeveloperPassStorageQuota}, apps::AppQuota,
auth::AchievementName, auth::AchievementName,
communities::{ communities::{
CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess, CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess,
@ -30,10 +29,10 @@ use tetratto_core::model::{
}, },
communities_permissions::CommunityPermission, communities_permissions::CommunityPermission,
journals::JournalPrivacyPermission, journals::JournalPrivacyPermission,
layouts::{CustomizablePage, LayoutPage, LayoutPrivacy},
littleweb::{DomainData, DomainTld, ServiceFsEntry}, littleweb::{DomainData, DomainTld, ServiceFsEntry},
oauth::AppScope, oauth::AppScope,
permissions::{FinePermission, SecondaryPermission}, permissions::{FinePermission, SecondaryPermission},
products::{ProductPrice, ProductType},
reactions::AssetType, reactions::AssetType,
stacks::{StackMode, StackPrivacy, StackSort}, 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}/avatar", get(auth::images::avatar_request))
.route("/auth/user/{id}/banner", get(auth::images::banner_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", post(auth::social::follow_request))
.route(
"/auth/user/{id}/follow/toggle",
post(auth::social::toggle_follow_request),
)
.route( .route(
"/auth/user/{id}/follow/cancel", "/auth/user/{id}/follow/cancel",
post(auth::social::cancel_follow_request), post(auth::social::cancel_follow_request),
@ -298,10 +293,6 @@ pub fn routes() -> Router {
"/auth/user/{id}/follow/accept", "/auth/user/{id}/follow/accept",
post(auth::social::accept_follow_request), 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", post(auth::social::block_request))
.route( .route(
"/auth/user/{id}/block_ip", "/auth/user/{id}/block_ip",
@ -323,10 +314,6 @@ pub fn routes() -> Router {
"/auth/user/{id}/role/2", "/auth/user/{id}/role/2",
post(auth::profile::update_user_secondary_role_request), post(auth::profile::update_user_secondary_role_request),
) )
.route(
"/auth/user/{id}/ban_reason",
post(auth::profile::update_user_ban_reason_request),
)
.route( .route(
"/auth/user/{id}", "/auth/user/{id}",
delete(auth::profile::delete_user_request), delete(auth::profile::delete_user_request),
@ -351,10 +338,6 @@ pub fn routes() -> Router {
"/auth/user/{id}/awaiting_purchase", "/auth/user/{id}/awaiting_purchase",
post(auth::profile::update_user_awaiting_purchase_request), post(auth::profile::update_user_awaiting_purchase_request),
) )
.route(
"/auth/user/{id}/deactivate",
post(auth::profile::update_user_is_deactivated_request),
)
.route( .route(
"/auth/user/{id}/totp", "/auth/user/{id}/totp",
post(auth::profile::enable_totp_request), post(auth::profile::enable_totp_request),
@ -424,7 +407,6 @@ pub fn routes() -> Router {
) )
// apps // apps
.route("/apps", post(apps::create_request)) .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}/title", post(apps::update_title_request))
.route("/apps/{id}/homepage", post(apps::update_homepage_request)) .route("/apps/{id}/homepage", post(apps::update_homepage_request))
.route("/apps/{id}/redirect", post(apps::update_redirect_request)) .route("/apps/{id}/redirect", post(apps::update_redirect_request))
@ -432,21 +414,9 @@ pub fn routes() -> Router {
"/apps/{id}/quota_status", "/apps/{id}/quota_status",
post(apps::update_quota_status_request), 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}/scopes", post(apps::update_scopes_request))
.route("/apps/{id}", delete(apps::delete_request))
.route("/apps/{id}/grant", post(apps::grant_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 // warnings
.route("/warnings/{id}", get(auth::user_warnings::get_request)) .route("/warnings/{id}", get(auth::user_warnings::get_request))
.route("/warnings/{id}", post(auth::user_warnings::create_request)) .route("/warnings/{id}", post(auth::user_warnings::create_request))
@ -495,7 +465,6 @@ pub fn routes() -> Router {
post(communities::communities::update_membership_role), post(communities::communities::update_membership_role),
) )
// ipbans // ipbans
.route("/bans/{ip}", get(auth::ipbans::check_request))
.route("/bans/{ip}", post(auth::ipbans::create_request)) .route("/bans/{ip}", post(auth::ipbans::create_request))
.route("/bans/{ip}", delete(auth::ipbans::delete_request)) .route("/bans/{ip}", delete(auth::ipbans::delete_request))
// reports // reports
@ -545,18 +514,6 @@ pub fn routes() -> Router {
"/service_hooks/stripe", "/service_hooks/stripe",
post(auth::connections::stripe::stripe_webhook), 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 // channels
.route("/channels", post(channels::channels::create_request)) .route("/channels", post(channels::channels::create_request))
.route( .route(
@ -580,14 +537,6 @@ pub fn routes() -> Router {
"/channels/{id}/kick", "/channels/{id}/kick",
post(channels::channels::kick_member_request), 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/{id}", get(channels::channels::get_request))
.route( .route(
"/channels/community/{id}", "/channels/community/{id}",
@ -676,8 +625,17 @@ pub fn routes() -> Router {
// uploads // uploads
.route("/uploads/{id}", get(uploads::get_request)) .route("/uploads/{id}", get(uploads::get_request))
.route("/uploads/{id}", delete(uploads::delete_request)) .route("/uploads/{id}", delete(uploads::delete_request))
.route("/uploads/{id}/data", get(uploads::get_json_request)) // layouts
.route("/uploads/{id}/alt", post(uploads::update_alt_request)) .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 // services
.route("/services", get(services::list_request)) .route("/services", get(services::list_request))
.route("/services", post(services::create_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}", get(domains::get_request))
.route("/domains/{id}", delete(domains::delete_request)) .route("/domains/{id}", delete(domains::delete_request))
.route("/domains/{id}/data", post(domains::update_data_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 { pub fn lw_routes() -> Router {
@ -846,11 +793,6 @@ pub struct UpdateUserAwaitingPurchase {
pub awaiting_purchase: bool, pub awaiting_purchase: bool,
} }
#[derive(Deserialize)]
pub struct UpdateUserIsDeactivated {
pub is_deactivated: bool,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateNotificationRead { pub struct UpdateNotificationRead {
pub read: bool, pub read: bool,
@ -876,11 +818,6 @@ pub struct UpdateSecondaryUserRole {
pub role: SecondaryPermission, pub role: SecondaryPermission,
} }
#[derive(Deserialize)]
pub struct UpdateUserBanReason {
pub reason: String,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateUserInviteCode { pub struct UpdateUserInviteCode {
pub invite_code: String, pub invite_code: String,
@ -916,8 +853,6 @@ pub struct CreateQuestion {
pub community: String, pub community: String,
#[serde(default)] #[serde(default)]
pub mask_owner: bool, pub mask_owner: bool,
#[serde(default)]
pub asking_about: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -1012,7 +947,6 @@ pub struct UpdatePostIsOpen {
pub struct CreateApp { pub struct CreateApp {
pub title: String, pub title: String,
pub homepage: String, pub homepage: String,
#[serde(default)]
pub redirect: String, pub redirect: String,
} }
@ -1036,11 +970,6 @@ pub struct UpdateAppQuotaStatus {
pub quota_status: AppQuota, pub quota_status: AppQuota,
} }
#[derive(Deserialize)]
pub struct UpdateAppStorageCapacity {
pub storage_capacity: DeveloperPassStorageQuota,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateAppScopes { pub struct UpdateAppScopes {
pub scopes: Vec<AppScope>, pub scopes: Vec<AppScope>,
@ -1126,6 +1055,27 @@ pub struct AwardAchievement {
pub name: AchievementName, 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<LayoutPage>,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateService { pub struct CreateService {
pub name: String, pub name: String,
@ -1158,53 +1108,3 @@ pub struct CreateDomain {
pub struct UpdateDomainData { pub struct UpdateDomainData {
pub data: Vec<(String, DomainData)>, 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,
}

View file

@ -3,7 +3,7 @@ use axum::{
extract::{Json, Path}, extract::{Json, Path},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_shared::unix_epoch_timestamp; use tetratto_shared::unix_epoch_timestamp;
use crate::{ use crate::{
get_user_from_token, get_user_from_token,
@ -267,7 +267,7 @@ pub async fn delete_by_dir_request(
} }
pub async fn render_markdown_request(Json(req): Json<RenderMarkdown>) -> impl IntoResponse { pub async fn render_markdown_request(Json(req): Json<RenderMarkdown>) -> impl IntoResponse {
tetratto_shared::markdown::render_markdown(&CustomEmoji::replace(&req.content), true) tetratto_shared::markdown::render_markdown(&CustomEmoji::replace(&req.content))
.replace("\\@", "@") .replace("\\@", "@")
.replace("%5C@", "@") .replace("%5C@", "@")
} }

View file

@ -5,7 +5,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{oauth, ApiReturn, Error}; use tetratto_core::model::{oauth, ApiReturn, Error};
pub async fn delete_request( pub async fn delete_request(

View file

@ -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<usize>,
Extension(data): Extension<State>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<State>,
JsonMultipart(uploads, req): JsonMultipart<CreateProduct>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateProductName>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateProductDescription>,
) -> 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<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateProductPrice>,
) -> 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<State>,
Path(id): Path<usize>,
) -> 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()),
}
}

View file

@ -5,7 +5,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{addr::RemoteAddr, oauth, reactions::Reaction, ApiReturn, Error}; use tetratto_core::model::{addr::RemoteAddr, oauth, reactions::Reaction, ApiReturn, Error};
pub async fn get_request( pub async fn get_request(

View file

@ -1,7 +1,7 @@
use super::CreateReport; use super::CreateReport;
use crate::{State, get_user_from_token}; use crate::{State, get_user_from_token};
use axum::{Extension, Json, extract::Path, response::IntoResponse}; 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}; use tetratto_core::model::{ApiReturn, Error, moderation::Report};
pub async fn create_request( pub async fn create_request(

View file

@ -4,7 +4,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{oauth, ApiReturn, Error}; use tetratto_core::model::{oauth, ApiReturn, Error};
pub async fn delete_request( pub async fn delete_request(

View file

@ -6,9 +6,8 @@ use crate::{
State, State,
}; };
use axum::{extract::Path, response::IntoResponse, Extension, Json}; use axum::{extract::Path, response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{auth::AchievementName, littleweb::Service, oauth, ApiReturn, Error}; use tetratto_core::model::{littleweb::Service, oauth, ApiReturn, Error};
use tetratto_shared::unix_epoch_timestamp;
pub async fn get_request( pub async fn get_request(
Path(id): Path<usize>, Path(id): Path<usize>,
@ -48,20 +47,11 @@ pub async fn create_request(
Json(req): Json<CreateService>, Json(req): Json<CreateService>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = &(data.read().await).0; 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, Some(ua) => ua,
None => return Json(Error::NotAllowed.into()), 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 { match data.create_service(Service::new(req.name, user.id)).await {
Ok(x) => Json(ApiReturn { Ok(x) => Json(ApiReturn {
ok: true, ok: true,
@ -157,17 +147,11 @@ pub async fn update_content_request(
// ... // ...
match data.update_service_files(id, &user, service.files).await { match data.update_service_files(id, &user, service.files).await {
Ok(_) => match data Ok(_) => Json(ApiReturn {
.update_service_revision(id, unix_epoch_timestamp() as i64) ok: true,
.await message: "Service updated".to_string(),
{ payload: (),
Ok(_) => Json(ApiReturn { }),
ok: true,
message: "Service updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
},
Err(e) => Json(e.into()), Err(e) => Json(e.into()),
} }
} }

View file

@ -4,7 +4,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::{ use tetratto_core::{
model::{ model::{
oauth, oauth,

View file

@ -1,8 +1,8 @@
use std::fs::exists; use std::fs::exists;
use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json}; use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json};
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use pathbufd::PathBufD; 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 super::auth::images::read_image;
use tetratto_core::model::{carp::CarpGraph, oauth, uploads::MediaType, ApiReturn, Error}; 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))) Ok(([("Content-Type", upload.what.mime())], Body::from(bytes)))
} }
pub async fn get_json_request(
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> 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( pub async fn delete_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
@ -90,25 +72,3 @@ pub async fn delete_request(
Err(e) => Json(e.into()), Err(e) => Json(e.into()),
} }
} }
pub async fn update_alt_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(props): Json<UpdateUploadAlt>,
) -> 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()),
}
}

View file

@ -7,7 +7,7 @@ use axum::{
response::IntoResponse, response::IntoResponse,
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use pathbufd::PathBufD; use pathbufd::PathBufD;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::permissions::FinePermission; use tetratto_core::model::permissions::FinePermission;

View file

@ -19,5 +19,5 @@ serve_asset!(atto_js_request: ATTO_JS("text/javascript"));
serve_asset!(me_js_request: ME_JS("text/javascript")); serve_asset!(me_js_request: ME_JS("text/javascript"));
serve_asset!(streams_js_request: STREAMS_JS("text/javascript")); serve_asset!(streams_js_request: STREAMS_JS("text/javascript"));
serve_asset!(carp_js_request: CARP_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!(proto_links_request: PROTO_LINKS_JS("text/javascript"));
serve_asset!(app_sdk_request: APP_SDK_JS("text/javascript"));

View file

@ -20,8 +20,11 @@ pub fn routes(config: &Config) -> Router {
.route("/js/me.js", get(assets::me_js_request)) .route("/js/me.js", get(assets::me_js_request))
.route("/js/streams.js", get(assets::streams_js_request)) .route("/js/streams.js", get(assets::streams_js_request))
.route("/js/carp.js", get(assets::carp_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/proto_links.js", get(assets::proto_links_request))
.route("/js/app_sdk.js", get(assets::app_sdk_request))
.nest_service( .nest_service(
"/public", "/public",
get_service(tower_http::services::ServeDir::new(&config.dirs.assets)), get_service(tower_http::services::ServeDir::new(&config.dirs.assets)),

View file

@ -4,7 +4,7 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{Error, auth::ConnectionService}; use tetratto_core::model::{Error, auth::ConnectionService};
use super::render_error; use super::render_error;

View file

@ -5,7 +5,7 @@ use axum::{
response::{Html, IntoResponse, Redirect}, response::{Html, IntoResponse, Redirect},
Extension, Json, Extension, Json,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{ use tetratto_core::model::{
channels::Message, communities_permissions::CommunityPermission, permissions::FinePermission, channels::Message, communities_permissions::CommunityPermission, permissions::FinePermission,
Error, Error,

View file

@ -1,5 +1,3 @@
use std::collections::HashMap;
use super::{render_error, PaginatedQuery, RepostsQuery, SearchedQuery}; use super::{render_error, PaginatedQuery, RepostsQuery, SearchedQuery};
use crate::{ use crate::{
assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, State, assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token, State,
@ -10,7 +8,7 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tera::Context; use tera::Context;
use tetratto_core::model::{ use tetratto_core::model::{
@ -124,20 +122,12 @@ macro_rules! community_context_bools {
) )
} else { } else {
false 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 { let can_manage_community = if let Some(ref membership) = membership {
membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_COMMUNITY) membership.role.check(tetratto_core::model::communities_permissions::CommunityPermission::MANAGE_COMMUNITY)
} else { } else {
false 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 { 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; let (_, reposting) = data.0.get_post_reposting(&post, &ignore_users, &user).await;
// check question // check question
let question = match data let question = match data.0.get_post_question(&post, &ignore_users).await {
.0
.get_post_question(&post, &ignore_users, &mut HashMap::new())
.await
{
Ok(q) => q, Ok(q) => q,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), 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; let reposting = data.0.get_post_reposting(&post, &ignore_users, &user).await;
// check question // check question
let question = match data let question = match data.0.get_post_question(&post, &ignore_users).await {
.0
.get_post_question(&post, &ignore_users, &mut HashMap::new())
.await
{
Ok(q) => q, Ok(q) => q,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
}; };
@ -1087,11 +1069,7 @@ pub async fn likes_request(
.await; .await;
// check question // check question
let question = match data let question = match data.0.get_post_question(&post, &ignore_users).await {
.0
.get_post_question(&post, &ignore_users, &mut HashMap::new())
.await
{
Ok(q) => q, Ok(q) => q,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
}; };

View file

@ -5,8 +5,8 @@ use axum::{
extract::Path, extract::Path,
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{apps::AppData, permissions::FinePermission, Error}; use tetratto_core::model::{permissions::FinePermission, Error};
/// `/developer` /// `/developer`
pub async fn home_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse { pub async fn home_request(jar: CookieJar, Extension(data): Extension<State>) -> 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 lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("app", &app); context.insert("app", &app);
context.insert("data_limit", &data_limit);
// return // return
Ok(Html(data.1.render("developer/app.html", &context).unwrap())) Ok(Html(data.1.render("developer/app.html", &context).unwrap()))

View file

@ -8,7 +8,7 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{communities::Community, Error}; use tetratto_core::model::{communities::Community, Error};
/// `/forges` /// `/forges`

View file

@ -3,7 +3,7 @@ use axum::{
response::{Html, IntoResponse, Redirect}, response::{Html, IntoResponse, Redirect},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use crate::{ use crate::{
assets::initial_context, assets::initial_context,
check_user_blocked_or_private, get_lang, get_user_from_token, check_user_blocked_or_private, get_lang, get_user_from_token,
@ -365,7 +365,7 @@ pub async fn global_view_request(
Ok(( Ok((
[( [(
"content-security-policy", "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()), Html(data.1.render("journals/app.html", &context).unwrap()),
)) ))

View file

@ -1,23 +1,18 @@
use super::render_error; use super::render_error;
use crate::{ use crate::{assets::initial_context, get_lang, get_user_from_token, State};
assets::initial_context, get_lang, get_user_from_token,
routes::pages::misc::NotificationsProps, State,
};
use axum::{ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
extract::{Query, Path}, extract::{Query, Path},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use tetratto_core::model::{littleweb::TLDS_VEC, permissions::SecondaryPermission, Error}; use tetratto_core::model::{littleweb::TLDS_VEC, Error};
use serde::Deserialize; use serde::Deserialize;
use tetratto_shared::hash::salt;
/// `/services` /// `/services`
pub async fn services_request( pub async fn services_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Query(props): Query<NotificationsProps>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = data.read().await; let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) { 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 { let list = match data.0.get_services_by_user(user.id).await {
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 {
Ok(x) => x, Ok(x) => x,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), 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 lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await; let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("list", &list); context.insert("list", &list);
context.insert("profile", &profile);
// return // return
Ok(Html( Ok(Html(
@ -68,7 +43,6 @@ pub async fn services_request(
pub async fn domains_request( pub async fn domains_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Query(props): Query<NotificationsProps>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let data = data.read().await; let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) { 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 { let list = match data.0.get_domains_by_user(user.id).await {
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 {
Ok(x) => x, Ok(x) => x,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), 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("list", &list);
context.insert("tlds", &*TLDS_VEC); context.insert("tlds", &*TLDS_VEC);
context.insert("profile", &profile);
// return // return
Ok(Html( Ok(Html(
@ -145,11 +99,7 @@ pub async fn service_request(
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
}; };
if user.id != service.owner if user.id != service.owner {
&& !user
.secondary_permissions
.check(SecondaryPermission::MANAGE_SERVICES)
{
return Err(Html( return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &None).await, 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)), Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
}; };
if user.id != domain.owner if user.id != domain.owner {
&& !user
.secondary_permissions
.check(SecondaryPermission::MANAGE_DOMAINS)
{
return Err(Html( return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &None).await, render_error(Error::NotAllowed, &jar, &data, &None).await,
)); ));
@ -231,26 +177,12 @@ pub async fn browser_home_request(
let data = data.read().await; let data = data.read().await;
let user = get_user_from_token!(jar, data.0); 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 lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await; let mut context = initial_context(&data.0.0.0, lang, &user).await;
context.insert("path", &""); context.insert("path", &"");
context.insert("session", &session);
// return // return
Ok(Html( Html(data.1.render("littleweb/browser.html", &context).unwrap())
data.1.render("littleweb/browser.html", &context).unwrap(),
))
} }
/// `/net/{uri}` /// `/net/{uri}`
@ -270,24 +202,10 @@ pub async fn browser_request(
uri = format!("atto://{uri}"); 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 lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await; let mut context = initial_context(&data.0.0.0, lang, &user).await;
context.insert("path", &uri);
context.insert("session", &session);
context.insert("path", &uri.replace("atto://", ""));
// return // return
Ok(Html( Html(data.1.render("littleweb/browser.html", &context).unwrap())
data.1.render("littleweb/browser.html", &context).unwrap(),
))
} }

View file

@ -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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<State>,
) -> 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<State>,
) -> 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(),
))
}

View file

@ -7,7 +7,7 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::model::{ use tetratto_core::model::{
auth::{AchievementName, DefaultTimelineChoice, ACHIEVEMENTS}, auth::{AchievementName, DefaultTimelineChoice, ACHIEVEMENTS},
@ -58,7 +58,7 @@ pub async fn index_request(
let list = match data let list = match data
.0 .0
.get_posts_from_user_communities(user.id, 12, req.page, &user) .get_posts_from_user_communities(user.id, 12, req.page)
.await .await
{ {
Ok(l) => match data Ok(l) => match data
@ -725,7 +725,7 @@ pub async fn swiss_army_timeline_request(
DefaultTimelineChoice::MyCommunities => { DefaultTimelineChoice::MyCommunities => {
if let Some(ref ua) = user { if let Some(ref ua) = user {
data.0 data.0
.get_posts_from_user_communities(ua.id, 12, req.page, ua) .get_posts_from_user_communities(ua.id, 12, req.page)
.await .await
} else { } else {
return Err(Html( return Err(Html(

View file

@ -5,7 +5,6 @@ pub mod developer;
pub mod forge; pub mod forge;
pub mod journals; pub mod journals;
pub mod littleweb; pub mod littleweb;
pub mod marketplace;
pub mod misc; pub mod misc;
pub mod mod_panel; pub mod mod_panel;
pub mod profile; pub mod profile;
@ -15,13 +14,14 @@ use axum::{
routing::{get, post}, routing::{get, post},
Router, Router,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::{ use tetratto_core::{
DataManager,
model::{Error, auth::User}, model::{Error, auth::User},
}; };
use crate::{assets::initial_context, get_lang, InnerState}; use crate::{assets::initial_context, get_lang};
pub fn routes() -> Router { pub fn routes() -> Router {
Router::new() Router::new()
@ -77,14 +77,6 @@ pub fn routes() -> Router {
"/auth/connections_link/app/{id}", "/auth/connections_link/app/{id}",
get(developer::connection_callback_request), 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 // profile
.route("/settings", get(profile::settings_request)) .route("/settings", get(profile::settings_request))
.route("/@{username}", get(profile::posts_request)) .route("/@{username}", get(profile::posts_request))
@ -155,11 +147,6 @@ pub fn routes() -> Router {
.route("/domains/{id}", get(littleweb::domain_request)) .route("/domains/{id}", get(littleweb::domain_request))
.route("/net", get(littleweb::browser_home_request)) .route("/net", get(littleweb::browser_home_request))
.route("/net/{*uri}", get(littleweb::browser_request)) .route("/net/{*uri}", get(littleweb::browser_request))
// marketplace
.route(
"/settings/seller",
get(marketplace::seller_settings_request),
)
} }
pub fn lw_routes() -> Router { pub fn lw_routes() -> Router {
@ -169,7 +156,7 @@ pub fn lw_routes() -> Router {
pub async fn render_error( pub async fn render_error(
e: Error, e: Error,
jar: &CookieJar, jar: &CookieJar,
data: &InnerState, data: &(DataManager, tera::Tera, reqwest::Client),
user: &Option<User>, user: &Option<User>,
) -> String { ) -> String {
let lang = get_lang!(jar, data.0); let lang = get_lang!(jar, data.0);

View file

@ -5,7 +5,7 @@ use axum::{
response::{Html, IntoResponse}, response::{Html, IntoResponse},
Extension, Extension,
}; };
use crate::cookie::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tetratto_core::{ use tetratto_core::{
cache::Cache, cache::Cache,

Some files were not shown because too many files have changed in this diff Show more