add: reclaims, tetratto_handler_account_username, entry owners

This commit is contained in:
trisua 2025-08-16 00:20:46 -04:00
parent 105d01b45d
commit f215d038b3
7 changed files with 177 additions and 6 deletions

View file

@ -264,12 +264,19 @@ video {
display: flex; display: flex;
} }
.dropdown .inner .button { .dropdown .inner .button,
.dropdown .inner .title {
padding: var(--pad-3) var(--pad-4); padding: var(--pad-3) var(--pad-4);
justify-content: flex-start; justify-content: flex-start;
width: 100%; width: 100%;
} }
.dropdown .inner .title {
font-weight: 500;
border-top: solid 1px var(--color-super-lowered);
margin-top: var(--pad-2);
}
.dropdown:has(.inner.open) .button:nth-child(1):not(.inner *) { .dropdown:has(.inner.open) .button:nth-child(1):not(.inner *) {
background: var(--color-raised); background: var(--color-raised);
} }
@ -517,6 +524,18 @@ img {
vertical-align: middle; vertical-align: middle;
} }
.img_sizer img {
width: 100%;
height: 100%;
}
.avatar {
--size: 18px;
width: var(--size);
height: var(--size);
aspect-ratio: 1 / 1;
}
blockquote { blockquote {
padding-left: 1rem; padding-left: 1rem;
border-left: solid 5px var(--color-green); border-left: solid 5px var(--color-green);

View file

@ -0,0 +1,39 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "Create reclaim for \"{{ entry.slug }}\" - {{ name }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(div
("class" "card container")
(h1 (text "{{ entry.slug }}"))
(p (text "Custom slug reclaims are handled through ") (b (text "{{ tetratto }}")) (text ". You'll need to have an account there to submit a claim request."))
(p (text "Please note that you are unlikely to receive a response unless your claim is accepted. Please do not submit additional requests for the same slug."))
(text "{% if metadata.tetratto_owner_username -%}")
; contact owner text
(p (text "Since this entry is connected to a user, it is encouraged that you directly contact the owner of the entry instead. If that doesn't work, you can create a regular request."))
(text "{%- endif %}")
(p (text "Once you're ready, you can submit a claim using the button below."))
(hr)
(ul
(li (b (text "Requsted slug: ")) (text "{{ entry.slug }}"))
(li (b (text "Last updated: ")) (text "{{ entry.edited / 1000|int|date(format=\"%Y-%m-%d %H:%M\", timezone=\"Etc/UTC\") }} UTC")))
(hr)
(text "{% if claimable -%}")
(text "{% if metadata.tetratto_owner_username -%}")
; contact owner button
(a
("href" "{{ tetratto }}/mail/compose?receivers={{ tetratto_owner_username }}&subject=Reclaim%20for%20%22{{ entry.slug }}%22")
("class" "button surface no_fill")
(text "{{ icon \"external-link\" }} Contact owner"))
(text "{%- endif %}")
(a
("href" "{{ tetratto }}/mail/compose?receivers={{ tetratto_handler_account_username }}&subject=Reclaim%20for%20%22{{ entry.slug }}%22")
("class" "button surface no_fill")
(text "{{ icon \"external-link\" }} Submit request"))
(text "{% else %}")
(span (text "This slug is currently not claimable as it was edited too recently. ") (a ("href" "/{{ entry.slug }}") ("class" "red") (text "Go back")))
(text "{%- endif %}"))
(text "{% endblock %}")

View file

@ -45,7 +45,8 @@
(a (a
("class" "button") ("class" "button")
("href" "https://trisua.com/t/fluffle") ("href" "https://trisua.com/t/fluffle")
(text "source")))) (text "source"))
(text "{% block dropdown %}{% endblock %}")))
(a (a
("class" "button camo fade") ("class" "button camo fade")

View file

@ -12,7 +12,6 @@
(text "{% if metadata.page_icon|length == 0 -%}") (text "{% if metadata.page_icon|length == 0 -%}")
(link ("rel" "icon") ("href" "/public/favicon.svg")) (link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{%- endif %}") (text "{%- endif %}")
(text "{% endblock %} {% block body %}") (text "{% endblock %} {% block body %}")
(div (div
("class" "flex flex_col gap_2") ("class" "flex flex_col gap_2")
@ -40,6 +39,20 @@
(script ("defer" "true") (text "setTimeout(() => { temporary_set_theme('{{ metadata.access_recommended_theme }}') }, 150);")) (script ("defer" "true") (text "setTimeout(() => { temporary_set_theme('{{ metadata.access_recommended_theme }}') }, 150);"))
(text "{%- endif %}") (text "{%- endif %}")
; owner
(text "{% if metadata.tetratto_owner_username|length > 0 -%}")
(span
("class" "flex items_center gap_2")
(text "Owner:")
(a
("class" "flex items_center gap_2")
("href" "{{ tetratto }}/@{{ metadata.tetratto_owner_username }}")
(img
("class" "avatar")
("src" "{{ tetratto }}/api/v1/auth/user/{{ metadata.tetratto_owner_username }}/avatar?selector_type=username"))
(text "{{ metadata.tetratto_owner_username }}")))
(text "{%- endif %}")
; views ; views
(text "{% if not metadata.option_disable_views -%}") (text "{% if not metadata.option_disable_views -%}")
(span (text "Views: {{ views }}")) (span (text "Views: {{ views }}"))
@ -55,7 +68,6 @@
(link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css")) (link ("rel" "stylesheet") ("href" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"))
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js")) (script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
(script (text "hljs.highlightAll();")) (script (text "hljs.highlightAll();"))
(text "{% endblock %}") (text "{% endblock %}")
(text "{% block nav_extras %}") (text "{% block nav_extras %}")
(button (button
@ -72,3 +84,11 @@
}, 150);")) }, 150);"))
(text "{%- endif %}") (text "{%- endif %}")
(text "{% endblock %}") (text "{% endblock %}")
(text "{% block dropdown %}")
(hr)
(a
("class" "button")
("href" "/{{ entry.slug }}/claim")
(text "claim"))
(text "{%- endblock %}")

View file

@ -3,8 +3,8 @@ use std::collections::HashSet;
pub fn render_markdown(input: &str) -> String { pub fn render_markdown(input: &str) -> String {
let html = tetratto_shared::markdown::render_markdown_dirty(&parse_page(&parse_details( let html = tetratto_shared::markdown::render_markdown_dirty(&parse_page(&parse_details(
&parse_text_color(&parse_highlight(&parse_link(&parse_image( &parse_text_color(&parse_highlight(&parse_link(&parse_image(
&parse_image_size(&parse_toc(&parse_underline(&parse_comment( &parse_image_size(&parse_toc(&parse_underline(&parse_markdown_element(
&input.replace("[/]", "<br />"), &parse_comment(&input.replace("[/]", "<br />")),
)))), )))),
)))), )))),
))) )))
@ -983,3 +983,26 @@ pub fn parse_toc(input: &str) -> String {
output output
} }
/// Handle the `<markdown>` HTML element.
fn parse_markdown_element_line(output: &mut String, buffer: &mut String, line: &str) {
let mut in_markdown = false;
for char in line.chars() {
if buffer.ends_with("<markdown>") {
in_markdown = true;
output.push_str(&buffer.replace("<markdown>", ""));
buffer.clear();
} else if in_markdown && buffer.ends_with("</markdown>") {
in_markdown = false;
output.push_str(&render_markdown(&buffer.replace("</markdown>", "")));
buffer.clear();
}
buffer.push(char);
}
}
pub fn parse_markdown_element(input: &str) -> String {
parser_ignores_pre!(parse_markdown_element_line, input)
}

View file

@ -371,6 +371,13 @@ pub struct EntryMetadata {
#[serde(default, alias = "SAFETY_CONTENT_WARNING")] #[serde(default, alias = "SAFETY_CONTENT_WARNING")]
#[validate(max_length = 512)] #[validate(max_length = 512)]
pub safety_content_warning: String, pub safety_content_warning: String,
/// The username of the owner of this entry on the Tetratto instance.
///
/// For security reasons, this really does nothing but show the owner in the
/// entry's info section.
#[serde(default, alias = "TETRATTO_OWNER_USERNAME")]
#[validate(max_length = 32)]
pub tetratto_owner_username: String,
} }
macro_rules! metadata_css { macro_rules! metadata_css {

View file

@ -42,6 +42,7 @@ pub fn routes() -> Router {
.route("/", get(index_request)) .route("/", get(index_request))
.route("/{slug}", get(view_request)) .route("/{slug}", get(view_request))
.route("/{slug}/edit", get(editor_request)) .route("/{slug}/edit", get(editor_request))
.route("/{slug}/claim", get(reclaim_request))
// api // api
.route("/api/v1/util/ip", get(util_ip)) .route("/api/v1/util/ip", get(util_ip))
.route("/api/v1/render", post(render_request)) .route("/api/v1/render", post(render_request))
@ -62,6 +63,10 @@ fn default_context(data: &DataClient, build_code: &str) -> Context {
"what_page_slug", "what_page_slug",
&var("WHAT_SLUG").unwrap_or("what".to_string()), &var("WHAT_SLUG").unwrap_or("what".to_string()),
); );
ctx.insert(
"tetratto_handler_account_username",
&var("TETRATTO_HANDLER_ACCOUNT_USERNAME").unwrap_or("fluffle".to_string()),
);
ctx.insert("build_code", &build_code); ctx.insert("build_code", &build_code);
ctx ctx
} }
@ -300,6 +305,63 @@ async fn editor_request(
Html(tera.render("edit.lisp", &ctx).unwrap()) Html(tera.render("edit.lisp", &ctx).unwrap())
} }
const MINIMUM_CLAIMABLE_TIME: usize = 604_800_000; // 1 week
async fn reclaim_request(
Extension(data): Extension<State>,
Path(mut slug): Path<String>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
slug = slug.to_lowercase();
let entry = match data
.query(&SimplifiedQuery {
query: AppDataSelectQuery::KeyIs(format!("entries('{}')", slug.to_lowercase())),
mode: AppDataSelectMode::One(0),
})
.await
{
Ok(r) => match r {
AppDataQueryResult::One(r) => serde_json::from_str::<Entry>(&r.value).unwrap(),
AppDataQueryResult::Many(_) => unreachable!(),
},
Err(_) => {
let mut ctx = default_context(&data, &build_code);
ctx.insert(
"error",
&Error::GeneralNotFound("entry".to_string()).to_string(),
);
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
// check metadata
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&entry.metadata))
{
Ok(x) => x,
Err(_) => EntryMetadata::default(),
};
if let Err(e) = metadata.validate() {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
// ...
let mut ctx = default_context(&data, &build_code);
ctx.insert("entry", &entry);
ctx.insert("metadata", &metadata);
ctx.insert(
"claimable",
&((unix_epoch_timestamp() - entry.edited) > MINIMUM_CLAIMABLE_TIME),
);
Html(tera.render("claim.lisp", &ctx).unwrap())
}
// api // api
#[derive(Deserialize)] #[derive(Deserialize)]
struct RenderMarkdown { struct RenderMarkdown {