mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use crate::error::{ApiError, ApiErrorKind};
use crate::models::GReaderError;
use crate::models::{Feeds, ItemRefs, QuickFeed, Stream, StreamType, Taggings, Unread, User};
use failure::ResultExt;
use log::error;
use reqwest::header::AUTHORIZATION;
use reqwest::{Client, StatusCode};
use url::Url;
pub struct GReaderApi {
base_uri: Url,
username: String,
password: String,
auth_token: Option<String>,
post_token: Option<String>,
}
impl GReaderApi {
pub fn new(url: &Url, username: &str, password: &str) -> Self {
let url_tmp = Url::parse(&(url.as_str().to_owned() + "/")).unwrap();
GReaderApi {
base_uri: url_tmp,
username: username.to_string(),
password: password.to_string(),
auth_token: None,
post_token: None,
}
}
async fn get_request(
&self,
query: String,
params: &mut Vec<(String, String)>,
client: &Client,
) -> Result<String, ApiError> {
let api_url: Url = self.base_uri.join(&query).context(ApiErrorKind::Url)?;
let auth_string = format!(
"GoogleLogin auth={}",
&self.auth_token.as_ref().unwrap().clone()
);
params.push(("output".to_string(), "json".to_string()));
let response = client
.get(api_url.clone())
.header(AUTHORIZATION, auth_string)
.query(¶ms)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: GReaderError =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
error!("GReader API: {}", error.errors.join("; "));
return Err(ApiErrorKind::GReader(error).into());
}
Ok(response)
}
async fn post_request(
&self,
query: String,
params: &mut Vec<(String, String)>,
form_params: Option<Vec<(String, String)>>,
client: &Client,
) -> Result<String, ApiError> {
let api_url: Url = self.base_uri.join(&query).context(ApiErrorKind::Url)?;
let auth_string = format!(
"GoogleLogin auth={}",
&self.auth_token.as_ref().unwrap().clone()
);
params.push(("output".to_string(), "json".to_string()));
if self.post_token.is_none() {
return Err(ApiErrorKind::Token.into());
}
let post_token: Option<&str> = self.post_token.as_deref();
params.push(("T".to_string(), post_token.unwrap().to_string()));
let response = client
.post(api_url.clone())
.header(AUTHORIZATION, auth_string)
.query(¶ms)
.form(&form_params)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: GReaderError =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
error!("GReader API: {}", error.errors.join("; "));
return Err(ApiErrorKind::GReader(error).into());
}
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(ApiErrorKind::GReader(error).into())
}
}
pub async fn login(&mut self, client: &Client) -> Result<(), ApiError> {
let path = format!(
"accounts/ClientLogin?Email={}&Passwd={}",
self.username, self.password
);
let api_url: Url = self.base_uri.join(&path).context(ApiErrorKind::Url)?;
let response = client
.post(api_url.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
return Err(ApiErrorKind::AccessDenied.into());
}
let auth_string = response.lines().nth(2).unwrap();
let auth_token = auth_string.split('=').nth(1).unwrap();
self.auth_token = Some(auth_token.to_string());
Ok(())
}
pub async fn token(&mut self, client: &Client) -> Result<(), ApiError> {
let mut response = self
.get_request("reader/api/0/token".to_string(), &mut Vec::new(), &client)
.await?;
response.pop();
self.post_token = Some(response);
Ok(())
}
pub async fn user_info(&self, client: &Client) -> Result<User, ApiError> {
let response = self
.get_request(
"reader/api/0/user-info".to_string(),
&mut Vec::new(),
&client,
)
.await?;
let user: User = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(user)
}
pub async fn unread_count(&self, client: &Client) -> Result<Unread, ApiError> {
let response = self
.get_request(
"reader/api/0/unread-count".to_string(),
&mut Vec::new(),
&client,
)
.await?;
let unread: Unread = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(unread)
}
pub async fn subscription_list(&self, client: &Client) -> Result<Feeds, ApiError> {
let response = self
.get_request(
"reader/api/0/subscription/list".to_string(),
&mut Vec::new(),
&client,
)
.await?;
let subscriptions: Feeds = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(subscriptions)
}
pub async fn subscription_create(
&self,
url: &Url,
name: &str,
to_stream: Option<&str>,
client: &Client,
) -> Result<(), ApiError> {
let mut params: Vec<(String, String)> = Vec::new();
params.push(("ac".to_string(), "subscribe".to_string()));
let feed = format!("feed/{}", &url.as_str());
params.push(("s".to_string(), feed));
params.push(("t".to_string(), name.to_string()));
if to_stream.is_some() {
params.push(("a".to_string(), to_stream.unwrap().to_string()));
}
let response = self
.post_request(
"reader/api/0/subscription/edit".to_string(),
&mut params,
None,
&client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn subscription_edit(
&self,
url: &Url,
name: &str,
from_stream: &str,
to_stream: &str,
client: &Client,
) -> Result<(), ApiError> {
let mut params: Vec<(String, String)> = Vec::new();
params.push(("ac".to_string(), "edit".to_string()));
let feed = format!("feed/{}", &url.as_str());
params.push(("s".to_string(), feed));
params.push(("t".to_string(), name.to_string()));
params.push(("r".to_string(), from_stream.to_string()));
params.push(("a".to_string(), to_stream.to_string()));
let response = self
.post_request(
"reader/api/0/subscription/edit".to_string(),
&mut params,
None,
&client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn subscription_delete(&self, url: &Url, client: &Client) -> Result<(), ApiError> {
let mut params: Vec<(String, String)> = Vec::new();
params.push(("ac".to_string(), "unsubscribe".to_string()));
let feed = format!("feed/{}", &url.as_str());
params.push(("s".to_string(), feed));
let response = self
.post_request(
"reader/api/0/subscription/edit".to_string(),
&mut params,
None,
&client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn subscription_quickadd(
&self,
url: &Url,
client: &Client,
) -> Result<QuickFeed, ApiError> {
let mut params: Vec<(String, String)> = Vec::new();
params.push(("quickadd".to_string(), (&url.as_str()).to_string()));
let response = self
.post_request(
"reader/api/0/subscription/quickadd".to_string(),
&mut params,
None,
&client,
)
.await?;
let subscriptions: QuickFeed =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(subscriptions)
}
pub async fn import(&self, opml: String, client: &Client) -> Result<u64, ApiError> {
let mut params: Vec<(String, String)> = Vec::new();
let api_url: Url = self
.base_uri
.join("reader/api/0/subscription/import")
.context(ApiErrorKind::Url)?;
let auth_string = format!(
"GoogleLogin auth={}",
&self.auth_token.as_ref().unwrap().clone()
);
if self.post_token.is_none() {
return Err(ApiErrorKind::Token.into());
}
let post_token: Option<&str> = self.post_token.as_deref();
params.push(("T".to_string(), post_token.unwrap().to_string()));
let response = client
.post(api_url.clone())
.header(AUTHORIZATION, auth_string)
.query(¶ms)
.body(opml)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: GReaderError =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
error!("GReader API: {}", error.errors.join("; "));
return Err(ApiErrorKind::GReader(error).into());
}
if response.starts_with("OK: ") {
Ok(response.replace("Ok: ", "").parse::<u64>().unwrap())
} else {
let error: GReaderError = GReaderError {
errors: vec![response],
};
Err(ApiErrorKind::GReader(error).into())
}
}
pub async fn export(&self, client: &Client) -> Result<String, ApiError> {
let api_url: Url = self
.base_uri
.join("reader/api/0/subscription/export")
.context(ApiErrorKind::Url)?;
let auth_string = format!(
"GoogleLogin auth={}",
&self.auth_token.as_ref().unwrap().clone()
);
let response = client
.get(api_url.clone())
.header(AUTHORIZATION, auth_string)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != StatusCode::OK {
let error: GReaderError =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
error!("GReader API: {}", error.errors.join("; "));
return Err(ApiErrorKind::GReader(error).into());
}
Ok(response)
}
#[cfg(feature = "feedhq")]
pub async fn subscribed(&self, stream_id: &str, client: &Client) -> Result<bool, ApiError> {
let mut params: Vec<(String, String)> = Vec::new();
params.push(("s".to_string(), stream_id.to_string()));
let response = self
.get_request("reader/api/0/subscribed".to_string(), &mut params, &client)
.await?;
println!("response {:?}", response);
match &response[..] {
"true" => Ok(true),
"false" => Ok(false),
_ => {
let error: GReaderError = GReaderError {
errors: vec![response.to_string()],
};
Err(ApiErrorKind::GReader(error).into())
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn stream_contents(
&self,
stream_id: &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<(String, String)> = Vec::new();
if reverse_order {
params.push(("r".to_string(), "o".to_string()));
}
if let Some(n) = amount {
params.push(("n".to_string(), n.to_string()))
}
if let Some(c) = continuation {
params.push(("c".to_string(), c.to_string()))
}
if let Some(s) = exclude_stream {
params.push(("xt".to_string(), s.to_string()))
}
if let Some(s) = include_stream {
params.push(("it".to_string(), s.to_string()))
}
if let Some(t) = filter_older {
params.push(("ot".to_string(), t.to_string()))
}
if let Some(t) = filter_newer {
params.push(("nt".to_string(), t.to_string()))
}
let query = format!("reader/api/0/stream/contents/{}", stream_id);
let response = self.post_request(query, &mut params, None, &client).await?;
let stream: Stream = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(stream)
}
#[allow(clippy::too_many_arguments)]
pub async fn items_ids(
&self,
stream_id: &str,
amount: 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<(String, String)> = Vec::new();
params.push(("s".to_string(), stream_id.to_string()));
params.push(("n".to_string(), amount.to_string()));
if let Some(c) = continuation {
params.push(("c".to_string(), c.to_string()))
}
if include_all_direct_stream_ids {
params.push(("includeAllDirectStreamIds".to_string(), "true".to_string()));
}
if let Some(s) = exclude_stream {
params.push(("xt".to_string(), s.to_string()))
}
if let Some(s) = include_stream {
params.push(("it".to_string(), s.to_string()))
}
if let Some(t) = filter_older {
params.push(("ot".to_string(), t.to_string()))
}
if let Some(t) = filter_newer {
params.push(("nt".to_string(), t.to_string()))
}
let response = self
.get_request(
"reader/api/0/stream/items/ids".to_string(),
&mut params,
&client,
)
.await?;
let item_refs: ItemRefs = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(item_refs)
}
#[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<(String, String)> = Vec::new();
params.push(("s".to_string(), stream_id.to_string()));
if get_latest_date {
params.push(("a".to_string(), "true".to_string()))
}
let response = self
.get_request(
"reader/api/0/stream/items/count".to_string(),
&mut params,
&client,
)
.await?;
Ok(response)
}
pub async fn items_contents(
&self,
item_ids: Vec<String>,
client: &Client,
) -> Result<Stream, ApiError> {
let mut form_params: Vec<(String, String)> = Vec::new();
for item_id in item_ids {
form_params.push(("i".to_string(), item_id.to_string()))
}
let response = self
.post_request(
"reader/api/0/stream/items/contents".to_string(),
&mut Vec::new(),
Some(form_params),
&client,
)
.await?;
let stream: Stream = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(stream)
}
pub async fn tag_list(&self, client: &Client) -> Result<Taggings, ApiError> {
let response = self
.get_request(
"reader/api/0/tag/list".to_string(),
&mut Vec::new(),
&client,
)
.await?;
let tags: Taggings = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(tags)
}
pub async fn tag_delete(
&self,
stream_type: StreamType,
id: &str,
client: &Client,
) -> Result<(), ApiError> {
let mut form_params: Vec<(String, String)> = Vec::new();
let target: String = String::from(stream_type);
form_params.push((target, id.to_string()));
let response = self
.post_request(
"reader/api/0/disable-tag".to_string(),
&mut Vec::new(),
Some(form_params),
&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 mut form_params: Vec<(String, String)> = Vec::new();
let target: String = String::from(stream_type);
form_params.push((target, old_name.to_string()));
form_params.push(("dest".to_string(), new_name.to_string()));
let response = self
.post_request(
"reader/api/0/rename-tag".to_string(),
&mut Vec::new(),
Some(form_params),
&client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
pub async fn tag_edit(
&self,
articles: Vec<(&str, Option<&str>, Option<&str>)>,
client: &Client,
) -> Result<(), ApiError> {
let mut form_params: Vec<(String, String)> = Vec::new();
for (item_id, add, remove) in articles {
if remove.is_some() {
form_params.push(("r".to_string(), remove.unwrap().to_string()));
}
if add.is_some() {
form_params.push(("a".to_string(), add.unwrap().to_string()));
}
form_params.push(("i".to_string(), item_id.to_string()));
}
let response = self
.post_request(
"reader/api/0/edit-tag".to_string(),
&mut Vec::new(),
Some(form_params),
&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<(String, String)> = Vec::new();
params.push(("s".to_string(), stream_id.to_string()));
if older_than.is_some() {
params.push(("ts".to_string(), older_than.unwrap().to_string()));
}
let response = self
.post_request(
"reader/api/0/mark-all-as-read".to_string(),
&mut params,
None,
&client,
)
.await?;
GReaderApi::chech_ok_response(&response)
}
#[cfg(any(feature = "feedhq", feature = "oldreader"))]
pub async fn preference_list(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(any(feature = "feedhq", feature = "oldreader"))]
pub async fn preference_stream_list(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(any(feature = "feedhq", feature = "oldreader"))]
pub async fn friends_list(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(feature = "oldreader")]
pub async fn friends_edit(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(feature = "innoreader")]
pub async fn create_active_search(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(feature = "innoreader")]
pub async fn delete_active_search(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
#[cfg(feature = "oldreader")]
pub async fn add_comment(&self, client: &Client) -> Result<(), ApiError> {
unimplemented!();
}
}