mod deserialize;
pub mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use crate::error::ApiError;
use crate::models::{
FavIcons, Feeds, FeverError, Groups, ItemStatus, Items, Links, SavedItems, UnreadItems,
};
use log::error;
use reqwest::{multipart::Form, Client, StatusCode};
use serde::Deserialize;
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
}
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 post_request(&self, client: &Client, query: Option<&str>) -> Result<String, ApiError> {
let full_query = if let Some(query) = query {
format!("?api&{}", query)
} else {
"?api".into()
};
let api_url: Url = self.base_uri.join(&full_query)?;
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?;
let status = response.status();
if status == StatusCode::UNAUTHORIZED {
return Err(ApiError::Unauthorized);
}
let response = response.text().await?;
if status != StatusCode::OK {
let error: FeverError = Self::deserialize(&response)?;
error!("Fever API: {}", error.error_message);
return Err(ApiError::Fever(error));
}
Ok(response)
}
fn check_auth(response: &str) -> Result<bool, ApiError> {
let tmp: serde_json::Value =
serde_json::from_str(response).map_err(|e| ApiError::Json {
source: e,
json: response.into(),
})?;
let auth_value = tmp.get("auth");
let authenticated = auth_value
.and_then(|value| value.as_i64())
.map(|auth| auth > 0)
.unwrap_or(false);
if !authenticated {
log::error!(
"Unauthenticated: expected to find 'auth' with value of 1. Instead found '{:?}'",
auth_value
);
}
Ok(authenticated)
}
pub async fn valid_credentials(&self, client: &Client) -> Result<bool, ApiError> {
let response = self.post_request(client, None).await?;
Self::check_auth(&response)
}
pub async fn get_api_version(&self, client: &Client) -> Result<i64, ApiError> {
let response = self.post_request(client, None).await?;
let result: serde_json::Value =
serde_json::from_str(&response).map_err(|e| ApiError::Json {
source: e,
json: response,
})?;
let api_version_value = result.get("api_version");
let api_version = api_version_value
.and_then(|v| v.as_i64())
.ok_or_else(|| {
log::error!("Unable to get version. Expected 'api_version' of type integer. Instead found '{:?}'", api_version_value);
ApiError::Unknown
})?;
Ok(api_version)
}
pub async fn get_groups(&self, client: &Client) -> Result<Groups, ApiError> {
let response = self.post_request(client, Some("groups")).await?;
Self::check_auth(&response)?;
let groups: Groups = Self::deserialize(&response)?;
Ok(groups)
}
pub async fn get_feeds(&self, client: &Client) -> Result<Feeds, ApiError> {
let response = self.post_request(client, Some("feeds")).await?;
Self::check_auth(&response)?;
let feeds: Feeds = Self::deserialize(&response)?;
Ok(feeds)
}
pub async fn get_favicons(&self, client: &Client) -> Result<FavIcons, ApiError> {
let response = self.post_request(client, Some("favicons")).await?;
Self::check_auth(&response)?;
let favicons: FavIcons = Self::deserialize(&response)?;
Ok(favicons)
}
pub async fn get_items(&self, client: &Client) -> Result<Items, ApiError> {
let response = self.post_request(client, Some("items")).await?;
Self::check_auth(&response)?;
let items: Items = Self::deserialize(&response)?;
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, Some(&query)).await?;
Self::check_auth(&response)?;
let items: Items = Self::deserialize(&response)?;
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, Some(&query)).await?;
Self::check_auth(&response)?;
let items: Items = Self::deserialize(&response)?;
Ok(items)
}
pub async fn get_items_with(
&self,
ids: Vec<ItemId>,
client: &Client,
) -> Result<Items, ApiError> {
if ids.len() > 50 {
return Err(ApiError::Input);
}
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, Some(&query)).await?;
Self::check_auth(&response)?;
let items: Items = Self::deserialize(&response)?;
Ok(items)
}
pub async fn get_links(&self, client: &Client) -> Result<Links, ApiError> {
let response = self.post_request(client, Some("links")).await?;
Self::check_auth(&response)?;
let links: Links = Self::deserialize(&response)?;
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, Some(&query)).await?;
Self::check_auth(&response)?;
let links: Links = Self::deserialize(&response)?;
Ok(links)
}
pub async fn get_unread_items(&self, client: &Client) -> Result<UnreadItems, ApiError> {
let response = self.post_request(client, Some("unread_item_ids")).await?;
Self::check_auth(&response)?;
let items: UnreadItems = Self::deserialize(&response)?;
Ok(items)
}
pub async fn get_saved_items(&self, client: &Client) -> Result<SavedItems, ApiError> {
let response = self.post_request(client, Some("saved_item_ids")).await?;
Self::check_auth(&response)?;
let items: SavedItems = Self::deserialize(&response)?;
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, Some(&query)).await?;
Self::check_auth(&response)?;
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, Some(&query)).await?;
Self::check_auth(&response)?;
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
}
}