use base64::engine::general_purpose::STANDARD as base64_std;
use base64::Engine;
pub use error::ApiError;
use models::Subscription;
pub use models::{
AddCategoryRequest, Category, CategoryModificationRequest, Entries, FeedModificationRequest,
IDRequest, MarkRequest, MultipleMarkRequest, ServerInfo, Settings, StarRequest,
SubscribeRequest, TagRequest, User,
};
use reqwest::header::AUTHORIZATION;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use url::Url;
pub mod error;
pub mod models;
#[cfg(test)]
mod tests;
pub struct CommafeedApi {
url: Url,
auth: String,
}
impl CommafeedApi {
pub fn new(url: &Url, user: &str, password: &str) -> Self {
Self {
url: url.clone(),
auth: Self::generate_basic_auth(user, password),
}
}
fn generate_basic_auth(username: &str, password: &str) -> String {
let auth = format!("{}:{}", username, password);
let auth = base64_std.encode(auth);
format!("Basic {auth}")
}
async fn send_request<T: Serialize>(
&self,
client: reqwest::RequestBuilder,
json_content: Option<T>,
) -> Result<String, ApiError> {
let mut client = client.header(AUTHORIZATION, &self.auth);
if let Some(json_content) = json_content {
client = client.json(&json_content);
}
let response = client.send().await?;
let response = response.error_for_status()?;
let text = response.text().await?;
Ok(text)
}
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)
}
pub async fn get_profile(&self, client: &Client) -> Result<User, ApiError> {
let api_url = self.url.join("user/profile")?;
let response = self.send_request(client.get(api_url), None::<()>).await?;
let user = Self::deserialize(&response)?;
Ok(user)
}
pub async fn get_settings(&self, client: &Client) -> Result<Settings, ApiError> {
let api_url = self.url.join("user/settings")?;
let response = self.send_request(client.get(api_url), None::<()>).await?;
let settings = Self::deserialize(&response)?;
Ok(settings)
}
pub async fn get_server_info(&self, client: &Client) -> Result<ServerInfo, ApiError> {
let api_url = self.url.join("server/get")?;
let response = self.send_request(client.get(api_url), None::<()>).await?;
let server_info = Self::deserialize(&response)?;
Ok(server_info)
}
pub async fn get_category_tree(&self, client: &Client) -> Result<Category, ApiError> {
let api_url = self.url.join("category/get")?;
let response = self.send_request(client.get(api_url), None::<()>).await?;
let tree = Self::deserialize(&response)?;
Ok(tree)
}
pub async fn create_category(
&self,
name: &str,
parent_id: Option<&str>,
client: &Client,
) -> Result<i64, ApiError> {
let api_url = self.url.join("category/add")?;
let add_request = AddCategoryRequest {
name: name.into(),
parent_id: parent_id.map(String::from),
};
let response = self
.send_request(client.post(api_url), Some(add_request))
.await?;
let category_id = Self::deserialize(&response)?;
Ok(category_id)
}
pub async fn delete_category(&self, category_id: i64, client: &Client) -> Result<(), ApiError> {
let api_url = self.url.join("category/delete")?;
let id_request = IDRequest { id: category_id };
self.send_request(client.post(api_url), Some(id_request))
.await?;
Ok(())
}
pub async fn modify_category(
&self,
category_id: i64,
new_title: Option<&str>,
parent_id: Option<&str>,
position: Option<i32>,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("category/modify")?;
let modify_request = CategoryModificationRequest {
id: category_id,
name: new_title.map(String::from),
parent_id: parent_id.map(String::from),
position,
};
self.send_request(client.post(api_url), Some(modify_request))
.await?;
Ok(())
}
pub async fn mark_category_read(
&self,
category_id: &str,
read: bool,
older_than: Option<i64>,
keywords: Option<&str>,
excluded: Option<Vec<i64>>,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("category/mark")?;
let mark_request = MarkRequest {
id: category_id.into(),
read,
older_than,
keywords: keywords.map(String::from),
excluded_subscriptions: excluded,
};
self.send_request(client.post(api_url), Some(mark_request))
.await?;
Ok(())
}
pub async fn set_tags(&self, tag_request: TagRequest, client: &Client) -> Result<(), ApiError> {
let api_url = self.url.join("entry/tags")?;
self.send_request(client.post(api_url), Some(tag_request))
.await?;
Ok(())
}
pub async fn get_tags(&self, client: &Client) -> Result<Vec<String>, ApiError> {
let api_url = self.url.join("entry/tags")?;
let response = self.send_request(client.get(api_url), None::<()>).await?;
let tags = Self::deserialize(&response)?;
Ok(tags)
}
pub async fn subscribe_to_feed_simple(
&self,
url: &str,
client: &Client,
) -> Result<i64, ApiError> {
let api_url = self.url.join("feed/subscribe")?;
let response = self.send_request(client.get(api_url), Some(url)).await?;
let feed_id = Self::deserialize(&response)?;
Ok(feed_id)
}
pub async fn subscribe_to_feed(
&self,
url: &str,
title: &str,
parent_id: Option<&str>,
client: &Client,
) -> Result<i64, ApiError> {
let api_url = self.url.join("feed/subscribe")?;
let subscribe_request = SubscribeRequest {
url: url.into(),
title: title.into(),
category_id: parent_id.map(String::from),
};
let response = self
.send_request(client.post(api_url), Some(subscribe_request))
.await?;
let feed_id = Self::deserialize(&response)?;
Ok(feed_id)
}
pub async fn unsubscribe_from_feed(
&self,
feed_id: i64,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("feed/unsubscribe")?;
let id_request = IDRequest { id: feed_id };
self.send_request(client.post(api_url), Some(id_request))
.await?;
Ok(())
}
pub async fn fetch_feed(
&self,
feed_id: i64,
client: &Client,
) -> Result<Subscription, ApiError> {
let endpoint = format!("feed/get/{feed_id}");
let api_url = self.url.join(&endpoint)?;
let response = self.send_request(client.get(api_url), None::<()>).await?;
let feed = Self::deserialize(&response)?;
Ok(feed)
}
pub async fn mark_feed_read(
&self,
feed_id: &str,
read: bool,
older_than: Option<i64>,
keywords: Option<&str>,
excluded: Option<Vec<i64>>,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("feed/mark")?;
let mark_request = MarkRequest {
id: feed_id.into(),
read,
older_than,
keywords: keywords.map(String::from),
excluded_subscriptions: excluded,
};
self.send_request(client.post(api_url), Some(mark_request))
.await?;
Ok(())
}
pub async fn modify_feed(
&self,
feed_id: i64,
new_title: Option<&str>,
category_id: Option<&str>,
position: Option<i32>,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("feed/modify")?;
let modify_request = FeedModificationRequest {
id: feed_id,
name: new_title.map(String::from),
category_id: category_id.map(String::from),
position,
filter: None,
};
self.send_request(client.post(api_url), Some(modify_request))
.await?;
Ok(())
}
pub async fn import_opml(&self, opml: &str, client: &Client) -> Result<(), ApiError> {
let api_url = self.url.join("feed/import")?;
self.send_request(client.post(api_url), Some(opml)).await?;
Ok(())
}
pub async fn export_opml(&self, client: &Client) -> Result<String, ApiError> {
let api_url = self.url.join("feed/export")?;
let opml = self.send_request(client.get(api_url), None::<()>).await?;
Ok(opml)
}
pub async fn get_favicon(&self, feed_id: i64, client: &Client) -> Result<Vec<u8>, ApiError> {
let api_url = self.url.join(&format!("feed/favicon/{feed_id}"))?;
let request = client.get(api_url).header(AUTHORIZATION, &self.auth);
let response = request.send().await?;
let response = response.error_for_status()?;
let data = response.bytes().await?;
Ok(data.to_vec())
}
pub async fn mark_entry_read(
&self,
id: &str,
read: bool,
older_than: Option<i64>,
keywords: Option<&str>,
excluded: Option<Vec<i64>>,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("entry/mark")?;
let mark_request = MarkRequest {
id: id.into(),
read,
older_than,
keywords: keywords.map(String::from),
excluded_subscriptions: excluded,
};
self.send_request(client.post(api_url), Some(mark_request))
.await?;
Ok(())
}
pub async fn mark_entry_starred(
&self,
request: StarRequest,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("entry/star")?;
self.send_request(client.post(api_url), Some(request))
.await?;
Ok(())
}
pub async fn mark_multiple_entries_read(
&self,
requests: Vec<MarkRequest>,
client: &Client,
) -> Result<(), ApiError> {
let api_url = self.url.join("entry/markMultiple")?;
let mark_request = MultipleMarkRequest { requests };
self.send_request(client.post(api_url), Some(mark_request))
.await?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn get_category_entries(
&self,
category_id: &str,
read: bool,
newer_than: Option<i64>,
offset: Option<i32>,
limit: Option<i32>,
order: Option<&str>,
keywords: Option<&str>,
only_ids: Option<bool>,
excluded_subscriptions: Option<&str>,
tag: Option<&str>,
client: &Client,
) -> Result<Entries, ApiError> {
let api_url = self.url.join("category/entries")?;
let read_type = if read { "all" } else { "unread" };
let mut request = client
.get(api_url)
.query(&[("id", category_id), ("readType", read_type)]);
if let Some(newer_than) = newer_than {
request = request.query(&[("newerThan", newer_than)]);
}
if let Some(offset) = offset {
request = request.query(&[("offset", offset)]);
}
if let Some(limit) = limit {
request = request.query(&[("limit", limit)]);
}
if let Some(order) = order {
request = request.query(&[("order", order)]);
}
if let Some(keywords) = keywords {
request = request.query(&[("keywords", keywords)]);
}
if let Some(only_ids) = only_ids {
request = request.query(&[("onlyIds", only_ids)]);
}
if let Some(excluded_subscriptions) = excluded_subscriptions {
request = request.query(&[("excludedSubscriptionIds", excluded_subscriptions)]);
}
if let Some(tag) = tag {
request = request.query(&[("tag", tag)]);
}
let response = self.send_request(request, None::<()>).await?;
let entries = Self::deserialize(&response)?;
Ok(entries)
}
#[allow(clippy::too_many_arguments)]
pub async fn get_feed_entries(
&self,
feed_id: &str,
read: bool,
newer_than: Option<i64>,
offset: Option<i32>,
limit: Option<i32>,
order: Option<&str>,
keywords: Option<&str>,
only_ids: Option<bool>,
client: &Client,
) -> Result<Entries, ApiError> {
let api_url = self.url.join("feed/entries")?;
let read_type = if read { "all" } else { "unread" };
let mut request = client
.get(api_url)
.query(&[("id", feed_id), ("readType", read_type)]);
if let Some(newer_than) = newer_than {
request = request.query(&[("newerThan", newer_than)]);
}
if let Some(offset) = offset {
request = request.query(&[("offset", offset)]);
}
if let Some(limit) = limit {
request = request.query(&[("limit", limit)]);
}
if let Some(order) = order {
request = request.query(&[("order", order)]);
}
if let Some(keywords) = keywords {
request = request.query(&[("keywords", keywords)]);
}
if let Some(only_ids) = only_ids {
request = request.query(&[("onlyIds", only_ids)]);
}
let response = self.send_request(request, None::<()>).await?;
let entries = Self::deserialize(&response)?;
Ok(entries)
}
}