diff --git a/Cargo.lock b/Cargo.lock index 6a7a6cb..89b2e6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,6 +78,19 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +[[package]] +name = "atom_syndication" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d0b2fa7aedc48c4fbe1d38b25c1462a6e7b962397f27a3a8d7cbb1c08f0008b" +dependencies = [ + "chrono", + "derive_builder", + "diligent-date-parser", + "quick-xml 0.18.1", + "serde", +] + [[package]] name = "atty" version = "0.2.14" @@ -227,6 +240,7 @@ checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" dependencies = [ "num-integer", "num-traits", + "serde", "time", ] @@ -239,7 +253,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", @@ -296,6 +310,66 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dhall" version = "0.5.3" @@ -344,6 +418,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "diligent-date-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28caca0eb64b9b22bdcab47424e0f7716af92d33ad035f765e5ec2b08cf14fcc" +dependencies = [ + "chrono", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -723,6 +806,12 @@ dependencies = [ "tokio-tls", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.0" @@ -1348,6 +1437,26 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-xml" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1e430bdcf30c9fdc25053b9c459bb1a4672af4617b6c783d7d91dc17c6bbb0" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc440ee4802a86e357165021e3e255a9143724da31db1e2ea540214c96a0f82" +dependencies = [ + "encoding_rs", + "memchr", +] + [[package]] name = "quote" version = "1.0.7" @@ -1586,6 +1695,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" +[[package]] +name = "rss" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99979205510c60f80a119dedbabd0b8426517384edf205322f8bcd51796bcef9" +dependencies = [ + "derive_builder", + "quick-xml 0.17.2", +] + [[package]] name = "ructe" version = "0.11.4" @@ -1817,6 +1936,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "syn" version = "1.0.34" @@ -2371,6 +2496,7 @@ name = "xesite" version = "2.0.0" dependencies = [ "anyhow", + "atom_syndication", "chrono", "comrak", "envy", @@ -2383,6 +2509,7 @@ dependencies = [ "pretty_env_logger", "prometheus", "rand 0.7.3", + "rss", "ructe", "serde", "serde_dhall", @@ -2390,8 +2517,15 @@ dependencies = [ "thiserror", "tokio", "warp", + "xml-rs", ] +[[package]] +name = "xml-rs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + [[package]] name = "yaml-rust" version = "0.4.4" diff --git a/Cargo.toml b/Cargo.toml index b2065b9..f389472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ build = "src/build.rs" [dependencies] anyhow = "1" +atom_syndication = { version = "0.9", features = ["with-serde"] } chrono = "0.4" comrak = "0.8" envy = "0.4" @@ -20,6 +21,7 @@ mime = "0.3.0" pretty_env_logger = "0" prometheus = { version = "0.9", default-features = false, features = ["process"] } rand = "0" +rss = "1" ructe = "0.11" serde_dhall = "0.5.3" serde = { version = "1", features = ["derive"] } @@ -27,6 +29,7 @@ serde_yaml = "0.8" thiserror = "1" tokio = { version = "0.2", features = ["macros"] } warp = "0.2" +xml-rs = "0.8" # workspace dependencies jsonfeed = { path = "./lib/jsonfeed" } diff --git a/src/app.rs b/src/app.rs index 51a7f85..2e8c449 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,5 +1,6 @@ use crate::{post::Post, signalboost::Person}; use anyhow::Result; +use atom_syndication as atom; use comrak::{markdown_to_html, ComrakOptions}; use serde::Deserialize; use std::{fs, path::PathBuf}; @@ -40,6 +41,8 @@ pub struct State { pub talks: Vec, pub everything: Vec, pub jf: jsonfeed::Feed, + pub rf: rss::Channel, + pub af: atom::Feed, } pub fn init(cfg: PathBuf) -> Result { @@ -64,6 +67,9 @@ pub fn init(cfg: PathBuf) -> Result { everything.sort(); everything.reverse(); + let mut ri: Vec = vec![]; + let mut ai: Vec = vec![]; + let mut jfb = jsonfeed::Feed::builder() .title("Christine Dodrill's Blog") .description("My blog posts and rants about various technology things.") @@ -80,9 +86,38 @@ pub fn init(cfg: PathBuf) -> Result { for post in &everything { let post = post.clone(); - jfb = jfb.item(post.into()); + jfb = jfb.item(post.clone().into()); + ri.push(post.clone().into()); + ai.push(post.clone().into()); } + let af = { + let mut af = atom::FeedBuilder::default(); + af.title("Christine Dodrill's Blog"); + af.id("https://christine.website/blog"); + af.generator({ + let mut generator = atom::Generator::default(); + generator.set_value(env!("CARGO_PKG_NAME")); + generator.set_version(env!("CARGO_PKG_VERSION").to_string()); + generator.set_uri("https://github.com/Xe/site".to_string()); + + generator + }); + af.entries(ai); + + af.build().unwrap() + }; + + let rf = { + let mut rf = rss::ChannelBuilder::default(); + rf.title("Christine Dodrill's Blog"); + rf.link("https://christine.website/blog"); + rf.generator(crate::APPLICATION_NAME.to_string()); + rf.items(ri); + + rf.build().unwrap() + }; + Ok(State { cfg: cfg, signalboost: sb, @@ -92,6 +127,8 @@ pub fn init(cfg: PathBuf) -> Result { talks: talks, everything: everything, jf: jfb.build(), + af: af, + rf: rf, }) } diff --git a/src/handlers/feeds.rs b/src/handlers/feeds.rs index a01f957..4a0922b 100644 --- a/src/handlers/feeds.rs +++ b/src/handlers/feeds.rs @@ -1,21 +1,63 @@ -use crate::{ - app::State, -}; +use crate::app::State; use lazy_static::lazy_static; -use prometheus::{IntCounterVec, register_int_counter_vec, opts}; -use std::{sync::Arc}; -use warp::{ - Reply, -}; +use prometheus::{opts, register_int_counter_vec, IntCounterVec}; +use std::sync::Arc; +use warp::{http::Response, Rejection, Reply}; lazy_static! { - static ref HIT_COUNTER: IntCounterVec = - register_int_counter_vec!(opts!("feed_hits", "Number of hits to various feeds"), &["kind"]) - .unwrap(); + static ref HIT_COUNTER: IntCounterVec = register_int_counter_vec!( + opts!("feed_hits", "Number of hits to various feeds"), + &["kind"] + ) + .unwrap(); } -pub fn jsonfeed(state: Arc) -> impl Reply { +pub async fn jsonfeed(state: Arc) -> Result { HIT_COUNTER.with_label_values(&["json"]).inc(); let state = state.clone(); - warp::reply::json(&state.jf) + Ok(warp::reply::json(&state.jf)) +} + +#[derive(Debug)] +pub enum RenderError { + WriteAtom(atom_syndication::Error), + WriteRss(rss::Error), + Build(warp::http::Error), +} + +impl warp::reject::Reject for RenderError {} + +pub async fn atom(state: Arc) -> Result { + HIT_COUNTER.with_label_values(&["atom"]).inc(); + let state = state.clone(); + let mut buf = Vec::new(); + state + .af + .write_to(&mut buf) + .map_err(RenderError::WriteAtom) + .map_err(warp::reject::custom)?; + Response::builder() + .status(200) + .header("Content-Type", "application/atom+xml") + .body(buf) + .map_err(RenderError::Build) + .map_err(warp::reject::custom) +} + + +pub async fn rss(state: Arc) -> Result { + HIT_COUNTER.with_label_values(&["rss"]).inc(); + let state = state.clone(); + let mut buf = Vec::new(); + state + .rf + .write_to(&mut buf) + .map_err(RenderError::WriteRss) + .map_err(warp::reject::custom)?; + Response::builder() + .status(200) + .header("Content-Type", "application/rss+xml") + .body(buf) + .map_err(RenderError::Build) + .map_err(warp::reject::custom) } diff --git a/src/main.rs b/src/main.rs index d783906..8fce749 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,10 +92,17 @@ async fn main() -> Result<()> { let css = warp::path("css").and(warp::fs::dir("./css")); let sw = warp::path("sw.js").and(warp::fs::file("./static/js/sw.js")); let robots = warp::path("robots.txt").and(warp::fs::file("./static/robots.txt")); + let favicon = warp::path("favicon.ico").and(warp::fs::file("./static/favicon/favicon.ico")); let jsonfeed = warp::path("blog.json") .and(with_state(state.clone())) - .map(handlers::feeds::jsonfeed); + .and_then(handlers::feeds::jsonfeed); + let atom = warp::path("blog.atom") + .and(with_state(state.clone())) + .and_then(handlers::feeds::atom); + let rss = warp::path("blog.rss") + .and(with_state(state.clone())) + .and_then(handlers::feeds::rss); let metrics_endpoint = warp::path("metrics").and(warp::path::end()).map(move || { let encoder = TextEncoder::new(); @@ -110,14 +117,12 @@ async fn main() -> Result<()> { }); let site = index - .or(contact.or(feeds)) - .or(resume.or(signalboost)) + .or(contact.or(feeds).or(resume.or(signalboost))) .or(blog_index.or(series.or(series_view).or(post_view))) .or(gallery_index.or(gallery_post_view)) .or(talk_index.or(talk_post_view)) - .or(jsonfeed) - .or(files.or(css)) - .or(sw.or(robots)) + .or(jsonfeed.or(atom).or(rss)) + .or(files.or(css).or(favicon).or(sw.or(robots))) .or(healthcheck.or(metrics_endpoint)) .map(|reply| { warp::reply::with_header( diff --git a/src/post/mod.rs b/src/post/mod.rs index c1c743f..02b8c69 100644 --- a/src/post/mod.rs +++ b/src/post/mod.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, Result}; +use atom_syndication as atom; use chrono::prelude::*; use glob::glob; use std::{cmp::Ordering, fs}; @@ -45,6 +46,49 @@ impl Into for Post { } } +impl Into for Post { + fn into(self) -> atom::Entry { + let mut content = atom::ContentBuilder::default(); + + content.src(format!("https://christine.website/{}", self.link)); + content.content_type(Some("html".into())); + content.value(Some(xml::escape::escape_str_pcdata(&self.body_html).into())); + + let content = content.build().unwrap(); + + let mut result = atom::EntryBuilder::default(); + result.title(self.front_matter.title); + let mut link = atom::Link::default(); + link.href = format!("https://christine.website/{}", self.link); + result.links(vec![link]); + result.content(content); + // result.published(Some( + // DateTime::::from_utc( + // NaiveDateTime::new(self.date, NaiveTime::from_hms(0, 0, 0)), + // Utc, + // ) + // .with_timezone(&Utc), + // )); + + result.build().unwrap() + } +} + +impl Into for Post { + fn into(self) -> rss::Item { + let mut guid = rss::Guid::default(); + guid.set_value(format!("https://christine.website/{}", self.link)); + let mut result = rss::ItemBuilder::default(); + result.title(Some(self.front_matter.title)); + result.link(format!("https://christine.website/{}", self.link)); + result.guid(guid); + result.author(Some("me@christine.website".to_string())); + result.content(self.body_html); + + result.build().unwrap() + } +} + impl Ord for Post { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(&other).unwrap()