pub mod error;
#[cfg(test)]
mod tests;
pub use crate::error::ApiError;
use reqwest::{Client, header::COOKIE, multipart};
use serde_json::{self, Value};
use url::Url;
pub struct NewsBlurApi {
base_uri: Url,
username: String,
password: String,
cookie: Option<String>,
}
impl NewsBlurApi {
pub fn new(url: &Url, username: &str, password: &str, cookie: Option<String>) -> Self {
NewsBlurApi {
base_uri: url.clone(),
username: username.to_string(),
password: password.to_string(),
cookie,
}
}
fn cookie(&self) -> Result<&str, ApiError> {
self.cookie.as_deref().ok_or(ApiError::AccessDenied)
}
pub async fn login(&mut self, client: &Client) -> Result<String, ApiError> {
let form = multipart::Form::new()
.text("username", self.username.clone())
.text("password", self.password.clone());
let api_url: Url = self.base_uri.join("api/login")?;
let response = client.post(api_url).multipart(form).send().await?;
let status = response.status();
if !status.is_success() {
return Err(ApiError::AccessDenied);
}
let cookie = response.cookies().next().ok_or(ApiError::AccessDenied)?;
let cookie_string = format!("{}={}", cookie.name(), cookie.value());
self.cookie = Some(cookie_string.clone());
Ok(cookie_string)
}
pub async fn logout(&self, client: &Client) -> Result<(), ApiError> {
let api_url: Url = self.base_uri.join("api/logout")?;
let cookie = self.cookie()?;
let response = client.post(api_url).header(COOKIE, cookie).send().await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn signup(&self, _client: &Client) -> Result<(), ApiError> {
unimplemented!()
}
pub async fn search_feed(&self, client: &Client, address: &str) -> Result<(), ApiError> {
let form = multipart::Form::new().text("address", address.to_string());
let api_url: Url = self.base_uri.join("rss_feeds/search_feed")?;
let cookie = self.cookie()?;
let response = client
.get(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn get_feeds(&self, client: &Client) -> Result<Value, ApiError> {
let form = multipart::Form::new().text("include_favicons", "false");
let api_url: Url = self.base_uri.join("reader/feeds")?;
let cookie = self.cookie()?;
let response = client
.get(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn favicons(&self, client: &Client, feed_id: &str) -> Result<Value, ApiError> {
let form = multipart::Form::new().text("feed_ids", feed_id.to_string());
let api_url: Url = self.base_uri.join("reader/favicons")?;
let cookie = self.cookie()?;
let response = client
.get(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn get_original_page(&self, client: &Client, id: &str) -> Result<String, ApiError> {
let request = format!("reader/page/{id}");
let api_url: Url = self.base_uri.join(&request)?;
let cookie = self.cookie()?;
let response = client.get(api_url).header(COOKIE, cookie).send().await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_text = response.text().await?;
Ok(response_text)
}
pub async fn get_original_text(&self, client: &Client, id: &str) -> Result<String, ApiError> {
let api_url: Url = self.base_uri.join("rss_feeds/original_text")?;
let query = vec![("story_hash", id.to_string())];
let cookie = self.cookie()?;
let response = client
.get(api_url)
.header(COOKIE, cookie)
.query(&query)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_text = response.text().await?;
Ok(response_text)
}
pub async fn refresh_feeds(&self, client: &Client) -> Result<Value, ApiError> {
let api_url: Url = self.base_uri.join("reader/refresh_feeds")?;
let cookie = self.cookie()?;
let response = client.get(api_url).header(COOKIE, cookie).send().await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn get_read_stories(&self, client: &Client, page: u32) -> Result<Value, ApiError> {
let query = vec![("page", format!("{page}"))];
let cookie = self.cookie()?;
let api_url: Url = self.base_uri.join("reader/read_stories")?;
let response = client
.get(api_url)
.header(COOKIE, cookie)
.query(&query)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn get_stories(
&self,
client: &Client,
id: &str,
include_content: bool,
page: u32,
) -> Result<Value, ApiError> {
let request = format!("reader/feed/{id}");
let cookie = self.cookie()?;
let mut query = Vec::new();
if include_content {
query.push(("include_content", "true".to_string()));
} else {
query.push(("include_content", "false".to_string()));
}
query.push(("page", format!("{page}")));
let api_url: Url = self.base_uri.join(&request)?;
let response = client
.get(api_url)
.header(COOKIE, cookie)
.query(&query)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn mark_stories_read(
&self,
client: &Client,
story_hashes: &[&str],
) -> Result<(), ApiError> {
let mut form = multipart::Form::new();
for story_hash in story_hashes {
form = form.text("story_hash", story_hash.to_string());
}
let api_url: Url = self.base_uri.join("reader/mark_story_hashes_as_read")?;
let cookie = self.cookie()?;
let response = client
.post(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn mark_story_unread(
&self,
client: &Client,
story_hashes: &[&str],
) -> Result<(), ApiError> {
let mut form = multipart::Form::new();
for story_hash in story_hashes {
form = form.text("story_hash", story_hash.to_string());
}
let api_url: Url = self.base_uri.join("reader/mark_story_hash_as_unread")?;
let cookie = self.cookie()?;
let response = client
.post(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn mark_story_hash_as_starred(
&self,
client: &Client,
story_hash: &str,
) -> Result<(), ApiError> {
let form = multipart::Form::new().text("story_hash", story_hash.to_string());
let api_url: Url = self.base_uri.join("reader/mark_story_hash_as_starred")?;
let cookie = self.cookie()?;
let response = client
.post(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn mark_story_hash_as_unstarred(
&self,
client: &Client,
story_hash: &str,
) -> Result<(), ApiError> {
let form = multipart::Form::new().text("story_hash", story_hash.to_string());
let api_url: Url = self.base_uri.join("reader/mark_story_hash_as_unstarred")?;
let cookie = self.cookie()?;
let response = client
.post(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn get_unread_story_hashes(&self, client: &Client) -> Result<Value, ApiError> {
let api_url: Url = self.base_uri.join("reader/unread_story_hashes")?;
let cookie = self.cookie()?;
let response = client.get(api_url).header(COOKIE, cookie).send().await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn get_stared_story_hashes(&self, client: &Client) -> Result<Value, ApiError> {
let api_url: Url = self.base_uri.join("reader/starred_story_hashes")?;
let cookie = self.cookie()?;
let response = client.get(api_url).header(COOKIE, cookie).send().await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn get_river_stories(
&self,
client: &Client,
hashes: &[&str],
) -> Result<Value, ApiError> {
let api_url: Url = self.base_uri.join("reader/river_stories")?;
let cookie = self.cookie()?;
let mut query = Vec::new();
for hash in hashes {
query.push(("h", hash));
}
let response = client
.get(api_url)
.header(COOKIE, cookie)
.query(&query)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
let response_json = response.json().await?;
Ok(response_json)
}
pub async fn mark_feed_read(&self, client: &Client, feed_id: &str) -> Result<(), ApiError> {
let form = multipart::Form::new().text("feed_id", feed_id.to_string());
let api_url: Url = self.base_uri.join("reader/mark_feed_as_read")?;
let cookie = self.cookie()?;
let response = client
.post(api_url)
.header(COOKIE, cookie)
.multipart(form)
.send()
.await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
pub async fn mark_all_read(&self, client: &Client) -> Result<(), ApiError> {
let api_url: Url = self.base_uri.join("reader/mark_all_as_read")?;
let cookie = self.cookie()?;
let response = client.post(api_url).header(COOKIE, cookie).send().await?;
if !response.status().is_success() {
return Err(ApiError::AccessDenied);
}
Ok(())
}
}