use crate::database::MicropubChannel;
use crate::frontend::IndiewebEndpoints;
use ellipse::Ellipse;
use warp::http::StatusCode;
use log::error;
/// Return a pretty location specifier from a geo: URI.
fn decode_geo_uri(uri: &str) -> String {
if let Some(part) = uri.split(':').collect::<Vec<_>>().get(1) {
if let Some(part) = part.split(';').next() {
let mut parts = part.split(',');
let lat = parts.next().unwrap();
let lon = parts.next().unwrap();
// TODO - format them as proper latitude and longitude
return format!("{}, {}", lat, lon);
} else {
uri.to_string()
}
} else {
uri.to_string()
}
}
mod onboarding;
pub use onboarding::OnboardingPage;
markup::define! {
Template<'a>(title: &'a str, blog_name: &'a str, endpoints: IndiewebEndpoints, feeds: Vec<MicropubChannel>, user: Option<String>, content: String) {
@markup::doctype()
html {
head {
title { @title }
link[rel="preconnect", href="https://fonts.gstatic.com"];
link[rel="stylesheet", href="/static/style.css"];
meta[name="viewport", content="initial-scale=1, width=device-width"];
// TODO: link rel= for common IndieWeb APIs: webmention, microsub
link[rel="micropub", href="/micropub"]; // Static, because it's built into the server itself
link[rel="authorization_endpoint", href=&endpoints.authorization_endpoint];
link[rel="token_endpoint", href=&endpoints.token_endpoint];
@if endpoints.webmention.is_some() {
link[rel="webmention", href=&endpoints.webmention.as_ref()];
}
@if endpoints.microsub.is_some() {
link[rel="microsub", href=&endpoints.microsub.as_ref()];
}
}
body {
// TODO Somehow compress headerbar into a menu when the screen space is tight
nav #headerbar {
ul {
li { a #homepage[href="/"] { @blog_name } }
@for feed in feeds.iter() {
li { a[href=&feed.uid] { @feed.name } }
}
li.shiftright {
@if user.is_none() {
a #login[href="/login"] { "Sign in" }
} else {
span {
@user.as_ref().unwrap() " - " a #logout[href="/logout"] { "Sign out" }
}
}
}
}
}
main {
@markup::raw(content)
}
}
}
}
Entry<'a>(post: &'a serde_json::Value) {
article."h-entry" {
header.metadata {
@if post["properties"]["name"][0].is_string() {
h1."p-name" {
@post["properties"]["name"][0].as_str().unwrap()
}
} else {
@if post["properties"]["author"][0].is_object() {
section."mini-h-card" {
a.larger[href=post["properties"]["author"][0]["properties"]["uid"][0].as_str().unwrap()] {
@if post["properties"]["author"][0]["properties"]["photo"][0].is_string() {
img[src=post["properties"]["author"][0]["properties"]["photo"][0].as_str().unwrap()] {}
}
@post["properties"]["author"][0]["properties"]["name"][0].as_str().unwrap()
}
}
}
}
div {
span {
a."u-url"."u-uid"[href=post["properties"]["uid"][0].as_str().unwrap()] {
time."dt-published"[datetime=post["properties"]["published"][0].as_str().unwrap()] {
@chrono::DateTime::parse_from_rfc3339(post["properties"]["published"][0].as_str().unwrap())
.map(|dt| dt.format("%a %b %e %T %Y").to_string())
.unwrap_or("ERROR: Couldn't parse the datetime".to_string())
}
}
}
@if post["properties"]["visibility"][0].as_str().unwrap_or("public") != "public" {
span."p-visibility"[value=post["properties"]["visibility"][0].as_str().unwrap()] {
@post["properties"]["visibility"][0].as_str().unwrap()
}
}
@if post["properties"]["category"].is_array() {
span {
ul.categories {
"Tagged: "
@for cat in post["properties"]["category"].as_array().unwrap() {
li."p-category" { @cat.as_str().unwrap() }
}
}
}
}
@if post["properties"]["in-reply-to"].is_array() {
// TODO: Rich reply contexts - blocked on MF2 parser
span {
"In reply to: "
ul.replyctx {
@for ctx in post["properties"]["in-reply-to"].as_array().unwrap() {
li { a."u-in-reply-to"[href=ctx.as_str().unwrap()] {
@ctx.as_str().unwrap().truncate_ellipse(24).as_ref()
} }
}
}
}
}
}
@if post["properties"]["url"].as_array().unwrap().len() > 1 {
hr;
ul {
"Pretty permalinks for this post:"
@for url in post["properties"]["url"].as_array().unwrap().iter().filter(|i| **i != post["properties"]["uid"][0]).map(|i| i.as_str().unwrap()) {
li {
a."u-url"[href=url] { @url }
}
}
}
}
@if post["properties"]["location"].is_array() || post["properties"]["checkin"].is_array() {
div {
@if post["properties"]["checkin"].is_array() {
span {
"Check-in to: "
@if post["properties"]["checkin"][0].is_string() {
// It's a URL
a."u-checkin"[href=post["properties"]["checkin"][0].as_str().unwrap()] {
@post["properties"]["checkin"][0].as_str().unwrap().truncate_ellipse(24).as_ref()
}
} else {
a."u-checkin"[href=post["properties"]["checkin"][0]["properties"]["uid"][0].as_str().unwrap()] {
@post["properties"]["checkin"][0]["properties"]["name"][0].as_str().unwrap()
}
}
}
}
@if post["properties"]["location"].is_array() {
span {
"Location: "
@if post["properties"]["location"][0].is_string() {
// It's a geo: URL
// We need to decode it
a."u-location"[href=post["properties"]["location"][0].as_str().unwrap()] {
@decode_geo_uri(post["properties"]["location"][0].as_str().unwrap())
}
} else {
// It's an inner h-geo object
a."u-location"[href=post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap()))] {
// I'm a lazy bitch
@decode_geo_uri(&post["properties"]["location"][0]["value"].as_str().map(|x| x.to_string()).unwrap_or(format!("geo:{},{}", post["properties"]["location"][0]["properties"]["latitude"][0].as_str().unwrap(), post["properties"]["location"][0]["properties"]["longitude"][0].as_str().unwrap())))
}
}
}
}
}
}
@if post["properties"]["ate"].is_array() || post["properties"]["drank"].is_array() {
div {
@if post["properties"]["ate"].is_array() {
span { ul {
"Ate:"
@for food in post["properties"]["ate"].as_array().unwrap() {
li {
@if food.is_string() {
// If this is a string, it's a URL.
a."u-ate"[href=food.as_str().unwrap()] {
@food.as_str().unwrap().truncate_ellipse(24).as_ref()
}
} else {
// This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
a."u-ate"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] {
@food["properties"]["name"][0].as_str()
.unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref())
}
}
}
}
} }
}
@if post["properties"]["drank"].is_array() {
span { ul {
"Drank:"
@for food in post["properties"]["drank"].as_array().unwrap() {
li {
@if food.is_string() {
// If this is a string, it's a URL.
a."u-drank"[href=food.as_str().unwrap()] {
@food.as_str().unwrap().truncate_ellipse(24).as_ref()
}
} else {
// This is a rich food object (mm, sounds tasty! I wanna eat something tasty)
a."u-drank"[href=food["properties"]["uid"][0].as_str().unwrap_or("#")] {
@food["properties"]["name"][0].as_str()
.unwrap_or(food["properties"]["uid"][0].as_str().unwrap_or("#").truncate_ellipse(24).as_ref())
}
}
}
}
} }
}
}
}
}
@PhotoGallery { photos: post["properties"]["photo"].as_array() }
@if post["properties"]["content"][0]["html"].is_string() {
main."e-content" {
@markup::raw(post["properties"]["content"][0]["html"].as_str().unwrap().trim())
}
}
@WebInteractions { post }
}
}
PhotoGallery<'a>(photos: Option<&'a Vec<serde_json::Value>>) {
@if photos.is_some() {
@for photo in photos.unwrap() {
@if photo.is_string() {
img."u-photo"[src=photo.as_str().unwrap(), loading="lazy"];
} else if photo.is_array() {
@if photo["thumbnail"].is_string() {
a."u-photo"[href=photo["value"].as_str().unwrap()] {
img[src=photo["thumbnail"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
}
} else {
img."u-photo"[src=photo["value"].as_str().unwrap(), loading="lazy", alt=photo["alt"].as_str().unwrap_or("")];
}
}
}
}
}
WebInteractions<'a>(post: &'a serde_json::Value) {
footer.webinteractions {
ul.counters {
li {
span.icon { "❤️" }
span.counter { @post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) }
}
li {
span.icon { "💬" }
span.counter { @post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) }
}
li {
span.icon { "🔄" }
span.counter { @post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) }
}
li {
span.icon { "🔖" }
span.counter { @post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) }
}
}
// Needs rich webmention support which may or may not depend on an MF2 parser
// Might circumvent with an external parser with CORS support
// why write this stuff in rust then tho
/*details {
summary { "Show comments and reactions" }
@if post["properties"]["like"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
// Show a facepile of likes for a post
}
@if post["properties"]["bookmark"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
// Show a facepile of bookmarks for a post
}
@if post["properties"]["repost"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
// Show a facepile of reposts for a post
}
@if post["properties"]["comment"].as_array().map(|a| a.len()).unwrap_or(0) > 0 {
// Show all the comments recursively (so we could do Salmention with them)
}
}*/
}
}
VCard<'a>(card: &'a serde_json::Value) {
article."h-card" {
@if card["properties"]["photo"][0].is_string() {
img."u-photo"[src=card["properties"]["photo"][0].as_str().unwrap()];
}
h1 {
a."u-url"."u-uid"."p-name"[href=card["properties"]["uid"][0].as_str().unwrap()] {
@card["properties"]["name"][0].as_str().unwrap()
}
}
@if card["properties"]["pronoun"].is_array() {
span {
"("
@for (i, pronoun) in card["properties"]["pronoun"].as_array().unwrap().iter().filter_map(|v| v.as_str()).enumerate() {
span."p-pronoun" {
@pronoun
}
// Insert commas between multiple sets of pronouns
@if i < (card["properties"]["pronoun"].as_array().unwrap().len() - 1) {", "}
}
")"
}
}
@if card["properties"]["note"].is_array() {
p."p-note" {
@card["properties"]["note"][0]["value"].as_str().unwrap_or_else(|| card["properties"]["note"][0].as_str().unwrap())
}
}
@if card["properties"]["url"].is_array() {
ul {
"Can be found elsewhere at:"
@for url in card["properties"]["url"].as_array().unwrap().iter().filter_map(|v| v.as_str()).filter(|v| v != &card["properties"]["uid"][0].as_str().unwrap()).filter(|v| !v.starts_with(&card["properties"]["author"][0].as_str().unwrap())) {
li { a."u-url"[href=url, rel="me"] { @url } }
}
}
}
}
}
Food<'a>(food: &'a serde_json::Value) {
article."h-food" {
header.metadata {
h1 {
a."p-name"."u-url"[href=food["properties"]["url"][0].as_str().unwrap()] {
@food["properties"]["name"][0].as_str().unwrap()
}
}
}
@PhotoGallery { photos: food["properties"]["photo"].as_array() }
}
}
Feed<'a>(feed: &'a serde_json::Value) {
div."h-feed" {
div.metadata {
@if feed["properties"]["name"][0].is_string() {
h1."p-name".titanic {
a[href=feed["properties"]["uid"][0].as_str().unwrap(), rel="feed"] {
@feed["properties"]["name"][0].as_str().unwrap()
}
}
}
}
@if feed["children"].is_array() {
@for child in feed["children"].as_array().unwrap() {
@match child["type"][0].as_str().unwrap() {
"h-entry" => { @Entry { post: child } }
"h-feed" => { @Feed { feed: child } }
"h-event" => {
@{error!("Templating error: h-events aren't implemented yet");}
}
"h-card" => { @VCard { card: child }}
something_else => {
@{error!("Templating error: found a {} object that couldn't be parsed", something_else);}
}
}
}
}
@if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == 0 {
p {
"Looks like you reached the end. Wanna jump back to the "
a[href=feed["properties"]["uid"][0].as_str().unwrap()] {
"beginning"
} "?"
}
}
@if feed["children"].as_array().map(|a| a.len()).unwrap_or(0) == super::POSTS_PER_PAGE {
a[rel="prev", href=feed["properties"]["uid"][0].as_str().unwrap().to_string()
+ "?after=" + feed["children"][super::POSTS_PER_PAGE - 1]["properties"]["uid"][0].as_str().unwrap()] {
"Older posts"
}
}
}
}
MainPage<'a>(feed: &'a serde_json::Value, card: &'a serde_json::Value) {
.sidebyside {
@VCard { card }
#dynamicstuff {
p { "This section will provide interesting statistics or tidbits about my life in this exact moment (with maybe a small delay)." }
p { "It will probably require JavaScript to self-update, but I promise to keep this widget lightweight and open-source!" }
p { small {
"JavaScript isn't a menace, stop fearing it or I will switch to WebAssembly "
"and knock your nico-nico-kneecaps so fast with its speed you won't even notice that... "
small { "omae ha mou shindeiru" }
@markup::raw("<!-- NANI?!!! -->")
} }
}
}
@Feed { feed }
}
ErrorPage(code: StatusCode, msg: Option<String>) {
h1 { @format!("HTTP {}", code) }
@match *code {
StatusCode::UNAUTHORIZED => {
p { "Looks like you need to authenticate yourself before seeing this page. Try logging in with IndieAuth using the Login button above!" }
}
StatusCode::FORBIDDEN => {
p { "Looks like you're forbidden from viewing this page." }
p {
"This might've been caused by being banned from viewing my website"
"or simply by trying to see what you're not supposed to see, "
"like a private post that's not intended for you. It's ok, it happens."
}
}
StatusCode::GONE => {
p { "Looks like the page you're trying to find is gone and is never coming back." }
}
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS => {
p { "The page is there, but I can't legally provide it to you because the censorship said so." }
}
StatusCode::NOT_FOUND => {
p { "Looks like there's no such page. Maybe you or someone else mistyped a URL or my database experienced data loss." }
}
StatusCode::IM_A_TEAPOT => {
p { "Wait, do you seriously expect my website to brew you coffee? It's not a coffee machine!" }
p {
small {
"I could brew you some coffee tho if we meet one day... "
small {
i {
"i-it's nothing personal, I just like brewing coffee, b-baka!!!~ >.<!"
}
}
}
}
}
StatusCode::BAD_REQUEST => {
@match msg {
None => {
p {
"There was an undescribed error in your request. "
"Please try again later or with a different request."
}
}
Some(msg) => {
p {
"There was a following error in your request: " @msg
}
}
}
}
_ => {
p { "It seems like you have found an error. Not to worry, it has already been logged." }
}
}
P { "For now, may I suggest to visit " a[href="/"] {"the main page"} " of this website?" }
}
}