Improve remote error handling

This commit is contained in:
D. Scott Boggs 2022-12-23 10:09:33 -05:00
parent 58ffee1970
commit ed497d96d4
15 changed files with 290 additions and 269 deletions

View File

@ -70,10 +70,11 @@ features = ["io"]
tokio-test = "0.4.2" tokio-test = "0.4.2"
futures-util = "0.3.25" futures-util = "0.3.25"
indoc = "1.0" indoc = "1.0"
pretty_env_logger = "0.3.0"
skeptic = "0.13" skeptic = "0.13"
tempfile = "3" tempfile = "3"
# for examples:
femme = "2.2.1" femme = "2.2.1"
html2text = "0.4.4"
[build-dependencies.skeptic] [build-dependencies.skeptic]
version = "0.13" version = "0.13"

View File

@ -38,18 +38,16 @@ features = ["toml"]
```rust,no_run ```rust,no_run
// src/main.rs // src/main.rs
use std::error::Error;
use mastodon_async::prelude::*; use mastodon_async::prelude::*;
use mastodon_async::helpers::toml; // requires `features = ["toml"]` use mastodon_async::helpers::toml; // requires `features = ["toml"]`
use mastodon_async::helpers::cli; use mastodon_async::{helpers::cli, Result};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> { async fn main() -> Result<()> {
let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") { let mastodon = if let Ok(data) = toml::from_file("mastodon-data.toml") {
Mastodon::from(data) Mastodon::from(data)
} else { } else {
register()? register().await?
}; };
let you = mastodon.verify_credentials().await?; let you = mastodon.verify_credentials().await?;
@ -59,14 +57,15 @@ async fn main() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
fn register() -> Result<Mastodon, Box<dyn Error>> { async fn register() -> Result<Mastodon> {
let registration = Registration::new("https://botsin.space") let registration = Registration::new("https://botsin.space")
.client_name("mastodon-async-examples") .client_name("mastodon-async-examples")
.build()?; .build()
let mastodon = cli::authenticate(registration)?; .await?;
let mastodon = cli::authenticate(registration).await?;
// Save app data for using on the next run. // 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) Ok(mastodon)
} }
@ -75,24 +74,23 @@ fn register() -> Result<Mastodon, Box<dyn Error>> {
It also supports the [Streaming API](https://docs.joinmastodon.org/api/streaming): It also supports the [Streaming API](https://docs.joinmastodon.org/api/streaming):
```rust,no_run ```rust,no_run
use mastodon_async::prelude::*; use mastodon_async::{prelude::*, Result, entities::event::Event};
use mastodon_async::entities::event::Event; use futures_util::TryStreamExt;
use std::error::Error;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<Error>> { async fn main() -> Result<()> {
let client = Mastodon::from(Data::default()); let client = Mastodon::from(Data::default());
client.stream_user() client.stream_user()
.await? .await?
.try_for_each(|event| { .try_for_each(|event| async move {
match event { match event {
Event::Update(ref status) => { /* .. */ }, Event::Update(ref status) => { /* .. */ },
Event::Notification(ref notification) => { /* .. */ }, Event::Notification(ref notification) => { /* .. */ },
Event::Delete(ref id) => { /* .. */ }, Event::Delete(ref id) => { /* .. */ },
Event::FiltersChanged => { /* .. */ }, Event::FiltersChanged => { /* .. */ },
} }
Ok(())
}) })
.await?; .await?;
Ok(()) Ok(())

View File

@ -1,17 +1,14 @@
#![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(dead_code))]
#![cfg_attr(not(feature = "toml"), allow(unused_imports))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))]
#[macro_use]
extern crate pretty_env_logger;
mod register; mod register;
use mastodon_async::Result;
use register::Mastodon;
use std::error;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn main() -> Result<(), Box<error::Error>> { #[tokio::main]
let mastodon = register::get_mastodon_data()?; 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 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); println!("{:#?}", new_follow);
Ok(()) Ok(())

View File

@ -1,18 +1,22 @@
#![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(dead_code))]
#![cfg_attr(not(feature = "toml"), allow(unused_imports))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))]
#[macro_use]
extern crate pretty_env_logger;
mod register; mod register;
use mastodon_async::Result;
use register::Mastodon;
use std::error;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn main() -> Result<(), Box<error::Error>> { #[tokio::main]
let mastodon = register::get_mastodon_data()?; async fn main() -> Result<()> {
for account in mastodon.follows_me()?.items_iter() { use futures::StreamExt;
let mastodon = register::get_mastodon_data().await?;
mastodon
.follows_me()
.await?
.items_iter()
.for_each(|account| async move {
println!("{}", account.acct); println!("{}", account.acct);
} })
.await;
Ok(()) Ok(())
} }

View File

@ -1,19 +1,27 @@
#![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(dead_code))]
#![cfg_attr(not(feature = "toml"), allow(unused_imports))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))]
#[macro_use]
extern crate pretty_env_logger;
mod register; mod register;
use futures_util::StreamExt;
use register::Mastodon; use mastodon_async::Result;
use std::error;
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn main() -> Result<(), Box<error::Error>> { #[tokio::main]
let mastodon = register::get_mastodon_data()?; async fn main() -> Result<()> {
let tl = mastodon.get_home_timeline()?; register::get_mastodon_data()
.await?
println!("{:#?}", tl); .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(()) Ok(())
} }

View File

@ -10,7 +10,6 @@ use mastodon_async::Result;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
use log::warn; use log::warn;
use mastodon_async::entities::prelude::Event;
femme::with_level(log::LevelFilter::Info); femme::with_level(log::LevelFilter::Info);
let mastodon = register::get_mastodon_data().await?; let mastodon = register::get_mastodon_data().await?;

View File

@ -45,8 +45,8 @@ pub async fn register() -> Result<Mastodon> {
} }
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
pub fn read_line(message: &str) -> Result<String> { pub fn read_line(message: impl AsRef<str>) -> Result<String> {
println!("{}", message); println!("{}", message.as_ref());
let mut input = String::new(); let mut input = String::new();
io::stdin().read_line(&mut input)?; io::stdin().read_line(&mut input)?;

View File

@ -1,15 +1,48 @@
#![cfg_attr(not(feature = "toml"), allow(dead_code))] #![cfg_attr(not(feature = "toml"), allow(dead_code))]
#![cfg_attr(not(feature = "toml"), allow(unused_imports))] #![cfg_attr(not(feature = "toml"), allow(unused_imports))]
#[macro_use]
extern crate pretty_env_logger;
mod register; mod register;
use mastodon_async::{Result, StatusBuilder, Visibility};
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
fn main() -> Result<(), Box<error::Error>> { fn bool_input(message: impl AsRef<str>, default: bool) -> Result<bool> {
let mastodon = register::get_mastodon_data()?; 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: ")?; 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(()) Ok(())
} }

View File

@ -11,7 +11,7 @@ pub struct Attachment {
#[serde(rename = "type")] #[serde(rename = "type")]
pub media_type: MediaType, pub media_type: MediaType,
/// URL of the locally hosted version of the image. /// URL of the locally hosted version of the image.
pub url: String, pub url: Option<String>,
/// For remote images, the remote URL of the original image. /// For remote images, the remote URL of the original image.
pub remote_url: Option<String>, pub remote_url: Option<String>,
/// URL of the preview image. /// URL of the preview image.

View File

@ -22,7 +22,12 @@ pub type Result<T> = ::std::result::Result<T, Error>;
pub enum Error { pub enum Error {
/// Error from the Mastodon API. This typically means something went /// Error from the Mastodon API. This typically means something went
/// wrong with your authentication or data. /// 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 /// Error deserialising to json. Typically represents a breaking change in
/// the Mastodon API /// the Mastodon API
Serde(SerdeError), Serde(SerdeError),
@ -40,10 +45,6 @@ pub enum Error {
ClientSecretRequired, ClientSecretRequired,
/// Missing Access Token. /// Missing Access Token.
AccessTokenRequired, AccessTokenRequired,
/// Generic client error.
Client(StatusCode),
/// Generic server error.
Server(StatusCode),
/// MastodonBuilder & AppBuilder error /// MastodonBuilder & AppBuilder error
MissingField(&'static str), MissingField(&'static str),
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
@ -79,39 +80,40 @@ impl fmt::Display for Error {
impl error::Error for Error { impl error::Error for Error {
fn source(&self) -> Option<&(dyn error::Error + 'static)> { fn source(&self) -> Option<&(dyn error::Error + 'static)> {
Some(match *self { match *self {
Error::Api(ref e) => e, Error::Serde(ref e) => Some(e),
Error::Serde(ref e) => e, Error::UrlEncoded(ref e) => Some(e),
Error::UrlEncoded(ref e) => e, Error::Http(ref e) => Some(e),
Error::Http(ref e) => e, Error::Io(ref e) => Some(e),
Error::Io(ref e) => e, Error::Url(ref e) => Some(e),
Error::Url(ref e) => e,
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
Error::TomlSer(ref e) => e, Error::TomlSer(ref e) => Some(e),
#[cfg(feature = "toml")] #[cfg(feature = "toml")]
Error::TomlDe(ref e) => e, Error::TomlDe(ref e) => Some(e),
Error::HeaderStrError(ref e) => e, Error::HeaderStrError(ref e) => Some(e),
Error::HeaderParseError(ref e) => e, Error::HeaderParseError(ref e) => Some(e),
#[cfg(feature = "env")] #[cfg(feature = "env")]
Error::Envy(ref e) => e, Error::Envy(ref e) => Some(e),
Error::SerdeQs(ref e) => e, Error::SerdeQs(ref e) => Some(e),
Error::IntConversion(ref e) => e, Error::IntConversion(ref e) => Some(e),
Error::Client(..) | Error::Server(..) => return None, Error::Api {
Error::ClientIdRequired => return None, ..
Error::ClientSecretRequired => return None, }
Error::AccessTokenRequired => return None, | Error::ClientIdRequired
Error::MissingField(_) => return None, | Error::ClientSecretRequired
Error::Other(..) => return None, | Error::AccessTokenRequired
}) | Error::MissingField(_)
| Error::Other(..) => None,
}
} }
} }
/// Error returned from the Mastodon API. /// Error returned from the Mastodon API.
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ApiError { pub struct ApiError {
/// The type of error. /// The error message.
pub error: Option<String>, pub error: String,
/// The description of the error. /// A longer description of the error, mainly provided with the OAuth API.
pub error_description: Option<String>, pub error_description: Option<String>,
} }
@ -143,7 +145,6 @@ from! {
SerdeError => Serde, SerdeError => Serde,
UrlEncodedError => UrlEncoded, UrlEncodedError => UrlEncoded,
UrlError => Url, UrlError => Url,
ApiError => Api,
#[cfg(feature = "toml")] TomlSerError => TomlSer, #[cfg(feature = "toml")] TomlSerError => TomlSer,
#[cfg(feature = "toml")] TomlDeError => TomlDe, #[cfg(feature = "toml")] TomlDeError => TomlDe,
HeaderStrError => HeaderStrError, HeaderStrError => HeaderStrError,
@ -213,16 +214,6 @@ mod tests {
assert_is!(err, Error::Url(..)); 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")] #[cfg(feature = "toml")]
#[test] #[test]
fn from_toml_ser_error() { fn from_toml_ser_error() {

View File

@ -1,7 +1,6 @@
use envy; use envy;
use crate::Result; use crate::{Data, Result};
use data::Data;
/// Attempts to deserialize a Data struct from the environment /// Attempts to deserialize a Data struct from the environment
pub fn from_env() -> Result<Data> { pub fn from_env() -> Result<Data> {

View File

@ -1,23 +1,27 @@
use std::time::Duration; use std::time::Duration;
use crate::errors::Result; use crate::{errors::Result, log_serde, Error};
use futures::pin_mut; use futures::pin_mut;
use futures_util::StreamExt; use futures_util::StreamExt;
use log::{as_serde, debug, trace, warn}; use log::{as_debug, as_serde, debug, trace, warn};
use reqwest::Response; use reqwest::Response;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::time::timeout; use tokio::time::timeout;
/// Adapter for reading JSON data from a response with better logging and a /// Adapter for reading JSON data from a response with better logging and a
/// fail-safe timeout. /// 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<T>(response: Response) -> Result<T> pub async fn read_response<T>(response: Response) -> Result<T>
where where
T: for<'de> Deserialize<'de> + Serialize, T: for<'de> Deserialize<'de> + Serialize,
{ {
let mut bytes = vec![]; let mut bytes = vec![];
let url = response.url().clone(); let url = response.url().clone();
// let status = log_serde!(response Status); let status = response.status();
// let headers = log_serde!(response Headers); trace!(status = log_serde!(response Status), headers = log_serde!(response Headers); "attempting to stream response");
let stream = response.bytes_stream(); let stream = response.bytes_stream();
pin_mut!(stream); pin_mut!(stream);
loop { loop {
@ -35,23 +39,36 @@ where
); );
} else { } else {
warn!( warn!(
url = url.as_str(), // status = status, headers = headers, url = url.as_str(),
data_received = bytes.len(); data_received = bytes.len();
"API response timed out" "API response timed out"
); );
break; break;
} }
} }
// done growing the vec, let's just do this once.
let bytes = bytes.as_slice();
trace!( trace!(
url = url.as_str(), // status = status, headers = headers, url = url.as_str(),
data_received = bytes.len(); data = String::from_utf8_lossy(bytes);
"parsing response" "parsing response"
); );
let result = serde_json::from_slice(bytes.as_slice())?; if status.is_success() {
// the the response should deserialize to T
let result = serde_json::from_slice(bytes)?;
debug!( debug!(
url = url.as_str(), // status = status, headers = headers, url = url.as_str(),
result = as_serde!(result); result = as_serde!(result);
"result parsed successfully" "result parsed successfully"
); );
Ok(result) 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,
})
}
} }

View File

@ -18,22 +18,12 @@ macro_rules! methods {
async fn $method_with_call_id<T: for<'de> serde::Deserialize<'de> + serde::Serialize>(&self, url: impl AsRef<str>, call_id: Uuid) -> Result<T> async fn $method_with_call_id<T: for<'de> serde::Deserialize<'de> + serde::Serialize>(&self, url: impl AsRef<str>, call_id: Uuid) -> Result<T>
{ {
use log::{debug, error, as_debug, as_serde}; use log::{debug, as_debug};
let url = url.as_ref(); let url = url.as_ref();
debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request"); 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() { read_response(response).await
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())
}
}
} }
} }
)+ )+
@ -57,22 +47,14 @@ macro_rules! paged_routes {
"```" "```"
), ),
pub async fn $name(&self) -> Result<Page<$ret>> { pub async fn $name(&self) -> Result<Page<$ret>> {
use log::{debug, as_debug, error}; use log::{debug, as_debug};
let url = self.route(concat!("/api/v1/", $url)); let url = self.route(concat!("/api/v1/", $url));
let call_id = uuid::Uuid::new_v4(); let call_id = uuid::Uuid::new_v4();
debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request"); 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 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())
}
}
}
} }
@ -88,7 +70,7 @@ macro_rules! paged_routes {
), ),
pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<Page<$ret>> { pub async fn $name<'a>(&self, $($param: $typ,)*) -> Result<Page<$ret>> {
use serde_urlencoded; use serde_urlencoded;
use log::{debug, as_debug, error}; use log::{debug, as_debug};
let call_id = uuid::Uuid::new_v4(); let call_id = uuid::Uuid::new_v4();
@ -117,18 +99,10 @@ macro_rules! paged_routes {
debug!(url = url, method = "get", call_id = as_debug!(call_id); "making API request"); 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 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())
}
}
}
} }
paged_routes!{$($rest)*} paged_routes!{$($rest)*}
@ -181,6 +155,62 @@ macro_rules! route_v2 {
route_v2!{$($rest)*} 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> { pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> {
use reqwest::multipart::{Form, Part}; use reqwest::multipart::{Form, Part};
use std::io::Read; use std::io::Read;
use log::{debug, error, as_debug, as_serde}; use log::{debug, error, as_debug};
use uuid::Uuid; use uuid::Uuid;
let call_id = Uuid::new_v4(); let call_id = Uuid::new_v4();
@ -232,20 +262,11 @@ macro_rules! route {
let response = self.authenticated(self.client.post(url)) let response = self.authenticated(self.client.post(url))
.multipart(form_data) .multipart(form_data)
.header("Accept", "application/json")
.send() .send()
.await?; .await?;
match response.error_for_status() { read_response(response).await
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())
}
}
} }
} }
@ -304,7 +325,7 @@ macro_rules! route {
"`\n# Errors\nIf `access_token` is not set.", "`\n# Errors\nIf `access_token` is not set.",
), ),
pub async fn $name(&self, $($param: $typ,)*) -> Result<$ret> { 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; use uuid::Uuid;
let call_id = Uuid::new_v4(); let call_id = Uuid::new_v4();
@ -324,20 +345,11 @@ macro_rules! route {
let response = self.authenticated(self.client.$method(url)) let response = self.authenticated(self.client.$method(url))
.json(&form_data) .json(&form_data)
.header("Accept", "application/json")
.send() .send()
.await?; .await?;
match response.error_for_status() { read_response(response).await
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())
}
}
} }
} }
@ -413,24 +425,16 @@ macro_rules! paged_routes_with_id {
"```" "```"
), ),
pub async fn $name(&self, id: &str) -> Result<Page<$ret>> { pub async fn $name(&self, id: &str) -> Result<Page<$ret>> {
use log::{debug, error, as_debug}; use log::{debug, as_debug};
use uuid::Uuid; use uuid::Uuid;
let call_id = Uuid::new_v4(); let call_id = Uuid::new_v4();
let url = self.route(&format!(concat!("/api/v1/", $url), id)); 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"); 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 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())
}
}
}
} }
paged_routes_with_id!{$($rest)*} paged_routes_with_id!{$($rest)*}
@ -469,13 +473,19 @@ tokio_test::block_on(async {
), ),
pub async fn $fn_name(&self) -> Result<impl TryStream<Ok=Event, Error=Error>> { pub async fn $fn_name(&self) -> Result<impl TryStream<Ok=Event, Error=Error>> {
let url = self.route(&format!("/api/v1/streaming/{}", $stream)); 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!( debug!(
status = log_serde!(response Status), url = &url, status = log_serde!(response Status), url = &url,
headers = log_serde!(response Headers); headers = log_serde!(response Headers);
"received API response" "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)* } streaming! { $($rest)* }
@ -513,13 +523,19 @@ tokio_test::block_on(async {
let mut url: Url = self.route(concat!("/api/v1/streaming/", stringify!($stream))).parse()?; let mut url: Url = self.route(concat!("/api/v1/streaming/", stringify!($stream))).parse()?;
url.query_pairs_mut().append_pair(stringify!($param), $param.as_ref()); url.query_pairs_mut().append_pair(stringify!($param), $param.as_ref());
let url = url.to_string(); 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!( debug!(
status = log_serde!(response Status), url = as_debug!(url), status = log_serde!(response Status), url = as_debug!(url),
headers = log_serde!(response Headers); headers = log_serde!(response Headers);
"received API response" "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)* } streaming! { $($rest)* }

View File

@ -91,7 +91,6 @@ impl Mastodon {
(post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty, (post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty,
(get (local: bool,)) get_public_timeline: "timelines/public" => Vec<Status>, (get (local: bool,)) get_public_timeline: "timelines/public" => Vec<Status>,
(post (uri: Cow<'static, str>,)) follows: "follows" => Account, (post (uri: Cow<'static, str>,)) follows: "follows" => Account,
(post multipart (file: impl AsRef<Path>,)) media: "media" => Attachment,
(post) clear_notifications: "notifications/clear" => Empty, (post) clear_notifications: "notifications/clear" => Empty,
(post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty, (post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty,
(get) get_push_subscription: "push/subscription" => Subscription, (get) get_push_subscription: "push/subscription" => Subscription,
@ -102,6 +101,8 @@ impl Mastodon {
route_v2! { route_v2! {
(get (q: &'a str, resolve: bool,)) search: "search" => SearchResult, (get (q: &'a str, resolve: bool,)) search: "search" => SearchResult,
(post multipart (file: impl AsRef<Path>,)) media: "media" => Attachment,
(post multipart (file: impl AsRef<Path>, thumbnail: impl AsRef<Path>,)) media_with_thumbnail: "media" => Attachment,
} }
route_id! { route_id! {
@ -169,14 +170,14 @@ impl Mastodon {
/// POST /api/v1/filters /// POST /api/v1/filters
pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> { pub async fn add_filter(&self, request: &mut AddFilterRequest) -> Result<Filter> {
Ok(self let response = self
.client .client
.post(self.route("/api/v1/filters")) .post(self.route("/api/v1/filters"))
.json(&request) .json(&request)
.send() .send()
.await? .await?;
.json()
.await?) read_response(response).await
} }
/// PUT /api/v1/filters/:id /// PUT /api/v1/filters/:id
@ -184,15 +185,7 @@ impl Mastodon {
let url = self.route(&format!("/api/v1/filters/{}", id)); let url = self.route(&format!("/api/v1/filters/{}", id));
let response = self.client.put(&url).json(&request).send().await?; let response = self.client.put(&url).json(&request).send().await?;
let status = response.status(); read_response(response).await
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?)
} }
/// Update the user credentials /// Update the user credentials
@ -201,15 +194,7 @@ impl Mastodon {
let url = self.route("/api/v1/accounts/update_credentials"); let url = self.route("/api/v1/accounts/update_credentials");
let response = self.client.patch(&url).json(&changes).send().await?; let response = self.client.patch(&url).json(&changes).send().await?;
let status = response.status(); read_response(response).await
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?)
} }
/// Post a new status to the account. /// Post a new status to the account.
@ -225,7 +210,7 @@ impl Mastodon {
headers = log_serde!(response Headers); headers = log_serde!(response Headers);
"received API response" "received API response"
); );
Ok(read_response(response).await?) read_response(response).await
} }
/// Get timeline filtered by a hashtag(eg. `#coffee`) either locally or /// 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"); debug!(url = url, method = stringify!($method), call_id = as_debug!(call_id); "making API request");
let response = self.client.get(&url).send().await?; let response = self.client.get(&url).send().await?;
match response.error_for_status() { Page::new(self.clone(), response, call_id).await
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())
},
}
} }
/// Returns the client account's relationship to a list of other accounts. /// 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?; let response = self.client.get(&url).send().await?;
match response.error_for_status() { Page::new(self.clone(), response, call_id).await
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())
},
}
} }
/// Add a push notifications subscription /// Add a push notifications subscription
@ -341,18 +307,7 @@ impl Mastodon {
); );
let response = self.client.post(url).json(&request).send().await?; let response = self.client.post(url).json(&request).send().await?;
match response.error_for_status() { read_response(response).await
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())
},
}
} }
/// Update the `data` portion of the push subscription associated with this /// 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?; let response = self.client.post(url).json(&request).send().await?;
match response.error_for_status() { read_response(response).await
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())
},
}
} }
/// Get all accounts that follow the authenticated user /// Get all accounts that follow the authenticated user

