diff options
| author | Austin Hellyer <[email protected]> | 2016-11-05 14:44:11 -0700 |
|---|---|---|
| committer | Austin Hellyer <[email protected]> | 2016-11-05 14:44:11 -0700 |
| commit | e3416d3f511894553b7625c501043077977ccb4d (patch) | |
| tree | c2f9f5d35a90c649dcc67c6d15e4a9ae8478cdeb /src/model | |
| parent | Update URLs to GitLab (diff) | |
| download | serenity-e3416d3f511894553b7625c501043077977ccb4d.tar.xz serenity-e3416d3f511894553b7625c501043077977ccb4d.zip | |
Add message reactions
Add message reaction structs and an enum to differentiate between the
two types of reactions, as well as event decoding and event handlers
with dispatches.
The following is, more or less, what is added:
- `reactions` field to the `Message` struct;
- `MessageReaction` struct, which is a consolidated form of reaction,
containing the type of reaction, the number of them, and whether the
current user has performed that type of reaction;
- `Reaction`, a struct containing the information about a reaction
- `ReactionType`, an enum to differentiate between the two types of
reactions: `Custom` (a guild's custom emoji) and `Unicode` (twemoji);
- Decoding for `MESSAGE_REACTION_ADD` and `MESSAGE_REACTION_REMOVE`;
- Permission flag `ADD_REACTIONS`;
- `Message::react` method;
- Three `http` payload senders: `create_reaction`, `delete_reaction`,
and `get_reaction_users`;
- Three `Context` methods of equal names to the above.
Diffstat (limited to 'src/model')
| -rw-r--r-- | src/model/channel.rs | 202 | ||||
| -rw-r--r-- | src/model/gateway.rs | 30 | ||||
| -rw-r--r-- | src/model/guild.rs | 3 | ||||
| -rw-r--r-- | src/model/permissions.rs | 24 | ||||
| -rw-r--r-- | src/model/utils.rs | 4 |
5 files changed, 254 insertions, 9 deletions
diff --git a/src/model/channel.rs b/src/model/channel.rs index ca9a3f8..fa26bc5 100644 --- a/src/model/channel.rs +++ b/src/model/channel.rs @@ -317,6 +317,31 @@ impl Message { http::pin_message(self.channel_id.0, self.id.0) } + /// React to the message with a custom [`Emoji`] or unicode character. + /// + /// **Note**: Requires the [Add Reactions] permission. + /// + /// # Errors + /// + /// Returns a [`ClientError::InvalidPermissions`] if the current user does + /// not have the required [permissions]. + /// + /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#variant.InvalidPermissions + /// [`Emoji`]: struct.Emoji.html + /// [Add Reactions]: permissions/constant.ADD_REACTIONS.html + /// [permissions]: permissions + pub fn react<R: Into<ReactionType>>(&self, reaction_type: R) -> Result<()> { + let req = permissions::ADD_REACTIONS; + + if !try!(utils::user_has_perms(self.channel_id, req)) { + return Err(Error::Client(ClientError::InvalidPermissions(req))); + } + + http::create_reaction(self.channel_id.0, + self.id.0, + reaction_type.into()) + } + /// Replies to the user, mentioning them prior to the content in the form /// of: `@<USER_ID>: YOUR_CONTENT`. /// @@ -603,3 +628,180 @@ impl fmt::Display for PublicChannel { fmt::Display::fmt(&self.mention(), f) } } + +impl Reaction { + /// Deletes the reaction, but only if the current user is the user who made + /// the reaction or has permission to. + /// + /// **Note**: Requires the [`Manage Messages`] permission, _if_ the current + /// user did not perform the reaction. + /// + /// # Errors + /// + /// Returns a [`ClientError::InvalidPermissions`] if the current user does + /// not have the required [permissions]. + /// + /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#variant.InvalidPermissions + /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html + /// [permissions]: permissions + pub fn delete(&self) -> Result<()> { + let user = if self.user_id == STATE.lock().unwrap().user.id { + None + } else { + Some(self.user_id.0) + }; + + // If the reaction is one _not_ made by the current user, then ensure + // that the current user has permission* to delete the reaction. + // + // Normally, users can only delete their own reactions. + // + // * The `Manage Messages` permission. + if user.is_some() { + let req = permissions::MANAGE_MESSAGES; + + if !utils::user_has_perms(self.channel_id, req).unwrap_or(true) { + return Err(Error::Client(ClientError::InvalidPermissions(req))); + } + } + + http::delete_reaction(self.channel_id.0, + self.message_id.0, + user, + self.emoji.clone()) + } + + /// Retrieves the list of [`User`]s who have reacted to a [`Message`] with a + /// certain [`Emoji`]. + /// + /// The default `limit` is `50` - specify otherwise to receive a different + /// maximum number of users. The maximum that may be retrieve at a time is + /// `100`, if a greater number is provided then it is automatically reduced. + /// + /// The optional `after` attribute is to retrieve the users after a certain + /// user. This is useful for pagination. + /// + /// **Note**: Requires the [Read Message History] permission. + /// + /// # Errors + /// + /// Returns a [`ClientError::InvalidPermissions`] if the current user does + /// not have the required [permissions]. + /// + /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#variant.InvalidPermissions + /// [`Emoji`]: struct.Emoji.html + /// [`Message`]: struct.Message.html + /// [`User`]: struct.User.html + /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html + /// [permissions]: permissions + pub fn users<R, U>(&self, + reaction_type: R, + limit: Option<u8>, + after: Option<U>) + -> Result<Vec<User>> + where R: Into<ReactionType>, + U: Into<UserId> { + http::get_reaction_users(self.channel_id.0, + self.message_id.0, + reaction_type.into(), + limit.unwrap_or(50), + after.map(|u| u.into().0)) + } +} + +/// The type of a [`Reaction`] sent. +/// +/// [`Reaction`]: struct.Reaction.html +#[derive(Clone, Debug)] +pub enum ReactionType { + /// A reaction with a [`Guild`]s custom [`Emoji`], which is unique to the + /// guild. + /// + /// [`Emoji`]: struct.Emoji.html + /// [`Guild`]: struct.Guild.html + Custom { + /// The Id of the custom [`Emoji`]. + /// + /// [`Emoji`]: struct.Emoji.html + id: EmojiId, + /// The name of the custom emoji. This is primarily used for decoration + /// and distinguishing the emoji client-side. + name: String, + }, + /// A reaction with a twemoji. + Unicode(String), +} + +impl ReactionType { + /// Creates a data-esque display of the type. This is not very useful for + /// displaying, as the primary client can not render it, but can be useful + /// for debugging. + /// + /// **Note**: This is mainly for use internally. There is otherwise most + /// likely little use for it. + #[inline(always)] + pub fn as_data(&self) -> String { + match *self { + ReactionType::Custom { id, ref name } => { + format!("{}:{}", id, name) + }, + ReactionType::Unicode(ref unicode) => unicode.clone(), + } + } + + pub fn decode(value: Value) -> Result<Self> { + let mut map = try!(into_map(value)); + let name = try!(remove(&mut map, "name").and_then(into_string)); + + // Only custom emoji reactions (`ReactionType::Custom`) have an Id. + Ok(match try!(opt(&mut map, "id", EmojiId::decode)) { + Some(id) => ReactionType::Custom { + id: id, + name: name, + }, + None => ReactionType::Unicode(name), + }) + } +} + +impl From<Emoji> for ReactionType { + fn from(emoji: Emoji) -> ReactionType { + ReactionType::Custom { + id: emoji.id, + name: emoji.name, + } + } +} + +impl From<String> for ReactionType { + fn from(unicode: String) -> ReactionType { + ReactionType::Unicode(unicode) + } +} + +impl fmt::Display for ReactionType { + /// Formats the reaction type, displaying the associated emoji in a + /// way that clients can understand. + /// + /// If the type is a [custom][`ReactionType::Custom`] emoji, then refer to + /// the documentation for [emoji's formatter][`Emoji::fmt`] on how this is + /// displayed. Otherwise, if the type is a + /// [unicode][`ReactionType::Unicode`], then the inner unicode is displayed. + /// + /// [`Emoji::fmt`]: struct.Emoji.html#method.fmt + /// [`ReactionType::Custom`]: enum.ReactionType.html#variant.Custom + /// [`ReactionType::Unicode`]: enum.ReactionType.html#variant.Unicode + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + ReactionType::Custom { id, ref name } => { + try!(f.write_char('<')); + try!(f.write_char(':')); + try!(f.write_str(&name)); + try!(f.write_char(':')); + try!(fmt::Display::fmt(&id, f)); + f.write_char('>') + }, + ReactionType::Unicode(ref unicode) => f.write_str(&unicode), + } + } +} diff --git a/src/model/gateway.rs b/src/model/gateway.rs index b267164..cf226df 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -212,6 +212,16 @@ pub struct PresencesReplaceEvent { pub presences: Vec<Presence>, } +#[derive(Clone, Debug)] +pub struct ReactionAddEvent { + pub reaction: Reaction, +} + +#[derive(Clone, Debug)] +pub struct ReactionRemoveEvent { + pub reaction: Reaction, +} + /// The "Ready" event, containing initial state #[derive(Clone, Debug)] pub struct ReadyEvent { @@ -439,6 +449,18 @@ pub enum Event { PresenceUpdate(PresenceUpdateEvent), /// The precense list of the user's friends should be replaced entirely PresencesReplace(PresencesReplaceEvent), + /// A reaction was added to a message. + /// + /// Fires the [`on_message_reaction_add`] event handler. + /// + /// [`on_message_reaction_add`]: ../client/struct.Client.html#method.on_message_reaction_add + ReactionAdd(ReactionAddEvent), + /// A reaction was removed to a message. + /// + /// Fires the [`on_message_reaction_remove`] event handler. + /// + /// [`on_message_reaction_remove`]: ../client/struct.Client.html#method.on_message_reaction_remove + ReactionRemove(ReactionRemoveEvent), /// The first event in a connection, containing the initial state. /// /// May also be received at a later time in the event of a reconnect. @@ -629,6 +651,14 @@ impl Event { channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)), ids: try!(decode_array(try!(remove(&mut value, "ids")), MessageId::decode)), })) + } else if kind == "MESSAGE_REACTION_ADD" { + Ok(Event::ReactionAdd(ReactionAddEvent { + reaction: try!(Reaction::decode(Value::Object(value))) + })) + } else if kind == "MESSAG_REACTION_REMOVE" { + Ok(Event::ReactionRemove(ReactionRemoveEvent { + reaction: try!(Reaction::decode(Value::Object(value))) + })) } else if kind == "MESSAGE_UPDATE" { missing!(value, Event::MessageUpdate(MessageUpdateEvent { id: try!(remove(&mut value, "id").and_then(MessageId::decode)), diff --git a/src/model/guild.rs b/src/model/guild.rs index 6bc14c5..1e4fea8 100644 --- a/src/model/guild.rs +++ b/src/model/guild.rs @@ -95,7 +95,8 @@ impl Emoji { impl fmt::Display for Emoji { /// Formats the emoji into a string that will cause Discord clients to /// render the emoji. - // This is in the format of: `<:NAME:EMOJI_ID>`. + /// + /// This is in the format of: `<:NAME:EMOJI_ID>`. fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { try!(f.write_str("<:")); try!(f.write_str(&self.name)); diff --git a/src/model/permissions.rs b/src/model/permissions.rs index 7eccc5c..3bcf219 100644 --- a/src/model/permissions.rs +++ b/src/model/permissions.rs @@ -42,6 +42,7 @@ use ::prelude::*; /// /// This includes the following permissions: /// +/// - [Add Reactions] /// - [Attach Files] /// - [Change Nickname] /// - [Connect] @@ -65,6 +66,7 @@ use ::prelude::*; /// permissions::general().toggle(permissions::SEND_TTS_MESSAGES); /// ``` /// +/// [Add Reactions]: constant.ADD_REACTIONS.html /// [Attach Files]: constant.ATTACH_FILES.html /// [Change Nickname]: constant.CHANGE_NICKNAME.html /// [Connect]: constant.CONNECT.html @@ -81,9 +83,9 @@ use ::prelude::*; pub fn general() -> Permissions { use self::*; - ATTACH_FILES | CHANGE_NICKNAME | CONNECT | CREATE_INVITE | EMBED_LINKS | - MENTION_EVERYONE | READ_MESSAGE_HISTORY | READ_MESSAGES | SEND_MESSAGES | - SEND_TTS_MESSAGES | SPEAK | USE_VAD | USE_EXTERNAL_EMOJIS + ADD_REACTIONS | ATTACH_FILES | CHANGE_NICKNAME | CONNECT | CREATE_INVITE | + EMBED_LINKS | MENTION_EVERYONE | READ_MESSAGE_HISTORY | READ_MESSAGES | + SEND_MESSAGES | SEND_TTS_MESSAGES | SPEAK | USE_VAD | USE_EXTERNAL_EMOJIS } /// Returns a set of text-only permissions with the original `@everyone` @@ -91,6 +93,7 @@ pub fn general() -> Permissions { /// /// This includes the text permissions given via [`general`]: /// +/// - [Add Reactions] /// - [Attach Files] /// - [Change Nickname] /// - [Create Invite] @@ -103,6 +106,7 @@ pub fn general() -> Permissions { /// - [Use External Emojis] /// /// [`general`]: fn.general.html +/// [Add Reactions]: constant.ADD_REACTIONS.html /// [Attach Files]: constant.ATTACH_FILES.html /// [Change Nickname]: constant.CHANGE_NICKNAME.html /// [Create Invite]: constant.CREATE_INVITE.html @@ -116,9 +120,9 @@ pub fn general() -> Permissions { pub fn text() -> Permissions { use self::*; - ATTACH_FILES | CHANGE_NICKNAME | CREATE_INVITE | EMBED_LINKS | - MENTION_EVERYONE | READ_MESSAGE_HISTORY | READ_MESSAGES | SEND_MESSAGES | - SEND_TTS_MESSAGES | USE_EXTERNAL_EMOJIS + ADD_REACTIONS | ATTACH_FILES | CHANGE_NICKNAME | CREATE_INVITE | + EMBED_LINKS | MENTION_EVERYONE | READ_MESSAGE_HISTORY | READ_MESSAGES | + SEND_MESSAGES | SEND_TTS_MESSAGES | USE_EXTERNAL_EMOJIS } /// Returns a set of voice-only permissions with the original `@everyone` @@ -166,6 +170,14 @@ bitflags! { /// /// [guild]: ../struct.LiveGuild.html const MANAGE_GUILD = 1 << 5, + /// [`Member`]s with this permission can add new [`Reaction`]s to a + /// [`Message`]. Members can still react using reactions already added + /// to messages without this permission. + /// + /// [`Member`]: ../struct.Member.html + /// [`Message`]: ../struct.Message.html + /// [`Reaction`]: ../struct.Reaction.html + const ADD_REACTIONS = 1 << 6, /// Allows reading messages in a guild channel. If a user does not have /// this permission, then they will not be able to see the channel. const READ_MESSAGES = 1 << 10, diff --git a/src/model/utils.rs b/src/model/utils.rs index 4ad97bf..0dbc41d 100644 --- a/src/model/utils.rs +++ b/src/model/utils.rs @@ -277,8 +277,8 @@ pub fn remove(map: &mut BTreeMap<String, Value>, key: &str) -> Result<Value> { #[doc(hidden)] pub fn user_has_perms(channel_id: ChannelId, - mut permissions: Permissions) - -> Result<bool> { + mut permissions: Permissions) + -> Result<bool> { let state = STATE.lock().unwrap(); let current_user = &state.user; |