chore: add /feed.xml to feeds
4907cd38
1 file(s) · +84 −0
| 280 | 280 | .replace('\'', "'") |
|
| 281 | 281 | } |
|
| 282 | 282 | ||
| 283 | + | async fn atom_feed_handler(State(state): State<Arc<AppState>>) -> Response { |
|
| 284 | + | let items = match fdb::list_items( |
|
| 285 | + | &state.db, |
|
| 286 | + | &fdb::ListItemsFilter { |
|
| 287 | + | limit: Some(100), |
|
| 288 | + | ..Default::default() |
|
| 289 | + | }, |
|
| 290 | + | ) { |
|
| 291 | + | Ok(items) => items, |
|
| 292 | + | Err(e) => { |
|
| 293 | + | tracing::error!("atom feed query failed: {e}"); |
|
| 294 | + | return StatusCode::INTERNAL_SERVER_ERROR.into_response(); |
|
| 295 | + | } |
|
| 296 | + | }; |
|
| 297 | + | ||
| 298 | + | let updated = items |
|
| 299 | + | .iter() |
|
| 300 | + | .map(|i| i.published_at) |
|
| 301 | + | .max() |
|
| 302 | + | .and_then(|ts| DateTime::from_timestamp(ts, 0)) |
|
| 303 | + | .unwrap_or_else(chrono::Utc::now) |
|
| 304 | + | .to_rfc3339(); |
|
| 305 | + | ||
| 306 | + | let base = state.base_url.trim_end_matches('/'); |
|
| 307 | + | let self_url = format!("{base}/feed.xml"); |
|
| 308 | + | ||
| 309 | + | let mut xml = String::with_capacity(4096); |
|
| 310 | + | xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); |
|
| 311 | + | xml.push_str("<feed xmlns=\"http://www.w3.org/2005/Atom\">\n"); |
|
| 312 | + | xml.push_str(" <title>Feeds</title>\n"); |
|
| 313 | + | xml.push_str(&format!( |
|
| 314 | + | " <link href=\"{}\" rel=\"self\" type=\"application/atom+xml\" />\n", |
|
| 315 | + | escape_xml(&self_url) |
|
| 316 | + | )); |
|
| 317 | + | xml.push_str(&format!(" <link href=\"{}\" />\n", escape_xml(base))); |
|
| 318 | + | xml.push_str(&format!(" <id>{}</id>\n", escape_xml(&self_url))); |
|
| 319 | + | xml.push_str(&format!(" <updated>{updated}</updated>\n")); |
|
| 320 | + | ||
| 321 | + | for item in &items { |
|
| 322 | + | let published = DateTime::from_timestamp(item.published_at, 0) |
|
| 323 | + | .unwrap_or_else(chrono::Utc::now) |
|
| 324 | + | .to_rfc3339(); |
|
| 325 | + | let author_name = match item.author.as_deref() { |
|
| 326 | + | Some(a) if !a.is_empty() => a, |
|
| 327 | + | _ => item.feed_title.as_str(), |
|
| 328 | + | }; |
|
| 329 | + | let entry_id = if item.guid.is_empty() { |
|
| 330 | + | &item.link |
|
| 331 | + | } else { |
|
| 332 | + | &item.guid |
|
| 333 | + | }; |
|
| 334 | + | ||
| 335 | + | xml.push_str(" <entry>\n"); |
|
| 336 | + | xml.push_str(&format!(" <title>{}</title>\n", escape_xml(&item.title))); |
|
| 337 | + | xml.push_str(&format!( |
|
| 338 | + | " <link href=\"{}\" />\n", |
|
| 339 | + | escape_xml(&item.link) |
|
| 340 | + | )); |
|
| 341 | + | xml.push_str(&format!(" <id>{}</id>\n", escape_xml(entry_id))); |
|
| 342 | + | xml.push_str(&format!(" <updated>{published}</updated>\n")); |
|
| 343 | + | xml.push_str(&format!(" <published>{published}</published>\n")); |
|
| 344 | + | xml.push_str(" <author>\n"); |
|
| 345 | + | xml.push_str(&format!( |
|
| 346 | + | " <name>{}</name>\n", |
|
| 347 | + | escape_xml(author_name) |
|
| 348 | + | )); |
|
| 349 | + | xml.push_str(" </author>\n"); |
|
| 350 | + | xml.push_str(&format!( |
|
| 351 | + | " <source><title>{}</title></source>\n", |
|
| 352 | + | escape_xml(&item.feed_title) |
|
| 353 | + | )); |
|
| 354 | + | xml.push_str(" </entry>\n"); |
|
| 355 | + | } |
|
| 356 | + | ||
| 357 | + | xml.push_str("</feed>\n"); |
|
| 358 | + | ||
| 359 | + | ( |
|
| 360 | + | [(header::CONTENT_TYPE, "application/atom+xml; charset=utf-8")], |
|
| 361 | + | xml, |
|
| 362 | + | ) |
|
| 363 | + | .into_response() |
|
| 364 | + | } |
|
| 365 | + | ||
| 283 | 366 | async fn static_handler(axum::extract::Path(path): axum::extract::Path<String>) -> Response { |
|
| 284 | 367 | match Static::get(&path) { |
|
| 285 | 368 | Some(file) => { |
|
| 652 | 735 | let app = Router::new() |
|
| 653 | 736 | .route("/", get(index_handler)) |
|
| 654 | 737 | .route("/feeds", get(feeds_handler)) |
|
| 738 | + | .route("/feed.xml", get(atom_feed_handler)) |
|
| 655 | 739 | .route("/static/{*path}", get(static_handler)) |
|
| 656 | 740 | .merge(admin_router) |
|
| 657 | 741 | .merge(api_router) |
|