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<String>
) -> 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::<FirstName>().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!("<p>{}</p>", content),
"value": content.to_string()
})
}
let uid = format!(
"https://{domain}/posts/{}-{}-{}",
rand::random::<Word>(),
rand::random::<Word>(),
rand::random::<Word>()
);
let dt = chrono::offset::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
match kind {
PostType::Note => {
let content = rand::random::<Paragraph>();
json!({
"type": ["h-entry"],
"properties": {
"content": [html(content)],
"published": [dt],
"uid": [&uid], "url": [&uid],
"author": [gen_hcard(domain)]
}
})
}
PostType::Article => {
let content = rand::random::<Paragraph>();
let name = rand::random::<Sentence>();
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::<Paragraph>();
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::<Paragraph>();
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<RefCell<Item>>) {
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::<FixedOffset>::from_utc(ndt, offset);
let expected: DateTime<FixedOffset> = 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<RefCell<Item>>) {
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::<Domain>().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::<Domain>().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::<Domain>().to_string(),
PostType::Note,
)),
PostType::LikeOfLink(format!(
"https://{}/posts/{}-{}-{}",
&rand::random::<Domain>(),
&rand::random::<Word>(),
&rand::random::<Word>(),
&rand::random::<Word>(),
)),
] {
let mf2 = gen_random_post(&rand::random::<Domain>().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::<Url>().ok())
.unwrap()
);
}
Some(PropertyValue::Item(_cite)) => {
todo!()
}
other => panic!("Unexpected value in like-of: {:?}", other),
}
} else {
unreachable!()
}
}
}
}