From 0617663b249f9ca488e5de652108b17d67fbaf45 Mon Sep 17 00:00:00 2001 From: Vika Date: Sat, 29 Jul 2023 21:59:56 +0300 Subject: Moved the entire Kittybox tree into the root --- templates/src/lib.rs | 367 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 templates/src/lib.rs (limited to 'templates/src/lib.rs') diff --git a/templates/src/lib.rs b/templates/src/lib.rs new file mode 100644 index 0000000..8d5d5fa --- /dev/null +++ b/templates/src/lib.rs @@ -0,0 +1,367 @@ +mod templates; +pub use templates::{ErrorPage, MainPage, Template}; +mod onboarding; +pub use onboarding::OnboardingPage; +mod indieauth; +pub use indieauth::AuthorizationRequestPage; +mod login; +pub use login::LoginPage; +mod mf2; +pub use mf2::{Entry, VCard, Feed, Food, POSTS_PER_PAGE}; + +pub mod assets { + use axum::response::{IntoResponse, Response}; + use axum::extract::Path; + use axum::http::StatusCode; + use axum::http::header::{CONTENT_TYPE, CONTENT_ENCODING, CACHE_CONTROL}; + + const ASSETS: include_dir::Dir<'static> = include_dir::include_dir!("$OUT_DIR/"); + const CACHE_FOR_A_DAY: &str = "max-age=86400"; + const GZIP: &str = "gzip"; + + pub async fn statics( + Path(path): Path + ) -> Response { + let content_type: &'static str = if path.ends_with(".js") { + "application/javascript" + } else if path.ends_with(".css") { + "text/css" + } else if path.ends_with(".html") { + "text/html; charset=\"utf-8\"" + } else { + "application/octet-stream" + }; + + match ASSETS.get_file(path.clone() + ".gz") { + Some(file) => (StatusCode::OK, + [(CONTENT_TYPE, content_type), + (CONTENT_ENCODING, GZIP), + (CACHE_CONTROL, CACHE_FOR_A_DAY)], + file.contents()).into_response(), + None => match ASSETS.get_file(path) { + Some(file) => (StatusCode::OK, + [(CONTENT_TYPE, content_type), + (CACHE_CONTROL, CACHE_FOR_A_DAY)], + file.contents()).into_response(), + None => StatusCode::NOT_FOUND.into_response() + } + } + } +} + +#[cfg(test)] +mod tests { + use faker_rand::en_us::internet::Domain; + use faker_rand::lorem::Word; + use microformats::types::{Document, Item, PropertyValue, Url}; + use serde_json::json; + use std::cell::RefCell; + use std::rc::Rc; + + enum PostType { + Note, + Article, + ReplyTo(serde_json::Value), + ReplyToLink(String), + LikeOf(serde_json::Value), + LikeOfLink(String), + } + + fn gen_hcard(domain: &str) -> serde_json::Value { + use faker_rand::en_us::names::FirstName; + + json!({ + "type": ["h-card"], + "properties": { + "name": [rand::random::().to_string()], + "photo": [format!("https://{domain}/media/me.png")], + "uid": [format!("https://{domain}/")], + "url": [format!("https://{domain}/")] + } + }) + } + + fn gen_random_post(domain: &str, kind: PostType) -> serde_json::Value { + use faker_rand::lorem::{Paragraph, Sentence}; + + fn html(content: Paragraph) -> serde_json::Value { + json!({ + "html": format!("

{}

