fever_api 0.6.0

rust implementation of the FEVER-API
Documentation
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 {
    /// Create a new instance of the 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
    }
}