diff --git a/src/http/mod.rs b/src/http/mod.rs index eb4235d005b..95d07aba1c1 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -45,7 +45,9 @@ use serde_json; use std::collections::BTreeMap; use std::default::Default; use std::fmt::Write as FmtWrite; +use std::fs::File; use std::io::{ErrorKind as IoErrorKind, Read}; +use std::path::PathBuf; use std::sync::{Arc, Mutex}; use ::constants; use ::internal::prelude::*; @@ -1378,6 +1380,7 @@ pub fn remove_group_recipient(group_id: u64, user_id: u64) -> Result<()> { /// if the file is too large to send. /// /// [`HttpError::InvalidRequest`]: enum.HttpError.html#variant.InvalidRequest +#[deprecated(since="0.2.0", note="Please use `send_files` instead.")] pub fn send_file(channel_id: u64, mut file: R, filename: &str, map: JsonMap) -> Result { let uri = format!(api!("/channels/{}/messages"), channel_id); @@ -1415,6 +1418,63 @@ pub fn send_file(channel_id: u64, mut file: R, filename: &str, map: Jso serde_json::from_reader::(response).map_err(From::from) } +/// Sends file(s) to a channel. +/// +/// # Errors +/// +/// Returns an +/// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] +/// if the file is too large to send. +/// +/// [`HttpError::InvalidRequest`]: enum.HttpError.html#variant.InvalidRequest +pub fn send_files>(channel_id: u64, files: Vec, map: JsonMap) + -> Result { + let uri = format!(api!("/channels/{}/messages"), channel_id); + let url = match Url::parse(&uri) { + Ok(url) => url, + Err(_) => return Err(Error::Url(uri)), + }; + + let mut request = Request::new(Method::Post, url)?; + request.headers_mut() + .set(header::Authorization(TOKEN.lock().unwrap().clone())); + request.headers_mut() + .set(header::UserAgent(constants::USER_AGENT.to_owned())); + + let mut request = Multipart::from_request(request)?; + let mut file_num = String::from("0".to_owned()); + + for file in files { + match file.into() { + AttachmentType::File((mut f, filename)) => { + request.write_stream(&file_num, &mut f, Some(&filename), None)?; + }, + AttachmentType::Path(p) => { + request.write_file(&file_num, &p)?; + }, + } + + unsafe { + let vec = file_num.as_mut_vec(); + vec[0] += 1; + } + } + + for (k, v) in map { + match v { + Value::Bool(false) => request.write_text(&k, "false")?, + Value::Bool(true) => request.write_text(&k, "true")?, + Value::Number(inner) => request.write_text(&k, inner.to_string())?, + Value::String(inner) => request.write_text(&k, inner)?, + _ => continue, + }; + } + + let response = request.send()?; + + serde_json::from_reader::(response).map_err(From::from) +} + /// Sends a message to a channel. pub fn send_message(channel_id: u64, map: &Value) -> Result { let body = map.to_string(); @@ -1544,6 +1604,32 @@ fn verify(expected_status_code: u16, mut response: HyperResponse) -> Result<()> Err(Error::Http(HttpError::InvalidRequest(response.status))) } +/// Enum that allows a user to pass a `Path` or a `File` type to `send_files` +pub enum AttachmentType { + /// Indicates that the `AttachmentType` is a `File` + File((File, String)), + /// Indicates that the `AttachmentType` is a `Path` + Path(PathBuf), +} + +impl From for AttachmentType { + fn from(s: String) -> AttachmentType { + AttachmentType::Path(PathBuf::from(&s)) + } +} + +impl<'a> From<&'a str> for AttachmentType { + fn from(s: &'a str) -> AttachmentType { + AttachmentType::Path(PathBuf::from(s)) + } +} + +impl<'a> From<(File, &'a str)> for AttachmentType { + fn from(f: (File, &str)) -> AttachmentType { + AttachmentType::File((f.0, f.1.to_owned())) + } +} + /// Representation of the method of a query to send for the [`get_guilds`] /// function. /// diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 121399a1940..086f749ff80 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -8,6 +8,8 @@ use ::builder::{CreateMessage, EditChannel, GetMessages}; use ::CACHE; #[cfg(feature="model")] use ::http; +#[cfg(feature="model")] +use ::http::AttachmentType; #[cfg(feature="model")] impl ChannelId { @@ -393,6 +395,8 @@ impl ChannelId { /// [`GuildChannel`]: struct.GuildChannel.html /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[deprecated(since="0.2.0", note="Please use `send_files` instead.")] + #[allow(deprecated)] pub fn send_file(&self, file: R, filename: &str, f: F) -> Result where F: FnOnce(CreateMessage) -> CreateMessage, R: Read { let mut map = f(CreateMessage::default()).0; @@ -410,6 +414,82 @@ impl ChannelId { http::send_file(self.0, file, filename, map) } + /// Sends a file along with optional message contents. The filename _must_ + /// be specified. + /// + /// Message contents may be passed by using the [`CreateMessage::content`] + /// method. + /// + /// An embed can _not_ be sent when sending a file. If you set one, it will + /// be automatically removed. + /// + /// The [Attach Files] and [Send Messages] permissions are required. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// + /// # Examples + /// + /// Send files with the paths `/path/to/file.jpg` and `/path/to/file2.jpg`: + /// + /// ```rust,no_run + /// use serenity::model::ChannelId; + /// + /// let channel_id = ChannelId(7); + /// + /// let paths = vec!["/path/to/file.jpg", "path/to/file2.jpg"]; + /// + /// let _ = channel_id.send_files(paths, |m| m.content("a file")); + /// ``` + /// + /// Send files using `File`: + /// + /// ```rust,no_run + /// use serenity::model::ChannelId; + /// use std::fs::File; + /// + /// let channel_id = ChannelId(7); + /// + /// let f1 = File::open("my_file.jpg").unwrap(); + /// let f2 = File::open("my_file2.jpg").unwrap(); + /// + /// let files = vec![(f1, "my_file.jpg"), (f2, "my_file2.jpg")]; + /// + /// let _ = channel_id.send_files(files, |m| m.content("a file")); + /// ``` + /// + /// # Errors + /// + /// If the content of the message is over the above limit, then a + /// [`ClientError::MessageTooLong`] will be returned, containing the number + /// of unicode code points over the limit. + /// + /// Returns an + /// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] + /// if the file is too large to send. + /// + /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong + /// [`HttpError::InvalidRequest`]: ../http/enum.HttpError.html#variant.InvalidRequest + /// [`CreateMessage::content`]: ../utils/builder/struct.CreateMessage.html#method.content + /// [`GuildChannel`]: struct.GuildChannel.html + /// [Attach Files]: permissions/constant.ATTACH_FILES.html + /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + pub fn send_files>(&self, files: Vec, f: F) -> Result + where F: FnOnce(CreateMessage) -> CreateMessage { + let mut map = f(CreateMessage::default()).0; + + if let Some(content) = map.get("content") { + if let Value::String(ref content) = *content { + if let Some(length_over) = Message::overflow_length(content) { + return Err(Error::Model(ModelError::MessageTooLong(length_over))); + } + } + } + + let _ = map.remove("embed"); + + http::send_files(self.0, files, map) + } + /// Sends a message to the channel. /// /// Refer to the documentation for [`CreateMessage`] for more information diff --git a/src/model/channel/group.rs b/src/model/channel/group.rs index aab3e3adbcf..6adedc60219 100644 --- a/src/model/channel/group.rs +++ b/src/model/channel/group.rs @@ -7,6 +7,8 @@ use ::model::*; use ::builder::{CreateMessage, GetMessages}; #[cfg(feature="model")] use ::http; +#[cfg(feature="model")] +use ::http::AttachmentType; /// A group channel - potentially including other [`User`]s - separate from a /// [`Guild`]. @@ -298,11 +300,37 @@ impl Group { /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[deprecated(since="0.2.0", note="Please use `send_files` instead.")] + #[allow(deprecated)] pub fn send_file(&self, file: R, filename: &str, f: F) -> Result where F: FnOnce(CreateMessage) -> CreateMessage, R: Read { self.channel_id.send_file(file, filename, f) } + /// Sends (a) file(s) along with optional message contents. + /// + /// Refer to [`ChannelId::send_file`] for examples and more information. + /// + /// The [Attach Files] and [Send Messages] permissions are required. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// + /// # Errors + /// + /// If the content of the message is over the above limit, then a + /// [`ClientError::MessageTooLong`] will be returned, containing the number + /// of unicode code points over the limit. + /// + /// [`ChannelId::send_file`]: struct.ChannelId.html#method.send_file + /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong + /// [Attach Files]: permissions/constant.ATTACH_FILES.html + /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[inline] + pub fn send_files>(&self, files: Vec, f: F) -> Result + where F: FnOnce(CreateMessage) -> CreateMessage { + self.channel_id.send_files(files, f) + } + /// Sends a message to the group with the given content. /// /// Refer to the documentation for [`CreateMessage`] for more information diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 8724de2223b..192206e9bef 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -10,6 +10,8 @@ use ::builder::{CreateInvite, CreateMessage, EditChannel, GetMessages}; use ::CACHE; #[cfg(feature="model")] use ::http; +#[cfg(feature="model")] +use ::http::AttachmentType; #[cfg(all(feature="model", feature="utils"))] use ::utils as serenity_utils; @@ -557,11 +559,37 @@ impl GuildChannel { /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[deprecated(since="0.2.0", note="Please use `send_files` instead.")] + #[allow(deprecated)] pub fn send_file(&self, file: R, filename: &str, f: F) -> Result where F: FnOnce(CreateMessage) -> CreateMessage, R: Read { self.id.send_file(file, filename, f) } + /// Sends (a) file(s) along with optional message contents. + /// + /// Refer to [`ChannelId::send_file`] for examples and more information. + /// + /// The [Attach Files] and [Send Messages] permissions are required. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// + /// # Errors + /// + /// If the content of the message is over the above limit, then a + /// [`ClientError::MessageTooLong`] will be returned, containing the number + /// of unicode code points over the limit. + /// + /// [`ChannelId::send_file`]: struct.ChannelId.html#method.send_file + /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong + /// [Attach Files]: permissions/constant.ATTACH_FILES.html + /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[inline] + pub fn send_files>(&self, files: Vec, f: F) -> Result + where F: FnOnce(CreateMessage) -> CreateMessage { + self.id.send_files(files, f) + } + /// Sends a message to the channel with the given content. /// /// **Note**: This will only work when a [`Message`] is received. diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 300f4021ab6..93ce5cb51ca 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -25,6 +25,8 @@ use ::model::*; #[cfg(feature="model")] use ::builder::{CreateMessage, GetMessages}; +#[cfg(feature="model")] +use ::http::AttachmentType; /// A container for any channel. #[derive(Clone, Debug)] @@ -293,11 +295,37 @@ impl Channel { /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[deprecated(since="0.2.0", note="Please use `send_files` instead.")] + #[allow(deprecated)] pub fn send_file(&self, file: R, filename: &str, f: F) -> Result where F: FnOnce(CreateMessage) -> CreateMessage, R: Read { self.id().send_file(file, filename, f) } + /// Sends (a) file(s) along with optional message contents. + /// + /// Refer to [`ChannelId::send_file`] for examples and more information. + /// + /// The [Attach Files] and [Send Messages] permissions are required. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// + /// # Errors + /// + /// If the content of the message is over the above limit, then a + /// [`ClientError::MessageTooLong`] will be returned, containing the number + /// of unicode code points over the limit. + /// + /// [`ChannelId::send_file`]: struct.ChannelId.html#method.send_file + /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong + /// [Attach Files]: permissions/constant.ATTACH_FILES.html + /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[inline] + pub fn send_files>(&self, files: Vec, f: F) -> Result + where F: FnOnce(CreateMessage) -> CreateMessage { + self.id().send_files(files, f) + } + /// Sends a message to the channel. /// /// Refer to the documentation for [`CreateMessage`] for more information diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index d63efff92a0..0432683db52 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -5,6 +5,8 @@ use ::model::*; #[cfg(feature="model")] use ::builder::{CreateMessage, GetMessages}; +#[cfg(feature="model")] +use ::http::AttachmentType; /// A Direct Message text channel with another user. #[derive(Clone, Debug, Deserialize)] @@ -244,11 +246,37 @@ impl PrivateChannel { /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[deprecated(since="0.2.0", note="Please use `send_files` instead.")] + #[allow(deprecated)] pub fn send_file(&self, file: R, filename: &str, f: F) -> Result where F: FnOnce(CreateMessage) -> CreateMessage, R: Read { self.id.send_file(file, filename, f) } + /// Sends (a) file(s) along with optional message contents. + /// + /// Refer to [`ChannelId::send_file`] for examples and more information. + /// + /// The [Attach Files] and [Send Messages] permissions are required. + /// + /// **Note**: Message contents must be under 2000 unicode code points. + /// + /// # Errors + /// + /// If the content of the message is over the above limit, then a + /// [`ClientError::MessageTooLong`] will be returned, containing the number + /// of unicode code points over the limit. + /// + /// [`ChannelId::send_file`]: struct.ChannelId.html#method.send_file + /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong + /// [Attach Files]: permissions/constant.ATTACH_FILES.html + /// [Send Messages]: permissions/constant.SEND_MESSAGES.html + #[inline] + pub fn send_files>(&self, files: Vec, f: F) -> Result + where F: FnOnce(CreateMessage) -> CreateMessage { + self.id.send_files(files, f) + } + /// Sends a message to the channel with the given content. /// /// Refer to the documentation for [`CreateMessage`] for more information