From 10c457eb347164f3f9d1be08bf7dfe56040a0ecd Mon Sep 17 00:00:00 2001 From: lashman Date: Mon, 13 Oct 2025 09:15:12 +0300 Subject: [PATCH] rss and atom feeds --- cruciverb-server/src/feeds.rs | 158 ++++++++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 cruciverb-server/src/feeds.rs diff --git a/cruciverb-server/src/feeds.rs b/cruciverb-server/src/feeds.rs new file mode 100644 index 0000000..2904f12 --- /dev/null +++ b/cruciverb-server/src/feeds.rs @@ -0,0 +1,158 @@ +use std::sync::Arc; + +use axum::extract::{Query, State}; +use axum::http::header; +use axum::response::{IntoResponse, Response}; + +use crate::state::AppState; + +#[derive(serde::Deserialize)] +pub struct FeedQuery { + pub preset: Option, +} + +pub async fn atom_feed( + State(state): State>, + Query(q): Query, +) -> Response { + let entries = get_recent_dailies(&state, q.preset.as_deref()).await; + let feed = build_atom(&entries); + ([(header::CONTENT_TYPE, "application/atom+xml; charset=utf-8")], feed).into_response() +} + +pub async fn rss_feed( + State(state): State>, + Query(q): Query, +) -> Response { + let entries = get_recent_dailies(&state, q.preset.as_deref()).await; + let feed = build_rss(&entries); + ([(header::CONTENT_TYPE, "application/rss+xml; charset=utf-8")], feed).into_response() +} + +pub async fn json_feed( + State(state): State>, + Query(q): Query, +) -> Response { + let entries = get_recent_dailies(&state, q.preset.as_deref()).await; + let feed = build_json_feed(&entries); + ([(header::CONTENT_TYPE, "application/feed+json; charset=utf-8")], feed).into_response() +} + +struct FeedEntry { + date: String, + preset: String, + short_id: String, + width: i64, + height: i64, +} + +async fn get_recent_dailies(state: &AppState, preset_filter: Option<&str>) -> Vec { + let dbi = state.db.lock().await; + let (query, param): (String, Option) = if let Some(preset) = preset_filter { + ("SELECT daily_date, preset, short_id, width, height FROM puzzles WHERE daily_date IS NOT NULL AND preset = ?1 ORDER BY daily_date DESC LIMIT 14".into(), Some(preset.to_string())) + } else { + ("SELECT daily_date, preset, short_id, width, height FROM puzzles WHERE daily_date IS NOT NULL ORDER BY daily_date DESC LIMIT 28".into(), None) + }; + + let mut stmt = dbi.db.prepare(&query).unwrap(); + let rows = if let Some(p) = ¶m { + stmt.query_map(rusqlite::params![p], |row| { + Ok(FeedEntry { + date: row.get::<_, String>(0)?, + preset: row.get::<_, String>(1)?, + short_id: row.get::<_, String>(2)?, + width: row.get::<_, i64>(3)?, + height: row.get::<_, i64>(4)?, + }) + }).unwrap().filter_map(|r| r.ok()).collect() + } else { + stmt.query_map([], |row| { + Ok(FeedEntry { + date: row.get::<_, String>(0)?, + preset: row.get::<_, String>(1)?, + short_id: row.get::<_, String>(2)?, + width: row.get::<_, i64>(3)?, + height: row.get::<_, i64>(4)?, + }) + }).unwrap().filter_map(|r| r.ok()).collect() + }; + rows +} + +fn build_atom(entries: &[FeedEntry]) -> String { + let mut xml = String::from(r#" + + cruciverb daily puzzles + urn:cruciverb:daily + "#); + + if let Some(e) = entries.first() { + xml.push_str(&format!("{}T00:00:00Z", e.date)); + } + xml.push_str("\n"); + + for e in entries { + xml.push_str(&format!( + r#" + cruciverb {} - {} - {}x{} + urn:cruciverb:{}-{} + + {}T00:00:00Z + {}x{} {} crossword puzzle + +"#, + e.preset, e.date, e.width, e.height, + e.preset, e.date, + e.short_id, + e.date, + e.width, e.height, e.preset, + )); + } + xml.push_str("\n"); + xml +} + +fn build_rss(entries: &[FeedEntry]) -> String { + let mut xml = String::from(r#" + + + cruciverb daily puzzles + daily crossword puzzles +"#); + + for e in entries { + xml.push_str(&format!( + r#" + cruciverb {} - {} - {}x{} + /p/{} + cruciverb-{}-{} + {}x{} {} crossword puzzle + +"#, + e.preset, e.date, e.width, e.height, + e.short_id, + e.preset, e.date, + e.width, e.height, e.preset, + )); + } + xml.push_str("\n\n"); + xml +} + +fn build_json_feed(entries: &[FeedEntry]) -> String { + let items: Vec = entries.iter().map(|e| { + serde_json::json!({ + "id": format!("cruciverb-{}-{}", e.preset, e.date), + "title": format!("cruciverb {} - {} - {}x{}", e.preset, e.date, e.width, e.height), + "url": format!("/p/{}", e.short_id), + "content_text": format!("{}x{} {} crossword puzzle", e.width, e.height, e.preset), + "date_published": format!("{}T00:00:00Z", e.date), + }) + }).collect(); + + serde_json::json!({ + "version": "https://jsonfeed.org/version/1.1", + "title": "cruciverb daily puzzles", + "items": items, + }).to_string() +}