diff --git a/src/lib.rs b/src/lib.rs index 4345abf..4504f8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,7 @@ pub use crate::data::Data; pub use crate::errors::{ApiError, Error, Result}; pub use isolang::Language; pub use crate::mastodon_client::{MastodonClient, MastodonUnauthenticated}; +pub use crate::media_builder::MediaBuilder; pub use crate::registration::Registration; pub use crate::requests::{ AddFilterRequest, @@ -145,6 +146,8 @@ pub mod helpers; /// Contains trait for converting `reqwest::Request`s to `reqwest::Response`s pub mod http_send; mod mastodon_client; +/// Constructing media attachments for a status. +pub mod media_builder; /// Handling multiple pages of entities. pub mod page; /// Registering your app. @@ -239,7 +242,6 @@ impl MastodonClient for Mastodon { (post (id: &str,)) reject_follow_request: "accounts/follow_requests/reject" => Empty, (get (q: &'a str, resolve: bool,)) search: "search" => SearchResult, (post (uri: Cow<'static, str>,)) follows: "follows" => Account, - (post multipart (file: Cow<'static, str>,)) media: "media" => Attachment, (post) clear_notifications: "notifications/clear" => Empty, (post (id: &str,)) dismiss_notification: "notifications/dismiss" => Empty, (get) get_push_subscription: "push/subscription" => Subscription, @@ -627,6 +629,38 @@ impl MastodonClient for Mastodon { Ok(EventReader(WebSocket(client))) } + + /// Equivalent to /api/v1/media + fn media(&self, media_builder: MediaBuilder) -> Result { + use reqwest::multipart::Form; + + let mut form_data = Form::new().file("file", media_builder.file.as_ref())?; + + if let Some(description) = media_builder.description { + form_data = form_data.text("description", description); + } + + if let Some(focus) = media_builder.focus { + let string = format!("{},{}", focus.0, focus.1); + form_data = form_data.text("focus", string); + } + + let response = self.send( + self.client + .post(&self.route("/api/v1/media")) + .multipart(form_data), + )?; + + 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)); + } + + deserialise(response) + } } #[derive(Debug)] diff --git a/src/macros.rs b/src/macros.rs index 0f9f1a2..8ee6623 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -144,41 +144,6 @@ macro_rules! route_v2 { macro_rules! route { - ((post multipart ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { - doc_comment! { - concat!( - "Equivalent to `post /api/v1/", - $url, - "`\n# Errors\nIf `access_token` is not set."), - fn $name(&self, $($param: $typ,)*) -> Result<$ret> { - use reqwest::multipart::Form; - - let form_data = Form::new() - $( - .file(stringify!($param), $param.as_ref())? - )*; - - let response = self.send( - self.client - .post(&self.route(concat!("/api/v1/", $url))) - .multipart(form_data) - )?; - - 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)); - } - - deserialise(response) - } - } - - route!{$($rest)*} - }; - ((get ($($param:ident: $typ:ty,)*)) $name:ident: $url:expr => $ret:ty, $($rest:tt)*) => { doc_comment! { concat!( diff --git a/src/mastodon_client.rs b/src/mastodon_client.rs index f65eda7..98d2cc3 100644 --- a/src/mastodon_client.rs +++ b/src/mastodon_client.rs @@ -3,6 +3,7 @@ use std::borrow::Cow; use crate::entities::prelude::*; use crate::errors::Result; use crate::http_send::{HttpSend, HttpSender}; +use crate::media_builder::MediaBuilder; use crate::page::Page; use crate::requests::{ AddFilterRequest, @@ -113,7 +114,7 @@ pub trait MastodonClient { unimplemented!("This method was not implemented"); } /// POST /api/v1/media - fn media(&self, file: Cow<'static, str>) -> Result { + fn media(&self, media_builder: MediaBuilder) -> Result { unimplemented!("This method was not implemented"); } /// POST /api/v1/notifications/clear diff --git a/src/media_builder.rs b/src/media_builder.rs new file mode 100644 index 0000000..5cd7639 --- /dev/null +++ b/src/media_builder.rs @@ -0,0 +1,70 @@ +use std::borrow::Cow; + +/// A builder pattern struct for constructing a media attachment. +#[derive(Debug, Default, Clone, Serialize)] +pub struct MediaBuilder { + /// The file name of the attachment to be uploaded. + pub file: Cow<'static, str>, + /// The alt text of the attachment. + pub description: Option>, + /// The focus point for images. + pub focus: Option<(f32, f32)>, +} + +impl MediaBuilder { + /// Create a new attachment from a file name. + pub fn new(file: Cow<'static, str>) -> Self { + MediaBuilder { + file, + description: None, + focus: None, + } + } + /// Set an alt text description for the attachment. + pub fn description(mut self, description: Cow<'static, str>) -> Self { + self.description = Some(description); + self + } + + /// Set a focus point for an image attachment. + pub fn focus(mut self, f1: f32, f2: f32) -> Self { + self.focus = Some((f1, f2)); + self + } +} + +// Convenience helper so that the mastodon.media() method can be called with a +// file name only (owned string). +impl From for MediaBuilder { + fn from(file: String) -> MediaBuilder { + MediaBuilder { + file: file.into(), + description: None, + focus: None, + } + } +} + +// Convenience helper so that the mastodon.media() method can be called with a +// file name only (borrowed string). +impl From<&'static str> for MediaBuilder { + fn from(file: &'static str) -> MediaBuilder { + MediaBuilder { + file: file.into(), + description: None, + focus: None, + } + } +} + +// Convenience helper so that the mastodon.media() method can be called with a +// file name only (Cow string). +impl From> for MediaBuilder { + fn from(file: Cow<'static, str>) -> MediaBuilder { + MediaBuilder { + file, + description: None, + focus: None, + } + } +}