diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/frontend/mod.rs | 53 | ||||
-rw-r--r-- | src/frontend/onboarding.rs | 2 | ||||
-rw-r--r-- | src/frontend/rss_conversion.rs | 65 | ||||
-rw-r--r-- | src/indieauth/mod.rs | 1 | ||||
-rw-r--r-- | src/login.rs | 6 |
5 files changed, 111 insertions, 16 deletions
diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 9ba1a69..478a48e 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -7,11 +7,14 @@ use axum::{ use axum_extra::{extract::Host, headers::HeaderMapExt}; use futures_util::FutureExt; use serde::Deserialize; -use std::convert::TryInto; +use std::{convert::TryInto, str::FromStr}; use tracing::{debug, error}; //pub mod login; pub mod onboarding; +mod rss_conversion; +use rss_conversion::feed_to_rss; + use kittybox_frontend_renderer::{ Entry, Feed, VCard, ErrorPage, Template, MainPage, @@ -22,6 +25,8 @@ pub use kittybox_frontend_renderer::assets::statics; #[derive(Debug, Deserialize)] pub struct QueryParams { after: Option<String>, + #[serde(default)] + rss: bool, } #[derive(Debug)] @@ -297,7 +302,8 @@ pub async fn homepage<D: Storage>( cursor: cursor.as_deref(), webring: crate::database::settings::Setting::into_inner(webring) } - .to_string(), + .to_string(), + rss_link: Some(&(feed_path + "?rss=true")) } .to_string(), ).into_response() @@ -333,6 +339,7 @@ pub async fn homepage<D: Storage>( msg: Some(err.msg().to_string()), } .to_string(), + rss_link: None, } .to_string(), ).into_response() @@ -357,7 +364,7 @@ pub async fn catchall<D: Storage>( .unwrap(); match get_post_from_database(&db, path.as_str(), query.after, user).await { - Ok((post, cursor)) => { + Ok((mut post, cursor)) => { let (blogname, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&host) .map(Result::unwrap_or_default), @@ -365,22 +372,18 @@ pub async fn catchall<D: Storage>( db.get_channels(&host).map(|i| i.unwrap_or_default()) ); let mut headers = axum::http::HeaderMap::new(); - headers.insert( - axum::http::header::CONTENT_TYPE, - axum::http::HeaderValue::from_static(r#"text/html; charset="utf-8""#), - ); + headers.typed_insert(axum_extra::headers::ContentType::html()); headers.insert( axum::http::header::X_CONTENT_TYPE_OPTIONS, axum::http::HeaderValue::from_static("nosniff") ); if user.is_some() { - headers.insert( - axum::http::header::CACHE_CONTROL, - axum::http::HeaderValue::from_static("private") - ); + headers.typed_insert(axum_extra::headers::CacheControl::new().with_private()); } - if post["type"][0].as_str() == Some("h-entry") { + let post_type = post.pointer("/type/0").and_then(|i| i.as_str()); + + if post_type == Some("h-entry") { let last_modified = post["properties"]["updated"] .as_array() .and_then(|v| v.last()) @@ -400,6 +403,26 @@ pub async fn catchall<D: Storage>( } } + if query.rss { + match post_type { + Some("h-feed") => { + headers.typed_insert(axum_extra::headers::ContentType::from_str("application/rss+xml").unwrap()); + return ( + StatusCode::OK, + headers, + feed_to_rss(post).to_string() + ).into_response() + }, + _ => { + headers.typed_insert(axum_extra::headers::ContentType::text_utf8()); + return (StatusCode::NOT_FOUND, headers, "RSS feeds cannot be generated for this document.").into_response() + } + } + } + let rss_link: Option<String> = match post_type { + Some("h-feed") => Some(post["properties"]["uid"][0].as_str().unwrap().to_owned() + "?rss=true"), + _ => None + }; // Render the homepage ( StatusCode::OK, @@ -409,7 +432,7 @@ pub async fn catchall<D: Storage>( blog_name: blogname.as_ref(), feeds: channels, user: session.as_deref(), - content: match post.pointer("/type/0").and_then(|i| i.as_str()) { + content: match post_type { Some("h-entry") => Entry { post: &post, from_feed: false, }.to_string(), Some("h-feed") => Feed { feed: &post, cursor: cursor.as_deref() }.to_string(), Some("h-card") => VCard { card: &post }.to_string(), @@ -417,6 +440,7 @@ pub async fn catchall<D: Storage>( unimplemented!("Template for MF2-JSON type {:?}", unknown) } }, + rss_link: rss_link.as_deref() } .to_string(), ).into_response() @@ -443,7 +467,8 @@ pub async fn catchall<D: Storage>( code: err.code(), msg: Some(err.msg().to_owned()), } - .to_string(), + .to_string(), + rss_link: None, } .to_string(), ).into_response() diff --git a/src/frontend/onboarding.rs b/src/frontend/onboarding.rs index 4588157..3537933 100644 --- a/src/frontend/onboarding.rs +++ b/src/frontend/onboarding.rs @@ -23,6 +23,7 @@ pub async fn get() -> Html<String> { feeds: vec![], user: None, content: OnboardingPage {}.to_string(), + rss_link: None, } .to_string(), ) @@ -153,6 +154,7 @@ pub async fn post<D: Storage + 'static>( msg: Some(err.msg().to_string()), } .to_string(), + rss_link: None, } .to_string(), ), 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() +} diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs index 00ae393..2fec907 100644 --- a/src/indieauth/mod.rs +++ b/src/indieauth/mod.rs @@ -269,6 +269,7 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>( user: db.get_post(me.as_str()).await.unwrap().unwrap(), app: h_app }.to_string(), + rss_link: None, }.to_string()) .into_response() } diff --git a/src/login.rs b/src/login.rs index eaa787c..edb59f9 100644 --- a/src/login.rs +++ b/src/login.rs @@ -34,7 +34,8 @@ async fn get<S: Storage + Send + Sync + 'static>( blog_name: blogname.as_ref(), feeds: channels, user: None, - content: LoginPage {}.to_string() + content: LoginPage {}.to_string(), + rss_link: None, }.to_string() ) } @@ -293,7 +294,8 @@ async fn logout_page() -> impl axum::response::IntoResponse { blog_name: "Kittybox", feeds: vec![], user: None, - content: LogoutPage {}.to_string() + content: LogoutPage {}.to_string(), + rss_link: None, }.to_string()) } |