mod deserialize;
pub mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use crate::error::ApiError;
use crate::models::GReaderError;
pub use crate::models::{AuthData, GoogleAuth, InoreaderAuth};
use crate::models::{Feeds, ItemRefs, QuickFeed, Stream, StreamType, Taggings, Unread, User};
use std::collections::HashMap;
#[cfg(any(feature = "feedhq", feature = "oldreader", feature = "inoreader"))]
use crate::models::StreamPrefs;
use chrono::{Duration, Utc};
use log::error;
use models::{AuthInput, OAuthResponse, PostToken};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use reqwest::{Client, StatusCode};
use serde::Deserialize;
use std::sync::Arc;
use std::sync::Mutex;
use url::Url;
#[derive(Clone, Debug)]
pub struct GReaderApi {
base_uri: Url,
auth_input: Arc<Mutex<AuthInput>>,
auth: Arc<Mutex<AuthData>>,
}
impl GReaderApi {
pub fn new(url: &Url, auth: AuthData) -> Self {
GReaderApi {
base_uri: url.clone(),
auth_input: Arc::new(Mutex::new(AuthInput::Uninitialized)),
auth: Arc::new(Mutex::new(auth)),
}
}
pub fn get_auth_data(&self) -> Result<AuthData, ApiError> {
Ok(self
.auth
.lock()
.map_err(|_| ApiError::InternalMutabilty)?
.clone())
}
pub fn set_aut_data(&self, auth: AuthData) -> Result<(), ApiError> {
*(self.auth.lock().map_err(|_| ApiError::InternalMutabilty)?) = auth;
Ok(())
}
async fn get_auth_headers(&self) -> Result<HeaderMap, ApiError> {
let mut headers = HeaderMap::new();
let auth_data = self
.auth
.lock()
.map_err(|_| ApiError::InternalMutabilty)?
.clone();
match &auth_data {
AuthData::Uninitialized => return Err(ApiError::Unknown),
AuthData::Google(auth_data) => {
if auth_data.auth_token.is_none() {
return Err(ApiError::NotLoggedIn)?;
}
if let Some(auth_token) = auth_data.auth_token.as_deref() {
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("GoogleLogin auth={}", auth_token)).unwrap(),
);
}
}
AuthData::Inoreader(auth_data) => {
let expires_in = auth_data.expires_at.signed_duration_since(Utc::now());
let expired = expires_in.num_seconds() <= 60;
if expired {
return Err(ApiError::TokenExpired)?;
}
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", auth_data.access_token)).unwrap(),
);
headers.insert(
"AppId",
HeaderValue::from_str(&auth_data.client_id).unwrap(),
);
headers.insert(
"AppKey",
HeaderValue::from_str(&auth_data.client_secret).unwrap(),
);
}
};
Ok(headers)
}
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)
}
async fn get_post_token(&self, client: &Client) -> Result<Option<String>, ApiError> {
let mut auth_data = self
.auth
.lock()
.map_err(|_| ApiError::InternalMutabilty)?
.clone();
let post_token = match &mut auth_data {
AuthData::Inoreader(_) | AuthData::Uninitialized => Ok(None),
AuthData::Google(auth_data) => {
if let Some(post_token) = auth_data.post_token.as_mut() {
if !post_token.is_valid() {
let mut response = self
.get_request("reader/api/0/token", vec![], client)
.await?;
let _ = response.pop();
post_token.update(&response);
Ok(Some(response))
} else {
Ok(Some(post_token.token.clone()))
}
} else {
let mut response = self
.get_request("reader/api/0/token", vec![], client)
.await?;
let _ = response.pop();
let post_token = PostToken::new(&response);
auth_data.post_token.replace(post_token);
Ok(Some(response))
}
}
};
*self.auth.lock().map_err(|_| ApiError::InternalMutabilty)? = auth_data;
post_token
}
async fn get_request(
&self,
query: &str,
mut params: Vec<(&str, String)>,
client: &Client,
) -> Result<String, ApiError> {
let api_url: Url = self.base_uri.join(query)?;
let mut query_params = Vec::new();
query_params.append(&mut params);
query_params.push(("output", "json".into()));
let auth_headers = self.get_auth_headers().await?;
let response = client
.get(api_url.clone())
.headers(auth_headers)
.query(&query_params)
.send()
.await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
if status == StatusCode::UNAUTHORIZED {
return Err(ApiError::AccessDenied)?;
}
let error = if let Ok(greader_error) = serde_json::from_str::<GReaderError>(&response) {
greader_error
} else {
GReaderError {
errors: vec![response.to_string()],
}
};
error!("GReader API: {}", error.errors.join("; "));
return Err(ApiError::parse_error(error));
}
Ok(response)
}
async fn post_request(
&self,
query: &str,
mut params: Vec<(&str, String)>,
mut form_params: Vec<(&str, String)>,
body: Option<&str>,
client: &Client,
) -> Result<String, ApiError> {
let api_url: Url = self.base_uri.join(query)?;
let mut query_params = Vec::new();
query_params.append(&mut params);
query_params.push(("output", "json".into()));
let mut post_params = Vec::new();
post_params.append(&mut form_params);
if let Some(post_token) = self.get_post_token(client).await? {
post_params.push(("T", post_token));
}
let mut headers = self.get_auth_headers().await?;
headers.append(
CONTENT_TYPE,
HeaderValue::from_str("application/x-www-form-urlencoded").unwrap(),
);
let request = client
.post(api_url.clone())
.headers(headers)
.query(&query_params)
.form(&post_params);
let request = if let Some(body) = body {
request.body(body.to_owned())
} else {
request
};
let response = request.send().await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
if status == StatusCode::UNAUTHORIZED {
return Err(ApiError::AccessDenied)?;
}
if status == StatusCode::BAD_REQUEST {
return Err(ApiError::BadRequest)?;
}
let error = if let Ok(greader_error) = serde_json::from_str::<GReaderError>(&response) {
greader_error
} else {
GReaderError {
errors: vec![response.to_string()],
}
};
error!("GReader API: {}", error.errors.join("; "));
return Err(ApiError::GReader(error));
}
Ok(response)
}
fn chech_ok_response(response: &str) -> Result<(), ApiError> {
if response == "OK" {
Ok(())
} else {
let error: GReaderError = GReaderError {
errors: vec![response.to_string()],
};
Err(ApiError::GReader(error))
}
}
pub async fn login(
&self,
auth_input: &AuthInput,
client: &Client,
) -> Result<AuthData, ApiError> {
*self.auth.lock().map_err(|_| ApiError::InternalMutabilty)? = match auth_input {
AuthInput::Uninitialized => return Err(ApiError::Input),
AuthInput::Inoreader(input) => {
let mut map: HashMap<String, String> = HashMap::new();
map.insert("code".into(), input.auth_code.clone());
map.insert("redirect_uri".into(), input.redirect_url.clone());
map.insert("client_id".into(), input.client_id.clone());
map.insert("client_secret".into(), input.client_secret.clone());
map.insert("scope".into(), "".into());
map.insert("grant_type".into(), "authorization_code".into());
let response = client
.post("https://www.inoreader.com/oauth2/token")
.form(&map)
.send()
.await?
.text()
.await?;
let oauth_response = Self::deserialize::<OAuthResponse>(&response)?;
let now = Utc::now();
let token_expires = now + Duration::seconds(oauth_response.expires_in);
AuthData::Inoreader(InoreaderAuth {
client_id: input.client_id.clone(),
client_secret: input.client_secret.clone(),
access_token: oauth_response.access_token,
refresh_token: oauth_response.refresh_token,
expires_at: token_expires,
})
}
AuthInput::Google(input) => {
let api_url: Url = self.base_uri.join("accounts/ClientLogin")?;
let response = client
.post(api_url.clone())
.query(&[("Email", &input.username), ("Passwd", &input.password)])
.send()
.await?;
let status = response.status();
let response = response.text().await?;
if status != StatusCode::OK {
return Err(ApiError::AccessDenied);
}
let auth_string = response.lines().nth(2).unwrap();
let auth_token = auth_string.split('=').nth(1).unwrap();
AuthData::Google(GoogleAuth {
username: input.username.clone(),
password: input.password.clone(),
auth_token: Some(auth_token.to_string()),
post_token: None,
})
}
};
let _post_token = self.get_post_token(client).await?;
*self
.auth_input
.lock()
.map_err(|_| ApiError::InternalMutabilty)? = auth_input.clone();
Ok(self
.auth
.lock()
.map_err(|_| ApiError::InternalMutabilty)?
.clone())
}
pub async fn user_info(&self, client: &Client) -> Result<User, ApiError> {
let response = self
.get_request("reader/api/0/user-info", vec![], client)
.await?;
Self::deserialize(&response)
}
pub async fn unread_count(&self, client: &Client) -> Result<Unread, ApiError> {
let response = self
.get_request("reader/api/0/unread-count", vec![], client)
.await?;
Self::deserialize(&response)
}
pub async fn subscription_list(&self, client: &Client) -> Result<Feeds, ApiError> {
let response = self
.get_request("reader/api/0/subscription/list", vec![], client)
.await?;
Self::deserialize(&response)
}
pub async fn subscription_create(
&self,
url: &Url,
name: Option<&str>,
to_stream: Option<&str>,
client: &Client,
) -> Result<(), ApiError> {
let mut params = Vec::new();
params.push(("ac", "subscribe".into()));
params.push(("s", format!("feed/{}", &url.as_str())));
if let Some(name) = name {
params.push(("t", name.into()));
}
if let Some(to_stream) = to_stream {
params.push(("a", to_stream.into()));
}
let response = self
.post_request(
"reader/api/0/subscription/edit",
params,
vec![],
None,
client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn subscription_edit(
&self,
item_id: &str,
name: Option<&str>,
from_stream: Option<&str>,
to_stream: Option<&str>,
client: &Client,
) -> Result<(), ApiError> {
let mut params = Vec::new();
params.push(("ac", "edit".into()));
params.push(("s", item_id.into()));
if let Some(name) = name {
params.push(("t", name.into()));
}
if let Some(from_stream) = from_stream {
params.push(("r", from_stream.into()));
}
if let Some(to_stream) = to_stream {
params.push(("a", to_stream.into()));
}
let response = self
.post_request(
"reader/api/0/subscription/edit",
params,
vec![],
None,
client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn subscription_delete(
&self,
stream_id: &str,
client: &Client,
) -> Result<(), ApiError> {
let params = vec![("ac", "unsubscribe".into()), ("s", stream_id.into())];
let response = self
.post_request(
"reader/api/0/subscription/edit",
params,
vec![],
None,
client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn subscription_quickadd(
&self,
url: &Url,
client: &Client,
) -> Result<QuickFeed, ApiError> {
let params = vec![("quickadd", url.as_str().into())];
let response = self
.post_request(
"reader/api/0/subscription/quickadd",
params,
vec![],
None,
client,
)
.await?;
let subscriptions: QuickFeed = Self::deserialize(&response)?;
Ok(subscriptions)
}
pub async fn import(&self, opml: String, client: &Client) -> Result<u64, ApiError> {
let response = self
.post_request(
"reader/api/0/subscription/import",
vec![],
vec![],
Some(&opml),
client,
)
.await?;
if response.starts_with("OK: ") {
Ok(response.replace("Ok: ", "").parse::<u64>().unwrap())
} else {
Err(ApiError::GReader(GReaderError {
errors: vec![response],
}))
}
}
pub async fn export(&self, client: &Client) -> Result<String, ApiError> {
self.get_request("reader/api/0/subscription/export", vec![], client)
.await
}
#[cfg(feature = "feedhq")]
pub async fn subscribed(&self, stream_id: &str, client: &Client) -> Result<bool, ApiError> {
let params = vec![("s", stream_id.into())];
let response = self
.get_request("reader/api/0/subscribed", params, client)
.await?;
match &response[..] {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(ApiError::GReader(GReaderError {
errors: vec![response.to_string()],
})),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn stream_contents(
&self,
stream_id: Option<&str>,
reverse_order: bool,
amount: Option<u64>,
continuation: Option<&str>,
exclude_stream: Option<&str>,
include_stream: Option<&str>,
filter_older: Option<i64>,
filter_newer: Option<i64>,
client: &Client,
) -> Result<Stream, ApiError> {
let mut params = Vec::new();
if reverse_order {
params.push(("r", "o".into()));
}
if let Some(n) = amount {
params.push(("n", n.to_string()));
}
if let Some(c) = continuation {
params.push(("c", c.into()));
}
if let Some(s) = exclude_stream {
params.push(("xt", s.into()));
}
if let Some(s) = include_stream {
params.push(("it", s.into()));
}
if let Some(t) = filter_older {
params.push(("ot", t.to_string()));
}
if let Some(t) = filter_newer {
params.push(("nt", t.to_string()));
}
let query = "reader/api/0/stream/contents";
let query = if let Some(stream_id) = stream_id {
format!("{}/{}", query, stream_id)
} else {
query.into()
};
let response = self
.post_request(&query, params, vec![], None, client)
.await?;
Self::deserialize(&response)
}
#[allow(clippy::too_many_arguments)]
pub async fn items_ids(
&self,
stream_id: Option<&str>,
amount: Option<u64>,
include_all_direct_stream_ids: bool,
continuation: Option<&str>,
exclude_stream: Option<&str>,
include_stream: Option<&str>,
filter_older: Option<i64>,
filter_newer: Option<i64>,
client: &Client,
) -> Result<ItemRefs, ApiError> {
let mut params = Vec::new();
if let Some(amount) = amount {
params.push(("n", amount.to_string()));
}
if let Some(stream_id) = stream_id {
params.push(("s", stream_id.into()));
}
if let Some(c) = continuation {
params.push(("c", c.into()));
}
if include_all_direct_stream_ids {
params.push(("includeAllDirectStreamIds", "true".into()));
}
if let Some(s) = exclude_stream {
params.push(("xt", s.into()));
}
if let Some(s) = include_stream {
params.push(("it", s.into()));
}
if let Some(t) = filter_older {
params.push(("ot", t.to_string()));
}
if let Some(t) = filter_newer {
params.push(("nt", t.to_string()));
}
let response = self
.get_request("reader/api/0/stream/items/ids", params, client)
.await?;
Self::deserialize(&response)
}
#[cfg(feature = "feedhq")]
pub async fn items_count(
&self,
stream_id: &str,
get_latest_date: bool,
client: &Client,
) -> Result<String, ApiError> {
let mut params = Vec::new();
params.push(("s", stream_id.into()));
if get_latest_date {
params.push(("a", "true".into()));
}
let response = self
.get_request("reader/api/0/stream/items/count", params, client)
.await?;
Ok(response)
}
pub async fn items_contents(
&self,
item_ids: Vec<String>,
client: &Client,
) -> Result<Stream, ApiError> {
let params = Vec::new();
let mut form_params = Vec::new();
for item_id in item_ids {
form_params.push(("i", item_id));
}
let response = self
.post_request(
"reader/api/0/stream/items/contents",
params,
form_params,
None,
client,
)
.await?;
Self::deserialize(&response)
}
pub async fn tag_list(&self, client: &Client) -> Result<Taggings, ApiError> {
let response = self
.get_request("reader/api/0/tag/list", vec![], client)
.await?;
Self::deserialize(&response)
}
pub async fn tag_delete(
&self,
stream_type: StreamType,
id: &str,
client: &Client,
) -> Result<(), ApiError> {
let form_params = vec![(stream_type.into(), id.into())];
let response = self
.post_request(
"reader/api/0/disable-tag",
vec![],
form_params,
None,
client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn tag_rename(
&self,
stream_type: StreamType,
old_name: &str,
new_name: &str,
client: &Client,
) -> Result<(), ApiError> {
let form_params = vec![
(stream_type.into(), old_name.into()),
("dest", new_name.into()),
];
let response = self
.post_request("reader/api/0/rename-tag", vec![], form_params, None, client)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn tag_edit(
&self,
item_ids: &[&str],
tag_add: Option<&str>,
tag_remove: Option<&str>,
client: &Client,
) -> Result<(), ApiError> {
if tag_add.is_none() && tag_remove.is_none() {
return Err(ApiError::Input);
}
let mut form_params = Vec::new();
for item_id in item_ids {
form_params.push(("i", item_id.to_string()));
}
if let Some(remove) = tag_remove {
form_params.push(("r", remove.into()));
}
if let Some(add) = tag_add {
form_params.push(("a", add.into()));
}
let response = self
.post_request("reader/api/0/edit-tag", vec![], form_params, None, client)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn mark_all_as_read(
&self,
stream_id: &str,
older_than: Option<u64>,
client: &Client,
) -> Result<(), ApiError> {
let mut params = Vec::new();
let form_params = vec![("s", stream_id.into())];
if let Some(older_than) = older_than {
params.push(("ts", older_than.to_string()));
}
let response = self
.post_request(
"reader/api/0/mark-all-as-read",
params,
form_params,
None,
client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
#[allow(unused)]
#[cfg(any(feature = "feedhq", feature = "oldreader"))]
pub async fn preference_list(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(any(feature = "feedhq", feature = "oldreader", feature = "inoreader"))]
pub async fn preference_stream_list(&self, client: &Client) -> Result<StreamPrefs, ApiError> {
let response = self
.get_request("reader/api/0/preference/stream/list", vec![], client)
.await?;
Self::deserialize(&response)
}
#[allow(unused)]
#[cfg(any(feature = "feedhq", feature = "oldreader"))]
pub async fn friends_list(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[allow(unused)]
#[cfg(feature = "oldreader")]
pub async fn friends_edit(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[allow(unused)]
#[cfg(feature = "inoreader")]
pub async fn inoreader_refresh_token(
&self,
client: &Client,
) -> Result<InoreaderAuth, ApiError> {
let auth_data = match &*self.auth.lock().map_err(|_| ApiError::InternalMutabilty)? {
AuthData::Inoreader(auth_data) => auth_data.clone(),
_ => return Err(ApiError::Token.into()),
};
let client_id = auth_data.client_id.clone();
let client_secret = auth_data.client_secret.clone();
let refresh_token = auth_data.refresh_token.clone();
let mut map: HashMap<String, String> = HashMap::new();
map.insert("client_id".into(), client_id);
map.insert("client_secret".into(), client_secret);
map.insert("grant_type".into(), "refresh_token".into());
map.insert("refresh_token".into(), refresh_token);
let response = client
.post("https://www.inoreader.com/oauth2/token")
.form(&map)
.send()
.await?
.text()
.await?;
let oauth_response: OAuthResponse = Self::deserialize(&response)?;
let now = Utc::now();
let token_expires = now + Duration::seconds(oauth_response.expires_in);
let inoreader_auth = InoreaderAuth {
client_id: auth_data.client_id.clone(),
client_secret: auth_data.client_secret.clone(),
access_token: oauth_response.access_token,
refresh_token: oauth_response.refresh_token,
expires_at: token_expires,
};
*self.auth.lock().map_err(|_| ApiError::InternalMutabilty)? =
AuthData::Inoreader(inoreader_auth.clone());
Ok(inoreader_auth)
}
#[allow(unused)]
#[cfg(feature = "inoreader")]
pub async fn create_active_search(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[allow(unused)]
#[cfg(feature = "inoreader")]
pub async fn delete_active_search(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[allow(unused)]
#[cfg(feature = "oldreader")]
pub async fn add_comment(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
}