systemprompt_generator/rss/
generator.rs1use super::xml::{RssChannel, RssItem, build_rss_xml};
2use anyhow::{Context, Result, anyhow};
3use std::path::Path;
4use std::sync::Arc;
5use systemprompt_database::DbPool;
6use systemprompt_models::{AppPaths, Config};
7use systemprompt_provider_contracts::{RssFeedContext, RssFeedProvider};
8use tokio::fs;
9
10use super::default_provider::DefaultRssFeedProvider;
11
12#[derive(Debug, Clone)]
13pub struct GeneratedFeed {
14 pub filename: String,
15 pub xml: String,
16 pub item_count: usize,
17}
18
19pub async fn generate_feed(db_pool: DbPool) -> Result<()> {
20 let provider = DefaultRssFeedProvider::new(Arc::clone(&db_pool)).await?;
21 let providers: Vec<Arc<dyn RssFeedProvider>> = vec![Arc::new(provider)];
22 let feeds = generate_feed_with_providers(&providers, db_pool).await?;
23
24 let web_dir = AppPaths::get()
25 .map_err(|e| anyhow!("{}", e))?
26 .web()
27 .dist()
28 .to_path_buf();
29
30 for feed in feeds {
31 let feed_path = web_dir.join(&feed.filename);
32 ensure_parent_exists(&feed_path).await?;
33 fs::write(&feed_path, &feed.xml).await?;
34 tracing::info!(
35 path = %feed_path.display(),
36 items = feed.item_count,
37 "Generated RSS feed"
38 );
39 }
40
41 Ok(())
42}
43
44pub async fn generate_feed_with_providers(
45 providers: &[Arc<dyn RssFeedProvider>],
46 _db_pool: DbPool,
47) -> Result<Vec<GeneratedFeed>> {
48 let global_config = Config::get()?;
49 let base_url = &global_config.api_external_url;
50
51 let mut feeds = Vec::new();
52
53 for provider in providers {
54 for spec in provider.feed_specs() {
55 let ctx = RssFeedContext {
56 base_url,
57 source_name: spec.source_id.as_str(),
58 };
59
60 let metadata = provider
61 .feed_metadata(&ctx)
62 .await
63 .context("Failed to fetch feed metadata")?;
64
65 let items = provider
66 .fetch_items(&ctx, spec.max_items)
67 .await
68 .context("Failed to fetch feed items")?;
69
70 let rss_items: Vec<RssItem> = items
71 .into_iter()
72 .map(|item| RssItem {
73 title: item.title,
74 link: item.link,
75 description: item.description,
76 pub_date: item.pub_date,
77 guid: item.guid,
78 author: item.author,
79 })
80 .collect();
81
82 let channel = RssChannel {
83 title: metadata.title,
84 link: metadata.link,
85 description: metadata.description,
86 items: rss_items.clone(),
87 };
88
89 let xml = build_rss_xml(&channel);
90
91 feeds.push(GeneratedFeed {
92 filename: spec.output_filename,
93 xml,
94 item_count: rss_items.len(),
95 });
96 }
97 }
98
99 if feeds.is_empty() {
100 return Err(anyhow!(
101 "No RSS feeds generated. Ensure at least one RssFeedProvider is registered."
102 ));
103 }
104
105 Ok(feeds)
106}
107
108async fn ensure_parent_exists(path: &Path) -> Result<()> {
109 if let Some(parent) = path.parent() {
110 if !parent.exists() {
111 fs::create_dir_all(parent).await?;
112 }
113 }
114 Ok(())
115}