diff options
author | Vika <vika@fireburn.ru> | 2025-03-13 11:06:23 +0300 |
---|---|---|
committer | Vika <vika@fireburn.ru> | 2025-03-13 11:06:23 +0300 |
commit | 4ca3c83d447fbf86a4f21d841a107cb28a64658e (patch) | |
tree | e196c4a83d602bdfeb84f5f5bb1973009e17cf22 /src/frontend/rss_conversion.rs | |
parent | ab6614c720064a5630100c1ba600942dcab2632f (diff) | |
download | kittybox-4ca3c83d447fbf86a4f21d841a107cb28a64658e.tar.zst |
WIP: RSS feed generator feature/rss
Some people are old-fashioned, and RSS feeds can be consumed even by laypeople unfamiliar with microformats2. This does violate DRY at first glance, but since the feeds are dynamically generated it's not repetition but rather format conversion, and as such does not violate DRY per se.
Diffstat (limited to 'src/frontend/rss_conversion.rs')
-rw-r--r-- | src/frontend/rss_conversion.rs | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/src/frontend/rss_conversion.rs b/src/frontend/rss_conversion.rs new file mode 100644 index 0000000..3448bf0 --- /dev/null +++ b/src/frontend/rss_conversion.rs @@ -0,0 +1,65 @@ +//! Conversion of MF2-JSON posts into RSS items. + +/// TTL of the RSS feeds generated by Kittybox. Compliant RSS +/// consumers must avoid refetching the feed before its TTL expires. +const RSS_TTL: std::time::Duration = std::time::Duration::from_secs(600); + +/// Convert a single MF2 post to an RSS item. +/// +/// Posts must have a UID. +pub fn post_to_rss(mut post: serde_json::Value) -> Option<rss::Item> { + let mut builder = rss::ItemBuilder::default(); + + let date = match post["properties"]["published"][0].take() { + serde_json::Value::String(d) => chrono::DateTime::<chrono::FixedOffset>::parse_from_rfc3339(&d).ok(), + _ => None + }; + if let Some(date) = date { + builder.pub_date(date.to_rfc2822()); + } + if let serde_json::Value::String(title) = post["properties"]["title"][0].take() { + builder.title(title); + } + if let serde_json::Value::String(summary) = post["properties"]["summary"][0].take() { + builder.description(summary); + } + // Do not emit posts that do not have GUIDs. + match post["properties"]["uid"][0].take() { + serde_json::Value::String(uid) => { + builder.guid(rss::GuidBuilder::default().value(uid).permalink(true).build()); + }, + _ => { return None; }, + } + // TODO: enclosures for u-photo, u-video, u-audio + // requires knowing MIME type for the included media file + // can enrich from media endpoint + + let item = builder.build(); + if item.title.is_some() || item.description.is_some() { + Some(item) + } else { + None + } +} + +/// Convert an entire MF2 h-feed into an RSS channel. +/// Fairly opinionated. Provided MF2-JSON object must be a feed. +pub fn feed_to_rss(mut post: serde_json::Value) -> rss::Channel { + let children: Vec<serde_json::Value> = match post["children"].take() { + serde_json::Value::Array(children) => children, + _ => Vec::default(), + }; + rss::ChannelBuilder::default() + .title(post["properties"]["name"][0].as_str().unwrap()) + .link(post["properties"]["uid"][0].as_str().unwrap()) + .generator(Some(concat!( + env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION") + ).to_string())) + .ttl((RSS_TTL.as_secs() / 60).to_string()) + .items(children + .into_iter() + .filter_map(post_to_rss) + .collect::<Vec<rss::Item>>() + ) + .build() +} |