rss and atom feeds
This commit is contained in:
@@ -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) = ¶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#"<?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()
|
||||
}
|
||||
Reference in New Issue
Block a user