add: safety_content_warning, master_pass env var

This commit is contained in:
trisua 2025-08-02 01:30:11 -04:00
parent a6a289c594
commit 15b417c6d6
9 changed files with 150 additions and 29 deletions

46
Cargo.lock generated
View file

@ -571,7 +571,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -623,7 +623,7 @@ dependencies = [
"tetratto-core", "tetratto-core",
"tetratto-shared", "tetratto-shared",
"tokio", "tokio",
"toml 0.9.2", "toml 0.9.4",
"tower-http", "tower-http",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@ -997,7 +997,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2", "socket2 0.5.10",
"system-configuration", "system-configuration",
"tokio", "tokio",
"tower-service", "tower-service",
@ -1951,7 +1951,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"ryu", "ryu",
"sha1_smol", "sha1_smol",
"socket2", "socket2 0.5.10",
"tokio", "tokio",
"tokio-util", "tokio-util",
"url", "url",
@ -2082,7 +2082,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2205,9 +2205,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.141" version = "1.0.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -2397,6 +2397,16 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@ -2513,7 +2523,7 @@ dependencies = [
"getrandom 0.3.3", "getrandom 0.3.3",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2571,7 +2581,7 @@ dependencies = [
"tetratto-l10n", "tetratto-l10n",
"tetratto-shared", "tetratto-shared",
"tokio", "tokio",
"toml 0.9.2", "toml 0.9.4",
"totp-rs", "totp-rs",
] ]
@ -2583,7 +2593,7 @@ checksum = "d96f5e41633c757e3519efb47c9b85d00d14322c1961360e126d0ecc0ea79b86"
dependencies = [ dependencies = [
"pathbufd", "pathbufd",
"serde", "serde",
"toml 0.9.2", "toml 0.9.4",
] ]
[[package]] [[package]]
@ -2711,9 +2721,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.46.1" version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@ -2723,9 +2733,9 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
"socket2", "socket2 0.6.0",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -2769,7 +2779,7 @@ dependencies = [
"postgres-protocol", "postgres-protocol",
"postgres-types", "postgres-types",
"rand 0.9.1", "rand 0.9.1",
"socket2", "socket2 0.5.10",
"tokio", "tokio",
"tokio-util", "tokio-util",
"whoami", "whoami",
@ -2824,9 +2834,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.2" version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -3385,7 +3395,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]

View file

@ -10,7 +10,7 @@ homepage = "https://fluffle.cc"
[dependencies] [dependencies]
tetratto-core = "12.0.2" tetratto-core = "12.0.2"
tetratto-shared = "12.0.6" tetratto-shared = "12.0.6"
tokio = { version = "1.46.1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
pathbufd = "0.1.4" pathbufd = "0.1.4"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
tera = "1.20.0" tera = "1.20.0"
@ -27,7 +27,7 @@ axum-extra = { version = "0.10.1", features = ["cookie"] }
nanoneo = "0.2.0" nanoneo = "0.2.0"
dotenv = "0.15.0" dotenv = "0.15.0"
glob = "0.3.2" glob = "0.3.2"
serde_json = "1.0.141" serde_json = "1.0.142"
toml = "0.9.2" toml = "0.9.4"
serde_valid = { version = "1.0.5", features = ["toml"] } serde_valid = { version = "1.0.5", features = ["toml"] }
regex = "1.11.1" regex = "1.11.1"

View file

@ -38,6 +38,8 @@ Once you've built the binary, it'll be located at (from the root `fluffle/` dire
All templates are compiled with [nanoneo](https://trisua.com/t/nanoneo), so it's recommended that you familiarize yourself with that syntax. All templates are compiled with [nanoneo](https://trisua.com/t/nanoneo), so it's recommended that you familiarize yourself with that syntax.
You can set a master password to be able to freely edit all entries by using the `MASTER_PASS` environment variable.
## Attribution ## Attribution
Fluffle is licensed under the AGPL-3.0 license. Tetratto is also licensed under the AGPL-3.0 license. Fluffle is licensed under the AGPL-3.0 license. Tetratto is also licensed under the AGPL-3.0 license.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -301,7 +301,7 @@ input {
border-left: solid 0px transparent; border-left: solid 0px transparent;
} }
input:focus { input:not([type="checkbox"]):focus {
outline: solid 2px var(--color-primary); outline: solid 2px var(--color-primary);
box-shadow: 0 0 0 4px oklch(87% 0.065 274.039 / 25%); box-shadow: 0 0 0 4px oklch(87% 0.065 274.039 / 25%);
background: var(--color-super-raised); background: var(--color-super-raised);
@ -316,6 +316,10 @@ input.surface {
background: var(--color-surface); background: var(--color-surface);
} }
input[type="checkbox"] {
height: max-content;
}
/* typo */ /* typo */
p { p {
margin-bottom: var(--pad-4); margin-bottom: var(--pad-4);

View file

@ -61,6 +61,14 @@
(button (button
("class" "button camo fade no_fill") ("class" "button camo fade no_fill")
("title" "Toggle high-contrast") ("title" "Toggle high-contrast")
("id" "toggle_high_contrast_button")
("onclick" "toggle_metadata_css(event)") ("onclick" "toggle_metadata_css(event)")
(text "{{ icon \"contrast\" }}")) (text "{{ icon \"contrast\" }}"))
(text "{% if \"EntryHighContrast\" in flags -%}")
(script
(text "setTimeout(() => {
toggle_metadata_css({ target: document.getElementById(\"toggle_high_contrast_button\") });
}, 150);"))
(text "{%- endif %}")
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -0,0 +1,54 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(text "{% if not metadata.page_title -%}")
(title
(text "{{ entry.slug }}"))
(text "{%- endif %} {{ metadata_head|safe }}")
(text "{% if not metadata.share_title -%}")
(meta ("property" "og:title") ("content" "{{ entry.slug }}"))
(meta ("property" "twitter:title") ("content" "{{ entry.slug }}"))
(text "{%- endif %}")
(text "{% if metadata.page_icon|length == 0 -%}")
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{%- endif %}")
(text "{% endblock %} {% block body %}")
(div
("class" "card container flex flex-col gap-1")
("id" "content_rect")
(p ("class" "fade") (text "Content warning:"))
(div (text "{{ metadata.safety_content_warning|markdown|safe }}"))
(hr)
(div
("class" "flex flex-col gap-4")
(label
("class" "flex flex-row gap-2 items-center")
("for" "open_in_high_contrast")
(input
("type" "checkbox")
("id" "open_in_high_contrast")
("name" "open_in_high_contrast"))
(span (text "Open in high contrast")))
(div
("class" "flex gap-2")
(button
("class" "button surface green")
("onclick" "accept()")
(text "Continue"))
(button
("class" "button surface red")
("onclick" "window.history.back()")
(text "Cancel")))))
(script
(text "const QFLAGS = [\"AcceptWarning\"];
function accept() {
if (document.getElementById(\"open_in_high_contrast\").checked) {
QFLAGS.push(\"EntryHighContrast\");
}
document.cookie = `Atto-QFlags=\"${JSON.stringify(QFLAGS)}\"; path=/`;
window.location.reload();
}"))
(text "{% endblock %}")

View file

@ -342,6 +342,7 @@ pub struct EntryMetadata {
pub content_font_weight: u32, pub content_font_weight: u32,
/// The text size of elements (separated by space). /// The text size of elements (separated by space).
#[serde(default, alias = "CONTENT_TEXT_SIZE")] #[serde(default, alias = "CONTENT_TEXT_SIZE")]
#[validate(max_length = 128)]
pub content_text_size: String, pub content_text_size: String,
/// The text size of elements by element tag. /// The text size of elements by element tag.
/// ///
@ -357,13 +358,19 @@ pub struct EntryMetadata {
pub content_text_align: TextAlignment, pub content_text_align: TextAlignment,
/// The base text color. /// The base text color.
#[serde(default, alias = "CONTENT_TEXT_COLOR")] #[serde(default, alias = "CONTENT_TEXT_COLOR")]
#[validate(max_length = 128)]
pub content_text_color: String, pub content_text_color: String,
/// The color of links. /// The color of links.
#[serde(default, alias = "CONTENT_LINK_COLOR")] #[serde(default, alias = "CONTENT_LINK_COLOR")]
#[validate(max_length = 128)]
pub content_link_color: String, pub content_link_color: String,
/// If paragraph elements have a margin below them. /// If paragraph elements have a margin below them.
#[serde(default, alias = "CONTENT_DISABLE_PARAGRAPH_MARGIN")] #[serde(default, alias = "CONTENT_DISABLE_PARAGRAPH_MARGIN")]
pub content_disable_paragraph_margin: bool, pub content_disable_paragraph_margin: bool,
/// The content warning shown before viewing this entry.
#[serde(default, alias = "SAFETY_CONTENT_WARNING")]
#[validate(max_length = 512)]
pub safety_content_warning: String,
} }
macro_rules! metadata_css { macro_rules! metadata_css {
@ -684,3 +691,15 @@ impl EntryMetadata {
output output
} }
} }
/// Flags that can be provided through the "Atto-QFlags" cookie to customize the
/// resulting page.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum QuickFlag {
/// If the entry's warning is automatically skipped.
AcceptWarning,
/// If the entry is automatically set to high-contrast mode.
EntryHighContrast,
}
pub type QuickFlags = Vec<QuickFlag>;

View file

@ -2,7 +2,7 @@ use std::env::var;
use crate::{ use crate::{
State, State,
model::{Entry, EntryMetadata}, model::{Entry, EntryMetadata, QuickFlag, QuickFlags},
}; };
use axum::{ use axum::{
Extension, Json, Router, Extension, Json, Router,
@ -126,11 +126,20 @@ pub struct ViewQuery {
} }
async fn view_request( async fn view_request(
jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Path(mut slug): Path<String>, Path(mut slug): Path<String>,
Query(props): Query<ViewQuery>, Query(props): Query<ViewQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await; let (ref data, ref tera, ref build_code) = *data.read().await;
let qflags: QuickFlags = match jar.get("Atto-QFlags") {
Some(x) => match serde_json::from_str(&x.value_trimmed()) {
Ok(x) => x,
Err(_) => QuickFlags::default(),
},
None => QuickFlags::default(),
};
slug = slug.to_lowercase(); slug = slug.to_lowercase();
let entry = match data let entry = match data
@ -222,8 +231,15 @@ async fn view_request(
ctx.insert("metadata_head", &metadata.head_tags()); ctx.insert("metadata_head", &metadata.head_tags());
ctx.insert("metadata_css", &metadata.css()); ctx.insert("metadata_css", &metadata.css());
ctx.insert("password", &props.key); ctx.insert("password", &props.key);
ctx.insert("flags", &qflags);
Html(tera.render("view.lisp", &ctx).unwrap()) if metadata.safety_content_warning.is_empty() | qflags.contains(&QuickFlag::AcceptWarning) {
// regular view
Html(tera.render("view.lisp", &ctx).unwrap())
} else {
// warning
Html(tera.render("warning.lisp", &ctx).unwrap())
}
} }
async fn editor_request( async fn editor_request(
@ -614,12 +630,20 @@ async fn edit_request(
let using_modify_code = edit_code == entry.modify_code; let using_modify_code = edit_code == entry.modify_code;
// check edit code // check edit code
if edit_code let mut using_master = false;
!= *if using_modify_code { if let Ok(master_pass) = var("MASTER_PASS") {
&entry.modify_code if req.edit_code == master_pass {
} else { using_master = true;
&entry.edit_code
} }
}
if !using_master
&& edit_code
!= *if using_modify_code {
&entry.modify_code
} else {
&entry.edit_code
}
{ {
return Json(Error::NotAllowed.into()); return Json(Error::NotAllowed.into());
} }