From 637f0b47dddcdda9ae278f3f67f48d137a89c468 Mon Sep 17 00:00:00 2001 From: "Wyatt J. Miller" Date: Tue, 10 Dec 2024 14:06:22 -0500 Subject: [PATCH] added rss generator --- backend/public/Cargo.lock | 55 ++++++++++++++++++ backend/public/Cargo.toml | 3 +- backend/public/src/main.rs | 1 + backend/public/src/routes/posts.rs | 43 +++++++++++++- backend/public/src/utils/mod.rs | 1 + backend/public/src/utils/rss.rs | 90 ++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 backend/public/src/utils/mod.rs create mode 100644 backend/public/src/utils/rss.rs diff --git a/backend/public/Cargo.lock b/backend/public/Cargo.lock index f29299d..a313c98 100644 --- a/backend/public/Cargo.lock +++ b/backend/public/Cargo.lock @@ -79,6 +79,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -586,6 +592,25 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -705,6 +730,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -1162,6 +1188,7 @@ dependencies = [ "tower_governor", "tracing", "tracing-subscriber", + "xml", ] [[package]] @@ -1908,6 +1935,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.4.13" @@ -2395,6 +2435,21 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "xml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede1c99c55b4b3ad0349018ef0eccbe954ce9c342334410707ee87177fcf2ab4" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "xml-rs" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea8b391c9a790b496184c29f7f93b9ed5b16abb306c05415b68bcc16e4d06432" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/backend/public/Cargo.toml b/backend/public/Cargo.toml index e094372..f4f7b7c 100644 --- a/backend/public/Cargo.toml +++ b/backend/public/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Wyatt J. Miller , @@ -51,6 +55,7 @@ impl PostsRoute { .route("/popular", get(PostsRoute::get_popular_posts)) .route("/hot", get(PostsRoute::get_hot_posts)) .route("/featured", get(PostsRoute::get_featured_posts)) + .route("/rss", get(PostsRoute::get_rss_posts)) .with_state(app_state.db.clone()) } @@ -104,6 +109,40 @@ impl PostsRoute { Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e.to_string())), } } + + // get rss posts + async fn get_rss_posts(State(pool): State>) -> impl IntoResponse { + match PostsDatasource::get_all(&pool).await { + Ok(posts) => { + let web_url = std::env::var("BASE_URI_WEB").expect("No environment variable found"); + let mapped_posts: HashMap = posts + .into_iter() + .map(|post| (post.post_id.to_string(), post)) + .collect(); + let xml: String = rss::generate_rss( + "Wyatt's blog", + "Wyatt and friends", + web_url.as_str(), + &mapped_posts, + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_DISPOSITION, + HeaderValue::from_str(r#"attachment; filename="posts.xml""#).unwrap(), + ); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/xml"), + ); + (headers, xml) + } + Err(e) => { + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", HeaderValue::from_static("text/plain")); + (headers, e.to_string()) + } + } + } } pub fn serialize_datetime( diff --git a/backend/public/src/utils/mod.rs b/backend/public/src/utils/mod.rs new file mode 100644 index 0000000..aaa67c0 --- /dev/null +++ b/backend/public/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod rss; diff --git a/backend/public/src/utils/rss.rs b/backend/public/src/utils/rss.rs new file mode 100644 index 0000000..6d09d32 --- /dev/null +++ b/backend/public/src/utils/rss.rs @@ -0,0 +1,90 @@ +use crate::routes::posts; +use std::collections::HashMap; +use xml::escape::escape_str_pcdata; + +pub struct RssEntry { + pub title: String, + pub link: String, + pub description: Option, + pub created_at: String, + pub author: String, + pub guid: String, +} + +impl From for RssEntry { + fn from(post: posts::Post) -> Self { + let web_url = std::env::var("BASE_URI_WEB").expect("Environment variable not found"); + let post_url = format!("{}{}{}", web_url, "/posts/", post.post_id.to_string()); + let author_full_name = format!("{} {}", post.first_name.unwrap(), post.last_name.unwrap()); + + Self { + title: post.title.clone(), + link: post_url.clone(), + description: Some(post.body.clone()), + created_at: post + .created_at + .expect("No timestamp was available") + .to_rfc2822(), + author: author_full_name, + guid: post_url.clone(), + } + } +} + +impl RssEntry { + pub fn to_item(&self) -> String { + format!( + r#" + + <![CDATA[{}]]> + + {} + {} + {} + + "#, + self.title, + self.description.clone().unwrap_or_default(), + self.created_at, + self.guid, + self.guid + ) + } +} + +pub fn generate_rss( + title: &str, + description: &str, + link: &str, + posts: &HashMap, +) -> String { + println!("{:?}", posts); + let values = posts.clone().into_values(); + println!("{:?}", values); + + let rss_entries = values + .map(|p| p.into()) + .map(|r: RssEntry| r.to_item()) + .collect::(); + + let safe_title = escape_str_pcdata(title); + let safe_description = escape_str_pcdata(description); + println!("{:?}", rss_entries); + + format!( + r#" + + + {safe_title} + {safe_description} + {link} + en-us + 60 + Kyouma 1.0.0-SE + + {} + + "#, + rss_entries + ) +}