feat: add rss feed and remove rev feature

This commit is contained in:
Jet 2026-03-26 18:15:43 -07:00
parent 3937d8fd75
commit 5356e2dbb4
No known key found for this signature in database
8 changed files with 268 additions and 13 deletions

View file

@ -1,9 +1,12 @@
use std::sync::Arc;
use axum::extract::State;
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::Json;
use base64::Engine;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
@ -25,6 +28,22 @@ pub struct QuestionStats {
answered: i64,
}
const SITE_URL: &str = "https://jetpham.com";
fn xml_escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&apos;")
}
fn rss_pub_date(timestamp: &str) -> String {
DateTime::parse_from_rfc3339(timestamp)
.map(|dt| dt.to_rfc2822())
.unwrap_or_else(|_| Utc::now().to_rfc2822())
}
pub async fn get_questions(
State(state): State<Arc<AppState>>,
) -> Result<Json<Vec<Question>>, StatusCode> {
@ -80,6 +99,63 @@ pub async fn get_question_stats(
Ok(Json(QuestionStats { asked, answered }))
}
pub async fn get_question_rss(
State(state): State<Arc<AppState>>,
) -> Result<impl IntoResponse, StatusCode> {
let db = state
.db
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut stmt = db
.prepare(
"SELECT id, question, answer, created_at, answered_at \
FROM questions WHERE answer IS NOT NULL \
ORDER BY answered_at DESC",
)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let questions = stmt
.query_map([], |row| {
Ok(Question {
id: row.get(0)?,
question: row.get(1)?,
answer: row.get(2)?,
created_at: row.get(3)?,
answered_at: row.get(4)?,
})
})
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.collect::<Result<Vec<_>, _>>()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let items = questions
.into_iter()
.map(|question| {
let guid = format!("{SITE_URL}/qa#question-{}", question.id);
let description = format!(
"Question: {}\n\nAnswer: {}",
question.question, question.answer
);
format!(
"<item><title>{}</title><link>{}</link><guid>{}</guid><pubDate>{}</pubDate><description>{}</description></item>",
xml_escape(&question.question),
xml_escape(&guid),
xml_escape(&guid),
xml_escape(&rss_pub_date(&question.answered_at)),
xml_escape(&description),
)
})
.collect::<Vec<_>>()
.join("");
let xml = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\"><channel><title>Jet Pham Q+A</title><link>{SITE_URL}/qa</link><description>Answered questions from Jet Pham&apos;s site</description><language>en-us</language>{items}</channel></rss>"
);
Ok(([(CONTENT_TYPE, "application/rss+xml; charset=utf-8")], xml))
}
#[derive(Deserialize)]
pub struct SubmitQuestion {
question: String,

View file

@ -53,6 +53,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
get(handlers::get_questions).post(handlers::post_question),
)
.route("/api/questions/stats", get(handlers::get_question_stats))
.route("/qa/rss.xml", get(handlers::get_question_rss))
.route("/api/webhook", post(handlers::webhook))
.layer(CorsLayer::permissive())
.with_state(state);