", content), + "value": content.to_string() + }) + } + + let uid = format!( + "https://{domain}/posts/{}-{}-{}", + rand::random::(), + rand::random::(), + rand::random::() + ); + let dt = chrono::offset::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + + match kind { + PostType::Note => { + let content = rand::random::(); + + json!({ + "type": ["h-entry"], + "properties": { + "content": [html(content)], + "published": [dt], + "uid": [&uid], "url": [&uid], + "author": [gen_hcard(domain)] + } + }) + } + PostType::Article => { + let content = rand::random::(); + let name = rand::random::(); + + json!({ + "type": ["h-entry"], + "properties": { + "content": [html(content)], + "published": [dt], + "uid": [&uid], "url": [&uid], + "author": [gen_hcard(domain)], + "name": [name.to_string()] + } + }) + } + PostType::ReplyTo(ctx) => { + let content = rand::random::(); + + json!({ + "type": ["h-entry"], + "properties": { + "content": [html(content)], + "published": [dt], + "uid": [&uid], "url": [&uid], + "author": [gen_hcard(domain)], + "in-reply-to": [{ + "type": ["h-cite"], + "properties": ctx["properties"] + }] + } + }) + } + PostType::ReplyToLink(link) => { + let content = rand::random::(); + + json!({ + "type": ["h-entry"], + "properties": { + "content": [html(content)], + "published": [dt], + "uid": [&uid], "url": [&uid], + "author": [gen_hcard(domain)], + "in-reply-to": [link] + } + }) + } + PostType::LikeOf(ctx) => { + json!({ + "type": ["h-entry"], + "properties": { + "published": [dt], + "author": [gen_hcard(domain)], + "uid": [&uid], "url": [&uid], + "like-of": [{ + "type": ["h-cite"], + "properties": ctx["properties"] + }] + } + }) + } + PostType::LikeOfLink(link) => { + json!({ + "type": ["h-entry"], + "properties": { + "published": [dt], + "author": [gen_hcard(domain)], + "uid": [&uid], "url": [&uid], + "like-of": [link] + } + }) + } + } + } + + fn check_dt_published(mf2: &serde_json::Value, item: &Rc>) { + use microformats::types::temporal::Value as TemporalValue; + + let _item = item.borrow(); + let props = _item.properties.borrow(); + assert!(props.contains_key("published")); + + if let Some(PropertyValue::Temporal(TemporalValue::Timestamp(item))) = + props.get("published").and_then(|v| v.first()) + { + use chrono::{DateTime, FixedOffset, NaiveDateTime}; + + // Faithfully reconstruct the original datetime + // I wonder why not just have an Enum that would + // get you either date, time or a datetime, + // potentially with an offset? + let offset = item.as_offset().unwrap().data; + let ndt: NaiveDateTime = item.as_date().unwrap().data + .and_time(item.as_time().unwrap().data) + // subtract the offset here, since we will add it back + - offset; + let dt = DateTime::::from_utc(ndt, offset); + + let expected: DateTime = chrono::DateTime::parse_from_rfc3339( + mf2["properties"]["published"][0].as_str().unwrap(), + ) + .unwrap(); + + assert_eq!(dt, expected); + } else { + unreachable!() + } + } + + fn check_e_content(mf2: &serde_json::Value, item: &Rc>) { + let _item = item.borrow(); + let props = _item.properties.borrow(); + assert!(props.contains_key("content")); + + if let Some(PropertyValue::Fragment(content)) = props.get("content").and_then(|v| v.first()) + { + assert_eq!( + content.html, + mf2["properties"]["content"][0]["html"].as_str().unwrap() + ); + } else { + unreachable!() + } + } + + #[test] + #[ignore = "see https://gitlab.com/maxburon/microformats-parser/-/issues/7"] + fn test_note() { + let mf2 = gen_random_post(&rand::random::().to_string(), PostType::Note); + + let html = crate::mf2::Entry { post: &mf2 }.to_string(); + + let url: Url = mf2 + .pointer("/properties/uid/0") + .and_then(|i| i.as_str()) + .and_then(|u| u.parse().ok()) + .unwrap(); + let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); + + if let Some(PropertyValue::Item(item)) = parsed.get_item_by_url(&url) { + let _item = item.borrow(); + let props = _item.properties.borrow(); + + check_e_content(&mf2, &item); + check_dt_published(&mf2, &item); + assert!(props.contains_key("uid")); + assert!(props.contains_key("url")); + assert!(props + .get("url") + .unwrap() + .iter() + .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap())); + // XXX: fails because of https://gitlab.com/maxburon/microformats-parser/-/issues/7 + assert!(!props.contains_key("name")); + } else { + unreachable!() + } + } + + #[test] + fn test_article() { + let mf2 = gen_random_post(&rand::random::().to_string(), PostType::Article); + let html = crate::mf2::Entry { post: &mf2 }.to_string(); + let url: Url = mf2 + .pointer("/properties/uid/0") + .and_then(|i| i.as_str()) + .and_then(|u| u.parse().ok()) + .unwrap(); + let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); + + if let Some(PropertyValue::Item(item)) = parsed.get_item_by_url(&url) { + let _item = item.borrow(); + let props = _item.properties.borrow(); + + check_e_content(&mf2, &item); + check_dt_published(&mf2, &item); + assert!(props.contains_key("uid")); + assert!(props.contains_key("url")); + assert!(props + .get("url") + .unwrap() + .iter() + .any(|i| i == props.get("uid").and_then(|v| v.first()).unwrap())); + assert!(props.contains_key("name")); + if let Some(PropertyValue::Plain(name)) = props.get("name").and_then(|v| v.first()) { + assert_eq!( + name, + mf2.pointer("/properties/name/0") + .and_then(|v| v.as_str()) + .unwrap() + ); + } else { + panic!("Name wasn't a plain property!"); + } + } else { + unreachable!() + } + } + + #[test] + fn test_like_of() { + for likeof in [ + PostType::LikeOf(gen_random_post( + &rand::random::().to_string(), + PostType::Note, + )), + PostType::LikeOfLink(format!( + "https://{}/posts/{}-{}-{}", + &rand::random::(), + &rand::random::(), + &rand::random::(), + &rand::random::(), + )), + ] { + let mf2 = gen_random_post(&rand::random::().to_string(), likeof); + let url: Url = mf2 + .pointer("/properties/uid/0") + .and_then(|i| i.as_str()) + .and_then(|u| u.parse().ok()) + .unwrap(); + let html = crate::mf2::Entry { post: &mf2 }.to_string(); + let parsed: Document = microformats::from_html(&html, url.clone()).unwrap(); + + if let Some(item) = parsed.items.get(0) { + let _item = item.borrow(); + let props = _item.properties.borrow(); + + check_dt_published(&mf2, item); + assert!(props.contains_key("like-of")); + match props.get("like-of").and_then(|v| v.first()) { + Some(PropertyValue::Url(url)) => { + assert_eq!( + url, + &mf2.pointer("/properties/like-of/0") + .and_then(|i| i.as_str()) + .or_else(|| mf2 + .pointer("/properties/like-of/0/properties/uid/0") + .and_then(|i| i.as_str())) + .and_then(|u| u.parse::().ok()) + .unwrap() + ); + } + Some(PropertyValue::Item(_cite)) => { + todo!() + } + other => panic!("Unexpected value in like-of: {:?}", other), + } + } else { + unreachable!() + } + } + } +} -- cgit 1.4.1