1pub mod config;
2pub mod metadata;
3
4use self::config::AccountConfig;
5use self::metadata::FeverMetadata;
6use crate::feed_api::{FeedApi, FeedApiError, FeedApiResult, FeedHeaderMap, Portal};
7use crate::models::{
8 self, ArticleID, Category, CategoryID, CategoryMapping, DirectLogin, FatArticle, FavIcon, Feed, FeedID, FeedMapping, FeedUpdateResult, Headline,
9 LoginData, Marked, NEWSFLASH_TOPLEVEL, PasswordLogin, PluginCapabilities, Read, SyncResult, TagID, Url,
10};
11use crate::util;
12use crate::util::favicons::EXPIRES_AFTER_DAYS;
13use async_trait::async_trait;
14
15use base64::Engine;
16use base64::engine::general_purpose::STANDARD as base64_std;
17use chrono::{Duration, Utc};
18use fever_api::FeverApi;
19use fever_api::error::ApiError as FeverApiError;
20use fever_api::models::{Feed as FeverFeed, FeedsGroups, Group as FeverCategory, Item as FeverEntry, ItemStatus};
21use futures::future;
22use reqwest::Client;
23use reqwest::header::{HeaderMap, HeaderValue};
24use std::collections::HashMap;
25use std::collections::HashSet;
26use std::convert::TryInto;
27use std::sync::Arc;
28
29impl From<FeverApiError> for FeedApiError {
30 fn from(error: FeverApiError) -> FeedApiError {
31 match error {
32 FeverApiError::Url(e) => FeedApiError::Url(e),
33 FeverApiError::Json { source, json } => FeedApiError::Json { source, json },
34 FeverApiError::Http(e) => FeedApiError::Network(e),
35 FeverApiError::Fever(fever_error) => FeedApiError::Api {
36 message: format!("Fever Error (code {})\nMessage: {}", fever_error.error_code, fever_error.error_message),
37 },
38 FeverApiError::Input => FeedApiError::Api {
39 message: FeverApiError::Input.to_string(),
40 },
41 FeverApiError::Token => FeedApiError::Api {
42 message: FeverApiError::Token.to_string(),
43 },
44 FeverApiError::Parse => FeedApiError::Api {
45 message: FeverApiError::Parse.to_string(),
46 },
47 FeverApiError::Unauthorized => FeedApiError::Auth,
48 FeverApiError::Unknown => FeedApiError::Unknown,
49 }
50 }
51}
52
53pub struct Fever {
54 api: Option<Arc<FeverApi>>,
55 portal: Arc<Box<dyn Portal>>,
56 logged_in: bool,
57 config: AccountConfig,
58}
59
60impl Fever {
61 fn convert_category(category: FeverCategory, sort_index: Option<i32>) -> (Category, CategoryMapping) {
62 let FeverCategory { id, title } = category;
63 let category_id = CategoryID::new(&id.to_string());
64
65 let category = Category {
66 category_id: category_id.clone(),
67 label: title,
68 };
69 let category_mapping = CategoryMapping {
70 parent_id: NEWSFLASH_TOPLEVEL.clone(),
71 category_id,
72 sort_index,
73 };
74
75 (category, category_mapping)
76 }
77
78 fn convert_category_vec(mut categories: Vec<FeverCategory>) -> (Vec<Category>, Vec<CategoryMapping>) {
79 categories
80 .drain(..)
81 .enumerate()
82 .map(|(i, c)| Self::convert_category(c, Some(i as i32)))
83 .unzip()
84 }
85
86 fn convert_feed(feed: FeverFeed) -> Feed {
87 let FeverFeed {
88 id,
89 favicon_id: _,
90 title,
91 url,
92 site_url,
93 is_spark: _,
94 last_updated_on_time: _,
95 } = feed;
96
97 Feed {
98 feed_id: FeedID::new(&id.to_string()),
99 label: title,
100 website: site_url.and_then(|url| Url::parse(&url).ok()),
101 feed_url: Url::parse(&url).ok(),
102 icon_url: None,
103 error_count: 0,
104 error_message: None,
105 }
106 }
107
108 fn convert_feed_vec(mut feeds: Vec<FeverFeed>, feeds_groups: Vec<FeedsGroups>) -> (Vec<Feed>, Vec<FeedMapping>) {
109 let mut group_mapping = HashMap::new();
110 for group in feeds_groups {
111 for feed_id in group.feed_ids {
112 group_mapping.insert(feed_id, group.group_id);
113 }
114 }
115
116 let mut mappings: Vec<FeedMapping> = Vec::new();
117 let feeds = feeds
118 .drain(..)
119 .enumerate()
120 .map(|(i, f)| {
121 mappings.push(FeedMapping {
122 feed_id: FeedID::new(&f.id.to_string()),
123 category_id: group_mapping
124 .get(&f.id)
125 .map(|id| CategoryID::new(&id.to_string()))
126 .unwrap_or_else(|| NEWSFLASH_TOPLEVEL.clone()),
127 sort_index: Some(i as i32),
128 });
129
130 Self::convert_feed(f)
131 })
132 .collect();
133
134 (feeds, mappings)
135 }
136
137 fn convert_entry(entry: FeverEntry, portal: Arc<Box<dyn Portal>>) -> FatArticle {
138 let FeverEntry {
139 id,
140 feed_id,
141 title,
142 author,
143 html,
144 url,
145 is_saved,
146 is_read,
147 created_on_time,
148 } = entry;
149
150 let article_id = ArticleID::new(&id.to_string());
151 let article_exists_locally = portal.get_article_exists(&article_id).unwrap_or(false);
152 let plain_text = if article_exists_locally {
153 None
154 } else {
155 Some(util::html2text::html2text(&html))
156 };
157 let summary = plain_text.as_deref().map(util::html2text::text2summary);
158 let thumbnail_url = crate::util::thumbnail::extract_thumbnail(&html);
159
160 FatArticle {
161 article_id,
162 title: match escaper::decode_html(&title) {
163 Ok(title) => Some(title),
164 Err(error) => {
165 tracing::warn!(?error.kind, %error.position, "Failed to decode html");
166 Some(title)
167 }
168 },
169 author: if author.is_empty() { None } else { Some(author) },
170 feed_id: FeedID::new(&feed_id.to_string()),
171 url: Url::parse(&url).ok(),
172 date: util::timestamp_to_datetime(created_on_time),
173 synced: Utc::now(),
174 updated: None,
175 summary,
176 html: Some(html),
177 scraped_content: None,
178 direction: None,
179 unread: if is_read { models::Read::Read } else { models::Read::Unread },
180 marked: if is_saved { models::Marked::Marked } else { models::Marked::Unmarked },
181 plain_text,
182 thumbnail_url,
183 }
184 }
185
186 fn convert_entry_vec(entries: Vec<FeverEntry>, feed_ids: &HashSet<FeedID>, portal: Arc<Box<dyn Portal>>) -> Vec<FatArticle> {
187 entries
188 .into_iter()
189 .filter_map(|e| {
190 let feed_ids = feed_ids.clone();
191 let portal = portal.clone();
192
193 let feed_id = FeedID::new(&e.feed_id.to_string());
194 if feed_ids.contains(&feed_id) || e.is_saved {
195 Some(Self::convert_entry(e, portal))
196 } else {
197 None
198 }
199 })
200 .collect()
201 }
202
203 pub async fn get_articles(
204 &self,
205 api: &Arc<FeverApi>,
206 item_ids: Vec<u64>,
207 client: &Client,
208 feeds: &[Feed],
209 ) -> Result<Vec<FatArticle>, FeverApiError> {
210 let batch_size: usize = 50;
211 let mut tasks = Vec::new();
212 let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();
213 let item_id_chunks = item_ids.chunks(batch_size);
214
215 for chunk in item_id_chunks {
216 let feed_ids = feed_ids.clone();
217 let client = client.clone();
218 let chunk = chunk.to_vec();
219 let api = api.clone();
220 let portal = self.portal.clone();
221
222 let task = tokio::spawn(async move {
223 let entries = api.get_items_with(chunk.to_vec(), &client).await?;
224 let converted_articles = Self::convert_entry_vec(entries.items, &feed_ids, portal);
225 Ok::<Vec<FatArticle>, FeedApiError>(converted_articles)
226 });
227
228 tasks.push(task);
229 }
230
231 let articles = future::join_all(tasks)
232 .await
233 .into_iter()
234 .filter_map(|res| if let Ok(Ok(v)) = res { Some(v) } else { None })
235 .flatten()
236 .collect();
237 Ok(articles)
238 }
239
240 fn ids_to_fever_ids(ids: &[ArticleID]) -> Vec<u64> {
241 ids.iter().filter_map(|article_id| article_id.to_string().parse::<u64>().ok()).collect()
242 }
243}
244
245#[async_trait]
246impl FeedApi for Fever {
247 fn features(&self) -> FeedApiResult<PluginCapabilities> {
248 Ok(PluginCapabilities::SUPPORT_CATEGORIES)
249 }
250
251 fn has_user_configured(&self) -> FeedApiResult<bool> {
252 Ok(self.api.is_some())
253 }
254
255 async fn is_reachable(&self, client: &Client) -> FeedApiResult<bool> {
256 if let Some(url) = self.config.get_url() {
257 let res = client.head(&url).send().await?;
258 Ok(res.status().is_success())
259 } else {
260 Err(FeedApiError::Login)
261 }
262 }
263
264 async fn is_logged_in(&self, _client: &Client) -> FeedApiResult<bool> {
265 Ok(self.logged_in)
266 }
267
268 async fn user_name(&self) -> Option<String> {
269 self.config.get_user_name()
270 }
271
272 async fn get_login_data(&self) -> Option<LoginData> {
273 if self.has_user_configured().unwrap_or(false) {
274 let user = self.config.get_user_name();
275 let password = self.config.get_password();
276
277 if let (Some(user), Some(password)) = (user, password) {
278 return Some(LoginData::Direct(DirectLogin::Password(PasswordLogin {
279 id: FeverMetadata::get_id(),
280 url: self.config.get_url(),
281 user,
282 password,
283 basic_auth: None, })));
285 }
286 }
287
288 None
289 }
290
291 async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
292 if let LoginData::Direct(DirectLogin::Password(data)) = data
293 && let Some(url_string) = data.url.clone()
294 {
295 let url = Url::parse(&url_string)?;
296 let api = if let Some(basic_auth) = &data.basic_auth {
297 FeverApi::new_with_http_auth(&url, &data.user, &data.password, &basic_auth.user, basic_auth.password.as_deref())
298 } else {
299 FeverApi::new(&url, &data.user, &data.password)
300 };
301
302 self.config.set_url(&url_string);
303 self.config.set_password(&data.password);
304 self.config.set_user_name(&data.user);
305 self.config
306 .set_http_user_name(data.basic_auth.as_ref().map(|auth| auth.user.clone()).as_deref());
307 self.config.set_http_password(data.basic_auth.and_then(|auth| auth.password).as_deref());
308 self.config.write()?;
309 let valid = api.valid_credentials(client).await?;
310 if valid {
311 self.api = Some(Arc::new(api));
312 self.logged_in = true;
313 return Ok(());
314 }
315 }
316
317 self.logged_in = false;
318 self.api = None;
319 Err(FeedApiError::Login)
320 }
321
322 async fn logout(&mut self, _client: &Client) -> FeedApiResult<()> {
323 self.config.delete()?;
324 Ok(())
325 }
326
327 async fn initial_sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
328 if let Some(api) = &self.api {
329 let categories = api.get_groups(client);
330 let feeds = api.get_feeds(client);
331
332 let starred_ids = api.get_saved_items(client);
334
335 let unread_ids = api.get_unread_items(client);
337
338 let entries = api.get_items(client);
342
343 let (categories, feeds, starred_ids, unread_ids, entries) = futures::try_join!(categories, feeds, starred_ids, unread_ids, entries)?;
344
345 let (categories, category_mappings) = Self::convert_category_vec(categories.groups);
346 let (feeds, feed_mappings) = Self::convert_feed_vec(feeds.feeds, feeds.feeds_groups);
347 let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();
348
349 let mut articles: Vec<FatArticle> = Vec::new();
350
351 let mut starred = self.get_articles(api, starred_ids.saved_item_ids, client, &feeds).await?;
352 articles.append(&mut starred);
353
354 let mut unread = self.get_articles(api, unread_ids.unread_item_ids, client, &feeds).await?;
355 articles.append(&mut unread);
356
357 let mut read_articles = Self::convert_entry_vec(entries.items, &feed_ids, self.portal.clone());
358 articles.append(&mut read_articles);
359
360 return Ok(SyncResult {
361 feeds: util::vec_to_option(feeds),
362 categories: util::vec_to_option(categories),
363 feed_mappings: util::vec_to_option(feed_mappings),
364 category_mappings: util::vec_to_option(category_mappings),
365 tags: None,
366 taggings: None,
367 headlines: None,
368 articles: util::vec_to_option(articles),
369 enclosures: None,
370 });
371 }
372 Err(FeedApiError::Login)
373 }
374
375 async fn sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
376 if let Some(api) = &self.api {
377 let categories = api.get_groups(client);
378 let feeds = api.get_feeds(client);
379
380 let fever_unread_ids = api.get_unread_items(client);
381 let fever_marked_ids = api.get_saved_items(client);
382
383 let (categories, feeds, fever_unread_ids, fever_marked_ids) = futures::try_join!(categories, feeds, fever_unread_ids, fever_marked_ids)?;
384
385 let (categories, category_mappings) = Self::convert_category_vec(categories.groups);
386 let (feeds, feed_mappings) = Self::convert_feed_vec(feeds.feeds, feeds.feeds_groups);
387 let feed_ids: HashSet<FeedID> = feeds.iter().map(|f| f.feed_id.clone()).collect();
388
389 let mut articles: Vec<FatArticle> = Vec::new();
390 let mut headlines: Vec<Headline> = Vec::new();
391
392 let local_unread_ids = self.portal.get_article_ids_unread_all()?;
394 let local_unread_ids = Self::ids_to_fever_ids(&local_unread_ids);
395 let local_unread_ids = local_unread_ids.into_iter().collect();
396
397 let local_marked_ids = self.portal.get_article_ids_marked_all()?;
399 let local_marked_ids = Self::ids_to_fever_ids(&local_marked_ids);
400 let local_marked_ids = local_marked_ids.into_iter().collect();
401
402 let fever_unread_ids: HashSet<u64> = fever_unread_ids.unread_item_ids.into_iter().collect();
404
405 let fever_marked_ids: HashSet<u64> = fever_marked_ids.saved_item_ids.into_iter().collect();
407
408 let missing_unread_ids = fever_unread_ids.difference(&local_unread_ids).cloned().collect();
409 let missing_marked_ids = fever_marked_ids.difference(&local_marked_ids).cloned().collect();
410
411 let missing_unread_articles = self.get_articles(api, missing_unread_ids, client, &feeds);
413
414 let missing_marked_articles = self.get_articles(api, missing_marked_ids, client, &feeds);
416
417 let entries = api.get_items(client);
419
420 let (mut missing_unread_articles, mut missing_marked_articles, latest_entries) =
421 futures::try_join!(missing_unread_articles, missing_marked_articles, entries)?;
422
423 let mut latest_articles = Self::convert_entry_vec(latest_entries.items, &feed_ids, self.portal.clone());
424
425 articles.append(&mut missing_unread_articles);
426 articles.append(&mut missing_marked_articles);
427 articles.append(&mut latest_articles);
428
429 let mut should_mark_read_headlines = local_unread_ids
431 .difference(&fever_unread_ids)
432 .cloned()
433 .map(|id| Headline {
434 article_id: ArticleID::new(&id.to_string()),
435 unread: Read::Read,
436 marked: if fever_marked_ids.contains(&id) {
437 Marked::Marked
438 } else {
439 Marked::Unmarked
440 },
441 })
442 .collect();
443 headlines.append(&mut should_mark_read_headlines);
444
445 let mut mark_headlines = fever_marked_ids
447 .iter()
448 .map(|id| Headline {
449 article_id: ArticleID::new(&id.to_string()),
450 marked: Marked::Marked,
451 unread: if fever_unread_ids.contains(id) { Read::Unread } else { Read::Read },
452 })
453 .collect();
454 headlines.append(&mut mark_headlines);
455
456 let mut missing_unmarked_headlines = local_marked_ids
458 .difference(&fever_marked_ids)
459 .cloned()
460 .map(|id| Headline {
461 article_id: ArticleID::new(&id.to_string()),
462 marked: Marked::Unmarked,
463 unread: if fever_unread_ids.contains(&id) { Read::Unread } else { Read::Read },
464 })
465 .collect();
466 headlines.append(&mut missing_unmarked_headlines);
467
468 return Ok(SyncResult {
469 feeds: util::vec_to_option(feeds),
470 categories: util::vec_to_option(categories),
471 feed_mappings: util::vec_to_option(feed_mappings),
472 category_mappings: util::vec_to_option(category_mappings),
473 tags: None,
474 taggings: None,
475 headlines: util::vec_to_option(headlines),
476 articles: util::vec_to_option(articles),
477 enclosures: None,
478 });
479 }
480 Err(FeedApiError::Login)
481 }
482
483 async fn fetch_feed(&self, _feed_id: &FeedID, _client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FeedUpdateResult> {
484 Err(FeedApiError::Unsupported)
485 }
486
487 async fn set_article_read(&self, articles: &[ArticleID], read: Read, client: &Client) -> FeedApiResult<()> {
488 if let Some(api) = &self.api {
489 let entries = Self::ids_to_fever_ids(articles);
490 let status = match read {
491 models::Read::Read => ItemStatus::Read,
492 models::Read::Unread => ItemStatus::Unread,
493 };
494 for entry in entries {
495 api.mark_item(status, entry, client).await?;
496 }
497
498 return Ok(());
499 }
500 Err(FeedApiError::Login)
501 }
502
503 async fn set_article_marked(&self, articles: &[ArticleID], marked: Marked, client: &Client) -> FeedApiResult<()> {
504 if let Some(api) = &self.api {
505 for article in articles {
506 if let Ok(entry_id) = article.as_str().parse::<i64>() {
507 match marked {
508 models::Marked::Marked => api.mark_item(ItemStatus::Saved, entry_id.try_into().unwrap(), client).await?,
509 models::Marked::Unmarked => api.mark_item(ItemStatus::Unsaved, entry_id.try_into().unwrap(), client).await?,
510 }
511 };
512 }
513
514 return Ok(());
515 }
516 Err(FeedApiError::Login)
517 }
518
519 async fn set_feed_read(&self, feeds: &[FeedID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
520 if let Some(api) = &self.api {
521 let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp();
522
523 for feed in feeds {
524 let id = feed.to_string().parse::<i64>().unwrap();
525 api.mark_feed(ItemStatus::Read, id, last_sync, client).await?;
526 }
527 return Ok(());
528 }
529 Err(FeedApiError::Login)
530 }
531
532 async fn set_category_read(&self, categories: &[CategoryID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
533 if let Some(api) = &self.api {
534 let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp();
535
536 for category in categories {
537 let id = category.to_string().parse::<i64>().unwrap();
538 api.mark_group(ItemStatus::Read, id, last_sync, client).await?;
539 }
540 return Ok(());
541 }
542 Err(FeedApiError::Login)
543 }
544
545 async fn set_tag_read(&self, _tags: &[TagID], _articles: &[ArticleID], _client: &Client) -> FeedApiResult<()> {
546 Err(FeedApiError::Unsupported)
547 }
548
549 async fn set_all_read(&self, _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
550 if let Some(api) = &self.api {
551 let last_sync = self.portal.get_config().read().await.get_last_sync();
552 api.mark_group(ItemStatus::Read, 0, last_sync.timestamp(), client).await?;
553 return Ok(());
554 }
555 Err(FeedApiError::Login)
556 }
557
558 async fn add_feed(
559 &self,
560 _url: &Url,
561 _title: Option<String>,
562 _category_id: Option<CategoryID>,
563 _client: &Client,
564 ) -> FeedApiResult<(Feed, Option<Category>)> {
565 Err(FeedApiError::Unsupported)
566 }
567
568 async fn remove_feed(&self, _id: &FeedID, _client: &Client) -> FeedApiResult<()> {
569 Err(FeedApiError::Unsupported)
570 }
571
572 async fn move_feed(&self, _feed_id: &FeedID, _from: &CategoryID, _to: &CategoryID, _client: &Client) -> FeedApiResult<()> {
573 Err(FeedApiError::Unsupported)
574 }
575
576 async fn rename_feed(&self, _feed_id: &FeedID, _new_title: &str, _client: &Client) -> FeedApiResult<FeedID> {
577 Err(FeedApiError::Unsupported)
578 }
579
580 async fn edit_feed_url(&self, _feed_id: &FeedID, _new_url: &str, _client: &Client) -> FeedApiResult<()> {
581 Err(FeedApiError::Unsupported)
582 }
583
584 async fn add_category<'a>(&self, _title: &str, _parent: Option<&'a CategoryID>, _client: &Client) -> FeedApiResult<CategoryID> {
585 Err(FeedApiError::Unsupported)
586 }
587
588 async fn remove_category(&self, _id: &CategoryID, _remove_children: bool, _client: &Client) -> FeedApiResult<()> {
589 Err(FeedApiError::Unsupported)
590 }
591
592 async fn rename_category(&self, _id: &CategoryID, _new_title: &str, _client: &Client) -> FeedApiResult<CategoryID> {
593 Err(FeedApiError::Unsupported)
594 }
595
596 async fn move_category(&self, _id: &CategoryID, _parent: &CategoryID, _client: &Client) -> FeedApiResult<()> {
597 Err(FeedApiError::Unsupported)
598 }
599
600 async fn import_opml(&self, _opml: &str, _client: &Client) -> FeedApiResult<()> {
601 Err(FeedApiError::Unsupported)
602 }
603
604 async fn add_tag(&self, _title: &str, _client: &Client) -> FeedApiResult<TagID> {
605 Err(FeedApiError::Unsupported)
606 }
607
608 async fn remove_tag(&self, _id: &TagID, _client: &Client) -> FeedApiResult<()> {
609 Err(FeedApiError::Unsupported)
610 }
611
612 async fn rename_tag(&self, _id: &TagID, _new_title: &str, _client: &Client) -> FeedApiResult<TagID> {
613 Err(FeedApiError::Unsupported)
614 }
615
616 async fn tag_article(&self, _article_id: &ArticleID, _tag_id: &TagID, _client: &Client) -> FeedApiResult<()> {
617 Err(FeedApiError::Unsupported)
618 }
619
620 async fn untag_article(&self, _article_id: &ArticleID, _tag_id: &TagID, _client: &Client) -> FeedApiResult<()> {
621 Err(FeedApiError::Unsupported)
622 }
623
624 async fn get_favicon(&self, feed_id: &FeedID, client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FavIcon> {
625 if let Some(api) = &self.api {
626 let fever_feed_id = feed_id.to_string().parse::<u64>().map_err(|e| FeedApiError::Api {
627 message: format!("Failed to parse feed id {e}"),
628 })?;
629
630 let feeds = api.get_feeds(client).await?;
631
632 let mut favicon_id = 0;
633
634 for feed in feeds.feeds {
635 if feed.id == fever_feed_id {
636 favicon_id = feed.favicon_id;
637 }
638 }
639
640 let favicon_set = api.get_favicons(client).await?.favicons;
641
642 if let Some(favicon) = favicon_set.get(&favicon_id)
643 && let Some(start) = favicon.data.find(',')
644 {
645 let data = base64_std.decode(&favicon.data[start + 1..]).map_err(|_| FeedApiError::Encryption)?;
646
647 let favicon = FavIcon {
648 feed_id: feed_id.clone(),
649 expires: Utc::now() + Duration::try_days(EXPIRES_AFTER_DAYS).unwrap(),
650 format: Some(favicon.mime_type.clone()),
651 etag: None,
652 source_url: None,
653 data: Some(data),
654 };
655
656 return Ok(favicon);
657 }
658 }
659
660 Err(FeedApiError::Login)
661 }
662}