greader_api/
lib.rs

1/*
2usefull docs:
3- https://github.com/FreshRSS/FreshRSS/blob/master/p/api/greader.php
4- https://github.com/theoldreader/api
5- https://feedhq.readthedocs.io/en/latest/api/index.html
6- https://www.inoreader.com/developers/
7*/
8
9mod deserialize;
10pub mod error;
11pub mod models;
12#[cfg(test)]
13mod tests;
14
15pub use crate::error::ApiError;
16use crate::models::GReaderError;
17pub use crate::models::{AuthData, GoogleAuth, InoreaderAuth};
18use crate::models::{Feeds, ItemRefs, QuickFeed, Stream, StreamType, Taggings, Unread, User};
19use std::collections::HashMap;
20
21#[cfg(any(feature = "feedhq", feature = "oldreader", feature = "inoreader"))]
22use crate::models::StreamPrefs;
23
24use chrono::{TimeDelta, Utc};
25use log::error;
26use models::{AuthInput, OAuthResponse, PostToken};
27use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE};
28use reqwest::{Client, StatusCode};
29use serde::Deserialize;
30use std::sync::Arc;
31use tokio::sync::Mutex;
32use url::Url;
33
34#[derive(Clone, Debug)]
35pub struct GReaderApi {
36    base_uri: Url,
37    auth_input: Arc<Mutex<AuthInput>>,
38    auth: Arc<Mutex<AuthData>>,
39}
40
41impl GReaderApi {
42    /// Create a new instance of the GReaderApi
43    pub fn new(url: &Url, auth: AuthData) -> Self {
44        GReaderApi {
45            base_uri: url.clone(),
46            auth_input: Arc::new(Mutex::new(AuthInput::Uninitialized)),
47            auth: Arc::new(Mutex::new(auth)),
48        }
49    }
50
51    pub async fn get_auth_data(&self) -> Result<AuthData, ApiError> {
52        Ok(self.auth.lock().await.clone())
53    }
54
55    pub async fn set_aut_data(&self, auth: AuthData) -> Result<(), ApiError> {
56        *(self.auth.lock().await) = auth;
57        Ok(())
58    }
59
60    async fn get_auth_headers(&self) -> Result<HeaderMap, ApiError> {
61        let mut headers = HeaderMap::new();
62
63        let auth_data = self.auth.lock().await.clone();
64
65        match &auth_data {
66            AuthData::Uninitialized => return Err(ApiError::Other("api is uninitialized".into())),
67            AuthData::Google(auth_data) => {
68                if auth_data.auth_token.is_none() {
69                    return Err(ApiError::NotLoggedIn)?;
70                }
71
72                if let Some(auth_token) = auth_data.auth_token.as_deref() {
73                    headers.insert(
74                        AUTHORIZATION,
75                        HeaderValue::from_str(&format!("GoogleLogin auth={auth_token}"))
76                            .expect("parse header value"),
77                    );
78                }
79            }
80            AuthData::Inoreader(auth_data) => {
81                // check if access_token is still valid
82                let expires_in = auth_data.expires_at.signed_duration_since(Utc::now());
83                let expired = expires_in.num_seconds() <= 60;
84
85                if expired {
86                    return Err(ApiError::TokenExpired)?;
87                }
88
89                headers.insert(
90                    AUTHORIZATION,
91                    HeaderValue::from_str(&format!("Bearer {}", auth_data.access_token))
92                        .expect("parse header value"),
93                );
94                headers.insert(
95                    "AppId",
96                    HeaderValue::from_str(&auth_data.client_id).expect("parse header value"),
97                );
98                headers.insert(
99                    "AppKey",
100                    HeaderValue::from_str(&auth_data.client_secret).expect("parse header value"),
101                );
102            }
103        };
104
105        Ok(headers)
106    }
107
108    fn deserialize<T: for<'a> Deserialize<'a>>(json: &str) -> Result<T, ApiError> {
109        let result: T = serde_json::from_str(json).map_err(|source| ApiError::Json {
110            source,
111            json: json.into(),
112        })?;
113        Ok(result)
114    }
115
116    async fn get_post_token(&self, client: &Client) -> Result<Option<String>, ApiError> {
117        let mut auth_data = self.auth.lock().await.clone();
118
119        let post_token = match &mut auth_data {
120            AuthData::Inoreader(_) | AuthData::Uninitialized => Ok(None),
121            AuthData::Google(auth_data) => {
122                if let Some(post_token) = auth_data.post_token.as_mut() {
123                    if !post_token.is_valid() {
124                        let mut response = self
125                            .get_request("reader/api/0/token", vec![], client)
126                            .await?;
127                        let _ = response.pop();
128                        post_token.update(&response);
129                        Ok(Some(response))
130                    } else {
131                        Ok(Some(post_token.token.clone()))
132                    }
133                } else {
134                    let mut response = self
135                        .get_request("reader/api/0/token", vec![], client)
136                        .await?;
137                    let _ = response.pop();
138                    let post_token = PostToken::new(&response);
139                    auth_data.post_token.replace(post_token);
140                    Ok(Some(response))
141                }
142            }
143        };
144
145        *self.auth.lock().await = auth_data;
146        post_token
147    }
148
149    async fn get_request(
150        &self,
151        query: &str,
152        mut params: Vec<(&str, String)>,
153        client: &Client,
154    ) -> Result<String, ApiError> {
155        let api_url: Url = self.base_uri.join(query)?;
156
157        let mut query_params = Vec::new();
158        query_params.append(&mut params);
159        query_params.push(("output", "json".into()));
160
161        let auth_headers = self.get_auth_headers().await?;
162
163        let response = client
164            .get(api_url.clone())
165            .headers(auth_headers)
166            .query(&query_params)
167            .send()
168            .await?;
169
170        let status = response.status();
171        let response = response.text().await?;
172        if status != StatusCode::OK {
173            if status == StatusCode::UNAUTHORIZED {
174                return Err(ApiError::AccessDenied);
175            } else if status == StatusCode::TOO_MANY_REQUESTS {
176                return Err(ApiError::ApiLimit);
177            }
178
179            let error = if let Ok(greader_error) = serde_json::from_str::<GReaderError>(&response) {
180                greader_error
181            } else {
182                GReaderError {
183                    errors: vec![response.to_string()],
184                }
185            };
186
187            error!("GReader API: {}", error.errors.join("; "));
188            return Err(ApiError::parse_error(error));
189        }
190        Ok(response)
191    }
192
193    async fn post_request(
194        &self,
195        query: &str,
196        mut params: Vec<(&str, String)>,
197        mut form_params: Vec<(&str, String)>,
198        body: Option<&str>,
199        client: &Client,
200    ) -> Result<String, ApiError> {
201        let api_url: Url = self.base_uri.join(query)?;
202        let mut query_params = Vec::new();
203        query_params.append(&mut params);
204        query_params.push(("output", "json".into()));
205
206        let mut post_params = Vec::new();
207        post_params.append(&mut form_params);
208        if let Some(post_token) = self.get_post_token(client).await? {
209            post_params.push(("T", post_token));
210        }
211
212        let mut headers = self.get_auth_headers().await?;
213        headers.append(
214            CONTENT_TYPE,
215            HeaderValue::from_static("application/x-www-form-urlencoded"),
216        );
217
218        let mut request = client
219            .post(api_url.clone())
220            .headers(headers)
221            .query(&query_params)
222            .form(&post_params);
223
224        if let Some(body) = body {
225            request = request.body(body.to_owned());
226        }
227
228        if body.is_none() && post_params.is_empty() {
229            request = request.header(CONTENT_LENGTH, 0);
230        }
231
232        let response = request.send().await?;
233
234        let status = response.status();
235        let response = response.text().await?;
236        if status != StatusCode::OK {
237            if status == StatusCode::UNAUTHORIZED {
238                return Err(ApiError::AccessDenied);
239            } else if status == StatusCode::BAD_REQUEST {
240                return Err(ApiError::BadRequest);
241            } else if status == StatusCode::TOO_MANY_REQUESTS {
242                return Err(ApiError::ApiLimit);
243            }
244
245            let error = if let Ok(greader_error) = serde_json::from_str::<GReaderError>(&response) {
246                greader_error
247            } else {
248                GReaderError {
249                    errors: vec![response.to_string()],
250                }
251            };
252
253            error!("GReader API: {}", error.errors.join("; "));
254            return Err(ApiError::GReader(error));
255        }
256        Ok(response)
257    }
258
259    fn check_ok_response(response: &str) -> Result<(), ApiError> {
260        if response == "OK" {
261            Ok(())
262        } else {
263            let error: GReaderError = GReaderError {
264                errors: vec![response.to_string()],
265            };
266            Err(ApiError::GReader(error))
267        }
268    }
269
270    pub async fn login(
271        &self,
272        auth_input: &AuthInput,
273        client: &Client,
274    ) -> Result<AuthData, ApiError> {
275        *self.auth.lock().await = match auth_input {
276            AuthInput::Uninitialized => return Err(ApiError::Input),
277            AuthInput::Inoreader(input) => {
278                let mut map: HashMap<String, String> = HashMap::new();
279                map.insert("code".into(), input.auth_code.clone());
280                map.insert("redirect_uri".into(), input.redirect_url.clone());
281                map.insert("client_id".into(), input.client_id.clone());
282                map.insert("client_secret".into(), input.client_secret.clone());
283                map.insert("scope".into(), "".into());
284                map.insert("grant_type".into(), "authorization_code".into());
285
286                let response = client
287                    .post("https://www.inoreader.com/oauth2/token")
288                    .form(&map)
289                    .send()
290                    .await?;
291
292                let status = response.status();
293                if status == StatusCode::UNAUTHORIZED {
294                    return Err(ApiError::AccessDenied);
295                } else if status == StatusCode::BAD_REQUEST {
296                    return Err(ApiError::BadRequest);
297                } else if status == StatusCode::TOO_MANY_REQUESTS {
298                    return Err(ApiError::ApiLimit);
299                }
300
301                let result = response.text().await?;
302
303                let oauth_response = Self::deserialize::<OAuthResponse>(&result)?;
304
305                let now = Utc::now();
306                let token_expires =
307                    now + TimeDelta::try_seconds(oauth_response.expires_in).unwrap();
308
309                AuthData::Inoreader(InoreaderAuth {
310                    client_id: input.client_id.clone(),
311                    client_secret: input.client_secret.clone(),
312                    access_token: oauth_response.access_token,
313                    refresh_token: oauth_response.refresh_token,
314                    expires_at: token_expires,
315                })
316            }
317            AuthInput::Google(input) => {
318                let api_url: Url = self.base_uri.join("accounts/ClientLogin")?;
319                let response = client
320                    .post(api_url.clone())
321                    .query(&[("Email", &input.username), ("Passwd", &input.password)])
322                    .send()
323                    .await?;
324
325                let status = response.status();
326                let response = response.text().await?;
327                if status == StatusCode::UNAUTHORIZED {
328                    return Err(ApiError::AccessDenied);
329                } else if status == StatusCode::BAD_REQUEST {
330                    return Err(ApiError::BadRequest);
331                } else if status == StatusCode::TOO_MANY_REQUESTS {
332                    return Err(ApiError::ApiLimit);
333                } else if status != StatusCode::OK {
334                    return Err(ApiError::Other(format!("Status Code {status}")));
335                }
336
337                let auth_token = Self::parse_login_response(&response)?;
338
339                AuthData::Google(GoogleAuth {
340                    username: input.username.clone(),
341                    password: input.password.clone(),
342                    auth_token: Some(auth_token),
343                    post_token: None,
344                })
345            }
346        };
347
348        let _post_token = self.get_post_token(client).await?;
349
350        *self.auth_input.lock().await = auth_input.clone();
351
352        Ok(self.auth.lock().await.clone())
353    }
354
355    fn parse_login_response(response: &str) -> Result<String, ApiError> {
356        let mut map = HashMap::new();
357        for line in response.lines() {
358            if line.trim().is_empty() {
359                continue;
360            }
361
362            let mut split = line.splitn(2, '=');
363
364            if let (Some(key), Some(value)) = (split.next(), split.next()) {
365                map.insert(key, value);
366            } else {
367                return Err(ApiError::Other(format!(
368                    "response doesn't contain '=' in line: {line}"
369                )));
370            }
371        }
372
373        match map.get("Auth") {
374            Some(auth) => Ok(auth.to_string()),
375            None => Err(ApiError::Other(format!(
376                "response doesn't contain auth token in line: {response}"
377            ))),
378        }
379    }
380
381    pub async fn user_info(&self, client: &Client) -> Result<User, ApiError> {
382        let response = self
383            .get_request("reader/api/0/user-info", vec![], client)
384            .await?;
385        Self::deserialize(&response)
386    }
387
388    pub async fn unread_count(&self, client: &Client) -> Result<Unread, ApiError> {
389        let response = self
390            .get_request("reader/api/0/unread-count", vec![], client)
391            .await?;
392        Self::deserialize(&response)
393    }
394
395    pub async fn subscription_list(&self, client: &Client) -> Result<Feeds, ApiError> {
396        let response = self
397            .get_request("reader/api/0/subscription/list", vec![], client)
398            .await?;
399        Self::deserialize(&response)
400    }
401
402    pub async fn subscription_create(
403        &self,
404        url: &Url,
405        name: Option<&str>,
406        to_stream: Option<&str>,
407        client: &Client,
408    ) -> Result<(), ApiError> {
409        let mut params = Vec::new();
410        params.push(("ac", "subscribe".into()));
411        params.push(("s", format!("feed/{}", &url.as_str())));
412
413        if let Some(name) = name {
414            params.push(("t", name.into()));
415        }
416        if let Some(to_stream) = to_stream {
417            params.push(("a", to_stream.into()));
418        }
419
420        let response = self
421            .post_request(
422                "reader/api/0/subscription/edit",
423                params,
424                vec![],
425                None,
426                client,
427            )
428            .await?;
429        GReaderApi::check_ok_response(&response)
430    }
431
432    pub async fn subscription_edit(
433        &self,
434        item_id: &str,
435        name: Option<&str>,
436        from_stream: Option<&str>,
437        to_stream: Option<&str>,
438        client: &Client,
439    ) -> Result<(), ApiError> {
440        let mut params = Vec::new();
441        params.push(("ac", "edit".into()));
442        params.push(("s", item_id.into()));
443
444        if let Some(name) = name {
445            params.push(("t", name.into()));
446        }
447        if let Some(from_stream) = from_stream {
448            params.push(("r", from_stream.into()));
449        }
450        if let Some(to_stream) = to_stream {
451            params.push(("a", to_stream.into()));
452        }
453
454        let response = self
455            .post_request(
456                "reader/api/0/subscription/edit",
457                params,
458                vec![],
459                None,
460                client,
461            )
462            .await?;
463        GReaderApi::check_ok_response(&response)
464    }
465
466    pub async fn subscription_delete(
467        &self,
468        stream_id: &str,
469        client: &Client,
470    ) -> Result<(), ApiError> {
471        let params = vec![("ac", "unsubscribe".into()), ("s", stream_id.into())];
472
473        let response = self
474            .post_request(
475                "reader/api/0/subscription/edit",
476                params,
477                vec![],
478                None,
479                client,
480            )
481            .await?;
482        GReaderApi::check_ok_response(&response)
483    }
484
485    pub async fn subscription_quickadd(
486        &self,
487        url: &Url,
488        client: &Client,
489    ) -> Result<QuickFeed, ApiError> {
490        let params = vec![("quickadd", url.as_str().into())];
491
492        let response = self
493            .post_request(
494                "reader/api/0/subscription/quickadd",
495                params,
496                vec![],
497                None,
498                client,
499            )
500            .await?;
501        let subscriptions: QuickFeed = Self::deserialize(&response)?;
502        Ok(subscriptions)
503    }
504
505    // untested
506    pub async fn import(&self, opml: String, client: &Client) -> Result<u64, ApiError> {
507        let response = self
508            .post_request(
509                "reader/api/0/subscription/import",
510                vec![],
511                vec![],
512                Some(&opml),
513                client,
514            )
515            .await?;
516
517        if response.starts_with("OK: ") {
518            let response = response.replace("Ok: ", "");
519            let response = response
520                .parse::<u64>()
521                .map_err(|e| ApiError::Other(format!("failed to parse response u64: {e}")))?;
522            Ok(response)
523        } else {
524            Err(ApiError::GReader(GReaderError {
525                errors: vec![response],
526            }))
527        }
528    }
529
530    // untested
531    pub async fn export(&self, client: &Client) -> Result<String, ApiError> {
532        self.get_request("reader/api/0/subscription/export", vec![], client)
533            .await
534    }
535
536    // untested
537    #[cfg(feature = "feedhq")]
538    pub async fn subscribed(&self, stream_id: &str, client: &Client) -> Result<bool, ApiError> {
539        let params = vec![("s", stream_id.into())];
540
541        let response = self
542            .get_request("reader/api/0/subscribed", params, client)
543            .await?;
544        match &response[..] {
545            "true" => Ok(true),
546            "false" => Ok(false),
547            _ => Err(ApiError::GReader(GReaderError {
548                errors: vec![response.to_string()],
549            })),
550        }
551    }
552
553    #[allow(clippy::too_many_arguments)]
554    pub async fn stream_contents(
555        &self,
556        stream_id: Option<&str>,
557        reverse_order: bool,
558        amount: Option<u64>,
559        continuation: Option<&str>,
560        exclude_stream: Option<&str>,
561        include_stream: Option<&str>,
562        filter_older: Option<i64>,
563        filter_newer: Option<i64>,
564        client: &Client,
565    ) -> Result<Stream, ApiError> {
566        let mut params = Vec::new();
567        if reverse_order {
568            params.push(("r", "o".into()));
569        }
570        if let Some(n) = amount {
571            params.push(("n", n.to_string()));
572        }
573        if let Some(c) = continuation {
574            params.push(("c", c.into()));
575        }
576        if let Some(s) = exclude_stream {
577            params.push(("xt", s.into()));
578        }
579        if let Some(s) = include_stream {
580            params.push(("it", s.into()));
581        }
582        if let Some(t) = filter_older {
583            params.push(("ot", t.to_string()));
584        }
585        if let Some(t) = filter_newer {
586            params.push(("nt", t.to_string()));
587        }
588
589        let query = "reader/api/0/stream/contents";
590        let query = if let Some(stream_id) = stream_id {
591            format!("{}/{}", query, stream_id)
592        } else {
593            query.into()
594        };
595        let response = self
596            .post_request(&query, params, vec![], None, client)
597            .await?;
598
599        Self::deserialize(&response)
600    }
601
602    #[allow(clippy::too_many_arguments)]
603    pub async fn items_ids(
604        &self,
605        stream_id: Option<&str>,
606        amount: Option<u64>,
607        include_all_direct_stream_ids: bool,
608        continuation: Option<&str>,
609        exclude_stream: Option<&str>,
610        include_stream: Option<&str>,
611        filter_older: Option<i64>,
612        filter_newer: Option<i64>,
613        client: &Client,
614    ) -> Result<ItemRefs, ApiError> {
615        let mut params = Vec::new();
616
617        if let Some(amount) = amount {
618            params.push(("n", amount.to_string()));
619        }
620        if let Some(stream_id) = stream_id {
621            params.push(("s", stream_id.into()));
622        }
623        if let Some(c) = continuation {
624            params.push(("c", c.into()));
625        }
626        if include_all_direct_stream_ids {
627            params.push(("includeAllDirectStreamIds", "true".into()));
628        }
629        if let Some(s) = exclude_stream {
630            params.push(("xt", s.into()));
631        }
632        if let Some(s) = include_stream {
633            params.push(("it", s.into()));
634        }
635        if let Some(t) = filter_older {
636            params.push(("ot", t.to_string()));
637        }
638        if let Some(t) = filter_newer {
639            params.push(("nt", t.to_string()));
640        }
641        let response = self
642            .get_request("reader/api/0/stream/items/ids", params, client)
643            .await?;
644
645        Self::deserialize(&response)
646    }
647
648    #[cfg(feature = "feedhq")]
649    pub async fn items_count(
650        &self,
651        stream_id: &str,
652        get_latest_date: bool,
653        client: &Client,
654    ) -> Result<String, ApiError> {
655        let mut params = Vec::new();
656        params.push(("s", stream_id.into()));
657
658        if get_latest_date {
659            params.push(("a", "true".into()));
660        }
661
662        let response = self
663            .get_request("reader/api/0/stream/items/count", params, client)
664            .await?;
665        Ok(response)
666    }
667
668    pub async fn items_contents(
669        &self,
670        item_ids: Vec<String>,
671        client: &Client,
672    ) -> Result<Stream, ApiError> {
673        let params = Vec::new();
674        let mut form_params = Vec::new();
675        for item_id in item_ids {
676            form_params.push(("i", item_id));
677        }
678
679        let response = self
680            .post_request(
681                "reader/api/0/stream/items/contents",
682                params,
683                form_params,
684                None,
685                client,
686            )
687            .await?;
688
689        Self::deserialize(&response)
690    }
691
692    pub async fn tag_list(&self, client: &Client) -> Result<Taggings, ApiError> {
693        let response = self
694            .get_request("reader/api/0/tag/list", vec![], client)
695            .await?;
696        Self::deserialize(&response)
697    }
698
699    pub async fn tag_delete(
700        &self,
701        stream_type: StreamType,
702        id: &str,
703        client: &Client,
704    ) -> Result<(), ApiError> {
705        let form_params = vec![(stream_type.into(), id.into())];
706
707        let response = self
708            .post_request(
709                "reader/api/0/disable-tag",
710                vec![],
711                form_params,
712                None,
713                client,
714            )
715            .await?;
716        GReaderApi::check_ok_response(&response)
717    }
718
719    pub async fn tag_rename(
720        &self,
721        stream_type: StreamType,
722        old_name: &str,
723        new_name: &str,
724        client: &Client,
725    ) -> Result<(), ApiError> {
726        let form_params = vec![
727            (stream_type.into(), old_name.into()),
728            ("dest", new_name.into()),
729        ];
730
731        let response = self
732            .post_request("reader/api/0/rename-tag", vec![], form_params, None, client)
733            .await?;
734        GReaderApi::check_ok_response(&response)
735    }
736
737    // TODO have better parameters, since it is not obvious what is add and remove
738    pub async fn tag_edit(
739        &self,
740        item_ids: &[&str],
741        tag_add: Option<&str>,
742        tag_remove: Option<&str>,
743        client: &Client,
744    ) -> Result<(), ApiError> {
745        if tag_add.is_none() && tag_remove.is_none() {
746            return Err(ApiError::Input);
747        }
748
749        let mut form_params = Vec::new();
750        for item_id in item_ids {
751            form_params.push(("i", item_id.to_string()));
752        }
753        if let Some(remove) = tag_remove {
754            form_params.push(("r", remove.into()));
755        }
756        if let Some(add) = tag_add {
757            form_params.push(("a", add.into()));
758        }
759
760        let response = self
761            .post_request("reader/api/0/edit-tag", vec![], form_params, None, client)
762            .await?;
763        GReaderApi::check_ok_response(&response)
764    }
765
766    pub async fn mark_all_as_read(
767        &self,
768        stream_id: &str,
769        older_than: Option<u64>,
770        client: &Client,
771    ) -> Result<(), ApiError> {
772        let mut params = Vec::new();
773        let form_params = vec![("s", stream_id.into())];
774
775        if let Some(older_than) = older_than {
776            params.push(("ts", older_than.to_string()));
777        }
778
779        let response = self
780            .post_request(
781                "reader/api/0/mark-all-as-read",
782                params,
783                form_params,
784                None,
785                client,
786            )
787            .await?;
788        GReaderApi::check_ok_response(&response)
789    }
790
791    #[allow(unused)]
792    #[cfg(any(feature = "feedhq", feature = "oldreader"))]
793    pub async fn preference_list(&self, client: &Client) -> Result<(), ApiError> {
794        unimplemented!();
795    }
796
797    #[cfg(any(feature = "feedhq", feature = "oldreader", feature = "inoreader"))]
798    pub async fn preference_stream_list(&self, client: &Client) -> Result<StreamPrefs, ApiError> {
799        let response = self
800            .get_request("reader/api/0/preference/stream/list", vec![], client)
801            .await?;
802
803        Self::deserialize(&response)
804    }
805
806    #[allow(unused)]
807    #[cfg(any(feature = "feedhq", feature = "oldreader"))]
808    pub async fn friends_list(&self, client: &Client) -> Result<(), ApiError> {
809        unimplemented!();
810    }
811    #[allow(unused)]
812    #[cfg(feature = "oldreader")]
813    pub async fn friends_edit(&self, client: &Client) -> Result<(), ApiError> {
814        unimplemented!();
815    }
816
817    #[allow(unused)]
818    #[cfg(feature = "inoreader")]
819    pub async fn inoreader_refresh_token(
820        &self,
821        client: &Client,
822    ) -> Result<InoreaderAuth, ApiError> {
823        let auth_data = match &*self.auth.lock().await {
824            AuthData::Inoreader(auth_data) => auth_data.clone(),
825            _ => return Err(ApiError::Token),
826        };
827
828        let client_id = auth_data.client_id.clone();
829        let client_secret = auth_data.client_secret.clone();
830        let refresh_token = auth_data.refresh_token.clone();
831
832        let mut map: HashMap<String, String> = HashMap::new();
833        map.insert("client_id".into(), client_id);
834        map.insert("client_secret".into(), client_secret);
835        map.insert("grant_type".into(), "refresh_token".into());
836        map.insert("refresh_token".into(), refresh_token);
837
838        let response = client
839            .post("https://www.inoreader.com/oauth2/token")
840            .form(&map)
841            .send()
842            .await?;
843
844        let status = response.status();
845        if status == StatusCode::UNAUTHORIZED {
846            return Err(ApiError::AccessDenied);
847        } else if status == StatusCode::BAD_REQUEST {
848            return Err(ApiError::BadRequest);
849        } else if status == StatusCode::TOO_MANY_REQUESTS {
850            return Err(ApiError::ApiLimit);
851        }
852
853        let result = response.text().await?;
854
855        let oauth_response: OAuthResponse = Self::deserialize(&result)?;
856
857        let now = Utc::now();
858        let token_expires = now + TimeDelta::try_seconds(oauth_response.expires_in).unwrap();
859
860        let inoreader_auth = InoreaderAuth {
861            client_id: auth_data.client_id.clone(),
862            client_secret: auth_data.client_secret.clone(),
863            access_token: oauth_response.access_token,
864            refresh_token: oauth_response.refresh_token,
865            expires_at: token_expires,
866        };
867
868        *self.auth.lock().await = AuthData::Inoreader(inoreader_auth.clone());
869
870        Ok(inoreader_auth)
871    }
872
873    #[allow(unused)]
874    #[cfg(feature = "inoreader")]
875    pub async fn create_active_search(&self, client: &Client) -> Result<(), ApiError> {
876        unimplemented!();
877    }
878
879    #[allow(unused)]
880    #[cfg(feature = "inoreader")]
881    pub async fn delete_active_search(&self, client: &Client) -> Result<(), ApiError> {
882        unimplemented!();
883    }
884
885    #[allow(unused)]
886    #[cfg(feature = "oldreader")]
887    pub async fn add_comment(&self, client: &Client) -> Result<(), ApiError> {
888        unimplemented!();
889    }
890}