mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use self::error::ApiError;
use self::models::{
AccessTokenResponse, Category, Collection, CollectionFeedInput, CollectionInput, Counts, Entry,
FeedlyError, Profile, ProfileUpdate, RefreshTokenResponse, SearchResult, Stream, Subscription,
SubscriptionInput, Tag,
};
use chrono::{DateTime, Duration, Utc};
use log::info;
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::sync::Arc;
use std::sync::Mutex;
use url::Url;
pub type AuthCode = String;
pub type AccessToken = String;
pub type RefreshToken = String;
const FEEDLY_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`')
.add(b'#')
.add(b'?')
.add(b'{')
.add(b'}')
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b']')
.add(b'\\')
.add(b'^')
.add(b'|')
.add(b'+');
pub struct FeedlyApi {
base_uri: Url,
client_id: String,
client_secret: String,
user_id: Arc<Mutex<Option<String>>>,
access_token: Arc<Mutex<AccessToken>>,
refresh_token: Arc<Mutex<RefreshToken>>,
token_expires: Arc<Mutex<DateTime<Utc>>>,
}
impl FeedlyApi {
pub fn new(
client_id: String,
client_secret: String,
access_token: AccessToken,
refresh_token: RefreshToken,
token_expires: DateTime<Utc>,
) -> Result<FeedlyApi, ApiError> {
let api = FeedlyApi {
base_uri: Self::base_uri()?,
client_id,
client_secret,
user_id: Arc::new(Mutex::new(None)),
access_token: Arc::new(Mutex::new(access_token)),
refresh_token: Arc::new(Mutex::new(refresh_token)),
token_expires: Arc::new(Mutex::new(token_expires)),
};
Ok(api)
}
pub fn login_url(client_id: &str, client_secret: &str) -> Result<Url, ApiError> {
let mut url = Self::base_uri()?.as_str().to_owned();
let auth_scope = Self::auth_scope()?.as_str().to_owned();
let redirect_url = Self::redirect_uri()?.as_str().to_owned();
url.push_str("v3/auth/auth");
url.push_str(&format!("?client_secret={}", client_secret));
url.push_str(&format!("&client_id={}", client_id));
url.push_str(&format!("&redirect_uri={}", redirect_url));
url.push_str(&format!("&scope={}", auth_scope));
url.push_str("&response_type=code");
url.push_str("&state=getting_code");
let url = Url::parse(&url)?;
Ok(url)
}
fn deserialize<T: for<'a> Deserialize<'a>>(json: &str) -> Result<T, ApiError> {
let result: T = serde_json::from_str(json).map_err(|source| ApiError::Json {
source,
json: json.into(),
})?;
Ok(result)
}
pub fn parse_redirected_url(url: &Url) -> Result<AuthCode, ApiError> {
if let Some(code) = url.query_pairs().find(|ref x| x.0 == "code") {
return Ok(code.1.to_string());
}
if let Some(error) = url.query_pairs().find(|ref x| x.0 == "error") {
if error.1 == "access_denied" {
return Err(ApiError::AccessDenied);
};
}
Err(ApiError::Unknown)
}
pub fn redirect_uri() -> Result<Url, ApiError> {
let url = Url::parse("http://localhost")?;
Ok(url)
}
fn auth_scope() -> Result<Url, ApiError> {
let url = Url::parse("https://cloud.feedly.com/subscriptions")?;
Ok(url)
}
fn base_uri() -> Result<Url, ApiError> {
let url = Url::parse("https://cloud.feedly.com")?;
Ok(url)
}
pub async fn initialize_user_id(&self, client: &Client) -> Result<(), ApiError> {
let user_id = {
(*self
.user_id
.lock()
.map_err(|_e| ApiError::InternalMutabilty)?)
.clone()
};
if user_id.is_none() {
let profile = self.get_profile(client).await?;
{
*self
.user_id
.lock()
.map_err(|_e| ApiError::InternalMutabilty)? = Some(profile.id);
}
}
Ok(())
}
pub fn parse_expiration_date(expires_in: &str) -> Result<DateTime<Utc>, ApiError> {
let timestamp = expires_in.parse::<i64>().map_err(|_| ApiError::Input)?;
let now = Utc::now();
let expires_datetime = now + Duration::seconds(timestamp);
Ok(expires_datetime)
}
pub fn gernerate_feed_id(url: &Url) -> String {
format!("feed/{}", url.as_str())
}
pub async fn generate_category_id(
&self,
title: &str,
client: &Client,
) -> Result<String, ApiError> {
self.initialize_user_id(client).await?;
let user_id = {
(*self
.user_id
.lock()
.map_err(|_e| ApiError::InternalMutabilty)?)
.clone()
};
if let Some(user_id) = user_id {
return Ok(format!("user/{}/category/{}", user_id, title));
}
Err(ApiError::Unknown)
}
pub async fn generate_tag_id(&self, title: &str, client: &Client) -> Result<String, ApiError> {
self.initialize_user_id(client).await?;
let user_id = {
(*self
.user_id
.lock()
.map_err(|_e| ApiError::InternalMutabilty)?)
.clone()
};
if let Some(user_id) = user_id {
return Ok(format!("user/{}/tag/{}", user_id, title));
}
Err(ApiError::Unknown)
}
pub async fn category_all(&self, client: &Client) -> Result<String, ApiError> {
self.generate_category_id("global.all", client).await
}
pub async fn tag_marked(&self, client: &Client) -> Result<String, ApiError> {
self.generate_tag_id("global.saved", client).await
}
pub async fn tag_read(&self, client: &Client) -> Result<String, ApiError> {
self.generate_tag_id("global.read", client).await
}
pub async fn request_auth_token(
client_id: &str,
client_secret: &str,
auth_code: AuthCode,
client: &Client,
) -> Result<AccessTokenResponse, ApiError> {
let input = json!(
{
"code" : auth_code,
"client_id" : client_id,
"client_secret" : client_secret,
"redirect_uri" : Self::redirect_uri()?.as_str(),
"state" : "feedly-api rust crate",
"grant_type" : "authorization_code"
}
);
let api_endpoint = Self::base_uri()?.join("/v3/auth/token")?;
let response = client
.post(api_endpoint)
.json(&input)
.send()
.await?
.text()
.await?;
let response: AccessTokenResponse = Self::deserialize(&response)?;
Ok(response)
}
pub async fn refresh_auth_token(
&self,
client: &Client,
) -> Result<RefreshTokenResponse, ApiError> {
let refresh_token = {
(*self
.refresh_token
.lock()
.map_err(|_e| ApiError::InternalMutabilty)?)
.clone()
};
let input = json!(
{
"refresh_token" : refresh_token,
"client_id" : self.client_id,
"client_secret" : self.client_secret,
"grant_type" : "refresh_token"
}
);
let api_endpoint = self.base_uri.clone().join("/v3/auth/token")?;
let response = client
.post(api_endpoint)
.json(&input)
.send()
.await?
.text()
.await?;
let response: RefreshTokenResponse = Self::deserialize(&response)?;
info!("Feedly refresh token: {:?}", response);
{
*self
.access_token
.lock()
.map_err(|_e| ApiError::InternalMutabilty)? = response.access_token.clone();
*self
.token_expires
.lock()
.map_err(|_e| ApiError::InternalMutabilty)? =
Utc::now() + Duration::seconds(response.expires_in as i64);
}
Ok(response)
}
async fn get_access_token(&self) -> Result<AccessToken, ApiError> {
let expires_at = {
*self
.token_expires
.lock()
.map_err(|_e| ApiError::InternalMutabilty)?
};
let expires_in = expires_at.signed_duration_since(Utc::now());
let expired = expires_in.num_seconds() <= 60;
if !expired {
let access_token = {
(*self
.access_token
.lock()
.map_err(|_e| ApiError::InternalMutabilty)?)
.clone()
};
return Ok(access_token);
}
Err(ApiError::TokenExpired.into())
}
async fn post_request<T: Serialize + ?Sized>(
&self,
json: &T,
api_endpoint: &str,
client: &Client,
) -> Result<String, ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
let response = client
.post(api_endpoint)
.header(AUTHORIZATION, token)
.json(json)
.send()
.await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
let error: FeedlyError = Self::deserialize(&response)?;
return Err(ApiError::parse_feedly_error(error));
}
Ok(response)
}
async fn get_request(&self, api_endpoint: &str, client: &Client) -> Result<String, ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
let response = client
.get(api_endpoint)
.header(AUTHORIZATION, token)
.send()
.await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
let error: FeedlyError = Self::deserialize(&response)?;
return Err(ApiError::parse_feedly_error(error));
}
Ok(response)
}
async fn put_request<T: Serialize + ?Sized>(
&self,
json: &T,
api_endpoint: &str,
client: &Client,
) -> Result<String, ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
let response = client
.put(api_endpoint)
.header(AUTHORIZATION, token)
.json(json)
.send()
.await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
let error: FeedlyError = Self::deserialize(&response)?;
return Err(ApiError::parse_feedly_error(error));
}
Ok(response)
}
async fn delete_request(&self, api_endpoint: &str, client: &Client) -> Result<(), ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self.base_uri.clone().join(api_endpoint)?;
let response = client
.delete(api_endpoint)
.header(AUTHORIZATION, token)
.send()
.await?;
if response.status() != StatusCode::OK {
let response = response.text().await?;
let error: FeedlyError = Self::deserialize(&response)?;
return Err(ApiError::parse_feedly_error(error));
}
Ok(())
}
pub async fn get_profile(&self, client: &Client) -> Result<Profile, ApiError> {
let response = self.get_request("/v3/profile", client).await?;
let profile: Profile = Self::deserialize(&response)?;
Ok(profile)
}
#[allow(clippy::too_many_arguments)]
pub async fn update_profile(
&self,
client: &Client,
email: Option<String>,
given_name: Option<String>,
family_name: Option<String>,
picture: Option<String>,
gender: Option<bool>,
locale: Option<String>,
twitter: Option<String>,
facebook: Option<String>,
) -> Result<Profile, ApiError> {
let update = ProfileUpdate {
email,
given_name,
family_name,
picture,
gender,
locale,
twitter,
facebook,
};
let response = self.post_request(&update, "/v3/profile", client).await?;
let profile: Profile = Self::deserialize(&response)?;
Ok(profile)
}
pub async fn get_categories(&self, client: &Client) -> Result<Vec<Category>, ApiError> {
let response = self
.get_request("/v3/categories?sort=feedly", client)
.await?;
let category_vec: Vec<Category> = Self::deserialize(&response)?;
Ok(category_vec)
}
pub async fn get_collections(&self, client: &Client) -> Result<Vec<Collection>, ApiError> {
let response = self
.get_request("/v3/collections?sort=feedly", client)
.await?;
let collections_vec: Vec<Collection> = Self::deserialize(&response)?;
Ok(collections_vec)
}
pub async fn get_collection(
&self,
id: &str,
client: &Client,
) -> Result<Vec<Collection>, ApiError> {
let api_endpoint = format!("/v3/collections/{}", id);
let response = self.get_request(&api_endpoint, client).await?;
let collection_vec: Vec<Collection> = Self::deserialize(&response)?;
Ok(collection_vec)
}
pub async fn update_category(
&self,
id: &str,
label: &str,
client: &Client,
) -> Result<(), ApiError> {
let input = json!(
{
"label" : label,
}
);
let id = utf8_percent_encode(&id, FEEDLY_ENCODE_SET).to_string();
let endpoint = FeedlyApi::category_api_endpoint(&id);
let _ = self.post_request(&input, &endpoint, client).await?;
Ok(())
}
pub async fn delete_category(&self, id: &str, client: &Client) -> Result<(), ApiError> {
let id = utf8_percent_encode(&id, FEEDLY_ENCODE_SET).to_string();
let endpoint = FeedlyApi::category_api_endpoint(&id);
self.delete_request(&endpoint, client).await?;
Ok(())
}
pub async fn get_subsriptions(&self, client: &Client) -> Result<Vec<Subscription>, ApiError> {
let response = self.get_request("/v3/subscriptions", client).await?;
let subscription_vec: Vec<Subscription> = Self::deserialize(&response)?;
Ok(subscription_vec)
}
pub async fn create_or_update_collection(
&self,
collection: CollectionInput,
client: &Client,
) -> Result<(), ApiError> {
let _ = self
.post_request(&collection, "/v3/collections", client)
.await?;
Ok(())
}
pub async fn add_feeds_to_collection(
&self,
collection_id: &str,
feeds: Vec<CollectionFeedInput>,
client: &Client,
) -> Result<(), ApiError> {
let api_endpoint = format!("/v3/collections/{}/feeds/.mput", collection_id);
let _ = self.post_request(&feeds, &api_endpoint, client).await?;
Ok(())
}
pub async fn remove_feeds_from_collection(
&self,
collection_id: &str,
feeds: Vec<CollectionFeedInput>,
client: &Client,
) -> Result<(), ApiError> {
let api_endpoint = format!("/v3/collections/{}/feeds/.mdelete", collection_id);
let _ = self.post_request(&feeds, &api_endpoint, client).await?;
Ok(())
}
pub async fn add_subscription(
&self,
subscription: SubscriptionInput,
client: &Client,
) -> Result<(), ApiError> {
let _ = self
.post_request(&subscription, "/v3/subscriptions", client)
.await?;
Ok(())
}
pub async fn update_subscriptions(
&self,
subscriptions: Vec<SubscriptionInput>,
client: &Client,
) -> Result<(), ApiError> {
let _ = self
.post_request(&subscriptions, "/v3/subscriptions/.mput", client)
.await?;
Ok(())
}
pub async fn delete_subscription(&self, id: &str, client: &Client) -> Result<(), ApiError> {
let id = utf8_percent_encode(&id, FEEDLY_ENCODE_SET).to_string();
let api_endpoint = FeedlyApi::subscription_api_endpoint(&id);
self.delete_request(&api_endpoint, client).await?;
Ok(())
}
pub async fn get_tags(&self, client: &Client) -> Result<Vec<Tag>, ApiError> {
let response = self.get_request("/v3/tags", client).await?;
Self::deserialize(&response)
}
fn category_api_endpoint(category_id: &str) -> String {
let mut api_endpoint = String::from("/v3/categories/");
api_endpoint.push_str(category_id);
api_endpoint
}
fn subscription_api_endpoint(subscription_id: &str) -> String {
let mut api_endpoint = String::from("/v3/subscriptions/");
api_endpoint.push_str(subscription_id);
api_endpoint
}
fn tag_api_endpoint(
tag_ids: Vec<&str>,
entry_ids: Option<Vec<&str>>,
) -> Result<String, ApiError> {
if tag_ids.is_empty() {
return Err(ApiError::Input);
}
let mut api_endpoint = String::from("/v3/tags/");
for tag_id in tag_ids {
let tag_id = utf8_percent_encode(tag_id, FEEDLY_ENCODE_SET).to_string();
api_endpoint.push_str(&tag_id);
api_endpoint.push(',');
}
api_endpoint = api_endpoint[..api_endpoint.len() - 1].to_owned();
if let Some(entry_ids) = entry_ids {
if entry_ids.is_empty() {
return Err(ApiError::Input);
}
api_endpoint.push('/');
for entry_id in entry_ids {
let entry_id = utf8_percent_encode(&entry_id, FEEDLY_ENCODE_SET).to_string();
api_endpoint.push_str(&entry_id);
api_endpoint.push(',');
}
api_endpoint = api_endpoint[..api_endpoint.len() - 1].to_owned();
}
Ok(api_endpoint)
}
pub async fn tag_entry(
&self,
entry_id: &str,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"entryId" : entry_id,
}
);
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
let _ = self.put_request(&json, &api_endpoint, client).await?;
Ok(())
}
pub async fn tag_entries(
&self,
entry_ids: Vec<&str>,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"entryIds" : entry_ids,
}
);
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
let _ = self.put_request(&json, &api_endpoint, client).await?;
Ok(())
}
pub async fn update_tag(
&self,
tag_id: &str,
label: &str,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"label" : label,
}
);
let api_endpoint = FeedlyApi::tag_api_endpoint(vec![tag_id], None)?;
let _ = self.post_request(&json, &api_endpoint, client).await?;
Ok(())
}
pub async fn untag_entries(
&self,
entry_ids: Vec<&str>,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, Some(entry_ids))?;
self.delete_request(&api_endpoint, client).await?;
Ok(())
}
pub async fn delete_tags(&self, tag_ids: Vec<&str>, client: &Client) -> Result<(), ApiError> {
let api_endpoint = FeedlyApi::tag_api_endpoint(tag_ids, None)?;
self.delete_request(&api_endpoint, client).await?;
Ok(())
}
pub async fn get_entries(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<Vec<Entry>, ApiError> {
let response = self
.post_request(&entry_ids, "/v3/entries/.mget", client)
.await?;
match Self::deserialize(&response) {
Ok(entries) => Ok(entries),
Err(error) => {
log::warn!("Failed to deserialize entries to struct: {}", error);
log::debug!("Trying to deserialize response manually");
if let Ok(entries_value) = serde_json::from_str::<serde_json::Value>(&response) {
Entry::manual_deserialize_vec(&entries_value)
} else {
Err(error)
}
}
}
}
pub async fn create_entry(
&self,
entry: Entry,
client: &Client,
) -> Result<Vec<String>, ApiError> {
let response = self.post_request(&entry, "/v3/entries/", client).await?;
Self::deserialize(&response)
}
fn stream_api_endpoint(
stream_id: &str,
continuation: Option<String>,
count: Option<u32>,
ranked: Option<&str>,
unread_only: Option<bool>,
newer_than: Option<u64>,
) -> String {
let mut api_endpoint = String::from("/v3/streams/contents?streamId=");
let stream_id = utf8_percent_encode(&stream_id, FEEDLY_ENCODE_SET).to_string();
api_endpoint.push_str(&stream_id);
if let Some(continuation) = continuation {
api_endpoint.push_str(&format!("&continuation={}", continuation));
}
if let Some(count) = count {
api_endpoint.push_str(&format!("&count={}", count));
}
if let Some(ranked) = ranked {
api_endpoint.push_str(&format!("&ranked={}", ranked));
}
if let Some(unread_only) = unread_only {
api_endpoint.push_str(&format!("&unreadOnly={}", unread_only));
}
if let Some(newer_than) = newer_than {
api_endpoint.push_str(&format!("&newerThan={}", newer_than));
}
api_endpoint
}
#[allow(clippy::too_many_arguments)]
pub async fn get_stream(
&self,
stream_id: &str,
continuation: Option<String>,
count: Option<u32>,
ranked: Option<&str>,
unread_only: Option<bool>,
newer_than: Option<u64>,
client: &Client,
) -> Result<Stream, ApiError> {
let api_endpoint = FeedlyApi::stream_api_endpoint(
stream_id,
continuation,
count,
ranked,
unread_only,
newer_than,
);
let response = self.get_request(&api_endpoint, client).await?;
let stream: Stream = match serde_json::from_str(&response) {
Ok(stream) => stream,
Err(error) => {
log::warn!("Failed to deserialize stream to struct: {}", error);
log::debug!("Trying to deserialize stream manually");
Stream::manual_deserialize(&response)?
}
};
Ok(stream)
}
pub async fn get_unread_counts(&self, client: &Client) -> Result<Counts, ApiError> {
let response = self.get_request("/v3/markers/counts", client).await?;
Self::deserialize(&response)
}
pub async fn mark_entries_read(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_entries_unread(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "keepUnread",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_feeds_read(
&self,
feed_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "feeds",
"feedIds" : feed_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_categories_read(
&self,
category_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "categories",
"categoryIds" : category_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_tags_read(
&self,
tag_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsRead",
"type" : "tags",
"tagIds" : tag_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_entries_saved(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsSaved",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
pub async fn mark_entries_unsaved(
&self,
entry_ids: Vec<&str>,
client: &Client,
) -> Result<(), ApiError> {
let json = json!(
{
"action" : "markAsUnsaved",
"type" : "entries",
"entryIds" : entry_ids
}
);
let _ = self.post_request(&json, "/v3/markers", client).await?;
Ok(())
}
#[allow(dead_code)]
pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
self.get_request("/v3/opml", client).await
}
pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
let token = self.get_access_token().await?;
let api_endpoint = self.base_uri.clone().join("/v3/opml")?;
let response = client
.post(api_endpoint)
.header(AUTHORIZATION, token)
.header(CONTENT_TYPE, "text/xml")
.body(opml.to_owned())
.send()
.await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
let error: FeedlyError = Self::deserialize(&response)?;
return Err(ApiError::parse_feedly_error(error));
}
Ok(())
}
pub async fn search_feedly_cloud(
client: &Client,
query: &str,
count: Option<u32>,
locale: Option<&str>,
) -> Result<SearchResult, ApiError> {
let mut query = format!(
"/v3/search/feeds?query={}",
utf8_percent_encode(&query, FEEDLY_ENCODE_SET)
);
if let Some(count) = count {
query.push_str(&format!("&count={}", count));
}
if let Some(locale) = locale {
let locale = utf8_percent_encode(&locale, FEEDLY_ENCODE_SET).to_string();
query.push_str(&format!("&locale={}", locale));
}
let api_endpoint = Self::base_uri()?.join(&query)?;
let response = client.get(api_endpoint).send().await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
let error: FeedlyError = Self::deserialize(&response)?;
return Err(ApiError::parse_feedly_error(error));
}
Self::deserialize(&response)
}
}