Merge pull request #24 from stevedylandev/chore/refactor-feeds
5feb49d3
chore: added freshrss helper
3 file(s) · +192 −199
chore: added freshrss helper
| 1 | 1 | use crate::models::{FeedItem, FreshRSSResponse, SubscriptionList}; |
|
| 2 | 2 | use std::time::Duration; |
|
| 3 | 3 | ||
| 4 | + | #[derive(Clone)] |
|
| 5 | + | pub struct FreshRSSConfig { |
|
| 6 | + | pub url: String, |
|
| 7 | + | pub username: String, |
|
| 8 | + | pub password: String, |
|
| 9 | + | } |
|
| 10 | + | ||
| 11 | + | impl FreshRSSConfig { |
|
| 12 | + | pub fn from_env() -> Option<Self> { |
|
| 13 | + | Some(Self { |
|
| 14 | + | url: std::env::var("FRESHRSS_URL").ok()?, |
|
| 15 | + | username: std::env::var("FRESHRSS_USERNAME").ok()?, |
|
| 16 | + | password: std::env::var("FRESHRSS_PASSWORD").ok()?, |
|
| 17 | + | }) |
|
| 18 | + | } |
|
| 19 | + | } |
|
| 20 | + | ||
| 21 | + | struct FreshRSSClient { |
|
| 22 | + | client: reqwest::Client, |
|
| 23 | + | base_url: String, |
|
| 24 | + | token: String, |
|
| 25 | + | } |
|
| 26 | + | ||
| 27 | + | impl FreshRSSClient { |
|
| 28 | + | async fn new(config: &FreshRSSConfig) -> Result<Self, String> { |
|
| 29 | + | let client = build_client(); |
|
| 30 | + | let auth_url = format!( |
|
| 31 | + | "{}/api/greader.php/accounts/ClientLogin?Email={}&Passwd={}", |
|
| 32 | + | config.url, config.username, config.password |
|
| 33 | + | ); |
|
| 34 | + | ||
| 35 | + | let text = client |
|
| 36 | + | .get(&auth_url) |
|
| 37 | + | .send() |
|
| 38 | + | .await |
|
| 39 | + | .map_err(|e| format!("Auth request failed: {e}"))? |
|
| 40 | + | .text() |
|
| 41 | + | .await |
|
| 42 | + | .map_err(|e| format!("Failed to read auth response: {e}"))?; |
|
| 43 | + | ||
| 44 | + | let token = text |
|
| 45 | + | .lines() |
|
| 46 | + | .find_map(|line| line.strip_prefix("Auth=")) |
|
| 47 | + | .map(|t| t.trim().to_string()) |
|
| 48 | + | .ok_or_else(|| "Authentication failed: no Auth token found".to_string())?; |
|
| 49 | + | ||
| 50 | + | Ok(Self { |
|
| 51 | + | client, |
|
| 52 | + | base_url: config.url.clone(), |
|
| 53 | + | token, |
|
| 54 | + | }) |
|
| 55 | + | } |
|
| 56 | + | ||
| 57 | + | fn api_url(&self, path: &str) -> String { |
|
| 58 | + | format!("{}/api/greader.php/{}", self.base_url, path) |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | fn auth_get(&self, path: &str) -> reqwest::RequestBuilder { |
|
| 62 | + | self.client |
|
| 63 | + | .get(self.api_url(path)) |
|
| 64 | + | .header("Authorization", format!("GoogleLogin auth={}", self.token)) |
|
| 65 | + | } |
|
| 66 | + | ||
| 67 | + | fn auth_post(&self, path: &str) -> reqwest::RequestBuilder { |
|
| 68 | + | self.client |
|
| 69 | + | .post(self.api_url(path)) |
|
| 70 | + | .header("Authorization", format!("GoogleLogin auth={}", self.token)) |
|
| 71 | + | } |
|
| 72 | + | ||
| 73 | + | async fn fetch_items(&self) -> Result<Vec<FeedItem>, String> { |
|
| 74 | + | let data: FreshRSSResponse = self |
|
| 75 | + | .auth_get("reader/api/0/stream/contents/reading-list?n=60&r=d") |
|
| 76 | + | .send() |
|
| 77 | + | .await |
|
| 78 | + | .map_err(|e| format!("Failed to fetch reading list: {e}"))? |
|
| 79 | + | .json() |
|
| 80 | + | .await |
|
| 81 | + | .map_err(|e| format!("Failed to parse FreshRSS response: {e}"))?; |
|
| 82 | + | ||
| 83 | + | let mut items: Vec<FeedItem> = data |
|
| 84 | + | .items |
|
| 85 | + | .iter() |
|
| 86 | + | .map(|item| { |
|
| 87 | + | let link = item |
|
| 88 | + | .canonical |
|
| 89 | + | .as_ref() |
|
| 90 | + | .and_then(|c| c.first()) |
|
| 91 | + | .map(|l| l.href.clone()) |
|
| 92 | + | .unwrap_or_default(); |
|
| 93 | + | ||
| 94 | + | FeedItem { |
|
| 95 | + | id: item.id.clone(), |
|
| 96 | + | title: item.title.clone(), |
|
| 97 | + | published: item.published, |
|
| 98 | + | author: item.origin.title.clone(), |
|
| 99 | + | link, |
|
| 100 | + | origin: item.origin.title.clone(), |
|
| 101 | + | } |
|
| 102 | + | }) |
|
| 103 | + | .collect(); |
|
| 104 | + | ||
| 105 | + | items.sort_by(|a, b| b.published.cmp(&a.published)); |
|
| 106 | + | Ok(items) |
|
| 107 | + | } |
|
| 108 | + | ||
| 109 | + | async fn fetch_subscriptions(&self) -> Result<SubscriptionList, String> { |
|
| 110 | + | let response = self |
|
| 111 | + | .auth_get("reader/api/0/subscription/list?output=json") |
|
| 112 | + | .send() |
|
| 113 | + | .await |
|
| 114 | + | .map_err(|e| format!("Failed to fetch subscriptions: {e}"))?; |
|
| 115 | + | ||
| 116 | + | if !response.status().is_success() { |
|
| 117 | + | return Err(format!("FreshRSS API error: {}", response.status())); |
|
| 118 | + | } |
|
| 119 | + | ||
| 120 | + | response |
|
| 121 | + | .json() |
|
| 122 | + | .await |
|
| 123 | + | .map_err(|e| format!("Failed to parse subscription list: {e}")) |
|
| 124 | + | } |
|
| 125 | + | ||
| 126 | + | async fn add_subscription(&self, feed_url: &str) -> Result<String, String> { |
|
| 127 | + | let response = self |
|
| 128 | + | .auth_post("reader/api/0/subscription/quickadd") |
|
| 129 | + | .form(&[("quickadd", feed_url)]) |
|
| 130 | + | .send() |
|
| 131 | + | .await |
|
| 132 | + | .map_err(|e| format!("Failed to add subscription: {e}"))?; |
|
| 133 | + | ||
| 134 | + | if !response.status().is_success() { |
|
| 135 | + | let status = response.status(); |
|
| 136 | + | let body = response.text().await.unwrap_or_default(); |
|
| 137 | + | return Err(format!("FreshRSS API error ({}): {}", status, body)); |
|
| 138 | + | } |
|
| 139 | + | ||
| 140 | + | let stream_id = format!("feed/{feed_url}"); |
|
| 141 | + | let response = self |
|
| 142 | + | .auth_post("reader/api/0/subscription/edit") |
|
| 143 | + | .form(&[ |
|
| 144 | + | ("ac", "edit"), |
|
| 145 | + | ("s", &stream_id), |
|
| 146 | + | ("a", "user/-/label/Feeds"), |
|
| 147 | + | ]) |
|
| 148 | + | .send() |
|
| 149 | + | .await |
|
| 150 | + | .map_err(|e| format!("Feed added but failed to set category: {e}"))?; |
|
| 151 | + | ||
| 152 | + | if !response.status().is_success() { |
|
| 153 | + | let status = response.status(); |
|
| 154 | + | let body = response.text().await.unwrap_or_default(); |
|
| 155 | + | return Err(format!( |
|
| 156 | + | "Feed added but failed to set category ({}): {}", |
|
| 157 | + | status, body |
|
| 158 | + | )); |
|
| 159 | + | } |
|
| 160 | + | ||
| 161 | + | Ok(format!("Successfully added feed: {feed_url}")) |
|
| 162 | + | } |
|
| 163 | + | } |
|
| 164 | + | ||
| 4 | 165 | fn build_client() -> reqwest::Client { |
|
| 5 | 166 | reqwest::Client::builder() |
|
| 6 | 167 | .timeout(Duration::from_secs(5)) |
|
| 142 | 303 | urls |
|
| 143 | 304 | } |
|
| 144 | 305 | ||
| 145 | - | async fn freshrss_auth( |
|
| 146 | - | client: &reqwest::Client, |
|
| 147 | - | freshrss_url: &str, |
|
| 148 | - | username: &str, |
|
| 149 | - | password: &str, |
|
| 150 | - | ) -> Result<String, String> { |
|
| 151 | - | let auth_url = format!( |
|
| 152 | - | "{}/api/greader.php/accounts/ClientLogin?Email={}&Passwd={}", |
|
| 153 | - | freshrss_url, username, password |
|
| 154 | - | ); |
|
| 155 | - | ||
| 156 | - | let response = client |
|
| 157 | - | .get(&auth_url) |
|
| 158 | - | .send() |
|
| 159 | - | .await |
|
| 160 | - | .map_err(|e| format!("Auth request failed: {e}"))?; |
|
| 161 | - | ||
| 162 | - | let text = response |
|
| 163 | - | .text() |
|
| 164 | - | .await |
|
| 165 | - | .map_err(|e| format!("Failed to read auth response: {e}"))?; |
|
| 166 | - | ||
| 167 | - | for line in text.lines() { |
|
| 168 | - | if let Some(token) = line.strip_prefix("Auth=") { |
|
| 169 | - | return Ok(token.trim().to_string()); |
|
| 170 | - | } |
|
| 171 | - | } |
|
| 172 | - | ||
| 173 | - | Err("Authentication failed: no Auth token found".to_string()) |
|
| 174 | - | } |
|
| 175 | - | ||
| 176 | - | pub async fn fetch_freshrss_items( |
|
| 177 | - | freshrss_url: &str, |
|
| 178 | - | username: &str, |
|
| 179 | - | password: &str, |
|
| 180 | - | ) -> Result<Vec<FeedItem>, String> { |
|
| 181 | - | let client = build_client(); |
|
| 182 | - | let token = freshrss_auth(&client, freshrss_url, username, password).await?; |
|
| 183 | - | ||
| 184 | - | let url = format!( |
|
| 185 | - | "{}/api/greader.php/reader/api/0/stream/contents/reading-list?n=60&r=d", |
|
| 186 | - | freshrss_url |
|
| 187 | - | ); |
|
| 188 | - | ||
| 189 | - | let response = client |
|
| 190 | - | .get(&url) |
|
| 191 | - | .header("Authorization", format!("GoogleLogin auth={token}")) |
|
| 192 | - | .send() |
|
| 193 | - | .await |
|
| 194 | - | .map_err(|e| format!("Failed to fetch reading list: {e}"))?; |
|
| 195 | - | ||
| 196 | - | let data: FreshRSSResponse = response |
|
| 197 | - | .json() |
|
| 198 | - | .await |
|
| 199 | - | .map_err(|e| format!("Failed to parse FreshRSS response: {e}"))?; |
|
| 200 | - | ||
| 201 | - | let mut items: Vec<FeedItem> = data |
|
| 202 | - | .items |
|
| 203 | - | .iter() |
|
| 204 | - | .map(|item| { |
|
| 205 | - | let link = item |
|
| 206 | - | .canonical |
|
| 207 | - | .as_ref() |
|
| 208 | - | .and_then(|c| c.first()) |
|
| 209 | - | .map(|l| l.href.clone()) |
|
| 210 | - | .unwrap_or_default(); |
|
| 211 | - | ||
| 212 | - | FeedItem { |
|
| 213 | - | id: item.id.clone(), |
|
| 214 | - | title: item.title.clone(), |
|
| 215 | - | published: item.published, |
|
| 216 | - | author: item.origin.title.clone(), |
|
| 217 | - | link, |
|
| 218 | - | origin: item.origin.title.clone(), |
|
| 219 | - | } |
|
| 220 | - | }) |
|
| 221 | - | .collect(); |
|
| 222 | - | ||
| 223 | - | items.sort_by(|a, b| b.published.cmp(&a.published)); |
|
| 224 | - | Ok(items) |
|
| 306 | + | pub async fn fetch_freshrss_items(config: &FreshRSSConfig) -> Result<Vec<FeedItem>, String> { |
|
| 307 | + | FreshRSSClient::new(config).await?.fetch_items().await |
|
| 225 | 308 | } |
|
| 226 | 309 | ||
| 227 | 310 | pub async fn fetch_freshrss_subscriptions( |
|
| 228 | - | freshrss_url: &str, |
|
| 229 | - | username: &str, |
|
| 230 | - | password: &str, |
|
| 311 | + | config: &FreshRSSConfig, |
|
| 231 | 312 | ) -> Result<SubscriptionList, String> { |
|
| 232 | - | let client = build_client(); |
|
| 233 | - | let token = freshrss_auth(&client, freshrss_url, username, password).await?; |
|
| 234 | - | ||
| 235 | - | let url = format!( |
|
| 236 | - | "{}/api/greader.php/reader/api/0/subscription/list?output=json", |
|
| 237 | - | freshrss_url |
|
| 238 | - | ); |
|
| 239 | - | ||
| 240 | - | let response = client |
|
| 241 | - | .get(&url) |
|
| 242 | - | .header("Authorization", format!("GoogleLogin auth={token}")) |
|
| 243 | - | .send() |
|
| 244 | - | .await |
|
| 245 | - | .map_err(|e| format!("Failed to fetch subscriptions: {e}"))?; |
|
| 246 | - | ||
| 247 | - | if !response.status().is_success() { |
|
| 248 | - | return Err(format!("FreshRSS API error: {}", response.status())); |
|
| 249 | - | } |
|
| 250 | - | ||
| 251 | - | let data: SubscriptionList = response |
|
| 252 | - | .json() |
|
| 253 | - | .await |
|
| 254 | - | .map_err(|e| format!("Failed to parse subscription list: {e}"))?; |
|
| 255 | - | ||
| 256 | - | Ok(data) |
|
| 313 | + | FreshRSSClient::new(config).await?.fetch_subscriptions().await |
|
| 257 | 314 | } |
|
| 258 | 315 | ||
| 259 | 316 | pub async fn add_freshrss_subscription( |
|
| 260 | - | freshrss_url: &str, |
|
| 261 | - | username: &str, |
|
| 262 | - | password: &str, |
|
| 317 | + | config: &FreshRSSConfig, |
|
| 263 | 318 | feed_url: &str, |
|
| 264 | 319 | ) -> Result<String, String> { |
|
| 265 | - | let client = build_client(); |
|
| 266 | - | let token = freshrss_auth(&client, freshrss_url, username, password).await?; |
|
| 267 | - | ||
| 268 | - | let url = format!( |
|
| 269 | - | "{}/api/greader.php/reader/api/0/subscription/quickadd", |
|
| 270 | - | freshrss_url |
|
| 271 | - | ); |
|
| 272 | - | ||
| 273 | - | let response = client |
|
| 274 | - | .post(&url) |
|
| 275 | - | .header("Authorization", format!("GoogleLogin auth={token}")) |
|
| 276 | - | .form(&[("quickadd", feed_url)]) |
|
| 277 | - | .send() |
|
| 278 | - | .await |
|
| 279 | - | .map_err(|e| format!("Failed to add subscription: {e}"))?; |
|
| 280 | - | ||
| 281 | - | if !response.status().is_success() { |
|
| 282 | - | let status = response.status(); |
|
| 283 | - | let body = response.text().await.unwrap_or_default(); |
|
| 284 | - | return Err(format!("FreshRSS API error ({}): {}", status, body)); |
|
| 285 | - | } |
|
| 286 | - | ||
| 287 | - | // Assign the "Feeds" category via subscription/edit |
|
| 288 | - | let edit_url = format!( |
|
| 289 | - | "{}/api/greader.php/reader/api/0/subscription/edit", |
|
| 290 | - | freshrss_url |
|
| 291 | - | ); |
|
| 292 | - | ||
| 293 | - | let stream_id = format!("feed/{feed_url}"); |
|
| 294 | - | let response = client |
|
| 295 | - | .post(&edit_url) |
|
| 296 | - | .header("Authorization", format!("GoogleLogin auth={token}")) |
|
| 297 | - | .form(&[ |
|
| 298 | - | ("ac", "edit"), |
|
| 299 | - | ("s", &stream_id), |
|
| 300 | - | ("a", "user/-/label/Feeds"), |
|
| 301 | - | ]) |
|
| 302 | - | .send() |
|
| 303 | - | .await |
|
| 304 | - | .map_err(|e| format!("Feed added but failed to set category: {e}"))?; |
|
| 305 | - | ||
| 306 | - | if !response.status().is_success() { |
|
| 307 | - | let status = response.status(); |
|
| 308 | - | let body = response.text().await.unwrap_or_default(); |
|
| 309 | - | return Err(format!( |
|
| 310 | - | "Feed added but failed to set category ({}): {}", |
|
| 311 | - | status, body |
|
| 312 | - | )); |
|
| 313 | - | } |
|
| 314 | - | ||
| 315 | - | Ok(format!("Successfully added feed: {feed_url}")) |
|
| 320 | + | FreshRSSClient::new(config).await?.add_subscription(feed_url).await |
|
| 316 | 321 | } |
|
| 317 | 322 | ||
| 318 | 323 | #[cfg(test)] |
|
| 373 | 378 | ||
| 374 | 379 | pub async fn get_feed_items( |
|
| 375 | 380 | url_query: Option<&str>, |
|
| 381 | + | freshrss_config: Option<&FreshRSSConfig>, |
|
| 376 | 382 | ) -> Result<(Vec<FeedItem>, Option<Vec<String>>), String> { |
|
| 377 | - | // Priority 1: URL query parameter |
|
| 378 | 383 | if let Some(query) = url_query { |
|
| 379 | 384 | let urls: Vec<String> = query |
|
| 380 | 385 | .split(',') |
|
| 388 | 393 | } |
|
| 389 | 394 | } |
|
| 390 | 395 | ||
| 391 | - | // Priority 2: Local OPML file |
|
| 392 | 396 | if let Ok(content) = tokio::fs::read_to_string("feeds.opml").await { |
|
| 393 | 397 | let urls = parse_opml(&content); |
|
| 394 | 398 | if !urls.is_empty() { |
|
| 397 | 401 | } |
|
| 398 | 402 | } |
|
| 399 | 403 | ||
| 400 | - | // Priority 3: FreshRSS fallback |
|
| 401 | - | if let Some((freshrss_url, username, password)) = crate::freshrss_env() { |
|
| 402 | - | let items = fetch_freshrss_items(&freshrss_url, &username, &password).await?; |
|
| 404 | + | if let Some(config) = freshrss_config { |
|
| 405 | + | let items = fetch_freshrss_items(config).await?; |
|
| 403 | 406 | return Ok((items, None)); |
|
| 404 | 407 | } |
|
| 405 | 408 | ||
| 406 | - | // Priority 4: DEFAULT_FEED env var |
|
| 407 | 409 | if let Ok(default_feed) = std::env::var("DEFAULT_FEED") { |
|
| 408 | 410 | let urls: Vec<String> = default_feed |
|
| 409 | 411 | .split(',') |
|
| 25 | 25 | admin_password: Option<String>, |
|
| 26 | 26 | cookie_secure: bool, |
|
| 27 | 27 | base_url: String, |
|
| 28 | + | freshrss_config: Option<feeds::FreshRSSConfig>, |
|
| 28 | 29 | } |
|
| 29 | 30 | ||
| 30 | 31 | struct TemplateFeedItem { |
|
| 64 | 65 | .unwrap_or_default() |
|
| 65 | 66 | } |
|
| 66 | 67 | ||
| 67 | - | fn freshrss_env() -> Option<(String, String, String)> { |
|
| 68 | - | let url = std::env::var("FRESHRSS_URL").ok()?; |
|
| 69 | - | let username = std::env::var("FRESHRSS_USERNAME").ok()?; |
|
| 70 | - | let password = std::env::var("FRESHRSS_PASSWORD").ok()?; |
|
| 71 | - | Some((url, username, password)) |
|
| 72 | - | } |
|
| 73 | - | ||
| 74 | 68 | async fn index_handler( |
|
| 75 | 69 | State(state): State<Arc<AppState>>, |
|
| 76 | 70 | Query(params): Query<HashMap<String, String>>, |
|
| 80 | 74 | .or_else(|| params.get("urls")) |
|
| 81 | 75 | .map(|s| s.as_str()); |
|
| 82 | 76 | ||
| 83 | - | let template = match feeds::get_feed_items(url_query).await { |
|
| 77 | + | let template = match feeds::get_feed_items(url_query, state.freshrss_config.as_ref()).await { |
|
| 84 | 78 | Ok((items, feed_urls)) => { |
|
| 85 | 79 | let template_items: Vec<TemplateFeedItem> = items |
|
| 86 | 80 | .into_iter() |
|
| 114 | 108 | } |
|
| 115 | 109 | ||
| 116 | 110 | async fn feeds_handler( |
|
| 111 | + | State(state): State<Arc<AppState>>, |
|
| 117 | 112 | Query(params): Query<HashMap<String, String>>, |
|
| 118 | 113 | ) -> Result<Response, StatusCode> { |
|
| 119 | 114 | let format = params |
|
| 121 | 116 | .map(|s| s.as_str()) |
|
| 122 | 117 | .unwrap_or("json"); |
|
| 123 | 118 | ||
| 124 | - | let freshrss_url = |
|
| 125 | - | std::env::var("FRESHRSS_URL").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; |
|
| 126 | - | let username = |
|
| 127 | - | std::env::var("FRESHRSS_USERNAME").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; |
|
| 128 | - | let password = |
|
| 129 | - | std::env::var("FRESHRSS_PASSWORD").map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; |
|
| 119 | + | let config = state |
|
| 120 | + | .freshrss_config |
|
| 121 | + | .as_ref() |
|
| 122 | + | .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; |
|
| 130 | 123 | ||
| 131 | - | let data = feeds::fetch_freshrss_subscriptions(&freshrss_url, &username, &password) |
|
| 124 | + | let data = feeds::fetch_freshrss_subscriptions(config) |
|
| 132 | 125 | .await |
|
| 133 | 126 | .map_err(|e| { |
|
| 134 | 127 | eprintln!("Failed to fetch subscriptions: {e}"); |
|
| 334 | 327 | State(state): State<Arc<AppState>>, |
|
| 335 | 328 | Query(q): Query<FlashQuery>, |
|
| 336 | 329 | ) -> Response { |
|
| 337 | - | let _ = state; // state available if needed later |
|
| 338 | - | ||
| 339 | - | let freshrss_configured = freshrss_env().is_some(); |
|
| 330 | + | let freshrss_configured = state.freshrss_config.is_some(); |
|
| 340 | 331 | ||
| 341 | - | let subscriptions = if freshrss_configured { |
|
| 342 | - | if let Some((url, user, pass)) = freshrss_env() { |
|
| 343 | - | feeds::fetch_freshrss_subscriptions(&url, &user, &pass) |
|
| 344 | - | .await |
|
| 345 | - | .ok() |
|
| 346 | - | .and_then(|list| list.subscriptions) |
|
| 347 | - | } else { |
|
| 348 | - | None |
|
| 349 | - | } |
|
| 332 | + | let subscriptions = if let Some(config) = &state.freshrss_config { |
|
| 333 | + | feeds::fetch_freshrss_subscriptions(config) |
|
| 334 | + | .await |
|
| 335 | + | .ok() |
|
| 336 | + | .and_then(|list| list.subscriptions) |
|
| 350 | 337 | } else { |
|
| 351 | 338 | None |
|
| 352 | 339 | }; |
|
| 366 | 353 | ||
| 367 | 354 | async fn add_feed_handler( |
|
| 368 | 355 | _session: auth::AuthSession, |
|
| 356 | + | State(state): State<Arc<AppState>>, |
|
| 369 | 357 | Form(form): Form<AddFeedForm>, |
|
| 370 | 358 | ) -> Response { |
|
| 371 | - | let (url, user, pass) = match freshrss_env() { |
|
| 372 | - | Some(env) => env, |
|
| 359 | + | let config = match &state.freshrss_config { |
|
| 360 | + | Some(c) => c, |
|
| 373 | 361 | None => { |
|
| 374 | 362 | return Redirect::to("/admin?error=FreshRSS+not+configured").into_response(); |
|
| 375 | 363 | } |
|
| 376 | 364 | }; |
|
| 377 | 365 | ||
| 378 | - | match feeds::add_freshrss_subscription(&url, &user, &pass, &form.feed_url).await { |
|
| 366 | + | match feeds::add_freshrss_subscription(config, &form.feed_url).await { |
|
| 379 | 367 | Ok(_) => Redirect::to("/admin?success=Feed+added+successfully").into_response(), |
|
| 380 | 368 | Err(e) => { |
|
| 381 | 369 | eprintln!("Failed to add feed: {e}"); |
|
| 400 | 388 | admin_password: std::env::var("ADMIN_PASSWORD").ok(), |
|
| 401 | 389 | cookie_secure, |
|
| 402 | 390 | base_url, |
|
| 391 | + | freshrss_config: feeds::FreshRSSConfig::from_env(), |
|
| 403 | 392 | }); |
|
| 404 | 393 | ||
| 405 | 394 | let app = Router::new() |
|
| 1 | - | #![allow(dead_code)] |
|
| 2 | 1 | use serde::{Deserialize, Serialize}; |
|
| 3 | 2 | ||
| 4 | 3 | #[derive(Debug, Clone, Serialize, Deserialize)] |
|
| 11 | 10 | pub origin: String, |
|
| 12 | 11 | } |
|
| 13 | 12 | ||
| 13 | + | #[allow(dead_code)] |
|
| 14 | 14 | #[derive(Debug, Deserialize)] |
|
| 15 | 15 | pub struct FreshRSSResponse { |
|
| 16 | 16 | pub id: String, |
|
| 19 | 19 | pub continuation: Option<String>, |
|
| 20 | 20 | } |
|
| 21 | 21 | ||
| 22 | + | #[allow(dead_code)] |
|
| 22 | 23 | #[derive(Debug, Deserialize)] |
|
| 23 | 24 | pub struct FreshRSSItem { |
|
| 24 | 25 | pub id: String, |
|
| 34 | 35 | pub href: String, |
|
| 35 | 36 | } |
|
| 36 | 37 | ||
| 38 | + | #[allow(dead_code)] |
|
| 37 | 39 | #[derive(Debug, Clone, Deserialize)] |
|
| 38 | 40 | pub struct FreshRSSOrigin { |
|
| 39 | 41 | #[serde(rename = "streamId")] |
|