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;
|
||||
}
|
||||
|
||||
.dropdown .inner .button {
|
||||
.dropdown .inner .button,
|
||||
.dropdown .inner .title {
|
||||
padding: var(--pad-3) var(--pad-4);
|
||||
justify-content: flex-start;
|
||||
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 *) {
|
||||
background: var(--color-raised);
|
||||
}
|
||||
|
@ -517,6 +524,18 @@ img {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.img_sizer img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
--size: 18px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding-left: 1rem;
|
||||
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
|
||||
("class" "button")
|
||||
("href" "https://trisua.com/t/fluffle")
|
||||
(text "source"))))
|
||||
(text "source"))
|
||||
(text "{% block dropdown %}{% endblock %}")))
|
||||
|
||||
(a
|
||||
("class" "button camo fade")
|
||||
|
|
|
@ -12,7 +12,6 @@
|
|||
(text "{% if metadata.page_icon|length == 0 -%}")
|
||||
(link ("rel" "icon") ("href" "/public/favicon.svg"))
|
||||
(text "{%- endif %}")
|
||||
|
||||
(text "{% endblock %} {% block body %}")
|
||||
(div
|
||||
("class" "flex flex_col gap_2")
|
||||
|
@ -40,6 +39,20 @@
|
|||
(script ("defer" "true") (text "setTimeout(() => { temporary_set_theme('{{ metadata.access_recommended_theme }}') }, 150);"))
|
||||
(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
|
||||
(text "{% if not metadata.option_disable_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"))
|
||||
(script ("src" "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"))
|
||||
(script (text "hljs.highlightAll();"))
|
||||
|
||||
(text "{% endblock %}")
|
||||
(text "{% block nav_extras %}")
|
||||
(button
|
||||
|
@ -72,3 +84,11 @@
|
|||
}, 150);"))
|
||||
(text "{%- endif %}")
|
||||
(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 {
|
||||
let html = tetratto_shared::markdown::render_markdown_dirty(&parse_page(&parse_details(
|
||||
&parse_text_color(&parse_highlight(&parse_link(&parse_image(
|
||||
&parse_image_size(&parse_toc(&parse_underline(&parse_comment(
|
||||
&input.replace("[/]", "<br />"),
|
||||
&parse_image_size(&parse_toc(&parse_underline(&parse_markdown_element(
|
||||
&parse_comment(&input.replace("[/]", "<br />")),
|
||||
)))),
|
||||
)))),
|
||||
)))
|
||||
|
@ -983,3 +983,26 @@ pub fn parse_toc(input: &str) -> String {
|
|||
|
||||
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")]
|
||||
#[validate(max_length = 512)]
|
||||
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 {
|
||||
|
|
|
@ -42,6 +42,7 @@ pub fn routes() -> Router {
|
|||
.route("/", get(index_request))
|
||||
.route("/{slug}", get(view_request))
|
||||
.route("/{slug}/edit", get(editor_request))
|
||||
.route("/{slug}/claim", get(reclaim_request))
|
||||
// api
|
||||
.route("/api/v1/util/ip", get(util_ip))
|
||||
.route("/api/v1/render", post(render_request))
|
||||
|
@ -62,6 +63,10 @@ fn default_context(data: &DataClient, build_code: &str) -> Context {
|
|||
"what_page_slug",
|
||||
&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
|
||||
}
|
||||
|
@ -300,6 +305,63 @@ async fn editor_request(
|
|||
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
|
||||
#[derive(Deserialize)]
|
||||
struct RenderMarkdown {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue