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() }