View File

@ -1,5 +1,10 @@
use super::{Mastodon, Result}; 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 futures::Stream;
use hyper_old_types::header::{parsing, Link, RelationType}; use hyper_old_types::header::{parsing, Link, RelationType};
use log::{as_debug, as_serde, debug, error, trace}; use log::{as_debug, as_serde, debug, error, trace};
@ -107,6 +112,8 @@ impl<'a, T: for<'de> Deserialize<'de> + Serialize> Page<T> {
/// Create a new Page. /// Create a new Page.
pub(crate) async fn new(mastodon: Mastodon, response: Response, call_id: Uuid) -> Result<Self> { pub(crate) async fn new(mastodon: Mastodon, response: Response, call_id: Uuid) -> Result<Self> {
let status = response.status();
if status.is_success() {
let (prev, next) = get_links(&response, call_id)?; let (prev, next) = get_links(&response, call_id)?;
let initial_items = read_response(response).await?; let initial_items = read_response(response).await?;
debug!( debug!(
@ -121,6 +128,13 @@ impl<'a, T: for<'de> Deserialize<'de> + Serialize> Page<T> {
mastodon, mastodon,
call_id, call_id,
}) })
} else {
let response = response.json().await?;
Err(Error::Api {
status,
response,
})
}
} }
} }