diff options
| -rw-r--r-- | src/client/context.rs | 97 | ||||
| -rw-r--r-- | src/model/channel.rs | 2 | ||||
| -rw-r--r-- | src/model/user.rs | 11 | ||||
| -rw-r--r-- | src/utils/builder/create_embed.rs | 327 | ||||
| -rw-r--r-- | src/utils/builder/create_message.rs | 26 | ||||
| -rw-r--r-- | src/utils/builder/mod.rs | 8 |
6 files changed, 442 insertions, 29 deletions
diff --git a/src/client/context.rs b/src/client/context.rs index 6443efa..fe69a57 100644 --- a/src/client/context.rs +++ b/src/client/context.rs @@ -884,18 +884,99 @@ impl Context { /// Sends a message to a [`Channel`]. /// - /// Note that often a nonce is not required and can be omitted in most - /// situations. + /// Refer to the documentation for [`CreateMessage`] for more information + /// regarding message restrictions and requirements. /// /// **Note**: Message contents must be under 2000 unicode code points. /// /// # Example /// + /// Send a message with just the content `test`: + /// /// ```rust,ignore /// // assuming you are in a context - /// let _ = context.send_message(message.channel_id, "Hello!", "", false); + /// let _ = context.send_message(message.channel_id, |f| f.content("test")); /// ``` /// + /// Send a message on `!ping` with a very descriptive [`Embed`]. This sends + /// a message content of `"Pong! Here's some info"`, with an embed with the + /// following attributes: + /// + /// - Dark gold in colour; + /// - A description of `"Information about the message just posted"`; + /// - A title of `"Message Information"`; + /// - A URL of `"https://rust-lang.org"`; + /// - An [author structure] containing an icon and the user's name; + /// - An inline [field structure] containing the message's content with a + /// label; + /// - An inline field containing the channel's name with a label; + /// - A footer containing the current user's icon and name, saying that the + /// information was generated by them. + /// + /// ```rust,no_run + /// use serenity::client::{STATE, Client, Context}; + /// use serenity::model::{Channel, Message}; + /// use serenity::utils::Colour; + /// use std::env; + /// + /// let mut client = Client::login_bot(&env::var("DISCORD_TOKEN").unwrap()); + /// client.with_framework(|f| f + /// .configure(|c| c.prefix("~")) + /// .on("ping", ping)); + /// + /// client.on_ready(|_context, ready| { + /// println!("{} is connected!", ready.user.name); + /// }); + /// + /// let _ = client.start(); + /// + /// fn ping(context: Context, message: Message, _arguments: Vec<String>) { + /// let state = STATE.lock().unwrap(); + /// let ch = state.find_channel(message.channel_id); + /// let name = match ch { + /// Some(Channel::Public(ch)) => ch.name.clone(), + /// _ => "Unknown".to_owned(), + /// }; + /// + /// let _ = context.send_message(message.channel_id, |m| m + /// .content("Pong! Here's some info") + /// .embed(|e| e + /// .colour(Colour::dark_gold()) + /// .description("Information about the message just posted") + /// .title("Message information") + /// .url("https://rust-lang.org") + /// .author(|mut a| { + /// a = a.name(&message.author.name); + /// + /// if let Some(avatar) = message.author.avatar_url() { + /// a = a.icon_url(&avatar); + /// } + /// + /// a + /// }) + /// .field(|f| f + /// .inline(true) + /// .name("Message content:") + /// .value(&message.content)) + /// .field(|f| f + /// .inline(true) + /// .name("Channel name:") + /// .value(&name)) + /// .footer(|mut f| { + /// f = f.text(&format!("Generated by {}", state.user.name)); + /// + /// if let Some(avatar) = state.user.avatar_url() { + /// f = f.icon_url(&avatar); + /// } + /// + /// f + /// }))); + /// } + /// ``` + /// + /// Note that for most use cases, your embed layout will _not_ be this ugly. + /// This is an example of a very involved and conditional embed. + /// /// # Errors /// /// Returns a [`ClientError::MessageTooLong`] if the content of the message @@ -904,13 +985,17 @@ impl Context { /// /// [`Channel`]: ../model/enum.Channel.html /// [`ClientError::MessageTooLong`]: enum.ClientError.html#variant.MessageTooLong + /// [`CreateMessage`]: ../utils/builder/struct.CreateMessage.html + /// [`Embed`]: ../model/struct.Embed.html + /// [author structure]: ../utils/builder/struct.CreateEmbedAuthor.html + /// [field structure]: ../utils/builder/struct.CreateEmbedField.html pub fn send_message<C, F>(&self, channel_id: C, f: F) -> Result<Message> where C: Into<ChannelId>, F: FnOnce(CreateMessage) -> CreateMessage { let map = f(CreateMessage::default()).0; - if let Some(ref content) = map.get(&"content".to_owned()) { - if let &&Value::String(ref content) = content { - if let Some(length_over) = Message::overflow_length(&content) { + if let Some(content) = map.get(&"content".to_owned()) { + if let Value::String(ref content) = *content { + if let Some(length_over) = Message::overflow_length(content) { return Err(Error::Client(ClientError::MessageTooLong(length_over))); } } diff --git a/src/model/channel.rs b/src/model/channel.rs index 72a1f09..7348a1c 100644 --- a/src/model/channel.rs +++ b/src/model/channel.rs @@ -276,7 +276,7 @@ impl Embed { #[cfg(feature = "methods")] #[inline(always)] pub fn fake<F>(f: F) -> Value where F: FnOnce(CreateEmbed) -> CreateEmbed { - f(CreateEmbed::default()).0.build() + Value::Object(f(CreateEmbed::default()).0) } } diff --git a/src/model/user.rs b/src/model/user.rs index ce3d3a5..cd5072a 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,13 +1,14 @@ use std::fmt; use super::utils::{into_map, into_string, remove, warn_field}; use super::{ + CurrentUser, FriendSourceFlags, GuildContainer, GuildId, Mention, RoleId, UserSettings, - User + User, }; use ::internal::prelude::*; use ::utils::decode_array; @@ -22,6 +23,14 @@ use ::client::http; #[cfg(feature = "state")] use ::client::STATE; +impl CurrentUser { + /// Returns the formatted URL of the user's icon, if one exists. + pub fn avatar_url(&self) -> Option<String> { + self.avatar.as_ref().map(|av| + format!(cdn_concat!("/avatars/{}/{}.jpg"), self.id, av)) + } +} + impl User { /// Returns the formatted URL of the user's icon, if one exists. pub fn avatar_url(&self) -> Option<String> { diff --git a/src/utils/builder/create_embed.rs b/src/utils/builder/create_embed.rs index 1abfb4e..4264175 100644 --- a/src/utils/builder/create_embed.rs +++ b/src/utils/builder/create_embed.rs @@ -1,42 +1,335 @@ +//! Developer note: +//! +//! This is a set of embed builders for rich embeds. +//! +//! These are used in the [`Context::send_message`] and +//! [`ExecuteWebhook::embeds`] methods, both as part of builders. +//! +//! The only builder that should be exposed is [`CreateEmbed`]. The rest of +//! these have no real reason for being exposed, but are for completeness' sake. +//! +//! Documentation for embeds can be found [here]. +//! +//! [`Context::send_message`]: ../../client/struct.Context.html#method.send_message +//! [`CreateEmbed`]: struct.CreateEmbed.html +//! [`ExecuteWebhook::embeds`]: struct.ExecuteWebhook.html#method.embeds +//! [here]: https://discordapp.com/developers/docs/resources/channel#embed-object + use serde_json::builder::ObjectBuilder; +use serde_json::Value; +use std::collections::BTreeMap; use std::default::Default; +use ::utils::Colour; /// A builder to create a fake [`Embed`] object, for use with the -/// [`ExecuteWebhook::embeds`] method. +/// [`Context::send_message`] and [`ExecuteWebhook::embeds`] methods. +/// +/// # Examples /// -/// [`Embed`]: ../model/struct.Embed.html +/// Refer to the documentation for [`Context::send_message`] for a very in-depth +/// example on how to use this. +/// +/// [`Context::send_message`]: ../../client/struct.Context.html#method.send_message +/// [`Embed`]: ../../model/struct.Embed.html /// [`ExecuteWebhook::embeds`]: struct.ExecuteWebhook.html#method.embeds -pub struct CreateEmbed(pub ObjectBuilder); +pub struct CreateEmbed(pub BTreeMap<String, Value>); impl CreateEmbed { + /// Set the author of the embed. + /// + /// Refer to the documentation for [`CreateEmbedAuthor`] for more + /// information. + /// + /// [`CreateEmbedAuthor`]: struct.CreateEmbedAuthor.html + pub fn author<F>(mut self, f: F) -> Self + where F: FnOnce(CreateEmbedAuthor) -> CreateEmbedAuthor { + let author = f(CreateEmbedAuthor::default()).0.build(); + + self.0.insert("author".to_owned(), author); + + CreateEmbed(self.0) + } + /// Set the colour of the left-hand side of the embed. - pub fn colour(self, colour: u64) -> Self { - CreateEmbed(self.0.insert("color", colour)) + /// + /// This is an alias of [`colour`]. + /// + /// [`colour`]: #method.colour + pub fn color<C: Into<Colour>>(self, colour: C) -> Self { + self.colour(colour.into()) } - /// Set the description. - pub fn description(self, description: &str) -> Self { - CreateEmbed(self.0.insert("description", description)) + /// Set the colour of the left-hand side of the embed. + pub fn colour<C: Into<Colour>>(mut self, colour: C) -> Self { + self.0.insert("color".to_owned(), Value::U64(colour.into().value as u64)); + + CreateEmbed(self.0) + } + + /// Set the description of the embed. + pub fn description(mut self, description: &str) -> Self { + self.0.insert("description".to_owned(), Value::String(description.to_owned())); + + CreateEmbed(self.0) + } + + /// Set a field. Note that this will not overwrite other fields, and will + /// add to them. + /// + /// Refer to the documentation for [`CreateEmbedField`] for more + /// information. + /// + /// [`CreateEmbedField`]: struct.CreateEmbedField.html + pub fn field<F>(mut self, f: F) -> Self + where F: FnOnce(CreateEmbedField) -> CreateEmbedField { + let field = f(CreateEmbedField::default()).0.build(); + + { + let key = "fields".to_owned(); + + let entry = self.0.remove(&key).unwrap_or_else(|| Value::Array(vec![])); + let mut arr = match entry { + Value::Array(inner) => inner, + _ => { + // The type of `entry` should always be a `Value::Array`. + // + // Theoretically this never happens, but you never know. + // + // In the event that it does, just return the current value. + return CreateEmbed(self.0); + }, + }; + arr.push(field); + + self.0.insert("fields".to_owned(), Value::Array(arr)); + } + + CreateEmbed(self.0) + } + + /// Set the footer of the embed. + /// + /// Refer to the documentation for [`CreateEmbedFooter`] for more + /// information. + /// + /// [`CreateEmbedFooter`]: struct.CreateEmbedFooter.html + pub fn footer<F>(mut self, f: F) -> Self + where F: FnOnce(CreateEmbedFooter) -> CreateEmbedFooter { + let footer = f(CreateEmbedFooter::default()).0.build(); + + self.0.insert("footer".to_owned(), footer); + + CreateEmbed(self.0) + } + + /// Set the thumbnail of the embed. + /// + /// Refer to the documentation for [`CreateEmbedThumbnail`] for more + /// information. + /// + /// [`CreateEmbedThumbnail`]: struct.CreateEmbedThumbnail.html + pub fn thumbnail<F>(mut self, f: F) -> Self + where F: FnOnce(CreateEmbedThumbnail) -> CreateEmbedThumbnail { + let thumbnail = f(CreateEmbedThumbnail::default()).0.build(); + + self.0.insert("thumbnail".to_owned(), thumbnail); + + CreateEmbed(self.0) } /// Set the timestamp. - pub fn timestamp(self, timestamp: &str) -> Self { - CreateEmbed(self.0.insert("timestamp", timestamp)) + pub fn timestamp(mut self, timestamp: &str) -> Self { + self.0.insert("timestamp".to_owned(), Value::String(timestamp.to_owned())); + + CreateEmbed(self.0) } - /// Set the title. - pub fn title(self, title: &str) -> Self { - CreateEmbed(self.0.insert("title", title)) + /// Set the title of the embed. + pub fn title(mut self, title: &str) -> Self { + self.0.insert("title".to_owned(), Value::String(title.to_owned())); + + CreateEmbed(self.0) } - /// Set the URL. - pub fn url(self, url: &str) -> Self { - CreateEmbed(self.0.insert("url", url)) + /// Set the URL to direct to when clicking on the title. + pub fn url(mut self, url: &str) -> Self { + self.0.insert("url".to_owned(), Value::String(url.to_owned())); + + CreateEmbed(self.0) } } impl Default for CreateEmbed { + /// Creates a builder with default values, setting the `type` to `rich`. fn default() -> CreateEmbed { - CreateEmbed(ObjectBuilder::new()) + let mut map = BTreeMap::new(); + map.insert("type".to_owned(), Value::String("rich".to_owned())); + + CreateEmbed(map) + } +} + +/// A builder to create a fake [`Embed`] object's author, for use with the +/// [`CreateEmbed::author`] method. +/// +/// Requires that you specify a [`name`]. +/// +/// [`Embed`]: ../../model/struct.Embed.html +/// [`CreateEmbed::author`]: struct.CreateEmbed.html#method.author +/// [`name`]: #method.name +pub struct CreateEmbedAuthor(pub ObjectBuilder); + +impl CreateEmbedAuthor { + /// Set the URL of the author's icon. + pub fn icon_url(self, icon_url: &str) -> Self { + CreateEmbedAuthor(self.0.insert("icon_url", icon_url)) + } + + /// Set the author's name. + pub fn name(self, name: &str) -> Self { + CreateEmbedAuthor(self.0.insert("name", name)) + } + + /// Set the author's URL. + pub fn url(self, url: &str) -> Self { + CreateEmbedAuthor(self.0.insert("url", url)) + } +} + +impl Default for CreateEmbedAuthor { + fn default() -> CreateEmbedAuthor { + CreateEmbedAuthor(ObjectBuilder::new()) + } +} + +/// A builder to create a fake [`Embed`] object's field, for use with the +/// [`CreateEmbed::field`] method. +/// +/// This does not require any field be set. `inline` is set to `true` by +/// default. +/// +/// [`Embed`]: ../../model/struct.Embed.html +/// [`CreateEmbed::field`]: struct.CreateEmbed.html#method.field +pub struct CreateEmbedField(pub ObjectBuilder); + +impl CreateEmbedField { + /// Set whether the field is inlined. + pub fn inline(self, inline: bool) -> Self { + CreateEmbedField(self.0.insert("inline", inline)) + } + + /// Set the field's name. + pub fn name(self, name: &str) -> Self { + CreateEmbedField(self.0.insert("name", name)) + } + + /// Set the field's value. + pub fn value(self, value: &str) -> Self { + CreateEmbedField(self.0.insert("value", value)) + } +} + +impl Default for CreateEmbedField { + /// Creates a builder with default values, setting the value of `inline` to + /// `true`. + fn default() -> CreateEmbedField { + CreateEmbedField(ObjectBuilder::new()) + } +} + +/// A builder to create a fake [`Embed`] object's footer, for use with the +/// [`CreateEmbed::footer`] method. +/// +/// This does not require any field be set. +/// +/// [`Embed`]: ../../model/struct.Embed.html +/// [`CreateEmbed::footer`]: struct.CreateEmbed.html#method.footer +pub struct CreateEmbedFooter(pub ObjectBuilder); + +impl CreateEmbedFooter { + /// Set the icon URL's value. This only supports HTTP(S). + pub fn icon_url(self, icon_url: &str) -> Self { + CreateEmbedFooter(self.0.insert("icon_url", icon_url)) + } + + /// Set the footer's text. + pub fn text(self, text: &str) -> Self { + CreateEmbedFooter(self.0.insert("text", text)) + } +} + +impl Default for CreateEmbedFooter { + fn default() -> CreateEmbedFooter { + CreateEmbedFooter(ObjectBuilder::new()) + } +} + +/// A builder to create a fake [`Embed`] object's thumbnail, for use with the +/// [`CreateEmbed::thumbnail`] method. +/// +/// Requires that you specify a [`url`]. +/// +/// [`Embed`]: ../../model/struct.Embed.html +/// [`CreateEmbed::thumbnail`]: struct.CreateEmbed.html#method.thumbnail +/// [`url`]: #method.url +pub struct CreateEmbedThumbnail(pub ObjectBuilder); + +impl CreateEmbedThumbnail { + /// Set the height of the thumbnail, in pixels. + pub fn height(self, height: u64) -> Self { + CreateEmbedThumbnail(self.0.insert("height", height)) + } + + /// Set the URL of the thumbnail. This only supports HTTP(S). + /// + /// _Must_ be specified. + pub fn url(self, url: &str) -> Self { + CreateEmbedThumbnail(self.0.insert("url", url)) + } + + /// Set the width of the thumbnail, in pixels. + pub fn width(self, width: u64) -> Self { + CreateEmbedThumbnail(self.0.insert("width", width)) + } +} + +impl Default for CreateEmbedThumbnail { + fn default() -> CreateEmbedThumbnail { + CreateEmbedThumbnail(ObjectBuilder::new()) + } +} + +/// A builder to create a fake [`Embed`] object's video, for use with the +/// [`CreateEmbed::video`] method. +/// +/// Requires that you specify a [`url`]. +/// +/// [`Embed`]: ../../model/struct.Embed.html +/// [`CreateEmbed::video`]: struct.CreateEmbed.html#method.video +/// [`url`]: #method.url +pub struct CreateEmbedVideo(pub ObjectBuilder); + +impl CreateEmbedVideo { + /// Set the height of the video, in pixels. + pub fn height(self, height: u64) -> Self { + CreateEmbedVideo(self.0.insert("height", height)) + } + + /// Set the source URL of the video. + /// + /// _Must_ be specified. + pub fn url(self, url: &str) -> Self { + CreateEmbedVideo(self.0.insert("url", url)) + } + + /// Set the width of the video, in pixels. + pub fn width(self, width: &str) -> Self { + CreateEmbedVideo(self.0.insert("width", width)) + } +} + +impl Default for CreateEmbedVideo { + fn default() -> CreateEmbedVideo { + CreateEmbedVideo(ObjectBuilder::new()) } } diff --git a/src/utils/builder/create_message.rs b/src/utils/builder/create_message.rs index dc601a4..9bbc461 100644 --- a/src/utils/builder/create_message.rs +++ b/src/utils/builder/create_message.rs @@ -1,11 +1,16 @@ use serde_json::Value; use std::collections::BTreeMap; use std::default::Default; +use super::CreateEmbed; /// A builder to specify the contents of an [`http::create_message`] request, /// primarily meant for use through [`Context::send_message`]. /// -/// `content` is the only required field. +/// There are two situations where different field requirements are present: +/// +/// 1. When sending an [`embed`], no other field is required; +/// 2. Otherwise, [`content`] is the only required field that is required to be +/// set. /// /// Note that if you only need to send the content of a message, without /// specifying other fields, then [`Context::say`] may be a more preferable @@ -13,7 +18,7 @@ use std::default::Default; /// /// # Examples /// -/// Sending a message with a content of `test` and applying text-to-speech: +/// Sending a message with a content of `"test"` and applying text-to-speech: /// /// ```rust,ignore /// // assuming you are in a context @@ -24,6 +29,8 @@ use std::default::Default; /// /// [`Context::say`]: ../../client/struct.Context.html#method.say /// [`Context::send_message`]: ../../client/struct.Context.html#method.send_message +/// [`content`]: #method.content +/// [`embed`]: #method.embed /// [`http::create_message`]: ../../client/http/fn.create_message.html pub struct CreateMessage(pub BTreeMap<String, Value>); @@ -37,6 +44,16 @@ impl CreateMessage { CreateMessage(self.0) } + /// Set an embed for the message. + pub fn embed<F>(mut self, f: F) -> Self + where F: FnOnce(CreateEmbed) -> CreateEmbed { + let embed = Value::Object(f(CreateEmbed::default()).0); + + self.0.insert("embed".to_owned(), embed); + + CreateMessage(self.0) + } + /// Set the nonce. This is used for validation of a sent message. You most /// likely don't need to worry about this. /// @@ -60,8 +77,11 @@ impl CreateMessage { } impl Default for CreateMessage { - /// Creates a map for sending a [`Message`], setting `tts` to `false` by + /// Creates a map for sending a [`Message`], setting [`tts`] to `false` by /// default. + /// + /// [`Message`]: ../../model/struct.Message.html + /// [`tts`]: #method.tts fn default() -> CreateMessage { let mut map = BTreeMap::default(); map.insert("tts".to_owned(), Value::Bool(false)); diff --git a/src/utils/builder/mod.rs b/src/utils/builder/mod.rs index 91ad940..d4da891 100644 --- a/src/utils/builder/mod.rs +++ b/src/utils/builder/mod.rs @@ -16,7 +16,13 @@ mod edit_role; mod execute_webhook; mod get_messages; -pub use self::create_embed::CreateEmbed; +pub use self::create_embed::{ + CreateEmbed, + CreateEmbedAuthor, + CreateEmbedField, + CreateEmbedThumbnail, + CreateEmbedVideo, +}; pub use self::create_invite::CreateInvite; pub use self::create_message::CreateMessage; pub use self::edit_channel::EditChannel; |