add: reclaims, tetratto_handler_account_username, entry owners
This commit is contained in:
parent
105d01b45d
commit
f215d038b3
7 changed files with 177 additions and 6 deletions
|
@ -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);
|
||||||
|
|
39
app/templates_src/claim.lisp
Normal file
39
app/templates_src/claim.lisp
Normal 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 %}")
|
|
@ -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")
|
||||||
|
|
|
@ -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 %}")
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue