mod deserialize;
pub mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use crate::error::{ApiError, ApiErrorKind};
use crate::models::{
FavIcons, Feeds, FeverError, Groups, ItemStatus, Items, Links, SavedItems, UnreadItems,
};
use failure::ResultExt;
use log::error;
use reqwest::{multipart::Form, Client, StatusCode};
use url::Url;
type FeedID = u64;
type GroupID = u64;
type FeedGroupID = u64;
type ItemID = u64;
type IconID = u64;
type LinkID = u64;
pub struct FeverApi {
base_uri: Url,
api_key: String,
http_user: Option<String>,
http_pass: Option<String>,
}
impl FeverApi {
pub fn new(url: &Url, username: &str, password: &str) -> Self {
let base_uri = url.clone();
let auth = format!("{}:{}", username, password);
let api_key = md5::compute(auth);
FeverApi {
base_uri,
api_key: format!("{:?}", api_key),
http_user: None,
http_pass: None,
}
}
pub fn new_with_http_auth(
url: &Url,
username: &str,
password: &str,
http_user: &str,
http_pass: Option<&str>,
) -> Self {
let mut api = Self::new(url, username, password);
api.http_user = Some(http_user.into());
api.http_pass = http_pass.map(|s| s.into());
api
}
async fn post_request(&self, client: &Client, query: String) -> Result<String, ApiError> {
let full_query = format!("?api&{}", query);
let api_url: Url = self.base_uri.join(&full_query).context(ApiErrorKind::Url)?;
let form = Form::new().text("api_key", self.api_key.clone());
let request_builder = client.post(api_url);
let request_builder = if let Some(http_user) = &self.http_user {
request_builder.basic_auth(http_user, self.http_pass.as_ref())
} else {
request_builder
};
let response = request_builder
.multipart(form)
.send()
.await
.context(ApiErrorKind::Http)?;
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status == StatusCode::UNAUTHORIZED {
return Err(ApiErrorKind::Unauthorized.into());
}
if status != StatusCode::OK {
let error: FeverError = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
error!("Fever API: {}", error.error_message);
return Err(ApiErrorKind::Fever(error).into());
}
Ok(response)
}
async fn check_auth(response: &str) -> Result<(), ApiError> {
let tmp: serde_json::Value = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
if tmp["auth"].as_i64().unwrap() == 0 {
return Err(ApiErrorKind::AccessDenied.into());
}
Ok(())
}
pub async fn valid_credentials(&self, client: &Client) -> Result<bool, ApiError> {
let response = self.post_request(&client, "".to_string()).await?;
let tmp: serde_json::Value = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
if tmp["auth"].as_i64().unwrap() == 0 {
return Ok(false);
}
Ok(true)
}
pub async fn get_api_version(&self, client: &Client) -> Result<i64, ApiError> {
let response = self.post_request(&client, "".to_string()).await?;
let result: serde_json::Value =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(result["api_version"].as_i64().unwrap())
}
pub async fn get_groups(&self, client: &Client) -> Result<Groups, ApiError> {
let response = self.post_request(&client, "groups".to_string()).await?;
Self::check_auth(&response).await?;
let groups: Groups = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(groups)
}
pub async fn get_feeds(&self, client: &Client) -> Result<Feeds, ApiError> {
let response = self.post_request(&client, "feeds".to_string()).await?;
Self::check_auth(&response).await?;
let feeds: Feeds = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(feeds)
}
pub async fn get_favicons(&self, client: &Client) -> Result<FavIcons, ApiError> {
let response = self.post_request(&client, "favicons".to_string()).await?;
Self::check_auth(&response).await?;
let favicons: FavIcons = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(favicons)
}
pub async fn get_items(&self, client: &Client) -> Result<Items, ApiError> {
let response = self.post_request(&client, "items".to_string()).await?;
Self::check_auth(&response).await?;
let items: Items = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(items)
}
pub async fn get_items_since(&self, id: ItemID, client: &Client) -> Result<Items, ApiError> {
let query = format!("items&since_id={}", id);
let response = self.post_request(&client, query).await?;
Self::check_auth(&response).await?;
let items: Items = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(items)
}
pub async fn get_items_max(&self, id: ItemID, client: &Client) -> Result<Items, ApiError> {
let query = format!("items&max_id={}", id);
let response = self.post_request(&client, query).await?;
Self::check_auth(&response).await?;
let items: Items = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(items)
}
pub async fn get_items_with(
&self,
ids: Vec<ItemID>,
client: &Client,
) -> Result<Items, ApiError> {
if ids.len() > 50 {
return Err(ApiErrorKind::Input.into());
}
let list = ids
.iter()
.map(ToString::to_string)
.collect::<Vec<String>>()
.join(",");
let query = format!("items&with_ids={}", list);
let response = self.post_request(&client, query).await?;
Self::check_auth(&response).await?;
let items: Items = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(items)
}
pub async fn get_links(&self, client: &Client) -> Result<Links, ApiError> {
let response = self.post_request(&client, "links".to_string()).await?;
Self::check_auth(&response).await?;
let links: Links = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(links)
}
pub async fn get_links_with(
&self,
offset: u64,
days: u64,
page: u64,
client: &Client,
) -> Result<Links, ApiError> {
let query = format!("links&offset={}&range={}&page={}", offset, days, page);
let response = self.post_request(&client, query).await?;
Self::check_auth(&response).await?;
let links: Links = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(links)
}
pub async fn get_unread_items(&self, client: &Client) -> Result<UnreadItems, ApiError> {
let response = self
.post_request(&client, "unread_item_ids".to_string())
.await?;
Self::check_auth(&response).await?;
let items: UnreadItems = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(items)
}
pub async fn get_saved_items(&self, client: &Client) -> Result<SavedItems, ApiError> {
let response = self
.post_request(&client, "saved_item_ids".to_string())
.await?;
Self::check_auth(&response).await?;
let items: SavedItems = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(items)
}
pub async fn mark_item(
&self,
status: ItemStatus,
id: ItemID,
client: &Client,
) -> Result<(), ApiError> {
let state: &str = status.into();
let query = format!("&mark=item&as={}&id={}", state, id);
let response = self.post_request(&client, query).await?;
Self::check_auth(&response).await?;
Ok(())
}
async fn mark_feed_or_group(
&self,
target: String,
status: ItemStatus,
id: i64,
before: i64,
client: &Client,
) -> Result<(), ApiError> {
let state: &str = status.into();
let query = format!("&mark={}&as={}&id={}&before={}", target, state, id, before);
let response = self.post_request(&client, query).await?;
Self::check_auth(&response).await?;
Ok(())
}
pub async fn mark_group(
&self,
status: ItemStatus,
id: i64,
before: i64,
client: &Client,
) -> Result<(), ApiError> {
self.mark_feed_or_group("group".to_string(), status, id, before, client)
.await
}
pub async fn mark_feed(
&self,
status: ItemStatus,
id: i64,
before: i64,
client: &Client,
) -> Result<(), ApiError> {
self.mark_feed_or_group("feed".to_string(), status, id, before, client)
.await
}
}