mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub use crate::error::{ApiError, ApiErrorKind};
use crate::models::{
Category, CategoryInput, Entry, EntryBatch, EntryStateUpdate, EntryStatus, FavIcon, Feed,
FeedCreation, FeedDiscovery, FeedModification, MinifluxError, OrderBy, OrderDirection, User,
UserCreation, UserModification,
};
use failure::ResultExt;
use log::error;
use reqwest::{header::AUTHORIZATION, Client, StatusCode};
use url::Url;
type FeedID = i64;
type CategoryID = i64;
type EntryID = i64;
type UserID = i64;
type IconID = i64;
pub struct MinifluxApi {
base_uri: Url,
auth: String,
}
impl MinifluxApi {
pub fn new(url: &Url, username: String, password: String) -> Self {
MinifluxApi {
base_uri: url.clone(),
auth: Self::generate_basic_auth(&username, &password),
}
}
fn generate_basic_auth(username: &str, password: &str) -> String {
let auth = format!("{}:{}", username, password);
let auth = base64::encode(&auth);
format!("Basic {}", auth)
}
async fn parse_error(
response: reqwest::Response,
expected_http: StatusCode,
) -> Result<String, ApiError> {
let status = response.status();
let response = response.text().await.context(ApiErrorKind::Http)?;
if status != expected_http {
let error: MinifluxError =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
error!("Miniflux API: {}", error.error_message);
return Err(ApiErrorKind::Miniflux(error).into());
}
Ok(response)
}
pub async fn discover_subscription(
&self,
url: Url,
client: &Client,
) -> Result<Vec<Feed>, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/discover")
.context(ApiErrorKind::Url)?;
let content = FeedDiscovery {
url: url.to_string(),
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.post(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let feeds: Vec<Feed> = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(feeds)
}
pub async fn get_feeds(&self, client: &Client) -> Result<Vec<Feed>, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/feeds")
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let feeds: Vec<Feed> = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(feeds)
}
pub async fn get_feed(&self, id: FeedID, client: &Client) -> Result<Feed, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}", id))
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let feed: Feed = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(feed)
}
pub async fn get_feed_icon(&self, id: FeedID, client: &Client) -> Result<FavIcon, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}/icon", id))
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let icon: FavIcon = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(icon)
}
pub async fn create_feed(
&self,
feed_url: &Url,
category_id: CategoryID,
client: &Client,
) -> Result<FeedID, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/feeds")
.context(ApiErrorKind::Url)?;
let content = FeedCreation {
feed_url: feed_url.to_string(),
category_id,
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.post(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::CREATED).await?;
let value: serde_json::Value =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
let id: FeedID = value
.get("feed_id")
.ok_or(ApiErrorKind::Json)?
.as_i64()
.ok_or(ApiErrorKind::Json)? as FeedID;
Ok(id)
}
#[allow(clippy::too_many_arguments)]
pub async fn update_feed(
&self,
id: FeedID,
title: Option<&str>,
category_id: Option<CategoryID>,
feed_url: Option<&str>,
site_url: Option<&str>,
username: Option<&str>,
password: Option<&str>,
user_agent: Option<&str>,
client: &Client,
) -> Result<Feed, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}", id))
.context(ApiErrorKind::Url)?;
let content = FeedModification {
title: title.map(|t| t.into()),
category_id,
feed_url: feed_url.map(|t| t.into()),
site_url: site_url.map(|t| t.into()),
username: username.map(|t| t.into()),
password: password.map(|t| t.into()),
scraper_rules: None,
rewrite_rules: None,
crawler: None,
user_agent: user_agent.map(|t| t.into()),
disabled: None,
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.put(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::CREATED).await?;
let feed: Feed = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(feed)
}
pub async fn refresh_feed_synchronous(
&self,
id: FeedID,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}/refresh", id))
.context(ApiErrorKind::Url)?;
let response = client
.put(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
Ok(())
}
pub async fn delete_feed(&self, id: FeedID, client: &Client) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}", id))
.context(ApiErrorKind::Url)?;
let response = client
.delete(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
Ok(())
}
pub async fn get_feed_entry(
&self,
feed_id: FeedID,
entry_id: EntryID,
client: &Client,
) -> Result<Entry, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}/entries/{}", feed_id, entry_id))
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let entry: Entry = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(entry)
}
pub async fn get_entry(&self, id: EntryID, client: &Client) -> Result<Entry, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/entries/{}", id))
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let entry: Entry = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(entry)
}
#[allow(clippy::too_many_arguments)]
pub async fn get_entries(
&self,
status: Option<EntryStatus>,
offset: Option<i64>,
limit: Option<i64>,
order: Option<OrderBy>,
direction: Option<OrderDirection>,
before: Option<i64>,
after: Option<i64>,
before_entry_id: Option<EntryID>,
after_entry_id: Option<EntryID>,
starred: Option<bool>,
client: &Client,
) -> Result<Vec<Entry>, ApiError> {
let mut api_url = self
.base_uri
.clone()
.join("v1/entries")
.context(ApiErrorKind::Url)?;
{
let mut query_pairs = api_url.query_pairs_mut();
query_pairs.clear();
if let Some(status) = status {
query_pairs.append_pair("status", status.into());
}
if let Some(offset) = offset {
query_pairs.append_pair("offset", &offset.to_string());
}
if let Some(limit) = limit {
query_pairs.append_pair("limit", &limit.to_string());
}
if let Some(order) = order {
query_pairs.append_pair("order", order.into());
}
if let Some(direction) = direction {
query_pairs.append_pair("direction", direction.into());
}
if let Some(before) = before {
query_pairs.append_pair("before", &before.to_string());
}
if let Some(after) = after {
query_pairs.append_pair("after", &after.to_string());
}
if let Some(before_entry_id) = before_entry_id {
query_pairs.append_pair("before_entry_id", &before_entry_id.to_string());
}
if let Some(after_entry_id) = after_entry_id {
query_pairs.append_pair("after_entry_id", &after_entry_id.to_string());
}
if let Some(starred) = starred {
query_pairs.append_pair("starred", &starred.to_string());
}
}
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let batch: EntryBatch = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(batch.entries)
}
#[allow(clippy::too_many_arguments)]
pub async fn get_feed_entries(
&self,
id: FeedID,
status: Option<EntryStatus>,
offset: Option<i64>,
limit: Option<i64>,
order: Option<OrderBy>,
direction: Option<OrderDirection>,
before: Option<i64>,
after: Option<i64>,
before_entry_id: Option<EntryID>,
after_entry_id: Option<EntryID>,
starred: Option<bool>,
client: &Client,
) -> Result<Vec<Entry>, ApiError> {
let mut api_url = self
.base_uri
.clone()
.join(&format!("v1/feeds/{}/entries", id))
.context(ApiErrorKind::Url)?;
{
let mut query_pairs = api_url.query_pairs_mut();
query_pairs.clear();
if let Some(status) = status {
query_pairs.append_pair("status", status.into());
}
if let Some(offset) = offset {
query_pairs.append_pair("offset", &offset.to_string());
}
if let Some(limit) = limit {
query_pairs.append_pair("limit", &limit.to_string());
}
if let Some(order) = order {
query_pairs.append_pair("order", order.into());
}
if let Some(direction) = direction {
query_pairs.append_pair("direction", direction.into());
}
if let Some(before) = before {
query_pairs.append_pair("before", &before.to_string());
}
if let Some(after) = after {
query_pairs.append_pair("after", &after.to_string());
}
if let Some(before_entry_id) = before_entry_id {
query_pairs.append_pair("before_entry_id", &before_entry_id.to_string());
}
if let Some(after_entry_id) = after_entry_id {
query_pairs.append_pair("after_entry_id", &after_entry_id.to_string());
}
if let Some(starred) = starred {
query_pairs.append_pair("starred", &starred.to_string());
}
}
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let batch: EntryBatch = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(batch.entries)
}
pub async fn update_entries_status(
&self,
ids: Vec<FeedID>,
status: EntryStatus,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/entries")
.context(ApiErrorKind::Url)?;
let status: &str = status.into();
let content = EntryStateUpdate {
entry_ids: ids,
status: status.to_owned(),
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.put(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
Ok(())
}
pub async fn toggle_bookmark(&self, id: EntryID, client: &Client) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/entries/{}/bookmark", id))
.context(ApiErrorKind::Url)?;
let response = client
.put(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
Ok(())
}
pub async fn get_categories(&self, client: &Client) -> Result<Vec<Category>, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/categories")
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let categories: Vec<Category> =
serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(categories)
}
pub async fn create_category(
&self,
title: &str,
client: &Client,
) -> Result<Category, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/categories")
.context(ApiErrorKind::Url)?;
let content = CategoryInput {
title: title.to_owned(),
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.post(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let category: Category = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(category)
}
pub async fn update_category(
&self,
id: CategoryID,
title: &str,
client: &Client,
) -> Result<Category, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/categories/{}", id))
.context(ApiErrorKind::Url)?;
let content = CategoryInput {
title: title.to_owned(),
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.put(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::CREATED).await?;
let category: Category = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(category)
}
pub async fn delete_category(&self, id: CategoryID, client: &Client) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/categories/{}", id))
.context(ApiErrorKind::Url)?;
let response = client
.delete(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::NO_CONTENT).await?;
Ok(())
}
pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/export")
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
Ok(response)
}
pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/import")
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.body(opml.to_owned())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::CREATED).await?;
Ok(())
}
pub async fn create_user(
&self,
username: &str,
password: &str,
is_admin: bool,
client: &Client,
) -> Result<User, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/users")
.context(ApiErrorKind::Url)?;
let content = UserCreation {
username: username.to_owned(),
password: password.to_owned(),
is_admin,
};
let content = serde_json::to_value(content).context(ApiErrorKind::Json)?;
let response = client
.post(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let user: User = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(user)
}
#[allow(clippy::too_many_arguments)]
pub async fn update_user(
&self,
id: UserID,
username: Option<String>,
password: Option<String>,
is_admin: Option<bool>,
theme: Option<String>,
language: Option<String>,
timezone: Option<String>,
entry_sorting_direction: Option<String>,
client: &Client,
) -> Result<User, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/users/{}", id))
.context(ApiErrorKind::Url)?;
let content = UserModification {
username,
password,
is_admin,
theme,
language,
timezone,
entry_sorting_direction,
};
let response = client
.put(api_url)
.header(AUTHORIZATION, self.auth.clone())
.json(&content)
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let user: User = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(user)
}
pub async fn get_current_user(&self, client: &Client) -> Result<User, ApiError> {
let api_url = self
.base_uri
.clone()
.join("v1/me")
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let user: User = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(user)
}
pub async fn get_user_by_id(&self, id: UserID, client: &Client) -> Result<User, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/users/{}", id))
.context(ApiErrorKind::Url)?;
let response = client
.post(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let user: User = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(user)
}
pub async fn get_user_by_name(
&self,
username: &str,
client: &Client,
) -> Result<User, ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/users/{}", username))
.context(ApiErrorKind::Url)?;
let response = client
.post(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let response = Self::parse_error(response, StatusCode::OK).await?;
let user: User = serde_json::from_str(&response).context(ApiErrorKind::Json)?;
Ok(user)
}
pub async fn delete_user(&self, id: UserID, client: &Client) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join(&format!("v1/users/{}", id))
.context(ApiErrorKind::Url)?;
let response = client
.delete(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::OK).await?;
Ok(())
}
pub async fn healthcheck(&self, client: &Client) -> Result<(), ApiError> {
let api_url = self
.base_uri
.clone()
.join("healthcheck")
.context(ApiErrorKind::Url)?;
let response = client
.get(api_url)
.header(AUTHORIZATION, self.auth.clone())
.send()
.await
.context(ApiErrorKind::Http)?;
let _ = Self::parse_error(response, StatusCode::OK).await?;
Ok(())
}
}