rss and atom feeds

This commit is contained in:
2025-10-13 09:15:12 +03:00
parent 4504a0f0b9
commit 10c457eb34
+158
View File
@@ -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<String>,
}
pub async fn atom_feed(
State(state): State<Arc<AppState>>,
Query(q): Query<FeedQuery>,
) -> 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<Arc<AppState>>,
Query(q): Query<FeedQuery>,
) -> 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<Arc<AppState>>,
Query(q): Query<FeedQuery>,
) -> 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<FeedEntry> {
let dbi = state.db.lock().await;
let (query, param): (String, Option<String>) = 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) = &param {
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#"<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>cruciverb daily puzzles</title>
<id>urn:cruciverb:daily</id>
<updated>"#);
if let Some(e) = entries.first() {
xml.push_str(&format!("{}T00:00:00Z", e.date));
}
xml.push_str("</updated>\n");
for e in entries {
xml.push_str(&format!(
r#" <entry>
<title>cruciverb {} - {} - {}x{}</title>
<id>urn:cruciverb:{}-{}</id>
<link href="/p/{}" />
<published>{}T00:00:00Z</published>
<summary>{}x{} {} crossword puzzle</summary>
</entry>
"#,
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("</feed>\n");
xml
}
fn build_rss(entries: &[FeedEntry]) -> String {
let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>cruciverb daily puzzles</title>
<description>daily crossword puzzles</description>
"#);
for e in entries {
xml.push_str(&format!(
r#" <item>
<title>cruciverb {} - {} - {}x{}</title>
<link>/p/{}</link>
<guid>cruciverb-{}-{}</guid>
<description>{}x{} {} crossword puzzle</description>
</item>
"#,
e.preset, e.date, e.width, e.height,
e.short_id,
e.preset, e.date,
e.width, e.height, e.preset,
));
}
xml.push_str("</channel>\n</rss>\n");
xml
}
fn build_json_feed(entries: &[FeedEntry]) -> String {
let items: Vec<serde_json::Value> = 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()
}