use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; use std::sync::Mutex; use tauri::{AppHandle, Manager, State}; #[derive(Serialize, Deserialize, Default, Clone)] pub struct RssConfig { pub urls: Vec, } #[derive(Default)] pub struct NewsState { pub openrouter_key: Mutex, pub groq_key: Mutex, pub rss_config: Mutex, } #[derive(Serialize, Deserialize, Clone, PartialEq, Debug)] #[serde(rename_all = "camelCase")] pub struct NewsArticle { pub content: String, pub author: String, pub created_at: String, } fn get_config_path(app: &AppHandle) -> PathBuf { app.path() .app_config_dir() .unwrap_or_else(|_| PathBuf::from(".")) .join("rss.ron") } #[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")?; *lock = key.trim().to_string(); Ok(()) } #[tauri::command] pub async fn save_groq_key(key: String, state: State<'_, NewsState>) -> Result<(), String> { let mut lock = state.groq_key.lock().map_err(|_| "Lock failed")?; *lock = key.trim().to_string(); Ok(()) } #[tauri::command] pub async fn load_rss_config( app: AppHandle, state: State<'_, NewsState>, ) -> Result, String> { let path = get_config_path(&app); 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, 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(()) } #[tauri::command] pub async fn fetch_ai_news( provider: String, state: State<'_, NewsState>, ) -> Result, String> { let (key, url, model, author) = match provider.as_str() { "groq" => { let k = state.groq_key.lock().map_err(|_| "Lock failed")?.clone(); ( k, "https://api.groq.com/openai/v1/chat/completions", "llama-3.1-8b-instant", "Groq AI", ) } _ => { let k = state .openrouter_key .lock() .map_err(|_| "Lock failed")? .clone(); ( k, "https://openrouter.ai/api/v1/chat/completions", "openai/gpt-oss-20b:free:online", "OpenRouter AI", ) } }; if key.is_empty() { return Err(format!("API Key für {} fehlt.", author)); } let client = reqwest::Client::new(); let response = client .post(url) .header("Authorization", format!("Bearer {}", key)) .json(&serde_json::json!({ "model": model, "messages": [ {"role": "system", "content": "Kurze Tech-News, 1-2 Sätze, Deutsch. Nutze Markdown."}, {"role": "user", "content": "Was gibt es neues in der Tech-Welt?"} ] })) .send() .await .map_err(|e| e.to_string())?; 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 beim Abrufen der Inhalte") .to_string(); Ok(vec![NewsArticle { content, author: author.into(), created_at: "Gerade eben".into(), }]) } #[tauri::command] pub async fn fetch_rss_news(state: State<'_, NewsState>) -> Result, String> { let urls = state.rss_config.lock().unwrap().urls.clone(); let mut all_articles = Vec::new(); let client = reqwest::Client::new(); 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, }); } } } } } Ok(all_articles) }