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