about summary refs log tree commit diff
path: root/templates/src/lib.rs
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2023-07-29 21:59:56 +0300
committerVika <vika@fireburn.ru>2023-07-29 21:59:56 +0300
commit0617663b249f9ca488e5de652108b17d67fbaf45 (patch)
tree11564b6c8fa37bf9203a0a4cc1c4e9cc088cb1a5 /templates/src/lib.rs
parent26c2b79f6a6380ae3224e9309b9f3352f5717bd7 (diff)
Moved the entire Kittybox tree into the root
Diffstat (limited to 'templates/src/lib.rs')
-rw-r--r--templates/src/lib.rs367
1 files changed, 367 insertions, 0 deletions
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<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!()
+            }
+        }
+    }
+}