Multi Feeds + Ron rss save

This commit is contained in:
2026-01-22 14:08:59 +01:00
parent 6d201280ed
commit 0334c5979d
5 changed files with 488 additions and 293 deletions

View File

@@ -20,6 +20,7 @@ serde_json = "1"
easy-nostr = { path = "./easy-nostr" }
tokio = { version = "1", features = ["full"] }
feed-rs = "2.3.1"
ron = "0.8"
# FIX: default-features entfernt und rustls-tls hinzugefügt
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }

View File

@@ -10,9 +10,10 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
home::fetch_nostr_posts,
news::save_openrouter_key,
news::load_rss_config,
news::save_rss_urls, // Geändert von save_rss_url zu save_rss_urls
news::fetch_ai_news,
news::fetch_rss_news,
news::save_rss_url,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -1,22 +1,35 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::State;
use tauri::{AppHandle, Manager, State};
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct RssConfig {
pub urls: Vec<String>,
}
#[derive(Default)]
pub struct NewsState {
pub openrouter_key: Mutex<String>,
pub rss_url: Mutex<String>, // Neu: Speicher für die RSS-URL
pub rss_config: Mutex<RssConfig>,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
pub struct NewsArticle {
pub content: String,
pub author: String,
pub created_at: u64,
pub created_at: String,
}
fn get_config_path(app: &AppHandle) -> PathBuf {
app.path()
.app_config_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("rss.ron")
}
// Bestehender Command für den API Key
#[tauri::command]
pub async fn save_openrouter_key(key: String, state: State<'_, NewsState>) -> Result<(), String> {
let mut lock = state.openrouter_key.lock().map_err(|_| "Lock failed")?;
@@ -24,11 +37,63 @@ pub async fn save_openrouter_key(key: String, state: State<'_, NewsState>) -> Re
Ok(())
}
// NEU: Command zum Speichern der RSS URL
#[tauri::command]
pub async fn save_rss_url(url: String, state: State<'_, NewsState>) -> Result<(), String> {
let mut lock = state.rss_url.lock().map_err(|_| "Lock failed")?;
*lock = url.trim().to_string();
pub async fn load_rss_config(
app: AppHandle,
state: State<'_, NewsState>,
) -> Result<Vec<String>, String> {
let path = get_config_path(&app);
// Initialisierung mit Standard-Feeds, falls Datei nicht existiert
if !path.exists() {
let default_urls = vec![
"https://www.nasa.gov/news-release/feed/".to_string(),
"https://www.heise.de/rss/heise-atom.xml".to_string(),
];
let config = RssConfig {
urls: default_urls.clone(),
};
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let ron_str = ron::to_string(&config).map_err(|e| e.to_string())?;
fs::write(&path, ron_str).map_err(|e| e.to_string())?;
let mut lock = state.rss_config.lock().unwrap();
lock.urls = default_urls.clone();
return Ok(default_urls);
}
let content = fs::read_to_string(path).map_err(|e| e.to_string())?;
let config: RssConfig = ron::from_str(&content).map_err(|e| e.to_string())?;
let mut lock = state.rss_config.lock().unwrap();
*lock = config.clone();
Ok(config.urls)
}
#[tauri::command]
pub async fn save_rss_urls(
urls: Vec<String>,
app: AppHandle,
state: State<'_, NewsState>,
) -> Result<(), String> {
let config = RssConfig { urls };
let path = get_config_path(&app);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let ron_str = ron::to_string(&config).map_err(|e| e.to_string())?;
fs::write(path, ron_str).map_err(|e| e.to_string())?;
let mut lock = state.rss_config.lock().unwrap();
*lock = config;
Ok(())
}
@@ -39,79 +104,74 @@ pub async fn fetch_ai_news(state: State<'_, NewsState>) -> Result<Vec<NewsArticl
.lock()
.map_err(|_| "Lock failed")?
.clone();
if key.is_empty() {
return Err("API Key fehlt.".to_string());
return Err("API Key fehlt.".into());
}
let client = reqwest::Client::new();
let body = serde_json::json!({
"model": "minimax/minimax-m2.1",
"messages": [
{"role": "system", "content": "Kurze News, 1-2 Sätze, Deutsch."},
{"role": "user", "content": "Tech-News."}
]
});
let response = client
.post("https://openrouter.ai/api/v1/chat/completions")
.header("Authorization", format!("Bearer {}", key))
.json(&body)
.json(&serde_json::json!({
"model": "minimax/minimax-m2.1",
"messages": [
{"role": "system", "content": "Kurze News, 1-2 Sätze, Deutsch."},
{"role": "user", "content": "Tech-News."}
]
}))
.send()
.await
.map_err(|e| e.to_string())?;
let json_res: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let content = json_res["choices"][0]["message"]["content"]
let json: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let content = json["choices"][0]["message"]["content"]
.as_str()
.unwrap_or("Fehler")
.to_string();
Ok(vec![NewsArticle {
content,
author: "OpenRouter AI".to_string(),
created_at: 0,
author: "OpenRouter AI".into(),
created_at: "Gerade eben".into(),
}])
}
// VERBESSERT: Lädt nun den echten Feed von der gespeicherten URL
#[tauri::command]
pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result<Vec<NewsArticle>, String> {
let url = state.rss_url.lock().map_err(|_| "Lock failed")?.clone();
let urls = state.rss_config.lock().unwrap().urls.clone();
let mut all_articles = Vec::new();
let client = reqwest::Client::new();
if url.is_empty() {
return Err("RSS URL fehlt. Bitte in den Einstellungen eintragen.".to_string());
for url in urls {
if let Ok(res) = client.get(&url).send().await {
if let Ok(bytes) = res.bytes().await {
if let Ok(feed) = feed_rs::parser::parse(&bytes[..]) {
let source = feed
.title
.map(|t| t.content)
.unwrap_or_else(|| "RSS".into());
for entry in feed.entries.into_iter().take(3) {
let date = entry
.published
.or(entry.updated)
.map(|d| d.format("%d.%m.%Y %H:%M").to_string())
.unwrap_or_else(|| "--.--.----".into());
all_articles.push(NewsArticle {
content: format!(
"### {}\n\n{}",
entry.title.map(|t| t.content).unwrap_or_default(),
entry.summary.map(|s| s.content).unwrap_or_default()
),
author: source.clone(),
created_at: date,
});
}
}
}
}
}
let response = reqwest::get(&url)
.await
.map_err(|e| e.to_string())?
.bytes()
.await
.map_err(|e| e.to_string())?;
let feed = feed_rs::parser::parse(&response[..]).map_err(|e| e.to_string())?;
let articles = feed
.entries
.into_iter()
.take(5)
.map(|entry| NewsArticle {
content: format!(
"### {}\n\n{}",
entry.title.map(|t| t.content).unwrap_or_default(),
entry
.summary
.map(|s| s.content)
.unwrap_or_else(|| "Kein Inhalt".to_string())
),
author: feed
.title
.as_ref()
.map(|t| t.content.clone())
.unwrap_or_else(|| "RSS".into()),
created_at: entry.updated.map(|d| d.timestamp() as u64).unwrap_or(0),
})
.collect();
Ok(articles)
Ok(all_articles)
}