From 5356e2dbb480944c70d1fee83b11a9c163dec054 Mon Sep 17 00:00:00 2001 From: Jet Date: Thu, 26 Mar 2026 18:15:43 -0700 Subject: [PATCH] feat: add rss feed and remove rev feature --- api/Cargo.lock | 180 ++++++++++++++++++++++++++++++++++++++++++++ api/Cargo.toml | 1 + api/src/handlers.rs | 76 +++++++++++++++++++ api/src/serve.rs | 1 + module.nix | 8 ++ src/global.d.ts | 2 - src/lib/site.ts | 7 +- vite.config.ts | 6 -- 8 files changed, 268 insertions(+), 13 deletions(-) diff --git a/api/Cargo.lock b/api/Cargo.lock index 847bb85..2b662d6 100644 --- a/api/Cargo.lock +++ b/api/Cargo.lock @@ -20,6 +20,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -41,6 +50,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.8" @@ -105,6 +120,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + [[package]] name = "bytes" version = "1.11.1" @@ -127,6 +148,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "chumsky" version = "0.9.3" @@ -436,6 +468,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -568,6 +624,7 @@ version = "0.1.0" dependencies = [ "axum", "base64", + "chrono", "lettre", "rusqlite", "serde", @@ -576,6 +633,16 @@ dependencies = [ "tower-http", ] +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -706,6 +773,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.37.3" @@ -907,6 +983,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" @@ -1287,6 +1369,51 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -1321,12 +1448,65 @@ dependencies = [ "semver", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/api/Cargo.toml b/api/Cargo.toml index a969a31..81164db 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] axum = "0.8" base64 = "0.22" +chrono = { version = "0.4", default-features = false, features = ["clock"] } lettre = "0.11" rusqlite = { version = "0.32", features = ["bundled"] } serde = { version = "1", features = ["derive"] } diff --git a/api/src/handlers.rs b/api/src/handlers.rs index 36e8828..824f219 100644 --- a/api/src/handlers.rs +++ b/api/src/handlers.rs @@ -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('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + +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>, ) -> Result>, 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>, +) -> Result { + 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::, _>>() + .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!( + "{}{}{}{}{}", + xml_escape(&question.question), + xml_escape(&guid), + xml_escape(&guid), + xml_escape(&rss_pub_date(&question.answered_at)), + xml_escape(&description), + ) + }) + .collect::>() + .join(""); + + let xml = format!( + "Jet Pham Q+A{SITE_URL}/qaAnswered questions from Jet Pham's siteen-us{items}" + ); + + Ok(([(CONTENT_TYPE, "application/rss+xml; charset=utf-8")], xml)) +} + #[derive(Deserialize)] pub struct SubmitQuestion { question: String, diff --git a/api/src/serve.rs b/api/src/serve.rs index f381b5c..0bb67a9 100644 --- a/api/src/serve.rs +++ b/api/src/serve.rs @@ -53,6 +53,7 @@ pub async fn run() -> Result<(), Box> { 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); diff --git a/module.nix b/module.nix index 8862bfc..7543683 100644 --- a/module.nix +++ b/module.nix @@ -144,6 +144,10 @@ in reverse_proxy 127.0.0.1:3003 } + handle /qa/rss.xml { + reverse_proxy 127.0.0.1:3003 + } + handle { root * ${package} try_files {path} /index.html @@ -161,6 +165,10 @@ in reverse_proxy 127.0.0.1:3003 } + handle /qa/rss.xml { + reverse_proxy 127.0.0.1:3003 + } + handle { root * ${package} try_files {path} /index.html diff --git a/src/global.d.ts b/src/global.d.ts index a266ba5..9193a96 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,7 +1,5 @@ /// -declare const __COMMIT_SHA__: string; - declare module "*.txt?raw" { const content: string; export default content; diff --git a/src/lib/site.ts b/src/lib/site.ts index bc699f8..a0f578a 100644 --- a/src/lib/site.ts +++ b/src/lib/site.ts @@ -1,7 +1,6 @@ const CLEARNET_HOST = "jetpham.com"; const ONION_HOST = "jet7tetd43snvjx3ng5jrhuwm2yhyp76tjtct5mtofg64apokcgq7fqd.onion"; -const COMMIT_BASE_URL = "https://git.extremist.software/jet/website/commit/"; const REPO_URL = "https://git.extremist.software/jet/website"; function isOnionHost(hostname: string): boolean { @@ -26,17 +25,15 @@ export function renderFooter() { const footer = document.getElementById("site-footer"); if (!footer) return; - const commitSha = __COMMIT_SHA__; - const shortSha = commitSha.slice(0, 7); const mirror = getMirrorLink(); footer.innerHTML = `