Skip to main content

systemprompt_generator/rss/
generator.rs

1use 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}