diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..693aa30 --- /dev/null +++ b/.env.sample @@ -0,0 +1,5 @@ +export TOKEN='snakeoil' +export CLIENT_ID='' +export CLIENT_SECRET='' +export REDIRECT='' +export BASE='https://mastodon.social' diff --git a/.gitignore b/.gitignore index a9d37c5..e08f5fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +.env diff --git a/src/entities/attachment.rs b/src/entities/attachment.rs index 5a0376f..5d67e13 100644 --- a/src/entities/attachment.rs +++ b/src/entities/attachment.rs @@ -1,12 +1,29 @@ #[derive(Debug, Clone, Deserialize)] pub struct Attachment { - pub id: u64, + pub id: String, #[serde(rename="type")] pub media_type: MediaType, pub url: String, - pub remote_url: String, + pub remote_url: Option, pub preview_url: String, pub text_url: Option, + pub meta: Option, + pub description: Option, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct Meta { + original: ImageDetails, + small: ImageDetails, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct ImageDetails { + width: u64, + height: u64, + size: String, + aspect: f64, + } #[derive(Debug, Deserialize, Clone, Copy)] diff --git a/src/lib.rs b/src/lib.rs index 04dad61..fad49b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,7 +19,7 @@ //! website: None, //! }; //! -//! let mut registration = Registration::new("https://mastodon.social")?; +//! let mut registration = Registration::new("https://mastodon.social"); //! registration.register(app)?; //! let url = registration.authorise()?; //! // Here you now need to open the url in the browser @@ -56,7 +56,7 @@ use std::io::Error as IoError; use json::Error as SerdeError; use reqwest::Error as HttpError; -use reqwest::Client; +use reqwest::{Client, StatusCode}; use reqwest::header::{Authorization, Bearer, Headers}; use entities::prelude::*; @@ -92,6 +92,49 @@ macro_rules! methods { macro_rules! route { + ((post multipart ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { + /// Equivalent to `/api/v1/ + #[doc = $url] + /// ` + /// + #[doc = "# Errors"] + /// If `access_token` is not set. + pub fn $name(&self, $($param: $typ,)*) -> Result<$ret> { + use std::io::Read; + use reqwest::multipart::Form; + + let form_data = Form::new() + $( + .file(stringify!($param), $param)? + )*; + + let mut response = self.client.post(&self.route(concat!("/api/v1/", $url))) + .headers(self.headers.clone()) + .multipart(form_data) + .send()?; + + let status = response.status().clone(); + + if status.is_client_error() { + return Err(Error::Client(status)); + } else if status.is_server_error() { + return Err(Error::Server(status)); + } + + let mut vec = Vec::new(); + + response.read_to_end(&mut vec)?; + + + match json::from_slice::<$ret>(&vec) { + Ok(res) => Ok(res), + Err(_) => Err(Error::Api(json::from_slice(&vec)?)), + } + } + + route!{$($rest)*} + }; + ((post ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { /// Equivalent to `/api/v1/ #[doc = $url] @@ -105,24 +148,32 @@ macro_rules! route { let form_data = json!({ $( stringify!($param): $param, - )* + )* }); let mut response = self.client.post(&self.route(concat!("/api/v1/", $url))) .headers(self.headers.clone()) - .form(&form_data) + .json(&form_data) .send()?; + let status = response.status().clone(); + + if status.is_client_error() { + return Err(Error::Client(status)); + } else if status.is_server_error() { + return Err(Error::Server(status)); + } + let mut vec = Vec::new(); response.read_to_end(&mut vec)?; - if let Ok(t) = json::from_slice(&vec) { - Ok(t) - } else { - Err(Error::Api(json::from_slice(&vec)?)) + match json::from_slice(&vec) { + Ok(res) => Ok(res), + Err(_) => Err(Error::Api(json::from_slice(&vec)?)), } } + route!{$($rest)*} }; @@ -196,6 +247,10 @@ pub enum Error { ClientSecretRequired, #[serde(skip_deserializing)] AccessTokenRequired, + #[serde(skip_deserializing)] + Client(StatusCode), + #[serde(skip_deserializing)] + Server(StatusCode), } impl fmt::Display for Error { @@ -211,6 +266,9 @@ impl StdError for Error { Error::Serde(ref e) => e.description(), Error::Http(ref e) => e.description(), Error::Io(ref e) => e.description(), + Error::Client(ref status) | Error::Server(ref status) => { + status.canonical_reason().unwrap_or("Unknown Status code") + }, Error::ClientIdRequired => "ClientIdRequired", Error::ClientSecretRequired => "ClientSecretRequired", Error::AccessTokenRequired => "AccessTokenRequired", @@ -256,15 +314,15 @@ impl Mastodon { } /// Creates a mastodon instance from the data struct. - pub fn from_data(data: Data) -> Result { + pub fn from_data(data: Data) -> Self { let mut headers = Headers::new(); headers.set(Authorization(Bearer { token: data.token.clone() })); - Ok(Mastodon { - client: Client::new()?, + Mastodon { + client: Client::new(), headers: headers, data: data, - }) + } } route! { @@ -279,7 +337,7 @@ impl Mastodon { (post (id: u64,)) reject_follow_request: "accounts/follow_requests/reject" => Empty, (post (uri: String,)) follows: "follows" => Account, (post) clear_notifications: "notifications/clear" => Empty, - (post (file: Vec,)) media: "media" => Attachment, + (post multipart (file: String,)) media: "media" => Attachment, (post (account_id: u64, status_ids: Vec, comment: String,)) report: "reports" => Report, (post (q: String, resolve: bool,)) search: "search" => SearchResult, diff --git a/src/registration.rs b/src/registration.rs index d725640..0407e1a 100644 --- a/src/registration.rs +++ b/src/registration.rs @@ -27,15 +27,15 @@ struct AccessToken { } impl Registration { - pub fn new>(base: I) -> Result { - Ok(Registration { + pub fn new>(base: I) -> Self { + Registration { base: base.into(), - client: Client::new()?, + client: Client::new(), client_id: None, client_secret: None, redirect: None, scopes: Scope::Read, - }) + } } /// Register the application with the server from the `base` url. @@ -56,7 +56,7 @@ impl Registration { /// website: None, /// }; /// - /// let mut registration = Registration::new("https://mastodon.social")?; + /// let mut registration = Registration::new("https://mastodon.social"); /// registration.register(app)?; /// let url = registration.authorise()?; /// // Here you now need to open the url in the browser diff --git a/tests/test.png b/tests/test.png new file mode 100644 index 0000000..89bf649 Binary files /dev/null and b/tests/test.png differ diff --git a/tests/upload_photo.rs b/tests/upload_photo.rs new file mode 100644 index 0000000..d4b0c61 --- /dev/null +++ b/tests/upload_photo.rs @@ -0,0 +1,29 @@ +extern crate mammut; +extern crate dotenv; + +use std::env; + +use mammut::{Data, Mastodon}; +use dotenv::dotenv; + +#[test] +fn upload_photo() { + dotenv().ok(); + run().unwrap(); +} + +fn run() -> mammut::Result<()> { + + let data = Data { + base: String::from(env::var("BASE").unwrap()), + client_id: String::from(env::var("CLIENT_ID").unwrap()), + client_secret: String::from(env::var("CLIENT_SECRET").unwrap()), + redirect: String::from(env::var("REDIRECT").unwrap()), + token: String::from(env::var("TOKEN").unwrap()), + }; + + let mastodon = Mastodon::from_data(data); + + mastodon.media("tests/test.png".into())?; + Ok(()) +}