From ed497d96d46fb680de23413175ee92d139fdca66 Mon Sep 17 00:00:00 2001 From: "D. Scott Boggs" Date: Fri, 23 Dec 2022 10:09:33 -0500 Subject: [PATCH] Improve remote error handling --- Cargo.toml | 3 +- README.md | 28 +++--- examples/follow_profile.rs | 13 ++- examples/follows_me.rs | 24 ++--- examples/home_timeline.rs | 30 ++++--- examples/log_events.rs | 1 - examples/register/mod.rs | 4 +- examples/upload_photo.rs | 43 +++++++-- src/entities/attachment.rs | 2 +- src/errors.rs | 73 +++++++--------- src/helpers/env.rs | 3 +- src/helpers/read_response.rs | 45 +++++++--- src/macros.rs | 164 +++++++++++++++++++---------------- src/mastodon.rs | 82 +++--------------- src/page.rs | 44 ++++++---- 15 files changed, 290 insertions(+), 269 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f63941..b747842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,10 +70,11 @@ features = ["io"] tokio-test = "0.4.2" futures-util = "0.3.25" indoc = "1.0" -pretty_env_logger = "0.3.0" skeptic = "0.13" tempfile = "3" +# for examples: femme = "2.2.1" +html2text = "0.4.4" [build-dependencies.skeptic] version = "0.13" diff --git a/README.md b/README.md index a30586e..3377555 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,16 @@ features = ["toml"] ```rust,no_run // src/main.rs -use std::error::Error; - use mastodon_async::prelude::*; use mastodon_async::helpers::toml; // requires `features = ["toml"]` -use mastodon_async::helpers::cli; +use mastodon_async::{helpers::cli, Result}; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<()> { let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") { Mastodon::from(data) } else { - register()? + register().await? }; let you = mastodon.verify_credentials().await?; @@ -59,14 +57,15 @@ async fn main() -> Result<(), Box> { Ok(()) } -fn register() -> Result> { +async fn register() -> Result { let registration = Registration::new("https://botsin.space") .client_name("mastodon-async-examples") - .build()?; - let mastodon = cli::authenticate(registration)?; + .build() + .await?; + let mastodon = cli::authenticate(registration).await?; // Save app data for using on the next run. - toml::to_file(&*mastodon, "mastodon-data.toml")?; + toml::to_file(&mastodon.data, "mastodon-data.toml")?; Ok(mastodon) } @@ -75,24 +74,23 @@ fn register() -> Result> { It also supports the [Streaming API](https://docs.joinmastodon.org/api/streaming): ```rust,no_run -use mastodon_async::prelude::*; -use mastodon_async::entities::event::Event; - -use std::error::Error; +use mastodon_async::{prelude::*, Result, entities::event::Event}; +use futures_util::TryStreamExt; #[tokio::main] -async fn main() -> Result<(), Box> { +async fn main() -> Result<()> { let client = Mastodon::from(Data::default()); client.stream_user() .await? - .try_for_each(|event| { + .try_for_each(|event| async move { match event { Event::Update(ref status) => { /* .. */ }, Event::Notification(ref notification) => { /* .. */ }, Event::Delete(ref id) => { /* .. */ }, Event::FiltersChanged => { /* .. */ }, } + Ok(()) }) .await?; Ok(()) diff --git a/examples/follow_profile.rs b/examples/follow_profile.rs index a5f7d4a..57e8f9d 100644 --- a/examples/follow_profile.rs +++ b/examples/follow_profile.rs @@ -1,17 +1,14 @@ #![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))] -#[macro_use] -extern crate pretty_env_logger; mod register; - -use register::Mastodon; -use std::error; +use mastodon_async::Result; #[cfg(feature = "toml")] -fn main() -> Result<(), Box> { - let mastodon = register::get_mastodon_data()?; +#[tokio::main] +async fn main() -> Result<()> { + let mastodon = register::get_mastodon_data().await?; let input = register::read_line("Enter the account id you'd like to follow: ")?; - let new_follow = mastodon.follow(input.trim())?; + let new_follow = mastodon.follow(input.trim()).await?; println!("{:#?}", new_follow); Ok(()) diff --git a/examples/follows_me.rs b/examples/follows_me.rs index c491994..be4b100 100644 --- a/examples/follows_me.rs +++ b/examples/follows_me.rs @@ -1,18 +1,22 @@ #![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))] -#[macro_use] -extern crate pretty_env_logger; mod register; - -use register::Mastodon; -use std::error; +use mastodon_async::Result; #[cfg(feature = "toml")] -fn main() -> Result<(), Box> { - let mastodon = register::get_mastodon_data()?; - for account in mastodon.follows_me()?.items_iter() { - println!("{}", account.acct); - } +#[tokio::main] +async fn main() -> Result<()> { + use futures::StreamExt; + + let mastodon = register::get_mastodon_data().await?; + mastodon + .follows_me() + .await? + .items_iter() + .for_each(|account| async move { + println!("{}", account.acct); + }) + .await; Ok(()) } diff --git a/examples/home_timeline.rs b/examples/home_timeline.rs index ae279a9..c97bf48 100644 --- a/examples/home_timeline.rs +++ b/examples/home_timeline.rs @@ -1,19 +1,27 @@ #![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))] -#[macro_use] -extern crate pretty_env_logger; mod register; - -use register::Mastodon; -use std::error; +use futures_util::StreamExt; +use mastodon_async::Result; #[cfg(feature = "toml")] -fn main() -> Result<(), Box> { - let mastodon = register::get_mastodon_data()?; - let tl = mastodon.get_home_timeline()?; - - println!("{:#?}", tl); - +#[tokio::main] +async fn main() -> Result<()> { + register::get_mastodon_data() + .await? + .get_home_timeline() + .await? + .items_iter() + .for_each(|status| async move { + print!( + "\ttoot from {}:\n{}", + status.account.display_name, + html2text::parse(status.content.as_bytes()) + .render_plain(90) + .into_string() + ) + }) + .await; Ok(()) } diff --git a/examples/log_events.rs b/examples/log_events.rs index 9f93fbb..48d3b32 100644 --- a/examples/log_events.rs +++ b/examples/log_events.rs @@ -10,7 +10,6 @@ use mastodon_async::Result; #[tokio::main] async fn main() -> Result<()> { use log::warn; - use mastodon_async::entities::prelude::Event; femme::with_level(log::LevelFilter::Info); let mastodon = register::get_mastodon_data().await?; diff --git a/examples/register/mod.rs b/examples/register/mod.rs index eefa2fd..8daedb1 100644 --- a/examples/register/mod.rs +++ b/examples/register/mod.rs @@ -45,8 +45,8 @@ pub async fn register() -> Result { } #[cfg(feature = "toml")] -pub fn read_line(message: &str) -> Result { - println!("{}", message); +pub fn read_line(message: impl AsRef) -> Result { + println!("{}", message.as_ref()); let mut input = String::new(); io::stdin().read_line(&mut input)?; diff --git a/examples/upload_photo.rs b/examples/upload_photo.rs index 52c5e09..b7806ef 100644 --- a/examples/upload_photo.rs +++ b/examples/upload_photo.rs @@ -1,15 +1,48 @@ #![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))] -#[macro_use] -extern crate pretty_env_logger; mod register; +use mastodon_async::{Result, StatusBuilder, Visibility}; #[cfg(feature = "toml")] -fn main() -> Result<(), Box> { - let mastodon = register::get_mastodon_data()?; +fn bool_input(message: impl AsRef, default: bool) -> Result { + let input = register::read_line(message.as_ref())?; + if let Some(first_char) = input.chars().next() { + match first_char { + 'Y' | 'y' => Ok(true), + 'N' | 'n' => Ok(false), + '\n' => Ok(default), + _ => { + print!( + "I didn't understand '{input}'. Please input something that begins with 'y' \ + or 'n', case insensitive: " + ); + bool_input(message, default) + }, + } + } else { + Ok(default) + } +} + +#[cfg(feature = "toml")] +#[tokio::main] +async fn main() -> Result<()> { + femme::with_level(femme::LevelFilter::Trace); + let mastodon = register::get_mastodon_data().await?; let input = register::read_line("Enter the path to the photo you'd like to post: ")?; - mastodon.media(input.into())?; + let media = mastodon.media(input).await?; + let status = StatusBuilder::new() + .status("Mastodon-async photo upload example/demo (automated post)") + .media_ids([media.id]) + .visibility(Visibility::Private) + .build()?; + let status = mastodon.new_status(status).await?; + println!("successfully uploaded status. It has the ID {}.", status.id); + if bool_input("would you like to delete the post now? (Y/n) ", true)? { + mastodon.delete_status(&status.id).await?; + println!("ok. done."); + } Ok(()) } diff --git a/src/entities/attachment.rs b/src/entities/attachment.rs index 3a93440..2115b9f 100644 --- a/src/entities/attachment.rs +++ b/src/entities/attachment.rs @@ -11,7 +11,7 @@ pub struct Attachment { #[serde(rename = "type")] pub media_type: MediaType, /// URL of the locally hosted version of the image. - pub url: String, + pub url: Option, /// For remote images, the remote URL of the original image. pub remote_url: Option, /// URL of the preview image. diff --git a/src/errors.rs b/src/errors.rs index cbe85a0..f04e545 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -22,7 +22,12 @@ pub type Result = ::std::result::Result; pub enum Error { /// Error from the Mastodon API. This typically means something went /// wrong with your authentication or data. - Api(ApiError), + Api { + /// The response status. + status: StatusCode, + /// The JSON-decoded error response from the server. + response: ApiError, + }, /// Error deserialising to json. Typically represents a breaking change in /// the Mastodon API Serde(SerdeError), @@ -40,10 +45,6 @@ pub enum Error { ClientSecretRequired, /// Missing Access Token. AccessTokenRequired, - /// Generic client error. - Client(StatusCode), - /// Generic server error. - Server(StatusCode), /// MastodonBuilder & AppBuilder error MissingField(&'static str), #[cfg(feature = "toml")] @@ -79,39 +80,40 @@ impl fmt::Display for Error { impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { - Some(match *self { - Error::Api(ref e) => e, - Error::Serde(ref e) => e, - Error::UrlEncoded(ref e) => e, - Error::Http(ref e) => e, - Error::Io(ref e) => e, - Error::Url(ref e) => e, + match *self { + Error::Serde(ref e) => Some(e), + Error::UrlEncoded(ref e) => Some(e), + Error::Http(ref e) => Some(e), + Error::Io(ref e) => Some(e), + Error::Url(ref e) => Some(e), #[cfg(feature = "toml")] - Error::TomlSer(ref e) => e, + Error::TomlSer(ref e) => Some(e), #[cfg(feature = "toml")] - Error::TomlDe(ref e) => e, - Error::HeaderStrError(ref e) => e, - Error::HeaderParseError(ref e) => e, + Error::TomlDe(ref e) => Some(e), + Error::HeaderStrError(ref e) => Some(e), + Error::HeaderParseError(ref e) => Some(e), #[cfg(feature = "env")] - Error::Envy(ref e) => e, - Error::SerdeQs(ref e) => e, - Error::IntConversion(ref e) => e, - Error::Client(..) | Error::Server(..) => return None, - Error::ClientIdRequired => return None, - Error::ClientSecretRequired => return None, - Error::AccessTokenRequired => return None, - Error::MissingField(_) => return None, - Error::Other(..) => return None, - }) + Error::Envy(ref e) => Some(e), + Error::SerdeQs(ref e) => Some(e), + Error::IntConversion(ref e) => Some(e), + Error::Api { + .. + } + | Error::ClientIdRequired + | Error::ClientSecretRequired + | Error::AccessTokenRequired + | Error::MissingField(_) + | Error::Other(..) => None, + } } } /// Error returned from the Mastodon API. -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ApiError { - /// The type of error. - pub error: Option, - /// The description of the error. + /// The error message. + pub error: String, + /// A longer description of the error, mainly provided with the OAuth API. pub error_description: Option, } @@ -143,7 +145,6 @@ from! { SerdeError => Serde, UrlEncodedError => UrlEncoded, UrlError => Url, - ApiError => Api, #[cfg(feature = "toml")] TomlSerError => TomlSer, #[cfg(feature = "toml")] TomlDeError => TomlDe, HeaderStrError => HeaderStrError, @@ -213,16 +214,6 @@ mod tests { assert_is!(err, Error::Url(..)); } - #[test] - fn from_api_error() { - let err: ApiError = ApiError { - error: None, - error_description: None, - }; - let err: Error = Error::from(err); - assert_is!(err, Error::Api(..)); - } - #[cfg(feature = "toml")] #[test] fn from_toml_ser_error() { diff --git a/src/helpers/env.rs b/src/helpers/env.rs index 00a1487..df44028 100644 --- a/src/helpers/env.rs +++ b/src/helpers/env.rs @@ -1,7 +1,6 @@ use envy; -use crate::Result; -use data::Data; +use crate::{Data, Result}; /// Attempts to deserialize a Data struct from the environment pub fn from_env() -> Result { diff --git a/src/helpers/read_response.rs b/src/helpers/read_response.rs index 66e6be8..0d876dc 100644 --- a/src/helpers/read_response.rs +++ b/src/helpers/read_response.rs @@ -1,23 +1,27 @@ use std::time::Duration; -use crate::errors::Result; +use crate::{errors::Result, log_serde, Error}; use futures::pin_mut; use futures_util::StreamExt; -use log::{as_serde, debug, trace, warn}; +use log::{as_debug, as_serde, debug, trace, warn}; use reqwest::Response; use serde::{Deserialize, Serialize}; use tokio::time::timeout; /// Adapter for reading JSON data from a response with better logging and a /// fail-safe timeout. +/// +/// The reason for this is largely because there was an issue with responses +/// being received, but not closed, we add a timeout on each read and try +/// to parse whatever we got before the timeout. pub async fn read_response(response: Response) -> Result where T: for<'de> Deserialize<'de> + Serialize, { let mut bytes = vec![]; let url = response.url().clone(); - // let status = log_serde!(response Status); - // let headers = log_serde!(response Headers); + let status = response.status(); + trace!(status = log_serde!(response Status), headers = log_serde!(response Headers); "attempting to stream response"); let stream = response.bytes_stream(); pin_mut!(stream); loop { @@ -35,23 +39,36 @@ where ); } else { warn!( - url = url.as_str(), // status = status, headers = headers, + url = url.as_str(), data_received = bytes.len(); "API response timed out" ); break; } } + // done growing the vec, let's just do this once. + let bytes = bytes.as_slice(); trace!( - url = url.as_str(), // status = status, headers = headers, - data_received = bytes.len(); + url = url.as_str(), + data = String::from_utf8_lossy(bytes); "parsing response" ); - let result = serde_json::from_slice(bytes.as_slice())?; - debug!( - url = url.as_str(), // status = status, headers = headers, - result = as_serde!(result); - "result parsed successfully" - ); - Ok(result) + if status.is_success() { + // the the response should deserialize to T + let result = serde_json::from_slice(bytes)?; + debug!( + url = url.as_str(), + result = as_serde!(result); + "result parsed successfully" + ); + Ok(result) + } else { + // we've received an error message, let's deserialize that instead. + let response = serde_json::from_slice(bytes)?; + debug!(status = as_debug!(status), response = as_serde!(response); "error received from API"); + Err(Error::Api { + status, + response, + }) + } } diff --git a/src/macros.rs b/src/macros.rs index cd48c2f..ce0c436 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -18,22 +18,12 @@ macro_rules! methods { async fn $method_with_call_id serde::Deserialize<'de> + serde::Serialize>(&self, url: impl AsRef, call_id: Uuid) -> Result { - use log::{debug, error, as_debug, as_serde}; + use log::{debug, as_debug}; let url = url.as_ref(); debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request"); - let response = self.authenticated(self.client.$method(url)).send().await?; - match response.error_for_status() { - Ok(response) => { - let response = read_response(response).await?; - debug!(response = as_serde!(response), url = url, method = stringify!($method), call_id = as_debug!(call_id); "received API response"); - Ok(response) - } - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - } - } + let response = self.authenticated(self.client.$method(url)).header("Accept", "application/json").send().await?; + read_response(response).await } } )+ @@ -57,21 +47,13 @@ macro_rules! paged_routes { "```" ), pub async fn $name(&self) -> Result> { - use log::{debug, as_debug, error}; + use log::{debug, as_debug}; let url = self.route(concat!("/api/v1/", $url)); let call_id = uuid::Uuid::new_v4(); debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request"); - let response = self.authenticated(self.client.$method(&url)).send().await?; + let response = self.authenticated(self.client.$method(&url)).header("Accept", "application/json").send().await?; - match response.error_for_status() { - Ok(response) => { - Page::new(self.clone(), response, call_id).await - } - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - } - } + Page::new(self.clone(), response, call_id).await } } @@ -88,7 +70,7 @@ macro_rules! paged_routes { ), pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result> { use serde_urlencoded; - use log::{debug, as_debug, error}; + use log::{debug, as_debug}; let call_id = uuid::Uuid::new_v4(); @@ -117,17 +99,9 @@ macro_rules! paged_routes { debug!(url = url, method = "get", call_id = as_debug!(call_id); "making API request"); - let response = self.authenticated(self.client.get(&url)).send().await?; + let response = self.authenticated(self.client.get(&url)).header("Accept", "application/json").send().await?; - match response.error_for_status() { - Ok(response) => { - Page::new(self.clone(), response, call_id).await - } - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - } - } + Page::new(self.clone(), response, call_id).await } } @@ -181,6 +155,62 @@ macro_rules! route_v2 { route_v2!{$($rest)*} }; + ((post multipart ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { + doc_comment! { + concat!( + "Equivalent to `post /api/v2/", + $url, + "`\n# Errors\nIf `access_token` is not set."), + pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> { + use reqwest::multipart::{Form, Part}; + use std::io::Read; + use log::{debug, error, as_debug}; + use uuid::Uuid; + + let call_id = Uuid::new_v4(); + + let form_data = Form::new() + $( + .part(stringify!($param), { + let path = $param.as_ref(); + match std::fs::File::open(path) { + Ok(mut file) => { + let mut data = if let Ok(metadata) = file.metadata() { + Vec::with_capacity(metadata.len().try_into()?) + } else { + vec![] + }; + file.read_to_end(&mut data)?; + Part::bytes(data) + } + Err(err) => { + error!(path = as_debug!(path), error = as_debug!(err); "error reading file contents for multipart form"); + return Err(err.into()); + } + } + }) + )*; + + let url = &self.route(concat!("/api/v2/", $url)); + + debug!( + url = url, method = stringify!($method), + multipart_form_data = as_debug!(form_data), call_id = as_debug!(call_id); + "making API request" + ); + + let response = self.authenticated(self.client.post(url)) + .multipart(form_data) + .header("Accept", "application/json") + .send() + .await?; + + read_response(response).await + } + } + + route!{$($rest)*} + }; () => {} } @@ -195,7 +225,7 @@ macro_rules! route { pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> { use reqwest::multipart::{Form, Part}; use std::io::Read; - use log::{debug, error, as_debug, as_serde}; + use log::{debug, error, as_debug}; use uuid::Uuid; let call_id = Uuid::new_v4(); @@ -232,20 +262,11 @@ macro_rules! route { let response = self.authenticated(self.client.post(url)) .multipart(form_data) + .header("Accept", "application/json") .send() .await?; - match response.error_for_status() { - Ok(response) => { - let response = read_response(response).await?; - debug!(response = as_serde!(response), url = url, method = stringify!($method), call_id = as_debug!(call_id); "received API response"); - Ok(response) - } - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - } - } + read_response(response).await } } @@ -304,7 +325,7 @@ macro_rules! route { "`\n# Errors\nIf `access_token` is not set.", ), pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> { - use log::{debug, error, as_debug, as_serde}; + use log::{debug, as_debug, as_serde}; use uuid::Uuid; let call_id = Uuid::new_v4(); @@ -324,20 +345,11 @@ macro_rules! route { let response = self.authenticated(self.client.$method(url)) .json(&form_data) + .header("Accept", "application/json") .send() .await?; - match response.error_for_status() { - Ok(response) => { - let response = read_response(response).await?; - debug!(response = as_serde!(response), url = $url, method = stringify!($method), call_id = as_debug!(call_id); "received API response"); - Ok(response) - } - Err(err) => { - error!(err = as_debug!(err), url = $url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - } - } + read_response(response).await } } @@ -413,23 +425,15 @@ macro_rules! paged_routes_with_id { "```" ), pub async fn $name(&self, id: &str) -> Result> { - use log::{debug, error, as_debug}; + use log::{debug, as_debug}; use uuid::Uuid; let call_id = Uuid::new_v4(); let url = self.route(&format!(concat!("/api/v1/", $url), id)); debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request"); - let response = self.authenticated(self.client.$method(&url)).send().await?; - match response.error_for_status() { - Ok(response) => { - Page::new(self.clone(), response, call_id).await - } - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - } - } + let response = self.authenticated(self.client.$method(&url)).header("Accept", "application/json").send().await?; + Page::new(self.clone(), response, call_id).await } } @@ -469,13 +473,19 @@ tokio_test::block_on(async { ), pub async fn $fn_name(&self) -> Result> { let url = self.route(&format!("/api/v1/streaming/{}", $stream)); - let response = self.authenticated(self.client.get(&url)).send().await?; + let response = self.authenticated(self.client.get(&url)).header("Accept", "application/json").send().await?; debug!( status = log_serde!(response Status), url = &url, headers = log_serde!(response Headers); "received API response" ); - Ok(event_stream(response.error_for_status()?, url)) + let status = response.status(); + if status.is_success() { + Ok(event_stream(response, url)) + } else { + let response = response.json().await?; + Err(Error::Api{ status, response }) + } } } streaming! { $($rest)* } @@ -513,13 +523,19 @@ tokio_test::block_on(async { let mut url: Url = self.route(concat!("/api/v1/streaming/", stringify!($stream))).parse()?; url.query_pairs_mut().append_pair(stringify!($param), $param.as_ref()); let url = url.to_string(); - let response = self.authenticated(self.client.get(url.as_str())).send().await?; + let response = self.authenticated(self.client.get(url.as_str())).header("Accept", "application/json").send().await?; debug!( status = log_serde!(response Status), url = as_debug!(url), headers = log_serde!(response Headers); "received API response" ); - Ok(event_stream(response.error_for_status()?, url)) + let status = response.status(); + if status.is_success() { + Ok(event_stream(response, url)) + } else { + let response = response.json().await?; + Err(Error::Api{ status, response }) + } } } streaming! { $($rest)* } diff --git a/src/mastodon.rs b/src/mastodon.rs index fa5c96d..692ed67 100644 --- a/src/mastodon.rs +++ b/src/mastodon.rs @@ -91,7 +91,6 @@ impl Mastodon { (post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty, (get (local: bool,)) get_public_timeline: "timelines/public" => Vec, (post (uri: Cow<'static, str>,)) follows: "follows" => Account, - (post multipart (file: impl AsRef,)) media: "media" => Attachment, (post) clear_notifications: "notifications/clear" => Empty, (post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty, (get) get_push_subscription: "push/subscription" => Subscription, @@ -102,6 +101,8 @@ impl Mastodon { route_v2! { (get (q: &'a str, resolve: bool,)) search: "search" => SearchResult, + (post multipart (file: impl AsRef,)) media: "media" => Attachment, + (post multipart (file: impl AsRef, thumbnail: impl AsRef,)) media_with_thumbnail: "media" => Attachment, } route_id! { @@ -169,14 +170,14 @@ impl Mastodon { /// POST /api/v1/filters pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result { - Ok(self + let response = self .client .post(self.route("/api/v1/filters")) .json(&request) .send() - .await? - .json() - .await?) + .await?; + + read_response(response).await } /// PUT /api/v1/filters/:id @@ -184,15 +185,7 @@ impl Mastodon { let url = self.route(&format!("/api/v1/filters/{}", id)); let response = self.client.put(&url).json(&request).send().await?; - let status = response.status(); - - if status.is_client_error() { - return Err(Error::Client(status.clone())); - } else if status.is_server_error() { - return Err(Error::Server(status.clone())); - } - - Ok(read_response(response).await?) + read_response(response).await } /// Update the user credentials @@ -201,15 +194,7 @@ impl Mastodon { let url = self.route("/api/v1/accounts/update_credentials"); let response = self.client.patch(&url).json(&changes).send().await?; - let status = response.status(); - - if status.is_client_error() { - return Err(Error::Client(status.clone())); - } else if status.is_server_error() { - return Err(Error::Server(status.clone())); - } - - Ok(read_response(response).await?) + read_response(response).await } /// Post a new status to the account. @@ -225,7 +210,7 @@ impl Mastodon { headers = log_serde!(response Headers); "received API response" ); - Ok(read_response(response).await?) + read_response(response).await } /// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or @@ -279,15 +264,7 @@ impl Mastodon { debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request"); let response = self.client.get(&url).send().await?; - match response.error_for_status() { - Ok(response) => Page::new(self.clone(), response, call_id).await, - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - // Cannot retrieve request body as it's been moved into the - // other match arm. - Err(err.into()) - }, - } + Page::new(self.clone(), response, call_id).await } /// Returns the client account's relationship to a list of other accounts. @@ -315,18 +292,7 @@ impl Mastodon { ); let response = self.client.get(&url).send().await?; - match response.error_for_status() { - Ok(response) => Page::new(self.clone(), response, call_id).await, - Err(err) => { - error!( - err = as_debug!(err), url = url, - method = stringify!($method), call_id = as_debug!(call_id), - account_ids = as_serde!(ids); - "error making API request" - ); - Err(err.into()) - }, - } + Page::new(self.clone(), response, call_id).await } /// Add a push notifications subscription @@ -341,18 +307,7 @@ impl Mastodon { ); let response = self.client.post(url).json(&request).send().await?; - match response.error_for_status() { - Ok(response) => { - let status = response.status(); - let response = read_response(response).await?; - debug!(status = as_debug!(status), response = as_serde!(response); "received API response"); - Ok(response) - }, - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - }, - } + read_response(response).await } /// Update the `data` portion of the push subscription associated with this @@ -368,18 +323,7 @@ impl Mastodon { ); let response = self.client.post(url).json(&request).send().await?; - match response.error_for_status() { - Ok(response) => { - let status = response.status(); - let response = read_response(response).await?; - debug!(status = as_debug!(status), response = as_serde!(response); "received API response"); - Ok(response) - }, - Err(err) => { - error!(err = as_debug!(err), url = url, method = stringify!($method), call_id = as_debug!(call_id); "error making API request"); - Err(err.into()) - }, - } + read_response(response).await } /// Get all accounts that follow the authenticated user diff --git a/src/page.rs b/src/page.rs index 5c31510..5c11dab 100644 --- a/src/page.rs +++ b/src/page.rs @@ -1,5 +1,10 @@ use super::{Mastodon, Result}; -use crate::{entities::itemsiter::ItemsIter, format_err, helpers::read_response::read_response}; +use crate::{ + entities::itemsiter::ItemsIter, + format_err, + helpers::read_response::read_response, + Error, +}; use futures::Stream; use hyper_old_types::header::{parsing, Link, RelationType}; use log::{as_debug, as_serde, debug, error, trace}; @@ -107,20 +112,29 @@ impl<'a, T: for<'de> Deserialize<'de> + Serialize> Page { /// Create a new Page. pub(crate) async fn new(mastodon: Mastodon, response: Response, call_id: Uuid) -> Result { - let (prev, next) = get_links(&response, call_id)?; - let initial_items = read_response(response).await?; - debug!( - initial_items = as_serde!(initial_items), prev = as_debug!(prev), - next = as_debug!(next), call_id = as_debug!(call_id); - "received first page from API call" - ); - Ok(Page { - initial_items, - next, - prev, - mastodon, - call_id, - }) + let status = response.status(); + if status.is_success() { + let (prev, next) = get_links(&response, call_id)?; + let initial_items = read_response(response).await?; + debug!( + initial_items = as_serde!(initial_items), prev = as_debug!(prev), + next = as_debug!(next), call_id = as_debug!(call_id); + "received first page from API call" + ); + Ok(Page { + initial_items, + next, + prev, + mastodon, + call_id, + }) + } else { + let response = response.json().await?; + Err(Error::Api { + status, + response, + }) + } } }