1mod config;
2pub mod metadata;
3mod oauth;
4
5use std::collections::HashSet;
6use std::sync::Arc;
7
8use self::config::AccountConfig;
9use self::metadata::InoreaderMetadata;
10use self::oauth::InoreaderOAuth;
11use crate::feed_api::{FeedApi, FeedApiError, FeedApiResult, FeedHeaderMap, Portal};
12use crate::models::{
13 self, ArticleID, Category, CategoryID, FavIcon, Feed, FeedID, FeedUpdateResult, Headline, LoginData, Marked, OAuthData, PluginCapabilities, Read,
14 StreamConversionResult, SyncResult, TagID, Url,
15};
16use crate::util::greader::{ArticleQuery, GReaderUtil, TAG_READ_STR, TAG_READING_LIST, TAG_STARRED_STR};
17use crate::{ParsedUrl, feed_parser, util};
18use async_trait::async_trait;
19use chrono::{DateTime, Utc};
20use greader_api::models::{AuthInput, InoreaderAuthInput, ItemId, StreamType};
21use greader_api::{ApiError, AuthData, GReaderApi};
22use reqwest::Client;
23use reqwest::header::{HeaderMap, HeaderValue};
24use tokio::sync::RwLock;
25
26pub struct Inoreader {
27 api: Option<GReaderApi>,
28 portal: Arc<Box<dyn Portal>>,
29 logged_in: bool,
30 config: Arc<RwLock<AccountConfig>>,
31}
32
33impl Inoreader {
34 async fn login_inoreader(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
35 let LoginData::OAuth(data) = data else { return Err(FeedApiError::Login) };
36
37 let oauth = InoreaderOAuth::new();
38
39 let url = Url::parse(&data.url)?;
40 let auth_code = oauth.parse_redirected_url(&url)?;
41
42 self.config.write().await.set_custom_api_secret(data.custom_api_secret.as_ref());
43
44 let (client_id, client_secret) = if let Some(user_api_secret) = data.custom_api_secret {
45 (user_api_secret.client_id, user_api_secret.client_secret)
47 } else {
48 (oauth.client_id, oauth.client_secret)
50 };
51
52 let api = GReaderApi::new(&oauth.base_uri, AuthData::Uninitialized);
53 let auth_data = api
54 .login(
55 &AuthInput::Inoreader(InoreaderAuthInput {
56 auth_code,
57 redirect_url: oauth.redirect_uri,
58 client_id,
59 client_secret,
60 }),
61 client,
62 )
63 .await?;
64
65 let auth_data = match auth_data {
66 AuthData::Inoreader(auth_data) => auth_data,
67 _ => return Err(FeedApiError::Login),
68 };
69
70 self.config.write().await.set_access_token(&auth_data.access_token);
71 self.config.write().await.set_refresh_token(&auth_data.refresh_token);
72 self.config.write().await.set_token_expires(&auth_data.expires_at.timestamp().to_string());
73
74 let user = api.user_info(client).await?;
75 self.config.write().await.set_user_name(&user.user_name);
76 self.config.write().await.set_user_id(&user.user_id);
77 self.config.write().await.write()?;
78 self.api = Some(api);
79
80 Ok(())
81 }
82
83 async fn is_token_expired(&self) -> Result<bool, ApiError> {
84 let timestamp = self.config.write().await.get_token_expires().ok_or(ApiError::TokenExpired)?;
85 let timestamp = timestamp.parse::<i64>().map_err(|_| ApiError::TokenExpired)?;
86
87 let expires_at: DateTime<Utc> = util::timestamp_to_datetime(timestamp);
88 let expires_in = expires_at.signed_duration_since(Utc::now());
89 Ok(expires_in.num_seconds() <= 60)
90 }
91
92 async fn refresh_token(&self, api: &GReaderApi, client: &Client) -> FeedApiResult<()> {
93 let response = api.inoreader_refresh_token(client).await?;
94 let token_expires = response.expires_at;
95 self.config.write().await.set_access_token(&response.access_token);
96 self.config.write().await.set_token_expires(&token_expires.timestamp().to_string());
97 self.config.write().await.write()?;
98 Ok(())
99 }
100
101 async fn check_and_update_token(&self, api: &GReaderApi, client: &Client) -> FeedApiResult<()> {
102 if self.is_token_expired().await? {
103 self.refresh_token(api, client).await?;
104 }
105
106 Ok(())
107 }
108}
109
110#[async_trait]
111impl FeedApi for Inoreader {
112 fn features(&self) -> FeedApiResult<PluginCapabilities> {
113 Ok(PluginCapabilities::ADD_REMOVE_FEEDS
114 | PluginCapabilities::SUPPORT_CATEGORIES
115 | PluginCapabilities::MODIFY_CATEGORIES
116 | PluginCapabilities::SUPPORT_TAGS)
117 }
118
119 fn has_user_configured(&self) -> FeedApiResult<bool> {
120 Ok(self.api.is_some())
121 }
122
123 async fn is_reachable(&self, client: &Client) -> FeedApiResult<bool> {
124 let res = client.head("https://www.inoreader.com/").send().await?;
125 Ok(res.status().is_success())
126 }
127
128 async fn is_logged_in(&self, _client: &Client) -> FeedApiResult<bool> {
129 Ok(self.logged_in)
130 }
131
132 async fn user_name(&self) -> Option<String> {
133 self.config.read().await.get_user_name()
134 }
135
136 async fn get_login_data(&self) -> Option<LoginData> {
137 if let Ok(true) = self.has_user_configured() {
138 return Some(LoginData::OAuth(OAuthData {
139 id: InoreaderMetadata::get_id(),
140 url: String::new(),
141 custom_api_secret: self.config.read().await.get_custom_api_secret(),
142 }));
143 }
144
145 None
146 }
147
148 async fn login(&mut self, data: LoginData, client: &Client) -> FeedApiResult<()> {
149 if let Err(error) = self.login_inoreader(data, client).await {
150 tracing::error!(%error, "Failed to log in");
151 self.api = None;
152 self.logged_in = false;
153 Err(error)
154 } else {
155 self.logged_in = true;
156 Ok(())
157 }
158 }
159
160 async fn logout(&mut self, _client: &Client) -> FeedApiResult<()> {
161 self.config.read().await.delete()?;
162 Ok(())
163 }
164
165 async fn initial_sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
166 if let Some(api) = &self.api {
167 self.check_and_update_token(api, client).await?;
168
169 let stream_preferences = api.preference_stream_list(client);
170 let feeds = api.subscription_list(client);
171 let tags = api.tag_list(client);
172
173 let (stream_preferences, feeds, tags) = futures::try_join!(stream_preferences, feeds, tags)?;
174
175 let user_id = self.config.read().await.get_user_id();
176 let (categories, category_mappings) =
177 GReaderUtil::convert_category_vec(feeds.subscriptions.clone(), Some(&tags), Some(&stream_preferences), user_id.as_deref());
178 let tags = GReaderUtil::convert_tag_list(tags, &categories);
179 let tag_ids: HashSet<TagID> = tags.iter().map(|f| f.tag_id.clone()).collect();
180 let (feeds, feed_mappings) = GReaderUtil::convert_feed_vec(feeds.subscriptions, Some(&stream_preferences));
181
182 let unread = GReaderUtil::get_articles(
183 api,
184 client,
185 self.portal.clone(),
186 ArticleQuery {
187 stream_id: Some(TAG_READING_LIST),
188 read: Some(Read::Unread),
189 marked: None,
190 tag_ids: &tag_ids,
191 limit: None,
192 last_sync: None,
193 },
194 );
195
196 let starred = GReaderUtil::get_articles(
197 api,
198 client,
199 self.portal.clone(),
200 ArticleQuery {
201 stream_id: Some(TAG_STARRED_STR),
202 read: None,
203 marked: None,
204 tag_ids: &tag_ids,
205 limit: None,
206 last_sync: None,
207 },
208 );
209
210 let latest = GReaderUtil::get_articles(
211 api,
212 client,
213 self.portal.clone(),
214 ArticleQuery {
215 stream_id: None,
216 read: Some(Read::Read),
217 marked: Some(Marked::Unmarked),
218 tag_ids: &tag_ids,
219 limit: Some(100),
220 last_sync: None,
221 },
222 );
223
224 let (unread, starred, latest) = futures::try_join!(unread, starred, latest)?;
225
226 let mut result = StreamConversionResult::new();
227 result.add(unread);
228 result.add(starred);
229 result.add(latest);
230
231 return Ok(SyncResult {
232 feeds: crate::util::vec_to_option(feeds),
233 categories: crate::util::vec_to_option(categories),
234 feed_mappings: crate::util::vec_to_option(feed_mappings),
235 category_mappings: crate::util::vec_to_option(category_mappings),
236 tags: crate::util::vec_to_option(tags),
237 taggings: crate::util::vec_to_option(result.taggings),
238 headlines: None,
239 articles: crate::util::vec_to_option(result.articles),
240 enclosures: crate::util::vec_to_option(result.enclosures),
241 });
242 }
243 Err(FeedApiError::Login)
244 }
245
246 async fn sync(&self, client: &Client, _custom_header: FeedHeaderMap) -> FeedApiResult<SyncResult> {
247 if let Some(api) = &self.api {
248 self.check_and_update_token(api, client).await?;
249
250 let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp();
251
252 let stream_preferences = api.preference_stream_list(client);
253 let feeds = api.subscription_list(client);
254 let tags = api.tag_list(client);
255
256 let (stream_preferences, feeds, tags) = futures::try_join!(stream_preferences, feeds, tags)?;
257
258 let user_id = self.config.read().await.get_user_id();
259 let (categories, category_mappings) =
260 GReaderUtil::convert_category_vec(feeds.subscriptions.clone(), Some(&tags), Some(&stream_preferences), user_id.as_deref());
261 let tags = GReaderUtil::convert_tag_list(tags, &categories);
262 let tag_ids: HashSet<TagID> = tags.iter().map(|f| f.tag_id.clone()).collect();
263 let (feeds, feed_mappings) = GReaderUtil::convert_feed_vec(feeds.subscriptions, Some(&stream_preferences));
264
265 let mut result = StreamConversionResult::new();
266
267 let new_unread = GReaderUtil::get_articles(
269 api,
270 client,
271 self.portal.clone(),
272 ArticleQuery {
273 stream_id: Some(TAG_READING_LIST),
274 read: Some(Read::Unread),
275 marked: None,
276 tag_ids: &tag_ids,
277 limit: None,
278 last_sync: Some(last_sync),
279 },
280 );
281
282 let new_marked = GReaderUtil::get_articles(
284 api,
285 client,
286 self.portal.clone(),
287 ArticleQuery {
288 stream_id: Some(TAG_STARRED_STR),
289 read: None,
290 marked: None,
291 tag_ids: &tag_ids,
292 limit: None,
293 last_sync: Some(last_sync),
294 },
295 );
296
297 let inoreader_unread_ids = GReaderUtil::get_article_ids(api, client, Some(TAG_READING_LIST), Some(Read::Unread), None, None);
299
300 let inoreader_marked_ids = GReaderUtil::get_article_ids(api, client, Some(TAG_STARRED_STR), None, None, None);
302
303 let (new_unread, new_marked, inoreader_unread_ids, inoreader_marked_ids) =
304 futures::try_join!(new_unread, new_marked, inoreader_unread_ids, inoreader_marked_ids)?;
305
306 result.add(new_unread);
307 result.add(new_marked);
308
309 let inoreader_unread_ids: HashSet<ArticleID> = inoreader_unread_ids
310 .into_iter()
311 .map(|item_id| {
312 let ItemId { id } = item_id;
313 ArticleID::from_owned(id)
314 })
315 .collect();
316
317 let inoreader_marked_ids: HashSet<ArticleID> = inoreader_marked_ids
318 .into_iter()
319 .map(|item_id| {
320 let ItemId { id } = item_id;
321 ArticleID::from_owned(id)
322 })
323 .collect();
324
325 let local_unread_ids = self.portal.get_article_ids_unread_all()?;
327 let local_unread_ids = local_unread_ids.into_iter().collect::<HashSet<_>>();
328
329 let mut should_mark_read_headlines = local_unread_ids
331 .difference(&inoreader_unread_ids)
332 .cloned()
333 .map(|id| {
334 let marked = if inoreader_marked_ids.contains(&id) {
335 Marked::Marked
336 } else {
337 Marked::Unmarked
338 };
339
340 Headline {
341 article_id: id,
342 unread: Read::Read,
343 marked,
344 }
345 })
346 .collect();
347 result.headlines.append(&mut should_mark_read_headlines);
348
349 let local_marked_ids = self.portal.get_article_ids_marked_all()?;
351 let local_marked_ids = local_marked_ids.into_iter().collect::<HashSet<_>>();
352
353 let mut mark_headlines = local_marked_ids
355 .difference(&inoreader_marked_ids)
356 .map(|id| Headline {
357 article_id: id.clone(),
358 marked: Marked::Marked,
359 unread: if inoreader_unread_ids.contains(id) { Read::Unread } else { Read::Read },
360 })
361 .collect();
362 result.headlines.append(&mut mark_headlines);
363
364 let mut missing_unmarked_headlines = local_marked_ids
366 .difference(&inoreader_marked_ids)
367 .cloned()
368 .map(|id| {
369 let unread = if inoreader_unread_ids.contains(&id) { Read::Unread } else { Read::Read };
370
371 Headline {
372 article_id: id,
373 marked: Marked::Unmarked,
374 unread,
375 }
376 })
377 .collect();
378 result.headlines.append(&mut missing_unmarked_headlines);
379
380 return Ok(SyncResult {
381 feeds: crate::util::vec_to_option(feeds),
382 categories: crate::util::vec_to_option(categories),
383 feed_mappings: crate::util::vec_to_option(feed_mappings),
384 category_mappings: crate::util::vec_to_option(category_mappings),
385 tags: crate::util::vec_to_option(tags),
386 taggings: crate::util::vec_to_option(result.taggings),
387 headlines: crate::util::vec_to_option(result.headlines),
388 articles: crate::util::vec_to_option(result.articles),
389 enclosures: crate::util::vec_to_option(result.enclosures),
390 });
391 }
392 Err(FeedApiError::Login)
393 }
394
395 async fn fetch_feed(&self, feed_id: &FeedID, client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FeedUpdateResult> {
396 if let Some(api) = &self.api {
397 let tags = api.tag_list(client);
398 let feeds = api.subscription_list(client);
399
400 let (feeds, tags) = futures::try_join!(feeds, tags)?;
401
402 let (categories, _category_mappings) = GReaderUtil::convert_category_vec(feeds.subscriptions.clone(), Some(&tags), None, None);
403 let tags = GReaderUtil::convert_tag_list(tags, &categories);
404 let tag_ids: HashSet<TagID> = tags.iter().map(|f| f.tag_id.clone()).collect();
405 let (feeds, _feed_mappings) = GReaderUtil::convert_feed_vec(feeds.subscriptions, None);
406 let feed = feeds.iter().find(|feed| &feed.feed_id == feed_id).cloned();
407
408 let result = GReaderUtil::get_articles(
409 api,
410 client,
411 self.portal.clone(),
412 ArticleQuery {
413 stream_id: Some(feed_id.as_str()),
414 read: None,
415 marked: None,
416 tag_ids: &tag_ids,
417 limit: Some(20),
418 last_sync: None,
419 },
420 )
421 .await?;
422
423 return Ok(FeedUpdateResult {
424 feed,
425 taggings: crate::util::vec_to_option(result.taggings),
426 articles: crate::util::vec_to_option(result.articles),
427 enclosures: crate::util::vec_to_option(result.enclosures),
428 });
429 } else {
430 Err(FeedApiError::Login)
431 }
432 }
433
434 async fn set_article_read(&self, articles: &[ArticleID], read: models::Read, client: &Client) -> FeedApiResult<()> {
435 if let Some(api) = &self.api {
436 self.check_and_update_token(api, client).await?;
437
438 let item_ids: Vec<_> = articles.iter().map(|id| id.as_str()).collect();
439 let (add_tag, remove_tag) = match read {
440 Read::Read => (Some(TAG_READ_STR), None),
441 Read::Unread => (None, Some(TAG_READ_STR)),
442 };
443 api.tag_edit(&item_ids, add_tag, remove_tag, client).await?;
444 Ok(())
445 } else {
446 Err(FeedApiError::Login)
447 }
448 }
449
450 async fn set_article_marked(&self, articles: &[ArticleID], marked: models::Marked, client: &Client) -> FeedApiResult<()> {
451 if let Some(api) = &self.api {
452 self.check_and_update_token(api, client).await?;
453
454 let item_ids: Vec<_> = articles.iter().map(|id| id.as_str()).collect();
455 let (add_tag, remove_tag) = match marked {
456 Marked::Marked => (Some(TAG_STARRED_STR), None),
457 Marked::Unmarked => (None, Some(TAG_STARRED_STR)),
458 };
459 api.tag_edit(&item_ids, add_tag, remove_tag, client).await?;
460 Ok(())
461 } else {
462 Err(FeedApiError::Login)
463 }
464 }
465
466 async fn set_feed_read(&self, feeds: &[FeedID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
467 if let Some(api) = &self.api {
468 self.check_and_update_token(api, client).await?;
469
470 let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp_micros() as u64;
471 let mut mark_read_futures = Vec::new();
472
473 for feed in feeds {
474 mark_read_futures.push(api.mark_all_as_read(feed.as_str(), Some(last_sync), client));
475 }
476
477 futures::future::try_join_all(mark_read_futures).await?;
478 Ok(())
479 } else {
480 Err(FeedApiError::Login)
481 }
482 }
483
484 async fn set_category_read(&self, categories: &[CategoryID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
485 if let Some(api) = &self.api {
486 self.check_and_update_token(api, client).await?;
487
488 let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp_micros() as u64;
489 let mut mark_read_futures = Vec::new();
490
491 for category in categories {
492 mark_read_futures.push(api.mark_all_as_read(category.as_str(), Some(last_sync), client));
493 }
494
495 futures::future::try_join_all(mark_read_futures).await?;
496 Ok(())
497 } else {
498 Err(FeedApiError::Login)
499 }
500 }
501
502 async fn set_tag_read(&self, tags: &[TagID], _articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
503 if let Some(api) = &self.api {
504 self.check_and_update_token(api, client).await?;
505
506 let last_sync = self.portal.get_config().read().await.get_last_sync().timestamp_micros() as u64;
507 let mut mark_read_futures = Vec::new();
508
509 for tag in tags {
510 mark_read_futures.push(api.mark_all_as_read(tag.as_str(), Some(last_sync), client));
511 }
512
513 futures::future::try_join_all(mark_read_futures).await?;
514 Ok(())
515 } else {
516 Err(FeedApiError::Login)
517 }
518 }
519
520 async fn set_all_read(&self, articles: &[ArticleID], client: &Client) -> FeedApiResult<()> {
521 self.set_article_read(articles, Read::Read, client).await
522 }
523
524 async fn add_feed(
525 &self,
526 url: &Url,
527 title: Option<String>,
528 category_id: Option<CategoryID>,
529 client: &Client,
530 ) -> FeedApiResult<(Feed, Option<Category>)> {
531 if let Some(api) = &self.api {
532 self.check_and_update_token(api, client).await?;
533
534 let feed_id = format!("feed/{}", &url.as_str());
535 let category_str = category_id.clone().map(|id| id.as_str().to_owned());
536
537 api.subscription_create(url, title.as_deref(), category_str.as_deref(), client).await?;
538
539 let feed_id = FeedID::new(&feed_id);
540 let semaphore = self.portal.get_download_semaphore();
541 let result = feed_parser::download_and_parse_feed(url, &feed_id, title, semaphore, client).await;
542
543 if result.is_err() {
544 tracing::warn!("parsing went wrong -> remove feed from freshrss account");
545 self.remove_feed(&feed_id, client).await?;
546 }
547
548 let feed = match result? {
549 ParsedUrl::SingleFeed(feed) => feed,
550 _ => {
551 let msg = "Expected Single Feed";
552 tracing::warn!("{msg}");
553 return Err(FeedApiError::Api { message: msg.into() });
554 }
555 };
556
557 let local_categories = self.portal.get_categories()?;
559 let category = if !local_categories.iter().any(|c| Some(&c.category_id) == category_id.as_ref()) {
560 let feeds = api.subscription_list(client).await?;
561 let user_id = self.config.read().await.get_user_id();
562 let (categories, _category_mappings) = GReaderUtil::convert_category_vec(feeds.subscriptions, None, None, user_id.as_deref());
563 categories.iter().find(|c| Some(&c.category_id) == category_id.as_ref()).cloned()
564 } else {
565 None
566 };
567
568 return Ok((*feed, category));
569 }
570 Err(FeedApiError::Login)
571 }
572
573 async fn remove_feed(&self, id: &FeedID, client: &Client) -> FeedApiResult<()> {
574 if let Some(api) = &self.api {
575 self.check_and_update_token(api, client).await?;
576
577 let feed_id_str = id.as_str();
578 api.subscription_delete(feed_id_str, client).await?;
579 Ok(())
580 } else {
581 Err(FeedApiError::Login)
582 }
583 }
584
585 async fn move_feed(&self, feed_id: &FeedID, from: &CategoryID, to: &CategoryID, client: &Client) -> FeedApiResult<()> {
586 if let Some(api) = &self.api {
587 self.check_and_update_token(api, client).await?;
588
589 api.subscription_edit(feed_id.as_str(), None, Some(from.as_str()), Some(to.as_str()), client)
590 .await?;
591 Ok(())
592 } else {
593 Err(FeedApiError::Login)
594 }
595 }
596
597 async fn rename_feed(&self, feed_id: &FeedID, new_title: &str, client: &Client) -> FeedApiResult<FeedID> {
598 if let Some(api) = &self.api {
599 self.check_and_update_token(api, client).await?;
600
601 api.subscription_edit(feed_id.as_str(), Some(new_title), None, None, client).await?;
602 Ok(feed_id.clone())
603 } else {
604 Err(FeedApiError::Login)
605 }
606 }
607
608 async fn edit_feed_url(&self, _feed_id: &FeedID, _new_url: &str, _client: &Client) -> FeedApiResult<()> {
609 Err(FeedApiError::Unsupported)
610 }
611
612 async fn add_category<'a>(&self, title: &str, parent: Option<&'a CategoryID>, _client: &Client) -> FeedApiResult<CategoryID> {
613 if let Some(_api) = &self.api {
614 if parent.is_some() {
615 return Err(FeedApiError::Unsupported);
616 }
617
618 let user_id = self.config.read().await.get_user_id();
621 let category_id = GReaderUtil::generate_tag_id(user_id.as_deref(), title);
622 Ok(CategoryID::new(&category_id))
623 } else {
624 Err(FeedApiError::Login)
625 }
626 }
627
628 async fn remove_category(&self, id: &CategoryID, remove_children: bool, client: &Client) -> FeedApiResult<()> {
629 if let Some(api) = &self.api {
630 self.check_and_update_token(api, client).await?;
631
632 if remove_children {
633 let mappings = self.portal.get_feed_mappings()?;
634
635 let feed_ids = mappings
636 .iter()
637 .filter(|m| &m.category_id == id)
638 .map(|m| m.feed_id.clone())
639 .collect::<Vec<FeedID>>();
640
641 for feed_id in feed_ids {
642 self.remove_feed(&feed_id, client).await?;
643 }
644 }
645
646 api.tag_delete(StreamType::Category, id.as_str(), client).await?;
647 Ok(())
648 } else {
649 Err(FeedApiError::Login)
650 }
651 }
652
653 async fn rename_category(&self, id: &CategoryID, new_title: &str, client: &Client) -> FeedApiResult<CategoryID> {
654 if let Some(api) = &self.api {
655 self.check_and_update_token(api, client).await?;
656
657 api.tag_rename(StreamType::Category, id.as_str(), new_title, client).await?;
658
659 let user_id = self.config.read().await.get_user_id();
660 Ok(CategoryID::new(&GReaderUtil::generate_tag_id(user_id.as_deref(), new_title)))
661 } else {
662 Err(FeedApiError::Login)
663 }
664 }
665
666 async fn move_category(&self, _id: &CategoryID, _parent: &CategoryID, _client: &Client) -> FeedApiResult<()> {
667 Err(FeedApiError::Unsupported)
668 }
669
670 async fn import_opml(&self, opml: &str, client: &Client) -> FeedApiResult<()> {
671 if let Some(api) = &self.api {
672 self.check_and_update_token(api, client).await?;
673
674 api.import(opml.to_owned(), client).await?;
675 Ok(())
676 } else {
677 Err(FeedApiError::Login)
678 }
679 }
680
681 async fn add_tag(&self, title: &str, _client: &Client) -> FeedApiResult<TagID> {
682 if let Some(_api) = &self.api {
683 let user_id = self.config.read().await.get_user_id();
686 let tag_id = GReaderUtil::generate_tag_id(user_id.as_deref(), title);
687 Ok(TagID::new(&tag_id))
688 } else {
689 Err(FeedApiError::Login)
690 }
691 }
692
693 async fn remove_tag(&self, id: &TagID, client: &Client) -> FeedApiResult<()> {
694 if let Some(api) = &self.api {
695 self.check_and_update_token(api, client).await?;
696 api.tag_delete(StreamType::Stream, id.as_str(), client).await?;
697 Ok(())
698 } else {
699 Err(FeedApiError::Login)
700 }
701 }
702
703 async fn rename_tag(&self, id: &TagID, new_title: &str, client: &Client) -> FeedApiResult<TagID> {
704 if let Some(api) = &self.api {
705 self.check_and_update_token(api, client).await?;
706 api.tag_rename(StreamType::Stream, id.as_str(), new_title, client).await?;
707
708 let user_id = self.config.read().await.get_user_id();
709 Ok(TagID::new(&GReaderUtil::generate_tag_id(user_id.as_deref(), new_title)))
710 } else {
711 Err(FeedApiError::Login)
712 }
713 }
714
715 async fn tag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()> {
716 if let Some(api) = &self.api {
717 self.check_and_update_token(api, client).await?;
718 api.tag_edit(&[article_id.as_str()], Some(tag_id.as_str()), None, client).await?;
719 Ok(())
720 } else {
721 Err(FeedApiError::Login)
722 }
723 }
724
725 async fn untag_article(&self, article_id: &ArticleID, tag_id: &TagID, client: &Client) -> FeedApiResult<()> {
726 if let Some(api) = &self.api {
727 self.check_and_update_token(api, client).await?;
728 api.tag_edit(&[article_id.as_str()], None, Some(tag_id.as_str()), client).await?;
729 Ok(())
730 } else {
731 Err(FeedApiError::Login)
732 }
733 }
734
735 async fn get_favicon(&self, _feed_id: &FeedID, _client: &Client, _custom_header: HeaderMap<HeaderValue>) -> FeedApiResult<FavIcon> {
736 Err(FeedApiError::Unsupported)
737 }
738}