aboutsummaryrefslogtreecommitdiff
path: root/src/model
diff options
context:
space:
mode:
authorAustin Hellyer <[email protected]>2016-09-19 09:00:03 -0700
committerAustin Hellyer <[email protected]>2016-10-18 11:14:27 -0700
commit8fc8c81403c3daa187ba96a7d488a64db21463bf (patch)
tree81bc4890c28b08ce806f69084617066bce863c2d /src/model
downloadserenity-8fc8c81403c3daa187ba96a7d488a64db21463bf.tar.xz
serenity-8fc8c81403c3daa187ba96a7d488a64db21463bf.zip
Initial commit
Diffstat (limited to 'src/model')
-rw-r--r--src/model/channel.rs605
-rw-r--r--src/model/gateway.rs784
-rw-r--r--src/model/guild.rs933
-rw-r--r--src/model/id.rs176
-rw-r--r--src/model/invite.rs47
-rw-r--r--src/model/misc.rs95
-rw-r--r--src/model/mod.rs140
-rw-r--r--src/model/permissions.rs446
-rw-r--r--src/model/user.rs146
-rw-r--r--src/model/utils.rs313
-rw-r--r--src/model/voice.rs0
11 files changed, 3685 insertions, 0 deletions
diff --git a/src/model/channel.rs b/src/model/channel.rs
new file mode 100644
index 0000000..ca9a3f8
--- /dev/null
+++ b/src/model/channel.rs
@@ -0,0 +1,605 @@
+use serde_json::builder::ObjectBuilder;
+use std::borrow::Cow;
+use std::fmt::{self, Write};
+use std::mem;
+use super::utils::{
+ decode_id,
+ into_map,
+ into_string,
+ opt,
+ remove,
+ warn_field,
+};
+use super::*;
+use super::utils;
+use ::builder::{CreateInvite, EditChannel};
+use ::client::{STATE, http};
+use ::prelude::*;
+use ::utils::decode_array;
+
+impl Attachment {
+ /// If this attachment is an image, then a tuple of the width and height
+ /// in pixels is returned.
+ pub fn dimensions(&self) -> Option<(u64, u64)> {
+ if let (Some(width), Some(height)) = (self.width, self.height) {
+ Some((width, height))
+ } else {
+ None
+ }
+ }
+}
+
+impl Channel {
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<Channel> {
+ let map = try!(into_map(value));
+ match req!(map.get("type").and_then(|x| x.as_u64())) {
+ 0 | 2 => PublicChannel::decode(Value::Object(map))
+ .map(Channel::Public),
+ 1 => PrivateChannel::decode(Value::Object(map))
+ .map(Channel::Private),
+ 3 => Group::decode(Value::Object(map))
+ .map(Channel::Group),
+ other => Err(Error::Decode("Expected value Channel type",
+ Value::U64(other))),
+ }
+ }
+
+ /// Deletes the inner channel.
+ ///
+ /// **Note**: There is no real function as _deleting_ a [`Group`]. The
+ /// closest functionality is leaving it.
+ ///
+ /// [`Group`]: struct.Group.html
+ pub fn delete(&self) -> Result<()> {
+ match *self {
+ Channel::Group(ref group) => {
+ let _ = try!(group.leave());
+ },
+ Channel::Private(ref private_channel) => {
+ let _ = try!(private_channel.delete());
+ },
+ Channel::Public(ref public_channel) => {
+ let _ = try!(public_channel.delete());
+ },
+ }
+
+ Ok(())
+ }
+
+ /// Retrieves the Id of the inner [`Group`], [`PublicChannel`], or
+ /// [`PrivateChannel`].
+ ///
+ /// [`Group`]: struct.Group.html
+ /// [`PublicChannel`]: struct.PublicChannel.html
+ /// [`PrivateChannel`]: struct.PrivateChannel.html
+ pub fn id(&self) -> ChannelId {
+ match *self {
+ Channel::Group(ref group) => group.channel_id,
+ Channel::Private(ref channel) => channel.id,
+ Channel::Public(ref channel) => channel.id,
+ }
+ }
+}
+
+impl fmt::Display for Channel {
+ /// Formats the channel into a "mentioned" string.
+ ///
+ /// This will return a different format for each type of channel:
+ ///
+ /// - [`Group`]s: the generated name retrievable via [`Group::name`];
+ /// - [`PrivateChannel`]s: the recipient's name;
+ /// - [`PublicChannel`]s: a string mentioning the channel that users who can
+ /// see the channel can click on.
+ ///
+ /// [`Group`]: struct.Group.html
+ /// [`Group::name`]: struct.Group.html#method.name
+ /// [`PublicChannel`]: struct.PublicChannel.html
+ /// [`PrivateChannel`]: struct.PrivateChannel.html
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let out = match *self {
+ Channel::Group(ref group) => group.name().to_owned(),
+ Channel::Private(ref channel) => Cow::Owned(channel.recipient.name.clone()),
+ Channel::Public(ref channel) => Cow::Owned(format!("{}", channel)),
+ };
+
+ fmt::Display::fmt(&out, f)
+ }
+}
+
+impl Group {
+ /// Adds the given user to the group. If the user is already in the group,
+ /// then nothing is done.
+ ///
+ /// **Note**: Groups have a limit of 10 recipients, including the current
+ /// user.
+ pub fn add_recipient<U: Into<UserId>>(&self, user: U) -> Result<()> {
+ let user = user.into();
+
+ // If the group already contains the recipient, do nothing.
+ if self.recipients.contains_key(&user) {
+ return Ok(());
+ }
+
+ http::add_group_recipient(self.channel_id.0, user.0)
+ }
+
+ /// Broadcasts that the current user is typing in the group.
+ pub fn broadcast_typing(&self) -> Result<()> {
+ http::broadcast_typing(self.channel_id.0)
+ }
+
+ /// Deletes multiple messages in the group.
+ ///
+ /// Refer to
+ /// [`Context::delete_messages`] for more information.
+ ///
+ /// **Note**: Only 2 to 100 messages may be deleted in a single request.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [`ClientError::DeleteMessageDaysAmount`] if the number of messages to
+ /// delete is not within the valid range.
+ ///
+ /// [`ClientError::DeleteMessageDaysAmount`]: ../client/enum.ClientError.html#DeleteMessageDaysAmount.v
+ /// [`Context::delete_messages`]: ../client/struct.Context.html#delete_messages
+ pub fn delete_messages(&self, message_ids: &[MessageId]) -> Result<()> {
+ if message_ids.len() < 2 || message_ids.len() > 100 {
+ return Err(Error::Client(ClientError::BulkDeleteAmount));
+ }
+
+ let ids: Vec<u64> = message_ids.into_iter()
+ .map(|message_id| message_id.0)
+ .collect();
+
+ let map = ObjectBuilder::new()
+ .insert("messages", ids)
+ .build();
+
+ http::delete_messages(self.channel_id.0, map)
+ }
+
+ /// Returns the formatted URI of the group's icon if one exists.
+ pub fn icon_url(&self) -> Option<String> {
+ self.icon.as_ref().map(|icon|
+ format!(cdn_concat!("/channel-icons/{}/{}.jpg"), self.channel_id, icon))
+ }
+
+ /// Leaves the group.
+ pub fn leave(&self) -> Result<Group> {
+ http::leave_group(self.channel_id.0)
+ }
+
+ /// Generates a name for the group.
+ ///
+ /// If there are no recipients in the group, the name will be "Empty Group".
+ /// Otherwise, the name is generated in a Comma Separated Value list, such
+ /// as "person 1, person 2, person 3".
+ pub fn name(&self) -> Cow<str> {
+ match self.name {
+ Some(ref name) => Cow::Borrowed(name),
+ None => {
+ let mut name = match self.recipients.values().nth(0) {
+ Some(recipient) => recipient.name.clone(),
+ None => return Cow::Borrowed("Empty Group"),
+ };
+
+ for recipient in self.recipients.values().skip(1) {
+ let _ = write!(name, ", {}", recipient.name);
+ }
+
+ Cow::Owned(name)
+ }
+ }
+ }
+
+ /// Retrieves the list of messages that have been pinned in the group.
+ pub fn pins(&self) -> Result<Vec<Message>> {
+ http::get_pins(self.channel_id.0)
+ }
+
+ /// Removes a recipient from the group. If the recipient is already not in
+ /// the group, then nothing is done.
+ ///
+ /// **Note**: This is only available to the group owner.
+ pub fn remove_recipient<U: Into<UserId>>(&self, user: U) -> Result<()> {
+ let user = user.into();
+
+ // If the group does not contain the recipient already, do nothing.
+ if !self.recipients.contains_key(&user) {
+ return Ok(());
+ }
+
+ http::remove_group_recipient(self.channel_id.0, user.0)
+ }
+
+ /// Sends a message to the group with the given content.
+ ///
+ /// Note that an @everyone mention will not be applied.
+ ///
+ /// **Note**: Requires the [Send Messages] permission.
+ ///
+ /// [Send Messages]: permissions/constant.SEND_MESSAGES.html
+ pub fn send_message(&self, content: &str) -> Result<Message> {
+ let map = ObjectBuilder::new()
+ .insert("content", content)
+ .insert("nonce", "")
+ .insert("tts", false)
+ .build();
+
+ http::send_message(self.channel_id.0, map)
+ }
+}
+
+impl Message {
+ /// Deletes the message.
+ ///
+ /// **Note**: The logged in user must either be the author of the message or
+ /// have the [Manage Messages] permission.
+ ///
+ /// **Note**: Requires the [Manage Messages] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidUser`] if the current user is not the
+ /// author.
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have the required permissions.
+ ///
+ /// [`ClientError::InvalidPermissions]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`ClientError::InvalidUser]: ../client/enum.ClientError.html#InvalidUser.v
+ /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html
+ pub fn delete(&self) -> Result<()> {
+ let req = permissions::MANAGE_MESSAGES;
+ let is_author = self.author.id != STATE.lock().unwrap().user.id;
+
+ if is_author {
+ return Err(Error::Client(ClientError::InvalidUser));
+ }
+
+ if !try!(utils::user_has_perms(self.channel_id, req)) && !is_author {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::delete_message(self.channel_id.0, self.id.0)
+ }
+
+ /// Edits this message, replacing the original content with new content.
+ ///
+ /// **Note**: You must be the author of the message to be able to do this.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [`ClientError::InvalidUser`] if the current user is not the author.
+ ///
+ /// [`ClientError::InvalidUser`]: ../client/enum.ClientError.html#InvalidUser.v
+ pub fn edit(&mut self, new_content: &str) -> Result<()> {
+ if self.author.id != STATE.lock().unwrap().user.id {
+ return Err(Error::Client(ClientError::InvalidUser));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("content", new_content)
+ .build();
+
+ match http::edit_message(self.channel_id.0, self.id.0, map) {
+ Ok(edited) => {
+ mem::replace(self, edited);
+
+ Ok(())
+ },
+ Err(why) => Err(why),
+ }
+ }
+
+ /// Pins this message to its channel.
+ ///
+ /// **Note**: Requires the [Manage Messages] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [`ClientError::InvalidPermissions`] if the current user does not have
+ /// the required permissions.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html
+ pub fn pin(&self) -> Result<()> {
+ let req = permissions::MANAGE_MESSAGES;
+
+ if !try!(utils::user_has_perms(self.channel_id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::pin_message(self.channel_id.0, self.id.0)
+ }
+
+ /// Replies to the user, mentioning them prior to the content in the form
+ /// of: `@<USER_ID>: YOUR_CONTENT`.
+ ///
+ /// User mentions are generally around 20 or 21 characters long.
+ ///
+ /// **Note**: Requires the [Send Messages] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [`ClientError::InvalidPermissions`] if the current user does not have
+ /// the required permissions.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Send Messages]: permissions/constant.SEND_MESSAGES.html
+ pub fn reply(&self, content: &str) -> Result<Message> {
+ let req = permissions::SEND_MESSAGES;
+
+ if !try!(utils::user_has_perms(self.channel_id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let mut gen = format!("{}", self.author.mention());
+ gen.push(':');
+ gen.push(' ');
+ gen.push_str(content);
+
+ let map = ObjectBuilder::new()
+ .insert("content", gen)
+ .insert("nonce", "")
+ .insert("tts", false)
+ .build();
+
+ http::send_message(self.channel_id.0, map)
+ }
+
+ /// Unpins the message from its channel.
+ ///
+ /// **Note**: Requires the [Manage Messages] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [`ClientError::InvalidPermissions`] if the current user does not have
+ /// the required permissions.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html
+ pub fn unpin(&self) -> Result<()> {
+ let req = permissions::MANAGE_MESSAGES;
+
+ if !try!(utils::user_has_perms(self.channel_id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::unpin_message(self.channel_id.0, self.id.0)
+ }
+}
+
+impl PermissionOverwrite {
+ pub fn decode(value: Value) -> Result<PermissionOverwrite> {
+ let mut map = try!(into_map(value));
+ let id = try!(remove(&mut map, "id").and_then(decode_id));
+ let kind = try!(remove(&mut map, "type").and_then(into_string));
+ let kind = match &*kind {
+ "member" => PermissionOverwriteType::Member(UserId(id)),
+ "role" => PermissionOverwriteType::Role(RoleId(id)),
+ _ => return Err(Error::Decode("Expected valid PermissionOverwrite type", Value::String(kind))),
+ };
+
+ missing!(map, PermissionOverwrite {
+ kind: kind,
+ allow: try!(remove(&mut map, "allow").and_then(Permissions::decode)),
+ deny: try!(remove(&mut map, "deny").and_then(Permissions::decode)),
+ })
+ }
+}
+
+impl PrivateChannel {
+ /// Broadcasts that the current user is typing to the recipient.
+ pub fn broadcast_typing(&self) -> Result<()> {
+ http::broadcast_typing(self.id.0)
+ }
+
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<PrivateChannel> {
+ let mut map = try!(into_map(value));
+ let mut recipients = try!(decode_array(try!(remove(&mut map, "recipients")),
+ User::decode));
+
+ missing!(map, PrivateChannel {
+ id: try!(remove(&mut map, "id").and_then(ChannelId::decode)),
+ kind: try!(remove(&mut map, "type").and_then(ChannelType::decode)),
+ last_message_id: try!(opt(&mut map, "last_message_id", MessageId::decode)),
+ last_pin_timestamp: try!(opt(&mut map, "last_pin_timestamp", into_string)),
+ recipient: recipients.remove(0),
+ })
+ }
+
+ /// Deletes the given message Ids from the private channel.
+ ///
+ /// **Note**: You can only delete your own messages.
+ ///
+ /// **Note** This method is only available to bot users.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [`ClientError::InvalidUser`] if the current user is not a bot user.
+ ///
+ /// [`ClientError::InvalidUser`]: ../client/enum.ClientError.html#InvalidOperationAsUser.v
+ pub fn delete_messages(&self, message_ids: &[MessageId]) -> Result<()> {
+ if !STATE.lock().unwrap().user.bot {
+ return Err(Error::Client(ClientError::InvalidOperationAsUser));
+ }
+
+ let ids: Vec<u64> = message_ids.into_iter()
+ .map(|message_id| message_id.0)
+ .collect();
+
+ let map = ObjectBuilder::new()
+ .insert("messages", ids)
+ .build();
+
+ http::delete_messages(self.id.0, map)
+ }
+
+ /// Deletes the channel. This does not delete the contents of the channel,
+ /// and is equivilant to closing a private channel on the client, which can
+ /// be re-opened.
+ pub fn delete(&self) -> Result<Channel> {
+ http::delete_channel(self.id.0)
+ }
+
+ /// Retrieves the list of messages that have been pinned in the private
+ /// channel.
+ pub fn pins(&self) -> Result<Vec<Message>> {
+ http::get_pins(self.id.0)
+ }
+
+ /// Sends a message to the recipient with the given content.
+ pub fn send_message(&self, content: &str) -> Result<Message> {
+ let map = ObjectBuilder::new()
+ .insert("content", content)
+ .insert("nonce", "")
+ .insert("tts", false)
+ .build();
+
+ http::send_message(self.id.0, map)
+ }
+}
+
+impl fmt::Display for PrivateChannel {
+ /// Formats the private channel, displaying the recipient's username.
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.write_str(&self.recipient.name)
+ }
+}
+
+impl PublicChannel {
+ /// Broadcasts to the channel that the current user is typing.
+ ///
+ /// For bots, this is a good indicator for long-running commands.
+ ///
+ /// **Note**: Requires the [Send Messages] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [ClientError::InvalidPermissions] if the current user does not have the
+ /// required permissions.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Send Messages]: permissions/constants.SEND_MESSAGES.html
+ pub fn broadcast_typing(&self) -> Result<()> {
+ http::broadcast_typing(self.id.0)
+ }
+
+ pub fn create_invite<F>(&self, f: F) -> Result<RichInvite>
+ where F: FnOnce(CreateInvite) -> CreateInvite {
+ let req = permissions::CREATE_INVITE;
+
+ if !try!(utils::user_has_perms(self.id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = f(CreateInvite::default()).0.build();
+
+ http::create_invite(self.id.0, map)
+ }
+
+ pub fn decode(value: Value) -> Result<PublicChannel> {
+ let mut map = try!(into_map(value));
+
+ let id = try!(remove(&mut map, "guild_id").and_then(GuildId::decode));
+
+ PublicChannel::decode_guild(Value::Object(map), id)
+ }
+
+ pub fn decode_guild(value: Value, guild_id: GuildId) -> Result<PublicChannel> {
+ let mut map = try!(into_map(value));
+ missing!(map, PublicChannel {
+ id: try!(remove(&mut map, "id").and_then(ChannelId::decode)),
+ name: try!(remove(&mut map, "name").and_then(into_string)),
+ guild_id: guild_id,
+ topic: try!(opt(&mut map, "topic", into_string)),
+ position: req!(try!(remove(&mut map, "position")).as_i64()),
+ kind: try!(remove(&mut map, "type").and_then(ChannelType::decode)),
+ last_message_id: try!(opt(&mut map, "last_message_id", MessageId::decode)),
+ permission_overwrites: try!(decode_array(try!(remove(&mut map, "permission_overwrites")), PermissionOverwrite::decode)),
+ bitrate: remove(&mut map, "bitrate").ok().and_then(|v| v.as_u64()),
+ user_limit: remove(&mut map, "user_limit").ok().and_then(|v| v.as_u64()),
+ last_pin_timestamp: try!(opt(&mut map, "last_pin_timestamp", into_string)),
+ })
+ }
+
+ /// Deletes this channel, returning the channel on a successful deletion.
+ pub fn delete(&self) -> Result<Channel> {
+ let req = permissions::MANAGE_CHANNELS;
+
+ if !try!(utils::user_has_perms(self.id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::delete_channel(self.id.0)
+ }
+ pub fn edit<F>(&mut self, f: F) -> Result<()>
+
+ where F: FnOnce(EditChannel) -> EditChannel {
+ let req = permissions::MANAGE_CHANNELS;
+
+ if !try!(utils::user_has_perms(self.id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("name", &self.name)
+ .insert("position", self.position)
+ .insert("type", self.kind.name());
+
+ let edited = f(EditChannel(map)).0.build();
+
+ match http::edit_channel(self.id.0, edited) {
+ Ok(channel) => {
+ mem::replace(self, channel);
+
+ Ok(())
+ },
+ Err(why) => Err(why),
+ }
+ }
+
+ /// Return a [`Mention`] which will link to this channel.
+ ///
+ /// [`Mention`]: struct.Mention.html
+ pub fn mention(&self) -> Mention {
+ self.id.mention()
+ }
+
+ pub fn pins(&self) -> Result<Vec<Message>> {
+ http::get_pins(self.id.0)
+ }
+
+ pub fn send_message(&self, content: &str) -> Result<Message> {
+ let req = permissions::SEND_MESSAGES;
+
+ if !try!(utils::user_has_perms(self.id, req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("content", content)
+ .insert("nonce", "")
+ .insert("tts", false)
+ .build();
+
+ http::send_message(self.id.0, map)
+ }
+}
+
+impl fmt::Display for PublicChannel {
+ /// Formas the channel, creating a mention of it.
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.mention(), f)
+ }
+}
diff --git a/src/model/gateway.rs b/src/model/gateway.rs
new file mode 100644
index 0000000..2e1b300
--- /dev/null
+++ b/src/model/gateway.rs
@@ -0,0 +1,784 @@
+use std::collections::{BTreeMap, HashMap};
+use super::utils::*;
+use super::*;
+use ::prelude::*;
+use ::utils::decode_array;
+
+#[derive(Clone, Debug)]
+pub struct CallCreateEvent {
+ pub call: Call,
+}
+
+#[derive(Clone, Debug)]
+pub struct CallDeleteEvent {
+ pub channel_id: ChannelId,
+}
+
+#[derive(Clone, Debug)]
+pub struct CallUpdateEvent {
+ pub channel_id: ChannelId,
+ pub message_id: MessageId,
+ pub region: String,
+ pub ringing: Vec<UserId>,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelCreateEvent {
+ pub channel: Channel,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelDeleteEvent {
+ pub channel: Channel,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelPinsAckEvent {
+ pub channel_id: ChannelId,
+ pub timestamp: String,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelPinsUpdateEvent {
+ pub channel_id: ChannelId,
+ pub last_pin_timestamp: Option<String>,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelRecipientAddEvent {
+ pub channel_id: ChannelId,
+ pub user: User,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelRecipientRemoveEvent {
+ pub channel_id: ChannelId,
+ pub user: User,
+}
+
+#[derive(Clone, Debug)]
+pub struct ChannelUpdateEvent {
+ pub channel: Channel,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildBanAddEvent {
+ pub guild_id: GuildId,
+ pub user: User,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildBanRemoveEvent {
+ pub guild_id: GuildId,
+ pub user: User,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildCreateEvent {
+ pub guild: LiveGuild,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildDeleteEvent {
+ pub guild: Guild,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildEmojisUpdateEvent {
+ pub emojis: HashMap<EmojiId, Emoji>,
+ pub guild_id: GuildId,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildIntegrationsUpdateEvent {
+ pub guild_id: GuildId,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildMemberAddEvent {
+ pub guild_id: GuildId,
+ pub member: Member,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildMemberRemoveEvent {
+ pub guild_id: GuildId,
+ pub user: User,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildMemberUpdateEvent {
+ pub guild_id: GuildId,
+ pub nick: Option<String>,
+ pub roles: Vec<RoleId>,
+ pub user: User,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildMembersChunkEvent {
+ pub guild_id: GuildId,
+ pub members: HashMap<UserId, Member>,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildRoleCreateEvent {
+ pub guild_id: GuildId,
+ pub role: Role,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildRoleDeleteEvent {
+ pub guild_id: GuildId,
+ pub role_id: RoleId,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildRoleUpdateEvent {
+ pub guild_id: GuildId,
+ pub role: Role,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildSyncEvent {
+ pub guild_id: GuildId,
+ pub large: bool,
+ pub members: HashMap<UserId, Member>,
+ pub presences: HashMap<UserId, Presence>,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildUnavailableEvent {
+ pub guild_id: GuildId,
+}
+
+#[derive(Clone, Debug)]
+pub struct GuildUpdateEvent {
+ pub guild: Guild,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct MessageAckEvent {
+ pub channel_id: ChannelId,
+ /// May be `None` if a private channel with no messages has closed.
+ pub message_id: Option<MessageId>,
+}
+
+#[derive(Clone, Debug)]
+pub struct MessageCreateEvent {
+ pub message: Message,
+}
+
+#[derive(Clone, Debug)]
+pub struct MessageDeleteBulkEvent {
+ pub channel_id: ChannelId,
+ pub ids: Vec<MessageId>,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct MessageDeleteEvent {
+ pub channel_id: ChannelId,
+ pub message_id: MessageId,
+}
+
+#[derive(Clone, Debug)]
+pub struct MessageUpdateEvent {
+ pub id: MessageId,
+ pub channel_id: ChannelId,
+ pub kind: Option<MessageType>,
+ pub content: Option<String>,
+ pub nonce: Option<String>,
+ pub tts: Option<bool>,
+ pub pinned: Option<bool>,
+ pub timestamp: Option<String>,
+ pub edited_timestamp: Option<String>,
+ pub author: Option<User>,
+ pub mention_everyone: Option<bool>,
+ pub mentions: Option<Vec<User>>,
+ pub mention_roles: Option<Vec<RoleId>>,
+ pub attachments: Option<Vec<Attachment>>,
+ pub embeds: Option<Vec<Value>>,
+}
+
+#[derive(Clone, Debug)]
+pub struct PresenceUpdateEvent {
+ pub guild_id: Option<GuildId>,
+ pub presence: Presence,
+ pub roles: Option<Vec<RoleId>>,
+}
+
+#[derive(Clone, Debug)]
+pub struct PresencesReplaceEvent {
+ pub presences: Vec<Presence>,
+}
+
+/// The "Ready" event, containing initial state
+#[derive(Clone, Debug)]
+pub struct ReadyEvent {
+ pub ready: Ready,
+}
+
+#[derive(Clone, Debug)]
+pub struct RelationshipAddEvent {
+ pub relationship: Relationship,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct RelationshipRemoveEvent {
+ pub kind: RelationshipType,
+ pub user_id: UserId,
+}
+
+#[derive(Clone, Debug)]
+pub struct ResumedEvent {
+ pub heartbeat_interval: u64,
+ pub trace: Vec<Option<String>>,
+}
+
+#[derive(Clone, Debug)]
+pub struct TypingStartEvent {
+ pub channel_id: ChannelId,
+ pub timestamp: u64,
+ pub user_id: UserId,
+}
+
+#[derive(Clone, Debug)]
+pub struct UnknownEvent {
+ pub kind: String,
+ pub value: BTreeMap<String, Value>
+}
+
+#[derive(Clone, Debug)]
+pub struct UserGuildSettingsUpdateEvent {
+ pub settings: UserGuildSettings,
+}
+
+#[derive(Clone, Debug)]
+pub struct UserNoteUpdateEvent {
+ pub note: String,
+ pub user_id: UserId,
+}
+
+#[derive(Clone, Debug)]
+pub struct UserUpdateEvent {
+ pub current_user: CurrentUser,
+}
+
+#[derive(Clone, Debug)]
+pub struct UserSettingsUpdateEvent {
+ pub enable_tts_command: Option<bool>,
+ pub inline_attachment_media: Option<bool>,
+ pub inline_embed_media: Option<bool>,
+ pub locale: Option<String>,
+ pub message_display_compact: Option<bool>,
+ pub render_embeds: Option<bool>,
+ pub show_current_game: Option<bool>,
+ pub theme: Option<String>,
+ pub convert_emoticons: Option<bool>,
+ pub friend_source_flags: Option<FriendSourceFlags>,
+}
+
+#[derive(Clone, Debug)]
+pub struct VoiceServerUpdateEvent {
+ pub channel_id: Option<ChannelId>,
+ pub endpoint: Option<String>,
+ pub guild_id: Option<GuildId>,
+ pub token: String,
+}
+
+#[derive(Clone, Debug)]
+pub struct VoiceStateUpdateEvent {
+ pub guild_id: Option<GuildId>,
+ pub voice_state: VoiceState,
+}
+
+#[derive(Debug, Clone)]
+pub enum GatewayEvent {
+ Dispatch(u64, Event),
+ Heartbeat(u64),
+ Reconnect,
+ InvalidateSession,
+ Hello(u64),
+ HeartbeatAck,
+}
+
+impl GatewayEvent {
+ pub fn decode(value: Value) -> Result<Self> {
+ let mut value = try!(into_map(value));
+ match req!(value.get("op").and_then(|x| x.as_u64())) {
+ 0 => Ok(GatewayEvent::Dispatch(
+ req!(try!(remove(&mut value, "s")).as_u64()),
+ try!(Event::decode(
+ try!(remove(&mut value, "t").and_then(into_string)),
+ try!(remove(&mut value, "d"))
+ ))
+ )),
+ 1 => Ok(GatewayEvent::Heartbeat(req!(try!(remove(&mut value, "s")).as_u64()))),
+ 7 => Ok(GatewayEvent::Reconnect),
+ 9 => Ok(GatewayEvent::InvalidateSession),
+ 10 => {
+ let mut data = try!(remove(&mut value, "d").and_then(into_map));
+ let interval = req!(try!(remove(&mut data, "heartbeat_interval")).as_u64());
+ Ok(GatewayEvent::Hello(interval))
+ },
+ 11 => Ok(GatewayEvent::HeartbeatAck),
+ _ => Err(Error::Decode("Unexpected opcode", Value::Object(value))),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum VoiceEvent {
+ Handshake {
+ heartbeat_interval: u64,
+ port: u16,
+ ssrc: u32,
+ modes: Vec<String>,
+ },
+ Ready {
+ mode: String,
+ secret_key: Vec<u8>,
+ },
+ SpeakingUpdate {
+ user_id: UserId,
+ ssrc: u32,
+ speaking: bool,
+ },
+ KeepAlive,
+ Unknown(u64, Value)
+}
+
+impl VoiceEvent {
+ pub fn decode(value: Value) -> Result<VoiceEvent> {
+ let mut value = try!(into_map(value));
+
+ let op = req!(try!(remove(&mut value, "op")).as_u64());
+ if op == 3 {
+ return Ok(VoiceEvent::KeepAlive)
+ }
+
+ let mut value = try!(remove(&mut value, "d").and_then(into_map));
+ if op == 2 {
+ missing!(value, VoiceEvent::Handshake {
+ heartbeat_interval: req!(try!(remove(&mut value, "heartbeat_interval")).as_u64()),
+ modes: try!(decode_array(try!(remove(&mut value, "modes")), into_string)),
+ port: req!(try!(remove(&mut value, "port")).as_u64()) as u16,
+ ssrc: req!(try!(remove(&mut value, "ssrc")).as_u64()) as u32,
+ })
+ } else if op == 4 {
+ missing!(value, VoiceEvent::Ready {
+ mode: try!(remove(&mut value, "mode").and_then(into_string)),
+ secret_key: try!(decode_array(try!(remove(&mut value, "secret_key")),
+ |v| Ok(req!(v.as_u64()) as u8)
+ )),
+ })
+ } else if op == 5 {
+ missing!(value, VoiceEvent::SpeakingUpdate {
+ user_id: try!(remove(&mut value, "user_id").and_then(UserId::decode)),
+ ssrc: req!(try!(remove(&mut value, "ssrc")).as_u64()) as u32,
+ speaking: req!(try!(remove(&mut value, "speaking")).as_bool()),
+ })
+ } else {
+ Ok(VoiceEvent::Unknown(op, Value::Object(value)))
+ }
+ }
+}
+
+/// Event received over a websocket connection
+#[derive(Clone, Debug)]
+pub enum Event {
+ /// A new group call has been created
+ CallCreate(CallCreateEvent),
+ /// A group call has been deleted (the call ended)
+ CallDelete(CallDeleteEvent),
+ /// A group call has been updated
+ CallUpdate(CallUpdateEvent),
+ ChannelCreate(ChannelCreateEvent),
+ ChannelDelete(ChannelDeleteEvent),
+ ChannelPinsAck(ChannelPinsAckEvent),
+ ChannelPinsUpdate(ChannelPinsUpdateEvent),
+ /// A user has been added to a group
+ ChannelRecipientAdd(ChannelRecipientAddEvent),
+ /// A user has been removed from a group
+ ChannelRecipientRemove(ChannelRecipientRemoveEvent),
+ ChannelUpdate(ChannelUpdateEvent),
+ GuildBanAdd(GuildBanAddEvent),
+ GuildBanRemove(GuildBanRemoveEvent),
+ GuildCreate(GuildCreateEvent),
+ GuildDelete(GuildDeleteEvent),
+ GuildEmojisUpdate(GuildEmojisUpdateEvent),
+ GuildIntegrationsUpdate(GuildIntegrationsUpdateEvent),
+ GuildMemberAdd(GuildMemberAddEvent),
+ GuildMemberRemove(GuildMemberRemoveEvent),
+ /// A member's roles have changed
+ GuildMemberUpdate(GuildMemberUpdateEvent),
+ GuildMembersChunk(GuildMembersChunkEvent),
+ GuildRoleCreate(GuildRoleCreateEvent),
+ GuildRoleDelete(GuildRoleDeleteEvent),
+ GuildRoleUpdate(GuildRoleUpdateEvent),
+ GuildSync(GuildSyncEvent),
+ /// When a guild is unavailable, such as due to a Discord server outage.
+ GuildUnavailable(GuildUnavailableEvent),
+ GuildUpdate(GuildUpdateEvent),
+ /// Another logged-in device acknowledged this message
+ MessageAck(MessageAckEvent),
+ MessageCreate(MessageCreateEvent),
+ MessageDelete(MessageDeleteEvent),
+ MessageDeleteBulk(MessageDeleteBulkEvent),
+ /// A message has been edited, either by the user or the system
+ MessageUpdate(MessageUpdateEvent),
+ /// A member's presence state (or username or avatar) has changed
+ PresenceUpdate(PresenceUpdateEvent),
+ /// The precense list of the user's friends should be replaced entirely
+ PresencesReplace(PresencesReplaceEvent),
+ /// The first event in a connection, containing the initial state.
+ ///
+ /// May also be received at a later time in the event of a reconnect.
+ Ready(ReadyEvent),
+ RelationshipAdd(RelationshipAddEvent),
+ RelationshipRemove(RelationshipRemoveEvent),
+ /// The connection has successfully resumed after a disconnect.
+ Resumed(ResumedEvent),
+ /// A user is typing; considered to last 5 seconds
+ TypingStart(TypingStartEvent),
+ /// Update to the logged-in user's guild-specific notification settings
+ UserGuildSettingsUpdate(UserGuildSettingsUpdateEvent),
+ /// Update to a note that the logged-in user has set for another user.
+ UserNoteUpdate(UserNoteUpdateEvent),
+ /// Update to the logged-in user's information
+ UserUpdate(UserUpdateEvent),
+ /// Update to the logged-in user's preferences or client settings
+ UserSettingsUpdate(UserSettingsUpdateEvent),
+ /// A member's voice state has changed
+ VoiceStateUpdate(VoiceStateUpdateEvent),
+ /// Voice server information is available
+ VoiceServerUpdate(VoiceServerUpdateEvent),
+ /// An event type not covered by the above
+ Unknown(UnknownEvent),
+}
+
+impl Event {
+ #[allow(cyclomatic_complexity)]
+ fn decode(kind: String, value: Value) -> Result<Event> {
+ if kind == "PRESENCES_REPLACE" {
+ return Ok(Event::PresencesReplace(PresencesReplaceEvent {
+ presences: try!(decode_array(value, Presence::decode)),
+ }));
+ }
+
+ let mut value = try!(into_map(value));
+
+ if kind == "CALL_CREATE" {
+ Ok(Event::CallCreate(CallCreateEvent {
+ call: try!(Call::decode(Value::Object(value))),
+ }))
+ } else if kind == "CALL_DELETE" {
+ missing!(value, Event::CallDelete(CallDeleteEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ }))
+ } else if kind == "CALL_UPDATE" {
+ missing!(value, Event::CallUpdate(CallUpdateEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ message_id: try!(remove(&mut value, "message_id").and_then(MessageId::decode)),
+ region: try!(remove(&mut value, "region").and_then(into_string)),
+ ringing: try!(decode_array(try!(remove(&mut value, "ringing")), UserId::decode)),
+ }))
+ } else if kind == "CHANNEL_CREATE" {
+ Ok(Event::ChannelCreate(ChannelCreateEvent {
+ channel: try!(Channel::decode(Value::Object(value))),
+ }))
+ } else if kind == "CHANNEL_DELETE" {
+ Ok(Event::ChannelDelete(ChannelDeleteEvent {
+ channel: try!(Channel::decode(Value::Object(value))),
+ }))
+ } else if kind == "CHANNEL_PINS_ACK" {
+ missing!(value, Event::ChannelPinsAck(ChannelPinsAckEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ timestamp: try!(remove(&mut value, "timestamp").and_then(into_string)),
+ }))
+ } else if kind == "CHANNEL_PINS_UPDATE" {
+ missing!(value, Event::ChannelPinsUpdate(ChannelPinsUpdateEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ last_pin_timestamp: try!(opt(&mut value, "last_pin_timestamp", into_string)),
+ }))
+ } else if kind == "CHANNEL_RECIPIENT_ADD" {
+ missing!(value, Event::ChannelRecipientAdd(ChannelRecipientAddEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ user: try!(remove(&mut value, "user").and_then(User::decode)),
+ }))
+ } else if kind == "CHANNEL_RECIPIENT_REMOVE" {
+ missing!(value, Event::ChannelRecipientRemove(ChannelRecipientRemoveEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ user: try!(remove(&mut value, "user").and_then(User::decode)),
+ }))
+ } else if kind == "CHANNEL_UPDATE" {
+ Ok(Event::ChannelUpdate(ChannelUpdateEvent {
+ channel: try!(Channel::decode(Value::Object(value))),
+ }))
+ } else if kind == "GUILD_BAN_ADD" {
+ missing!(value, Event::GuildBanAdd(GuildBanAddEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ user: try!(remove(&mut value, "user").and_then(User::decode)),
+ }))
+ } else if kind == "GUILD_BAN_REMOVE" {
+ missing!(value, Event::GuildBanRemove(GuildBanRemoveEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ user: try!(remove(&mut value, "user").and_then(User::decode)),
+ }))
+ } else if kind == "GUILD_CREATE" {
+ if remove(&mut value, "unavailable").ok().and_then(|v| v.as_bool()).unwrap_or(false) {
+ Ok(Event::GuildUnavailable(GuildUnavailableEvent {
+ guild_id: try!(remove(&mut value, "id").and_then(GuildId::decode)),
+ }))
+ } else {
+ Ok(Event::GuildCreate(GuildCreateEvent {
+ guild: try!(LiveGuild::decode(Value::Object(value))),
+ }))
+ }
+ } else if kind == "GUILD_DELETE" {
+ if remove(&mut value, "unavailable").ok().and_then(|v| v.as_bool()).unwrap_or(false) {
+ Ok(Event::GuildUnavailable(GuildUnavailableEvent {
+ guild_id: try!(remove(&mut value, "id").and_then(GuildId::decode)),
+ }))
+ } else {
+ Ok(Event::GuildDelete(GuildDeleteEvent {
+ guild: try!(Guild::decode(Value::Object(value))),
+ }))
+ }
+ } else if kind == "GUILD_EMOJIS_UPDATE" {
+ missing!(value, Event::GuildEmojisUpdate(GuildEmojisUpdateEvent {
+ emojis: try!(remove(&mut value, "emojis").and_then(decode_emojis)),
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ }))
+ } else if kind == "GUILD_INTEGRATIONS_UPDATE" {
+ missing!(value, Event::GuildIntegrationsUpdate(GuildIntegrationsUpdateEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ }))
+ } else if kind == "GUILD_MEMBER_ADD" {
+ Ok(Event::GuildMemberAdd(GuildMemberAddEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ member: try!(Member::decode(Value::Object(value))),
+ }))
+ } else if kind == "GUILD_MEMBER_REMOVE" {
+ missing!(value, Event::GuildMemberRemove(GuildMemberRemoveEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ user: try!(remove(&mut value, "user").and_then(User::decode)),
+ }))
+ } else if kind == "GUILD_MEMBER_UPDATE" {
+ missing!(value, Event::GuildMemberUpdate(GuildMemberUpdateEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ nick: try!(opt(&mut value, "nick", into_string)),
+ roles: try!(decode_array(try!(remove(&mut value, "roles")), RoleId::decode)),
+ user: try!(remove(&mut value, "user").and_then(User::decode)),
+ }))
+ } else if kind == "GUILD_MEMBERS_CHUNK" {
+ missing!(value, Event::GuildMembersChunk(GuildMembersChunkEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ members: try!(remove(&mut value, "members").and_then(decode_members)),
+ }))
+ } else if kind == "GUILD_ROLE_CREATE" {
+ missing!(value, Event::GuildRoleCreate(GuildRoleCreateEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ role: try!(remove(&mut value, "role").and_then(Role::decode)),
+ }))
+ } else if kind == "GUILD_ROLE_DELETE" {
+ missing!(value, Event::GuildRoleDelete(GuildRoleDeleteEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ role_id: try!(remove(&mut value, "role_id").and_then(RoleId::decode)),
+ }))
+ } else if kind == "GUILD_ROLE_UPDATE" {
+ missing!(value, Event::GuildRoleUpdate(GuildRoleUpdateEvent {
+ guild_id: try!(remove(&mut value, "guild_id").and_then(GuildId::decode)),
+ role: try!(remove(&mut value, "role").and_then(Role::decode)),
+ }))
+ } else if kind == "GUILD_SYNC" {
+ missing!(value, Event::GuildSync(GuildSyncEvent {
+ guild_id: try!(remove(&mut value, "id").and_then(GuildId::decode)),
+ large: req!(try!(remove(&mut value, "large")).as_bool()),
+ members: try!(remove(&mut value, "members").and_then(decode_members)),
+ presences: try!(remove(&mut value, "presences").and_then(decode_presences)),
+ }))
+ } else if kind == "GUILD_UPDATE" {
+ Ok(Event::GuildUpdate(GuildUpdateEvent {
+ guild: try!(Guild::decode(Value::Object(value))),
+ }))
+ } else if kind == "MESSAGE_ACK" {
+ missing!(value, Event::MessageAck(MessageAckEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ message_id: try!(opt(&mut value, "message_id", MessageId::decode)),
+ }))
+ } else if kind == "MESSAGE_CREATE" {
+ Ok(Event::MessageCreate(MessageCreateEvent {
+ message: try!(Message::decode(Value::Object(value))),
+ }))
+ } else if kind == "MESSAGE_DELETE" {
+ missing!(value, Event::MessageDelete(MessageDeleteEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ message_id: try!(remove(&mut value, "id").and_then(MessageId::decode)),
+ }))
+ } else if kind == "MESSAGE_DELETE_BULK" {
+ missing!(value, Event::MessageDeleteBulk(MessageDeleteBulkEvent {
+ 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_UPDATE" {
+ missing!(value, Event::MessageUpdate(MessageUpdateEvent {
+ id: try!(remove(&mut value, "id").and_then(MessageId::decode)),
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ kind: try!(opt(&mut value, "type", MessageType::decode)),
+ content: try!(opt(&mut value, "content", into_string)),
+ nonce: remove(&mut value, "nonce").and_then(into_string).ok(),
+ tts: remove(&mut value, "tts").ok().and_then(|v| v.as_bool()),
+ pinned: remove(&mut value, "pinned").ok().and_then(|v| v.as_bool()),
+ timestamp: try!(opt(&mut value, "timestamp", into_string)),
+ edited_timestamp: try!(opt(&mut value, "edited_timestamp", into_string)),
+ author: try!(opt(&mut value, "author", User::decode)),
+ mention_everyone: remove(&mut value, "mention_everyone").ok().and_then(|v| v.as_bool()),
+ mentions: try!(opt(&mut value, "mentions", |v| decode_array(v, User::decode))),
+ mention_roles: try!(opt(&mut value, "mention_roles", |v| decode_array(v, RoleId::decode))),
+ attachments: try!(opt(&mut value, "attachments", |v| decode_array(v, Attachment::decode))),
+ embeds: try!(opt(&mut value, "embeds", |v| decode_array(v, Ok))),
+ }))
+ } else if kind == "PRESENCE_UPDATE" {
+ let guild_id = try!(opt(&mut value, "guild_id", GuildId::decode));
+ let roles = try!(opt(&mut value, "roles", |v| decode_array(v, RoleId::decode)));
+ let presence = try!(Presence::decode(Value::Object(value)));
+ Ok(Event::PresenceUpdate(PresenceUpdateEvent {
+ guild_id: guild_id,
+ presence: presence,
+ roles: roles,
+ }))
+ } else if kind == "RELATIONSHIP_ADD" {
+ Ok(Event::RelationshipAdd(RelationshipAddEvent {
+ relationship: try!(Relationship::decode(Value::Object(value))),
+ }))
+ } else if kind == "RELATIONSHIP_REMOVE" {
+ missing!(value, Event::RelationshipRemove(RelationshipRemoveEvent {
+ kind: try!(remove(&mut value, "type").and_then(RelationshipType::decode)),
+ user_id: try!(remove(&mut value, "id").and_then(UserId::decode)),
+ }))
+ } else if kind == "READY" {
+ Ok(Event::Ready(ReadyEvent {
+ ready: try!(Ready::decode(Value::Object(value))),
+ }))
+ } else if kind == "RESUMED" {
+ missing!(value, Event::Resumed(ResumedEvent {
+ heartbeat_interval: req!(try!(remove(&mut value, "heartbeat_interval")).as_u64()),
+ trace: try!(remove(&mut value, "_trace").and_then(|v| decode_array(v, |v| Ok(into_string(v).ok())))),
+ }))
+ } else if kind == "TYPING_START" {
+ missing!(value, Event::TypingStart(TypingStartEvent {
+ channel_id: try!(remove(&mut value, "channel_id").and_then(ChannelId::decode)),
+ timestamp: req!(try!(remove(&mut value, "timestamp")).as_u64()),
+ user_id: try!(remove(&mut value, "user_id").and_then(UserId::decode)),
+ }))
+ } else if kind == "USER_GUILD_SETTINGS_UPDATE" {
+ Ok(Event::UserGuildSettingsUpdate(UserGuildSettingsUpdateEvent {
+ settings: try!(UserGuildSettings::decode(Value::Object(value))),
+ }))
+ } else if kind == "USER_NOTE_UPDATE" {
+ missing!(value, Event::UserNoteUpdate(UserNoteUpdateEvent {
+ note: try!(remove(&mut value, "note").and_then(into_string)),
+ user_id: try!(remove(&mut value, "id").and_then(UserId::decode)),
+ }))
+ } else if kind == "USER_SETTINGS_UPDATE" {
+ missing!(value, Event::UserSettingsUpdate(UserSettingsUpdateEvent {
+ enable_tts_command: remove(&mut value, "enable_tts_command").ok().and_then(|v| v.as_bool()),
+ inline_attachment_media: remove(&mut value, "inline_attachment_media").ok().and_then(|v| v.as_bool()),
+ inline_embed_media: remove(&mut value, "inline_embed_media").ok().and_then(|v| v.as_bool()),
+ locale: try!(opt(&mut value, "locale", into_string)),
+ message_display_compact: remove(&mut value, "message_display_compact").ok().and_then(|v| v.as_bool()),
+ render_embeds: remove(&mut value, "render_embeds").ok().and_then(|v| v.as_bool()),
+ show_current_game: remove(&mut value, "show_current_game").ok().and_then(|v| v.as_bool()),
+ theme: try!(opt(&mut value, "theme", into_string)),
+ convert_emoticons: remove(&mut value, "convert_emoticons").ok().and_then(|v| v.as_bool()),
+ friend_source_flags: try!(opt(&mut value, "friend_source_flags", FriendSourceFlags::decode)),
+ }))
+ } else if kind == "USER_UPDATE" {
+ Ok(Event::UserUpdate(UserUpdateEvent {
+ current_user: try!(CurrentUser::decode(Value::Object(value))),
+ }))
+ } else if kind == "VOICE_SERVER_UPDATE" {
+ missing!(value, Event::VoiceServerUpdate(VoiceServerUpdateEvent {
+ guild_id: try!(opt(&mut value, "guild_id", GuildId::decode)),
+ channel_id: try!(opt(&mut value, "channel_id", ChannelId::decode)),
+ endpoint: try!(opt(&mut value, "endpoint", into_string)),
+ token: try!(remove(&mut value, "token").and_then(into_string)),
+ }))
+ } else if kind == "VOICE_STATE_UPDATE" {
+ Ok(Event::VoiceStateUpdate(VoiceStateUpdateEvent {
+ guild_id: try!(opt(&mut value, "guild_id", GuildId::decode)),
+ voice_state: try!(VoiceState::decode(Value::Object(value))),
+ }))
+ } else {
+ Ok(Event::Unknown(UnknownEvent {
+ kind: kind,
+ value: value,
+ }))
+ }
+ }
+}
+
+impl Game {
+ pub fn playing(name: String) -> Game {
+ Game {
+ kind: GameType::Playing,
+ name: name,
+ url: None,
+ }
+ }
+
+ pub fn streaming(name: String, url: String) -> Game {
+ Game {
+ kind: GameType::Streaming,
+ name: name,
+ url: Some(url),
+ }
+ }
+
+ pub fn decode(value: Value) -> Result<Option<Game>> {
+ let mut map = try!(into_map(value));
+
+ let name = match map.remove("name") {
+ Some(Value::Null) | None => return Ok(None),
+ Some(v) => try!(into_string(v)),
+ };
+
+ if name.trim().is_empty() {
+ return Ok(None);
+ }
+
+ missing!(map, Some(Game {
+ name: name,
+ kind: try!(opt(&mut map, "type", GameType::decode)).unwrap_or(GameType::Playing),
+ url: try!(opt(&mut map, "url", into_string)),
+ }))
+ }
+}
+
+impl Presence {
+ pub fn decode(value: Value) -> Result<Presence> {
+ let mut value = try!(into_map(value));
+ let mut user_map = try!(remove(&mut value, "user").and_then(into_map));
+
+ let (user_id, user) = if user_map.len() > 1 {
+ let user = try!(User::decode(Value::Object(user_map)));
+ (user.id, Some(user))
+ } else {
+ (try!(remove(&mut user_map, "id").and_then(UserId::decode)), None)
+ };
+
+ let game = match value.remove("game") {
+ None | Some(Value::Null) => None,
+ Some(v) => try!(Game::decode(v)),
+ };
+
+ missing!(value, Presence {
+ user_id: user_id,
+ status: try!(remove(&mut value, "status").and_then(OnlineStatus::decode_str)),
+ last_modified: try!(opt(&mut value, "last_modified", |v| Ok(req!(v.as_u64())))),
+ game: game,
+ user: user,
+ nick: try!(opt(&mut value, "nick", into_string)),
+ })
+ }
+}
diff --git a/src/model/guild.rs b/src/model/guild.rs
new file mode 100644
index 0000000..6bc14c5
--- /dev/null
+++ b/src/model/guild.rs
@@ -0,0 +1,933 @@
+use serde_json::builder::ObjectBuilder;
+use std::collections::HashMap;
+use std::{fmt, mem};
+use super::utils::{
+ decode_emojis,
+ decode_members,
+ decode_presences,
+ decode_roles,
+ decode_voice_states,
+ into_map,
+ into_string,
+ opt,
+ remove,
+ warn_field
+};
+use super::*;
+use ::builder::{EditGuild, EditMember, EditRole};
+use ::client::{STATE, http};
+use ::prelude::*;
+use ::utils::{Colour, decode_array};
+
+impl From<Guild> for GuildContainer {
+ fn from(guild: Guild) -> GuildContainer {
+ GuildContainer::Guild(guild)
+ }
+}
+
+impl From<GuildId> for GuildContainer {
+ fn from(guild_id: GuildId) -> GuildContainer {
+ GuildContainer::Id(guild_id)
+ }
+}
+
+impl From<u64> for GuildContainer {
+ fn from(id: u64) -> GuildContainer {
+ GuildContainer::Id(GuildId(id))
+ }
+}
+
+impl Emoji {
+ /// Finds the [`Guild`] that owns the emoji by looking through the state.
+ ///
+ /// [`Guild`]: struct.Guild.html
+ pub fn find_guild_id(&self) -> Option<GuildId> {
+ STATE.lock()
+ .unwrap()
+ .guilds
+ .values()
+ .find(|guild| guild.emojis.contains_key(&self.id))
+ .map(|guild| guild.id)
+ }
+
+ /// Deletes the emoji.
+ ///
+ /// **Note**: The [Manage Emojis] permission is required.
+ ///
+ /// **Note**: Only user accounts may use this method.
+ ///
+ /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html
+ pub fn delete(&self) -> Result<()> {
+ match self.find_guild_id() {
+ Some(guild_id) => http::delete_emoji(guild_id.0, self.id.0),
+ None => Err(Error::Client(ClientError::ItemMissing)),
+ }
+ }
+
+ /// Edits the emoji by updating it with a new name.
+ ///
+ /// **Note**: The [Manage Emojis] permission is required.
+ ///
+ /// **Note**: Only user accounts may use this method.
+ ///
+ /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html
+ pub fn edit(&mut self, name: &str) -> Result<()> {
+ match self.find_guild_id() {
+ Some(guild_id) => {
+ let map = ObjectBuilder::new()
+ .insert("name", name)
+ .build();
+
+ match http::edit_emoji(guild_id.0, self.id.0, map) {
+ Ok(emoji) => {
+ mem::replace(self, emoji);
+
+ Ok(())
+ },
+ Err(why) => Err(why),
+ }
+ },
+ None => Err(Error::Client(ClientError::ItemMissing)),
+ }
+ }
+}
+
+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>`.
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ try!(f.write_str("<:"));
+ try!(f.write_str(&self.name));
+ try!(fmt::Write::write_char(f, ':'));
+ try!(fmt::Display::fmt(&self.id, f));
+ fmt::Write::write_char(f, '>')
+ }
+}
+
+impl GuildInfo {
+ /// Returns the formatted URL of the guild's icon, if the guild has an icon.
+ pub fn icon_url(&self) -> Option<String> {
+ self.icon.as_ref().map(|icon|
+ format!(cdn_concat!("/icons/{}/{}.jpg"), self.id, icon))
+ }
+}
+
+impl Guild {
+ /// Finds a role by Id within the guild.
+ pub fn find_role<R: Into<RoleId>>(&self, role_id: R) -> Option<&Role> {
+ self.roles.get(&role_id.into())
+ }
+
+ /// Returns a formatted URL of the guild's icon, if the guild has an icon.
+ pub fn icon_url(&self) -> Option<String> {
+ self.icon.as_ref().map(|icon|
+ format!(cdn_concat!("/icons/{}/{}.jpg"), self.id, icon))
+ }
+}
+
+impl LiveGuild {
+ fn has_perms(&self, mut permissions: Permissions) -> Result<bool> {
+ let member = match self.get_member(STATE.lock().unwrap().user.id) {
+ Some(member) => member,
+ None => return Err(Error::Client(ClientError::ItemMissing)),
+ };
+
+ let perms = self.permissions_for(ChannelId(self.id.0), member.user.id);
+
+ permissions.remove(perms);
+
+ Ok(permissions.is_empty())
+ }
+
+ /// Ban a [`User`] from the guild. All messages by the
+ /// user within the last given number of days given will be deleted. This
+ /// may be a range between `0` and `7`.
+ ///
+ /// **Note**: Requires the [Ban Members] permission.
+ ///
+ /// # Examples
+ ///
+ /// Ban a member for 4 days:
+ ///
+ /// ```rust,ignore
+ /// // assumes a `user` and `guild` have already been bound
+ /// let _ = guild.ban(user, 4);
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// Returns a [`ClientError::DeleteMessageDaysAmount`] if the number of
+ /// days' worth of messages to delete is over the maximum.
+ ///
+ /// [`ClientError::DeleteMessageDaysAmount`]: ../client/enum.ClientError.html#DeleteMessageDaysAmount.v
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`User`]: struct.User.html
+ /// [Ban Members]: permissions/constant.BAN_MEMBERS.html
+ pub fn ban<U: Into<UserId>>(&self, user: U, delete_message_days: u8)
+ -> Result<()> {
+ if delete_message_days > 7 {
+ return Err(Error::Client(ClientError::DeleteMessageDaysAmount(delete_message_days)));
+ }
+
+ let req = permissions::BAN_MEMBERS;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::ban_user(self.id.0, user.into().0, delete_message_days)
+ }
+
+ /// Retrieves a list of [`Ban`]s for the guild.
+ ///
+ /// **Note**: Requires the [Ban Members] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`Ban`]: struct.Ban.html
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Ban Members]: permissions/constant.BAN_MEMBERS.html
+ pub fn bans(&self) -> Result<Vec<Ban>> {
+ let req = permissions::BAN_MEMBERS;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::get_bans(self.id.0)
+ }
+
+ /// Creates a new [`Channel`] in the guild.
+ ///
+ /// **Note**: Requires the [Manage Channels] permission.
+ ///
+ /// # Examples
+ ///
+ /// ```rust,ignore
+ /// use serenity::models::ChannelType;
+ ///
+ /// let _ = guild.create_channel("my-test-channel", ChannelType::Text);
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`Channel`]: struct.Channel.html
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Manage Channels]: permissions/constants.MANAGE_CHANNELS.html
+ pub fn create_channel(&mut self, name: &str, kind: ChannelType)
+ -> Result<Channel> {
+ let req = permissions::MANAGE_CHANNELS;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("name", name)
+ .insert("type", kind.name())
+ .build();
+
+ http::create_channel(self.id.0, map)
+ }
+
+ /// Creates a new [`Role`] in the guild with the data set,
+ /// if any.
+ ///
+ /// See the documentation for [`Context::create_role`] on how to use this.
+ ///
+ /// **Note**: Requires the
+ /// [Manage Roles] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`Context::create_role`]: ../client/struct.Context.html#method.create_role
+ /// [`Role`]: struct.Role.html
+ /// [Manage Roles]: permissions/constants.MANAGE_ROLES.html
+ pub fn create_role<F>(&self, f: F) -> Result<Role>
+ where F: FnOnce(EditRole) -> EditRole {
+ let req = permissions::MANAGE_ROLES;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let role = {
+ try!(http::create_role(self.id.0))
+ };
+ let map = f(EditRole::default()).0.build();
+
+ http::edit_role(self.id.0, role.id.0, map)
+ }
+
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<LiveGuild> {
+ let mut map = try!(into_map(value));
+
+ let id = try!(remove(&mut map, "id").and_then(GuildId::decode));
+
+ let public_channels = {
+ let mut public_channels = HashMap::new();
+
+ let vals = try!(decode_array(try!(remove(&mut map, "channels")),
+ |v| PublicChannel::decode_guild(v, id)));
+
+ for public_channel in vals {
+ public_channels.insert(public_channel.id, public_channel);
+ }
+
+ public_channels
+ };
+
+ missing!(map, LiveGuild {
+ afk_channel_id: try!(opt(&mut map, "afk_channel_id", ChannelId::decode)),
+ afk_timeout: req!(try!(remove(&mut map, "afk_timeout")).as_u64()),
+ channels: public_channels,
+ default_message_notifications: req!(try!(remove(&mut map, "default_message_notifications")).as_u64()),
+ emojis: try!(remove(&mut map, "emojis").and_then(decode_emojis)),
+ features: try!(remove(&mut map, "features").and_then(|v| decode_array(v, into_string))),
+ icon: try!(opt(&mut map, "icon", into_string)),
+ id: id,
+ joined_at: try!(remove(&mut map, "joined_at").and_then(into_string)),
+ large: req!(try!(remove(&mut map, "large")).as_bool()),
+ member_count: req!(try!(remove(&mut map, "member_count")).as_u64()),
+ members: try!(remove(&mut map, "members").and_then(decode_members)),
+ mfa_level: req!(try!(remove(&mut map, "mfa_level")).as_u64()),
+ name: try!(remove(&mut map, "name").and_then(into_string)),
+ owner_id: try!(remove(&mut map, "owner_id").and_then(UserId::decode)),
+ presences: try!(remove(&mut map, "presences").and_then(decode_presences)),
+ region: try!(remove(&mut map, "region").and_then(into_string)),
+ roles: try!(remove(&mut map, "roles").and_then(decode_roles)),
+ splash: try!(opt(&mut map, "splash", into_string)),
+ verification_level: try!(remove(&mut map, "verification_level").and_then(VerificationLevel::decode)),
+ voice_states: try!(remove(&mut map, "voice_states").and_then(decode_voice_states)),
+ })
+ }
+
+
+ /// Deletes the current guild if the current account is the owner of the
+ /// guild.
+ ///
+ /// **Note**: Requires the current user to be the owner of the guild.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidUser`] if the current user is not the
+ /// guild owner.
+ ///
+ /// [`ClientError::InvalidUser`]: ../client/enum.ClientError.html#InvalidUser.v
+ pub fn delete(&self) -> Result<Guild> {
+ if self.owner_id != STATE.lock().unwrap().user.id {
+ let req = permissions::MANAGE_GUILD;
+
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::delete_guild(self.id.0)
+ }
+
+ /// Edits the current guild with new data where specified. See the
+ /// documentation for [`Context::edit_guild`] on how to use this.
+ ///
+ /// **Note**: Requires the current user to have the [Manage Guild]
+ /// permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`Context::edit_guild`]: ../client/struct.Context.html#method.edit_guild
+ /// [Manage Guild]: permissions/constants.MANAGE_GUILD.html
+ pub fn edit<F>(&mut self, f: F) -> Result<()>
+ where F: FnOnce(EditGuild) -> EditGuild {
+ let req = permissions::MANAGE_GUILD;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = f(EditGuild::default()).0.build();
+
+ match http::edit_guild(self.id.0, map) {
+ Ok(guild) => {
+ self.afk_channel_id = guild.afk_channel_id;
+ self.afk_timeout = guild.afk_timeout;
+ self.default_message_notifications = guild.default_message_notifications;
+ self.emojis = guild.emojis;
+ self.features = guild.features;
+ self.icon = guild.icon;
+ self.mfa_level = guild.mfa_level;
+ self.name = guild.name;
+ self.owner_id = guild.owner_id;
+ self.region = guild.region;
+ self.roles = guild.roles;
+ self.splash = guild.splash;
+ self.verification_level = guild.verification_level;
+
+ Ok(())
+ },
+ Err(why) => Err(why),
+ }
+ }
+
+ /// Attempts to retrieve a [`PublicChannel`] with the given Id.
+ ///
+ /// [`PublicChannel`]: struct.PublicChannel.html
+ pub fn get_channel<C: Into<ChannelId>>(&self, channel_id: C)
+ -> Option<&PublicChannel> {
+ self.channels.get(&channel_id.into())
+ }
+
+ /// Retrieves the active invites for the guild.
+ ///
+ /// **Note**: Requires the [Manage Guild] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html
+ pub fn get_invites(&self) -> Result<Vec<RichInvite>> {
+ let req = permissions::MANAGE_GUILD;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::get_guild_invites(self.id.0)
+ }
+
+ /// Attempts to retrieve the given user's member instance in the guild.
+ pub fn get_member<U: Into<UserId>>(&self, user_id: U) -> Option<&Member> {
+ self.members.get(&user_id.into())
+ }
+
+ /// Retrieves the first [`Member`] found that matches the name - with an
+ /// optional discriminator - provided.
+ ///
+ /// Searching with a discriminator given is the most precise form of lookup,
+ /// as no two people can share the same username *and* discriminator.
+ ///
+ /// If a member can not be found by username or username#discriminator,
+ /// then a search will be done for the nickname. When searching by nickname,
+ /// the hash (`#`) and everything after it is included in the search.
+ ///
+ /// The following are valid types of searches:
+ ///
+ /// - **username**: "zey"
+ /// - **username and discriminator**: "zey#5479"
+ /// - **nickname**: "zeyla" or "zeylas#nick"
+ ///
+ /// [`Member`]: struct.Member.html
+ pub fn get_member_named(&self, name: &str) -> Option<&Member> {
+ let hash_pos = name.find('#');
+
+ let (name, discrim) = if let Some(pos) = hash_pos {
+ let split = name.split_at(pos);
+
+ let discrim = match split.1.parse::<u16>() {
+ Ok(discrim) => discrim,
+ Err(_why) => return None,
+ };
+
+ (split.0, Some(discrim))
+ } else {
+ (&name[..], None::<u16>)
+ };
+
+ self.members
+ .iter()
+ .find(|&(_member_id, member)| {
+ let name_matches = member.user.name == name;
+ let discrim_matches = match discrim {
+ Some(discrim) => member.user.discriminator == discrim,
+ None => true,
+ };
+
+ name_matches && discrim_matches
+ }).or(self.members.iter().find(|&(_member_id, member)| {
+ member.nick.as_ref().map_or(false, |nick| nick == name)
+ })).map(|(_member_id, member)| member)
+ }
+
+ /// Returns the formatted URL of the guild's icon, if one exists.
+ pub fn icon_url(&self) -> Option<String> {
+ self.icon.as_ref().map(|icon|
+ format!(cdn_concat!("/icons/{}/{}.jpg"), self.id, icon))
+ }
+
+ /// Checks if the guild is 'large'. A guild is considered large if it has
+ /// more than 250 members.
+ pub fn is_large(&self) -> bool {
+ self.members.len() > 250
+ }
+
+ /// Leaves the guild.
+ pub fn leave(&self) -> Result<Guild> {
+ http::leave_guild(self.id.0)
+ }
+
+ /// Calculate a [`User`]'s permissions in a given channel in the guild.
+ ///
+ /// [`User`]: struct.User.html
+ pub fn permissions_for<C, U>(&self, channel_id: C, user_id: U)
+ -> Permissions where C: Into<ChannelId>, U: Into<UserId> {
+ use super::permissions::*;
+
+ let user_id = user_id.into();
+
+ // The owner has all permissions in all cases.
+ if user_id == self.owner_id {
+ return Permissions::all();
+ }
+
+ let channel_id = channel_id.into();
+
+ // Start by retrieving the @everyone role's permissions.
+ let everyone = match self.roles.get(&RoleId(self.id.0)) {
+ Some(everyone) => everyone,
+ None => {
+ error!("@everyone role ({}) missing in {}", self.id, self.name);
+
+ return Permissions::empty();
+ },
+ };
+
+ // Create a base set of permissions, starting with `@everyone`s.
+ let mut permissions = everyone.permissions;
+
+ let member = match self.members.get(&user_id) {
+ Some(member) => member,
+ None => return everyone.permissions,
+ };
+
+ for &role in &member.roles {
+ if let Some(role) = self.roles.get(&role) {
+ permissions |= role.permissions;
+ } else {
+ warn!("perms: {:?} on {:?} has non-existent role {:?}", member.user.id, self.id, role);
+ }
+ }
+
+ // Administrators have all permissions in any channel.
+ if permissions.contains(ADMINISTRATOR) {
+ return Permissions::all();
+ }
+
+ if let Some(channel) = self.channels.get(&channel_id) {
+ // If this is a text channel, then throw out voice permissions.
+ if channel.kind == ChannelType::Text {
+ permissions &= !(CONNECT | SPEAK | MUTE_MEMBERS |
+ DEAFEN_MEMBERS | MOVE_MEMBERS | USE_VAD);
+ }
+
+ // Apply the permission overwrites for the channel for each of the
+ // overwrites that - first - applies to the member's roles, and then
+ // the member itself.
+ //
+ // First apply the denied permission overwrites for each, then apply
+ // the allowed.
+
+ // Roles
+ for overwrite in &channel.permission_overwrites {
+ if let PermissionOverwriteType::Role(role) = overwrite.kind {
+ if !member.roles.contains(&role) || role.0 == self.id.0 {
+ continue;
+ }
+
+ permissions = (permissions & !overwrite.deny) | overwrite.allow;
+ }
+ }
+
+ // Member
+ for overwrite in &channel.permission_overwrites {
+ if PermissionOverwriteType::Member(user_id) != overwrite.kind {
+ continue;
+ }
+
+ permissions = (permissions & !overwrite.deny) | overwrite.allow;
+ }
+ } else {
+ warn!("Guild {} does not contain channel {}", self.id, channel_id);
+ }
+
+ // The default channel is always readable.
+ if channel_id.0 == self.id.0 {
+ permissions |= READ_MESSAGES;
+ }
+
+ // No SEND_MESSAGES => no message-sending-related actions
+ // If the member does not have the `SEND_MESSAGES` permission, then
+ // throw out message-able permissions.
+ if !permissions.contains(SEND_MESSAGES) {
+ permissions &= !(SEND_TTS_MESSAGES |
+ MENTION_EVERYONE |
+ EMBED_LINKS |
+ ATTACH_FILES);
+ }
+
+ // If the member does not have the `READ_MESSAGES` permission, then
+ // throw out actionable permissions.
+ if !permissions.contains(READ_MESSAGES) {
+ permissions &= KICK_MEMBERS | BAN_MEMBERS | ADMINISTRATOR |
+ MANAGE_GUILD | CHANGE_NICKNAME | MANAGE_NICKNAMES;
+ }
+
+ permissions
+ }
+
+ /// Retrieves the count of the number of [`Member`]s that would be pruned
+ /// with the number of given days.
+ ///
+ /// See the documentation on [`GuildPrune`] for more information.
+ ///
+ /// **Note**: Requires the [Kick Members] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`GuildPrune`]: struct.GuildPrune.html
+ /// [`Member`]: struct.Member.html
+ /// [Kick Members]: permissions/constant.KICK_MEMBERS.html
+ pub fn prune_count(&self, days: u16) -> Result<GuildPrune> {
+ let req = permissions::KICK_MEMBERS;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("days", days)
+ .build();
+
+ http::get_guild_prune_count(self.id.0, map)
+ }
+
+ /// Starts a prune of [`Member`]s.
+ ///
+ /// See the documentation on [`GuildPrune`] for more information.
+ ///
+ /// **Note**: Requires the [Kick Members] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`GuildPrune`]: struct.GuildPrune.html
+ /// [`Member`]: struct.Member.html
+ /// [Kick Members]: permissions/constant.KICK_MEMBERS.html
+ pub fn start_prune(&self, days: u16) -> Result<GuildPrune> {
+ let req = permissions::KICK_MEMBERS;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("days", days)
+ .build();
+
+ http::start_guild_prune(self.id.0, map)
+ }
+
+ /// Unbans the given [`User`] from the guild.
+ ///
+ /// **Note**: Requires the [Ban Members] permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::InvalidPermissions`] if the current user does
+ /// not have permission to perform bans.
+ ///
+ /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#InvalidPermissions.v
+ /// [`User`]: struct.User.html
+ /// [Ban Members]: permissions/constant.BAN_MEMBERS.html
+ pub fn unban<U: Into<UserId>>(&self, user: U) -> Result<()> {
+ let req = permissions::BAN_MEMBERS;
+
+ if !try!(self.has_perms(req)) {
+ return Err(Error::Client(ClientError::InvalidPermissions(req)));
+ }
+
+ http::remove_ban(self.id.0, user.into().0)
+ }
+}
+
+impl Member {
+ /// Adds a [`Role`] to the member, editing its roles
+ /// in-place if the request was successful.
+ ///
+ /// **Note**: Requires the [Manage Roles] permission.
+ ///
+ /// [`Role`]: struct.Role.html
+ /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html
+ pub fn add_role<R: Into<RoleId>>(&mut self, role_id: R) -> Result<()> {
+ self.add_roles(&[role_id.into()])
+ }
+
+ /// Adds one or multiple [`Role`]s to the member, editing
+ /// its roles in-place if the request was successful.
+ ///
+ /// **Note**: Requires the [Manage Roles] permission.
+ ///
+ /// [`Role`]: struct.Role.html
+ /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html
+ pub fn add_roles(&mut self, role_ids: &[RoleId]) -> Result<()> {
+ let guild_id = try!(self.find_guild());
+ self.roles.extend_from_slice(role_ids);
+
+ let map = EditMember::default().roles(&self.roles).0.build();
+
+ match http::edit_member(guild_id.0, self.user.id.0, map) {
+ Ok(()) => Ok(()),
+ Err(why) => {
+ self.roles.retain(|r| !role_ids.contains(r));
+
+ Err(why)
+ }
+ }
+ }
+
+ /// Ban the member from its guild, deleting the last X number of
+ /// days' worth of messages.
+ ///
+ /// **Note**: Requires the [Ban Members] role.
+ ///
+ /// [Ban Members]: permissions/constant.BAN_MEMBERS.html
+ pub fn ban(&self, delete_message_days: u8) -> Result<()> {
+ let guild_id = try!(self.find_guild());
+
+ http::ban_user(guild_id.0,
+ self.user.id.0,
+ delete_message_days)
+ }
+
+ /// Calculates the member's display name.
+ ///
+ /// The nickname takes priority over the member's username if it exists.
+ pub fn display_name(&self) -> &str {
+ self.nick.as_ref().unwrap_or(&self.user.name)
+ }
+
+ /// Edits the member with the given data. See [`Context::edit_member`] for
+ /// more information.
+ ///
+ /// See [`EditMember`] for the permission(s) required for separate builder
+ /// methods, as well as usage of this.
+ ///
+ /// [`Context::edit_member`]: ../client/struct.Context.html#method.edit_member
+ /// [`EditMember`]: ../builder/struct.EditMember.html
+ pub fn edit<F>(&self, f: F) -> Result<()>
+ where F: FnOnce(EditMember) -> EditMember {
+ let guild_id = try!(self.find_guild());
+ let map = f(EditMember::default()).0.build();
+
+ http::edit_member(guild_id.0, self.user.id.0, map)
+ }
+
+ /// Finds the Id of the [`Guild`] that the member is in.
+ ///
+ /// [`Guild`]: struct.Guild.html
+ pub fn find_guild(&self) -> Result<GuildId> {
+ STATE.lock()
+ .unwrap()
+ .guilds
+ .values()
+ .find(|guild| {
+ guild.members
+ .iter()
+ .any(|(_member_id, member)| {
+ let joined_at = member.joined_at == self.joined_at;
+ let user_id = member.user.id == self.user.id;
+
+ joined_at && user_id
+ })
+ })
+ .map(|x| x.id)
+ .ok_or(Error::Client(ClientError::GuildNotFound))
+ }
+
+ /// Removes a [`Role`] from the member.
+ ///
+ /// **Note**: Requires the [Manage Roles] permission.
+ ///
+ /// [`Role`]: struct.Role.html
+ /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html
+ pub fn remove_role<R: Into<RoleId>>(&mut self, role_id: R) -> Result<()> {
+ self.remove_roles(&[role_id.into()])
+ }
+
+ /// Removes one or multiple [`Role`]s from the member.
+ ///
+ /// **Note**: Requires the [Manage Roles] permission.
+ ///
+ /// [`Role`]: struct.Role.html
+ /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html
+ pub fn remove_roles(&mut self, role_ids: &[RoleId]) -> Result<()> {
+ let guild_id = try!(self.find_guild());
+ self.roles.retain(|r| !role_ids.contains(r));
+
+ let map = EditMember::default().roles(&self.roles).0.build();
+
+ match http::edit_member(guild_id.0, self.user.id.0, map) {
+ Ok(()) => Ok(()),
+ Err(why) => {
+ self.roles.extend_from_slice(role_ids);
+
+ Err(why)
+ },
+ }
+ }
+}
+
+impl fmt::Display for Member {
+ /// Mentions the user so that they receive a notification.
+ ///
+ /// # Examples
+ ///
+ /// ```rust,ignore
+ /// // assumes a `member` has already been bound
+ /// println!("{} is a member!", member);
+ /// ```
+ // This is in the format of `<@USER_ID>`.
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.user.mention(), f)
+ }
+}
+
+impl PossibleGuild<LiveGuild> {
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<Self> {
+ let mut value = try!(into_map(value));
+ if remove(&mut value, "unavailable").ok().and_then(|v| v.as_bool()).unwrap_or(false) {
+ remove(&mut value, "id").and_then(GuildId::decode).map(PossibleGuild::Offline)
+ } else {
+ LiveGuild::decode(Value::Object(value)).map(PossibleGuild::Online)
+ }
+ }
+
+ /// Retrieves the Id of the inner [`Guild`].
+ ///
+ /// [`Guild`]: struct.Guild.html
+ pub fn id(&self) -> GuildId {
+ match *self {
+ PossibleGuild::Offline(guild_id) => guild_id,
+ PossibleGuild::Online(ref live_guild) => live_guild.id,
+ }
+ }
+}
+
+impl PossibleGuild<Guild> {
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<Self> {
+ let mut value = try!(into_map(value));
+ if remove(&mut value, "unavailable").ok().and_then(|v| v.as_bool()).unwrap_or(false) {
+ remove(&mut value, "id").and_then(GuildId::decode).map(PossibleGuild::Offline)
+ } else {
+ Guild::decode(Value::Object(value)).map(PossibleGuild::Online)
+ }
+ }
+
+ /// Retrieves the Id of the inner [`Guild`].
+ ///
+ /// [`Guild`]: struct.Guild.html
+ pub fn id(&self) -> GuildId {
+ match *self {
+ PossibleGuild::Offline(id) => id,
+ PossibleGuild::Online(ref live_guild) => live_guild.id,
+ }
+ }
+}
+
+impl Role {
+ /// Generates a colour representation of the role. See
+ /// [the documentation] on Colour for more information.
+ ///
+ /// [the documentation]: ../utils/struct.Colour.html
+ pub fn colour(&self) -> Colour {
+ Colour::new(self.colour as u32)
+ }
+
+ /// Deletes the role.
+ ///
+ /// **Note** Requires the [Manage Roles] permission.
+ ///
+ /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html
+ pub fn delete(&self) -> Result<()> {
+ let guild_id = try!(self.find_guild());
+
+ http::delete_role(guild_id.0, self.id.0)
+ }
+
+ /// Searches the state for the guild that owns the role.
+ ///
+ /// # Errors
+ ///
+ /// Returns a [`ClientError::GuildNotFound`] if a guild is not in the state
+ /// that contains the role.
+ ///
+ /// [`ClientError::GuildNotFound`]: ../client/enum.ClientError.html#GuildNotFound.v
+ pub fn find_guild(&self) -> Result<GuildId> {
+ STATE.lock()
+ .unwrap()
+ .guilds
+ .values()
+ .find(|guild| guild.roles.contains_key(&RoleId(self.id.0)))
+ .map(|x| x.id)
+ .ok_or(Error::Client(ClientError::GuildNotFound))
+ }
+
+ /// Check that the role has the given permission.
+ pub fn has_permission(&self, permission: Permissions) -> bool {
+ self.permissions.contains(permission)
+ }
+
+ /// Checks whether the role has all of the given permissions.
+ ///
+ /// The 'precise' argument is used to check if the role's permissions are
+ /// precisely equivilant to the given permissions. If you need only check
+ /// that the role has at least the given permissions, pass `false`.
+ pub fn has_permissions(&self, permissions: Permissions, precise: bool)
+ -> bool {
+ if precise {
+ self.permissions == permissions
+ } else {
+ self.permissions.contains(permissions)
+ }
+ }
+
+ /// Return a `Mention` which will ping members of the role.
+ pub fn mention(&self) -> Mention {
+ self.id.mention()
+ }
+}
+
+impl fmt::Display for Role {
+ /// Format a mention for the role, pinging its members.
+ // This is in the format of: `<@&ROLE_ID>`.
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.mention(), f)
+ }
+}
diff --git a/src/model/id.rs b/src/model/id.rs
new file mode 100644
index 0000000..97ee6d6
--- /dev/null
+++ b/src/model/id.rs
@@ -0,0 +1,176 @@
+use super::*;
+use ::client::{STATE, http};
+use ::prelude::*;
+
+impl ChannelId {
+ /// Search the state for the channel with the Id.
+ pub fn find(&self) -> Option<Channel> {
+ STATE.lock().unwrap().find_channel(*self)
+ }
+
+ /// Search the state for the channel. If it can't be found, the channel is
+ /// requested over REST.
+ pub fn get(&self) -> Result<Channel> {
+ if let Some(channel) = STATE.lock().unwrap().find_channel(*self) {
+ return Ok(channel);
+ }
+
+ http::get_channel(self.0)
+ }
+
+ /// Returns a [Mention](struct.Mention.html) which will link to the
+ /// channel.
+ pub fn mention(&self) -> Mention {
+ Mention {
+ id: self.0,
+ prefix: "<#",
+ }
+ }
+}
+
+impl From<Channel> for ChannelId {
+ fn from(channel: Channel) -> ChannelId {
+ match channel {
+ Channel::Group(group) => group.channel_id,
+ Channel::Private(channel) => channel.id,
+ Channel::Public(channel) => channel.id,
+ }
+ }
+}
+
+impl From<PrivateChannel> for ChannelId {
+ fn from(private_channel: PrivateChannel) -> ChannelId {
+ private_channel.id
+ }
+}
+
+impl From<PublicChannel> for ChannelId {
+ fn from(public_channel: PublicChannel) -> ChannelId {
+ public_channel.id
+ }
+}
+
+impl From<Emoji> for EmojiId {
+ fn from(emoji: Emoji) -> EmojiId {
+ emoji.id
+ }
+}
+
+impl GuildId {
+ /// Search the state for the guild.
+ pub fn find(&self) -> Option<LiveGuild> {
+ STATE.lock().unwrap().find_guild(*self).cloned()
+ }
+
+ /// Requests the guild over REST.
+ ///
+ /// Note that this will not be a complete guild, as REST does not send
+ /// all data with a guild retrieval.
+ pub fn get(&self) -> Result<Guild> {
+ http::get_guild(self.0)
+ }
+
+ /// Mentions the [Guild](struct.Guild.html)'s default channel.
+ pub fn mention(&self) -> Mention {
+ Mention {
+ id: self.0,
+ prefix: "<#",
+ }
+ }
+}
+
+impl From<Guild> for GuildId {
+ fn from(guild: Guild) -> GuildId {
+ guild.id
+ }
+}
+
+impl From<GuildInfo> for GuildId {
+ fn from(guild_info: GuildInfo) -> GuildId {
+ guild_info.id
+ }
+}
+
+impl From<InviteGuild> for GuildId {
+ fn from(invite_guild: InviteGuild) -> GuildId {
+ invite_guild.id
+ }
+}
+
+impl From<LiveGuild> for GuildId {
+ fn from(live_guild: LiveGuild) -> GuildId {
+ live_guild.id
+ }
+}
+
+impl From<Integration> for IntegrationId {
+ fn from(integration: Integration) -> IntegrationId {
+ integration.id
+ }
+}
+
+impl From<Message> for MessageId {
+ fn from(message: Message) -> MessageId {
+ message.id
+ }
+}
+
+impl From<Role> for RoleId {
+ fn from(role: Role) -> RoleId {
+ role.id
+ }
+}
+
+impl RoleId {
+ /// Search the state for the role.
+ pub fn find(&self) -> Option<Role> {
+ STATE.lock()
+ .unwrap()
+ .guilds
+ .values()
+ .find(|guild| guild.roles.contains_key(self))
+ .map(|guild| guild.roles.get(self))
+ .and_then(|v| match v {
+ Some(v) => Some(v),
+ None => None,
+ })
+ .cloned()
+ }
+
+ /// Returns a [Mention](struct.Mention.html) which will ping members of the
+ /// role.
+ pub fn mention(&self) -> Mention {
+ Mention {
+ id: self.0,
+ prefix: "<@&",
+ }
+ }
+}
+
+impl From<CurrentUser> for UserId {
+ fn from(current_user: CurrentUser) -> UserId {
+ current_user.id
+ }
+}
+
+impl From<Member> for UserId {
+ fn from(member: Member) -> UserId {
+ member.user.id
+ }
+}
+
+impl From<User> for UserId {
+ fn from(user: User) -> UserId {
+ user.id
+ }
+}
+
+impl UserId {
+ /// Returns a [Mention](struct.Mention.html) which will ping the user.
+ pub fn mention(&self) -> Mention {
+ Mention {
+ id: self.0,
+ prefix: "<@",
+ }
+ }
+}
diff --git a/src/model/invite.rs b/src/model/invite.rs
new file mode 100644
index 0000000..4324a67
--- /dev/null
+++ b/src/model/invite.rs
@@ -0,0 +1,47 @@
+use super::{Invite, RichInvite};
+use ::client::http;
+use ::prelude::*;
+
+impl Invite {
+ /// Accepts an invite.
+ ///
+ /// Refer to the documentation for [`Context::accept_invite`] for
+ /// restrictions on accepting an invite.
+ ///
+ /// [`Context::accept_invite`]: ../client/struct.Context.html#method.accept_invite
+ pub fn accept(&self) -> Result<Invite> {
+ http::accept_invite(&self.code)
+ }
+
+ /// Deletes an invite.
+ ///
+ /// Refer to the documentation for [`Context::delete_invite`] for more
+ /// information.
+ ///
+ /// [`Context::delete_invite`]: ../client/struct.Context.html#method.delete_invite
+ pub fn delete(&self) -> Result<Invite> {
+ http::delete_invite(&self.code)
+ }
+}
+
+impl RichInvite {
+ /// Accepts an invite.
+ ///
+ /// Refer to the documentation for [`Context::accept_invite`] for
+ /// restrictions on accepting an invite.
+ ///
+ /// [`Context::accept_invite`]: ../client/struct.Context.html#method.accept_invite
+ pub fn accept(&self) -> Result<Invite> {
+ http::accept_invite(&self.code)
+ }
+
+ /// Deletes an invite.
+ ///
+ /// Refer to the documentation for [`Context::delete_invite`] for more
+ /// information.
+ ///
+ /// [`Context::delete_invite`]: ../client/struct.Context.html#method.delete_invite
+ pub fn delete(&self) -> Result<Invite> {
+ http::delete_invite(&self.code)
+ }
+}
diff --git a/src/model/misc.rs b/src/model/misc.rs
new file mode 100644
index 0000000..6b8a90e
--- /dev/null
+++ b/src/model/misc.rs
@@ -0,0 +1,95 @@
+use std::fmt;
+use super::{
+ ChannelId,
+ Channel,
+ Emoji,
+ Member,
+ RoleId,
+ Role,
+ UserId,
+ User,
+ IncidentStatus
+};
+use ::prelude::*;
+
+pub trait Mentionable {
+ fn mention(&self) -> String;
+}
+
+impl Mentionable for ChannelId {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+impl Mentionable for Channel {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+impl Mentionable for Emoji {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+impl Mentionable for Member {
+ fn mention(&self) -> String {
+ format!("{}", self.user)
+ }
+}
+
+impl Mentionable for RoleId {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+impl Mentionable for Role {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+impl Mentionable for UserId {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+impl Mentionable for User {
+ fn mention(&self) -> String {
+ format!("{}", self)
+ }
+}
+
+/// A mention targeted at a certain model.
+///
+/// A mention can be created by calling `.mention()` on anything that is
+/// mentionable - or an item's Id - and can be formatted into a string using
+/// [`format!`]:
+///
+/// ```rust,ignore
+/// let message = format!("Mentioning {}", user.mention());
+/// ```
+///
+/// If a `String` is required, call `mention.to_string()`.
+pub struct Mention {
+ pub prefix: &'static str,
+ pub id: u64,
+}
+
+impl fmt::Display for Mention {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ try!(f.write_str(self.prefix));
+ try!(fmt::Display::fmt(&self.id, f));
+ fmt::Write::write_char(f, '>')
+ }
+}
+
+impl IncidentStatus {
+ pub fn decode(value: Value) -> Result<Self> {
+ Self::decode_str(value)
+ }
+}
diff --git a/src/model/mod.rs b/src/model/mod.rs
new file mode 100644
index 0000000..8478cc2
--- /dev/null
+++ b/src/model/mod.rs
@@ -0,0 +1,140 @@
+pub mod permissions;
+
+#[macro_use]
+mod utils;
+
+mod channel;
+mod gateway;
+mod guild;
+mod id;
+mod invite;
+mod misc;
+mod user;
+mod voice;
+
+pub use self::channel::*;
+pub use self::gateway::*;
+pub use self::guild::*;
+pub use self::id::*;
+pub use self::invite::*;
+pub use self::misc::*;
+pub use self::permissions::Permissions;
+pub use self::user::*;
+pub use self::voice::*;
+
+use self::utils::*;
+use std::collections::HashMap;
+use std::fmt;
+use ::prelude::*;
+use ::utils::decode_array;
+
+// All of the enums and structs are imported here. These are built from the
+// build script located at `./build.rs`.
+//
+// These use definitions located in `./definitions`, to map to structs and
+// enums, each respectively located in their own folder.
+//
+// For structs, this will almost always include their decode method, although
+// some require their own decoding due to many special fields.
+//
+// For enums, this will include the variants, and will automatically generate
+// the number/string decoding methods where appropriate.
+//
+// As only the struct/enum itself and common mappings can be built, this leaves
+// unique methods on each to be implemented here.
+include!(concat!(env!("OUT_DIR"), "/models/built.rs"));
+
+macro_rules! id {
+ ($(#[$attr:meta] $name:ident;)*) => {
+ $(
+ #[$attr]
+ #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+ pub struct $name(pub u64);
+
+ impl $name {
+ fn decode(value: Value) -> Result<Self> {
+ decode_id(value).map($name)
+ }
+ }
+
+ impl fmt::Display for $name {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+ }
+
+ impl From<u64> for $name {
+ fn from(id_as_u64: u64) -> $name {
+ $name(id_as_u64)
+ }
+ }
+ )*
+ }
+}
+
+id! {
+ /// An identifier for a Channel
+ ChannelId;
+ /// An identifier for an Emoji
+ EmojiId;
+ /// An identifier for a Guild
+ GuildId;
+ /// An identifier for an Integration
+ IntegrationId;
+ /// An identifier for a Message
+ MessageId;
+ /// An identifier for a Role
+ RoleId;
+ /// An identifier for a User
+ UserId;
+}
+
+/// A container for any channel.
+#[derive(Debug, Clone)]
+pub enum Channel {
+ /// A group. A group comprises of only one channel.
+ Group(Group),
+ /// A private channel to another [`User`]. No other users may access the
+ /// channel. For multi-user "private channels", use a group.
+ Private(PrivateChannel),
+ /// A [text] or [voice] channel within a [`Guild`].
+ ///
+ /// [`Guild`]: struct.Guild.html
+ /// [text]: enum.ChannelType.html#Text.v
+ /// [voice]: enum.ChannelType.html#Voice.v
+ Public(PublicChannel),
+}
+
+/// A container for guilds.
+///
+/// This is used to differentiate whether a guild itself can be used or whether
+/// a guild needs to be retrieved from the state.
+pub enum GuildContainer {
+ /// A guild which can have its contents directly searched.
+ Guild(Guild),
+ /// A guild's id, which can be used to search the state for a guild.
+ Id(GuildId),
+}
+
+/// The type of edit being made to a Channel's permissions.
+///
+/// This is for use with methods such as `Context::create_permission`.
+///
+/// [`Context::create_permission`]: ../client/
+#[derive(Copy, Clone, Eq, PartialEq, Debug)]
+pub enum PermissionOverwriteType {
+ /// A member which is having its permission overwrites edited.
+ Member(UserId),
+ /// A role which is having its permission overwrites edited.
+ Role(RoleId),
+}
+
+/// A guild which may or may not currently be available.
+#[derive(Debug, Clone)]
+pub enum PossibleGuild<T> {
+ /// An indicator that a guild is currently unavailable for at least one of
+ /// a variety of reasons.
+ Offline(GuildId),
+ /// An indicator that a guild is currently available.
+ Online(T),
+}
diff --git a/src/model/permissions.rs b/src/model/permissions.rs
new file mode 100644
index 0000000..7eccc5c
--- /dev/null
+++ b/src/model/permissions.rs
@@ -0,0 +1,446 @@
+//! A set of permissions for a role or user. These can be assigned directly
+//! to a role or as a channel's permission overrides.
+//!
+//! For convenience, methods for each permission are available, which can be
+//! used to test if the set of permissions contains a single permission.
+//! This can simplify code and reduce a potential import.
+//!
+//! Permissions follow a heirarchy:
+//!
+//! - An account can grant roles to users that are of a lower position than
+//! its highest role;
+//! - An account can edit roles lesser than its highest role, but can only
+//! grant permissions they have;
+//! - An account can move only roles lesser than its highest role;
+//! - An account can only kick/ban accounts with a lesser role than its top
+//! role.
+//!
+//! **Note**: The following permissions require the owner account (e.g. the
+//! owner of a bot) to use two-factor authentication in the case that a guild
+//! has guild-wide 2FA enabled:
+//!
+//! - [Administrator]
+//! - [Ban Members]
+//! - [Kick Members]
+//! - [Manage Channels]
+//! - [Manage Guild]
+//! - [Manage Messages]
+//! - [Manage Roles]
+//!
+//! [Administrator]: constant.ADMINISTRATOR.html
+//! [Ban Members]: constant.BAN_MEMBERS.html
+//! [Kick Members]: constant.KICK_MEMBERS.html
+//! [Manage Channels]: constant.MANAGE_CHANNELS.html
+//! [Manage Guild]: constant.MANAGE_GUILD.html
+//! [Manage Messages]: constant.MANAGE_MESSAGES.html
+//! [Manage Roles]: constant.MANAGE_ROLES.html
+
+use ::prelude::*;
+
+/// Returns a set of permissions with the original @everyone permissions set
+/// to true.
+///
+/// This includes the following permissions:
+///
+/// - [Attach Files]
+/// - [Change Nickname]
+/// - [Connect]
+/// - [Create Invite]
+/// - [Embed Links]
+/// - [Mention Everyone]
+/// - [Read Message History]
+/// - [Read Messages]
+/// - [Send Messages]
+/// - [Send TTS Messages]
+/// - [Speak]
+/// - [Use External Emojis]
+/// - [Use VAD]
+///
+/// **Note**: The [Send TTS Messages] permission is set to `true`. Consider
+/// setting this to `false`, via:
+///
+/// ```rust,ignore
+/// use serenity::models::permissions;
+///
+/// permissions::general().toggle(permissions::SEND_TTS_MESSAGES);
+/// ```
+///
+/// [Attach Files]: constant.ATTACH_FILES.html
+/// [Change Nickname]: constant.CHANGE_NICKNAME.html
+/// [Connect]: constant.CONNECT.html
+/// [Create Invite]: constant.CREATE_INVITE.html
+/// [Embed Links]: constant.EMBED_LINKS.html
+/// [Mention Everyone]: constant.MENTION_EVERYONE.html
+/// [Read Message History]: constant.READ_MESSAGE_HISTORY.html
+/// [Read Messages]: constant.READ_MESSAGES.html
+/// [Send Messages]: constant.SEND_MESSAGES.html
+/// [Send TTS Messages]: constant.SEND_TTS_MESSAGES.html
+/// [Speak]: constant.SPEAK.html
+/// [Use External Emojis]: constant.USE_EXTERNAL_EMOJIS.html
+/// [Use VAD]: constant.USE_VAD.html
+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
+}
+
+/// Returns a set of text-only permissions with the original `@everyone`
+/// permissions set to true.
+///
+/// This includes the text permissions given via [`general`]:
+///
+/// - [Attach Files]
+/// - [Change Nickname]
+/// - [Create Invite]
+/// - [Embed Links]
+/// - [Mention Everyone]
+/// - [Read Message History]
+/// - [Read Messages]
+/// - [Send Messages]
+/// - [Send TTS Messages]
+/// - [Use External Emojis]
+///
+/// [`general`]: fn.general.html
+/// [Attach Files]: constant.ATTACH_FILES.html
+/// [Change Nickname]: constant.CHANGE_NICKNAME.html
+/// [Create Invite]: constant.CREATE_INVITE.html
+/// [Embed Links]: constant.EMBED_LINKS.html
+/// [Mention Everyone]: constant.MENTION_EVERYONE.html
+/// [Read Message History]: constant.READ_MESSAGE_HISTORY.html
+/// [Read Messages]: constant.READ_MESSAGES.html
+/// [Send Messages]: constant.SEND_MESSAGES.html
+/// [Send TTS Messages]: constant.SEND_TTS_MESSAGES.html
+/// [Use External Emojis]: constant.USE_EXTERNAL_EMOJIS.html
+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
+}
+
+/// Returns a set of voice-only permissions with the original `@everyone`
+/// permissions set to true.
+///
+/// This includes the voice permissions given via [`general`]:
+///
+/// - [Connect]
+/// - [Speak]
+/// - [Use VAD]
+///
+/// [`general`]: fn.general.html
+/// [Connect]: constant.CONNECT.html
+/// [Speak]: constant.SPEAK.html
+/// [Use VAD]: constant.USE_VAD.html
+pub fn voice() -> Permissions {
+ use self::*;
+
+ CONNECT | SPEAK | USE_VAD
+}
+
+bitflags! {
+ pub flags Permissions: u64 {
+ /// Allows for the creation of [`RichInvite`]s.
+ ///
+ /// [`RichInvite`]: ../struct.RichInvite.html
+ const CREATE_INVITE = 1 << 0,
+ /// Allows for the kicking of guild [member]s.
+ ///
+ /// [member]: ../struct.Member.html
+ const KICK_MEMBERS = 1 << 1,
+ /// Allows the banning of guild [member]s.
+ ///
+ /// [member]: ../struct.Member.html
+ const BAN_MEMBERS = 1 << 2,
+ /// Allows all permissions, bypassing channel [permission overwrite]s.
+ ///
+ /// [permission overwrite]: ../struct.PermissionOverwrite.html
+ const ADMINISTRATOR = 1 << 3,
+ /// Allows management and editing of guild [channel]s.
+ ///
+ /// [channel]: ../struct.PublicChannel.html
+ const MANAGE_CHANNELS = 1 << 4,
+ /// Allows management and editing of the [guild].
+ ///
+ /// [guild]: ../struct.LiveGuild.html
+ const MANAGE_GUILD = 1 << 5,
+ /// 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,
+ /// Allows sending messages in a guild channel.
+ const SEND_MESSAGES = 1 << 11,
+ /// Allows the sending of text-to-speech messages in a channel.
+ const SEND_TTS_MESSAGES = 1 << 12,
+ /// Allows the deleting of other messages in a guild channel.
+ ///
+ /// **Note**: This does not allow the editing of other messages.
+ const MANAGE_MESSAGES = 1 << 13,
+ /// Allows links from this user - or users of this role - to be
+ /// embedded, with potential data such as a thumbnail, description, and
+ /// page name.
+ const EMBED_LINKS = 1 << 14,
+ /// Allows uploading of files.
+ const ATTACH_FILES = 1 << 15,
+ /// Allows the reading of a channel's message history.
+ const READ_MESSAGE_HISTORY = 1 << 16,
+ /// Allows the usage of the `@everyone` mention, which will notify all
+ /// users in a channel. The `@here` mention will also be available, and
+ /// can be used to mention all non-offline users.
+ ///
+ /// **Note**: You probably want this to be disabled for most roles and
+ /// users.
+ const MENTION_EVERYONE = 1 << 17,
+ /// Allows the usage of custom emojis from other guilds.
+ ///
+ /// This does not dictate whether custom emojis in this guild can be
+ /// used in other guilds.
+ const USE_EXTERNAL_EMOJIS = 1 << 18,
+ /// Allows the joining of a voice channel.
+ const CONNECT = 1 << 20,
+ /// Allows the user to speak in a voice channel.
+ const SPEAK = 1 << 21,
+ /// Allows the muting of members in a voice channel.
+ const MUTE_MEMBERS = 1 << 22,
+ /// Allows the deafening of members in a voice channel.
+ const DEAFEN_MEMBERS = 1 << 23,
+ /// Allows the moving of members from one voice channel to another.
+ const MOVE_MEMBERS = 1 << 24,
+ /// Allows the usage of voice-activity-detection in a [voice] channel.
+ ///
+ /// If this is disabled, then [`Member`]s must use push-to-talk.
+ ///
+ /// [`Member`]: ../struct.Member.html
+ /// [voice]: ../enum.ChannelType.html#Voice.v
+ const USE_VAD = 1 << 25,
+ /// Allows members to change their own nickname in the guild.
+ const CHANGE_NICKNAME = 1 << 26,
+ /// Allows members to change other members' nicknames.
+ const MANAGE_NICKNAMES = 1 << 27,
+ /// Allows management and editing of roles below their own.
+ const MANAGE_ROLES = 1 << 28,
+ /// Allows management of webhooks.
+ const MANAGE_WEBHOOKS = 1 << 29,
+ /// Allows management of emojis created without the use of an
+ /// [`Integration`].
+ ///
+ /// [`Integration`]: ../struct.Integration.html
+ const MANAGE_EMOJIS = 1 << 30,
+ }
+}
+
+impl Permissions {
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<Permissions> {
+ Ok(Self::from_bits_truncate(value.as_u64().unwrap()))
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Administrator] permission.
+ ///
+ /// [Administrator]: constant.ADMINISTRATOR.html
+ pub fn administrator(&self) -> bool {
+ self.contains(self::ADMINISTRATOR)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Attach Files] permission.
+ ///
+ /// [Attach Files]: constant.ATTACH_FILES.html
+ pub fn attach_files(&self) -> bool {
+ self.contains(self::ATTACH_FILES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Ban Members] permission.
+ ///
+ /// [Ban Members]: constant.BAN_MEMBERS.html
+ pub fn ban_members(&self) -> bool {
+ self.contains(self::BAN_MEMBERS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Change Nickname] permission.
+ ///
+ /// [Change Nickname]: constant.CHANGE_NICKNAME.html
+ pub fn change_nickname(&self) -> bool {
+ self.contains(self::CHANGE_NICKNAME)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Connect] permission.
+ ///
+ /// [Connect]: constant.CONNECT.html
+ pub fn connect(&self) -> bool {
+ self.contains(self::CONNECT)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Create Invite] permission.
+ ///
+ /// [Create Invite]: constant.CREATE_INVITE.html
+ pub fn create_invite(&self) -> bool {
+ self.contains(self::CREATE_INVITE)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Deafen Members] permission.
+ ///
+ /// [Deafen Members]: constant.DEAFEN_MEMBERS.html
+ pub fn deafen_members(&self) -> bool {
+ self.contains(self::DEAFEN_MEMBERS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Embed Links] permission.
+ ///
+ /// [Embed Links]: constant.EMBED_LINKS.html
+ pub fn embed_links(&self) -> bool {
+ self.contains(self::EMBED_LINKS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Use External Emojis] permission.
+ ///
+ /// [Use External Emojis]: constant.USE_EXTERNAL_EMOJIS.html
+ pub fn external_emojis(&self) -> bool {
+ self.contains(self::USE_EXTERNAL_EMOJIS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Kick Members] permission.
+ ///
+ /// [Kick Members]: constant.KICK_MEMBERS.html
+ pub fn kick_members(&self) -> bool {
+ self.contains(self::KICK_MEMBERS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Channels] permission.
+ ///
+ /// [Manage Channels]: constant.MANAGE_CHANNELS.html
+ pub fn manage_channels(&self) -> bool {
+ self.contains(self::MANAGE_CHANNELS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Emojis] permission.
+ ///
+ /// [Manage Emojis]: constant.MANAGE_EMOJIS.html
+ pub fn manage_emojis(&self) -> bool {
+ self.contains(self::MANAGE_EMOJIS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Guild] permission.
+ ///
+ /// [Manage Guild]: constant.MANAGE_GUILD.html
+ pub fn manage_guild(&self) -> bool {
+ self.contains(self::MANAGE_GUILD)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Messages] permission.
+ ///
+ /// [Manage Messages]: constant.MANAGE_MESSAGES.html
+ pub fn manage_messages(&self) -> bool {
+ self.contains(self::MANAGE_MESSAGES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Nicknames] permission.
+ ///
+ /// [Manage Nicknames]: constant.MANAGE_NICKNAMES.html
+ pub fn manage_nicknames(&self) -> bool {
+ self.contains(self::MANAGE_NICKNAMES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Roles] permission.
+ ///
+ /// [Manage Roles]: constant.MANAGE_ROLES.html
+ pub fn manage_roles(&self) -> bool {
+ self.contains(self::MANAGE_ROLES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Manage Webhooks] permission.
+ ///
+ /// [Manage Webhooks]: constant.MANAGE_WEBHOOKS.html
+ pub fn manage_webhooks(&self) -> bool {
+ self.contains(self::MANAGE_WEBHOOKS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Mention Everyone] permission.
+ ///
+ /// [Mention Everyone]: constant.MENTION_EVERYONE.html
+ pub fn mention_everyone(&self) -> bool {
+ self.contains(self::MENTION_EVERYONE)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Move Members] permission.
+ ///
+ /// [Move Members]: constant.MOVE_MEMBERS.html
+ pub fn move_members(&self) -> bool {
+ self.contains(self::MOVE_MEMBERS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Mute Members] permission.
+ ///
+ /// [Mute Members]: constant.MUTE_MEMBERS.html
+ pub fn mute_members(&self) -> bool {
+ self.contains(self::MUTE_MEMBERS)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Read Message History] permission.
+ ///
+ /// [Read Message History]: constant.READ_MESSAGE_HISTORY.html
+ pub fn read_message_history(&self) -> bool {
+ self.contains(self::READ_MESSAGE_HISTORY)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Read Messages] permission.
+ ///
+ /// [Read Messages]: constant.READ_MESSAGES.html
+ pub fn read_messages(&self) -> bool {
+ self.contains(self::READ_MESSAGES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Send Messages] permission.
+ ///
+ /// [Send Messages]: constant.SEND_MESSAGES.html
+ pub fn send_messages(&self) -> bool {
+ self.contains(self::SEND_MESSAGES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Send TTS Messages] permission.
+ ///
+ /// [Send TTS Messages]: constant.SEND_TTS_MESSAGES.html
+ pub fn send_tts_messages(&self) -> bool {
+ self.contains(self::SEND_TTS_MESSAGES)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Speak] permission.
+ ///
+ /// [Speak]: constant.SPEAK.html
+ pub fn speak(&self) -> bool {
+ self.contains(self::SPEAK)
+ }
+
+ /// Shorthand for checking that the set of permissions contains the
+ /// [Use VAD] permission.
+ ///
+ /// [Use VAD]: constant.USE_VAD.html
+ pub fn use_vad(&self) -> bool {
+ self.contains(self::USE_VAD)
+ }
+}
diff --git a/src/model/user.rs b/src/model/user.rs
new file mode 100644
index 0000000..cb235ab
--- /dev/null
+++ b/src/model/user.rs
@@ -0,0 +1,146 @@
+use serde_json::builder::ObjectBuilder;
+use std::fmt;
+use super::utils::{into_map, into_string, remove, warn_field};
+use super::{
+ FriendSourceFlags,
+ GuildContainer,
+ GuildId,
+ Mention,
+ Message,
+ RoleId,
+ UserSettings,
+ User
+};
+use ::client::{STATE, http};
+use ::prelude::*;
+use ::utils::decode_array;
+
+impl User {
+ /// 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))
+ }
+
+ /// This is an alias of [direct_message].
+ ///
+ /// [direct_message]: #method.direct_message
+ pub fn dm(&self, content: &str) -> Result<Message> {
+ self.direct_message(content)
+ }
+
+ /// Send a direct message to a user. This will create or retrieve the
+ /// PrivateChannel over REST if one is not already in the State, and then
+ /// send a message to it.
+ pub fn direct_message(&self, content: &str)
+ -> Result<Message> {
+ let private_channel_id = {
+ let finding = STATE.lock()
+ .unwrap()
+ .private_channels
+ .values()
+ .find(|ch| ch.recipient.id == self.id)
+ .map(|ch| ch.id);
+
+ if let Some(finding) = finding {
+ finding
+ } else {
+ let map = ObjectBuilder::new()
+ .insert("recipient_id", self.id.0)
+ .build();
+
+ try!(http::create_private_channel(map)).id
+ }
+ };
+
+ let map = ObjectBuilder::new()
+ .insert("content", content)
+ .insert("nonce", "")
+ .insert("tts", false)
+ .build();
+
+ http::send_message(private_channel_id.0, map)
+ }
+
+ /// Check if a user has a [`Role`]. This will retrieve the
+ /// [`Guild`] from the [`State`] if
+ /// it is available, and then check if that guild has the given [`Role`].
+ ///
+ /// If the [`Guild`] is not present, then the guild will be retrieved from
+ /// the API and the state will be updated with it.
+ ///
+ /// If there are issues with requesting the API, then `false` will be
+ /// returned.
+ ///
+ /// Three forms of data may be passed in to the guild parameter: either a
+ /// [`Guild`] itself, a [`GuildId`], or a `u64`.
+ ///
+ /// # Examples
+ ///
+ /// Check if a guild has a [`Role`] by Id:
+ ///
+ /// ```rust,ignore
+ /// // Assumes a 'guild' and `role_id` have already been defined
+ /// context.message.author.has_role(guild, role_id);
+ /// ```
+ ///
+ /// [`Guild`]: struct.Guild.html
+ /// [`GuildId`]: struct.GuildId.html
+ /// [`Role`]: struct.Role.html
+ /// [`State`]: ../ext/state/struct.State.html
+ pub fn has_role<G, R>(&self, guild: G, role: R) -> bool
+ where G: Into<GuildContainer>, R: Into<RoleId> {
+ let role_id = role.into();
+
+ match guild.into() {
+ GuildContainer::Guild(guild) => {
+ guild.find_role(role_id).is_some()
+ },
+ GuildContainer::Id(guild_id) => {
+ let state = STATE.lock().unwrap();
+
+ state.find_role(guild_id, role_id).is_some()
+ },
+ }
+ }
+
+ /// Return a [`Mention`] which will ping this user.
+ ///
+ /// [`Mention`]: struct.Mention.html
+ pub fn mention(&self) -> Mention {
+ self.id.mention()
+ }
+}
+
+impl fmt::Display for User {
+ /// Formats a string which will mention the user.
+ // This is in the format of: `<@USER_ID>`
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.id.mention(), f)
+ }
+}
+
+impl UserSettings {
+ #[doc(hidden)]
+ pub fn decode(value: Value) -> Result<Option<UserSettings>> {
+ let mut map = try!(into_map(value));
+
+ if map.is_empty() {
+ return Ok(None);
+ }
+
+ missing!(map, UserSettings {
+ convert_emoticons: req!(try!(remove(&mut map, "convert_emoticons")).as_bool()),
+ enable_tts_command: req!(try!(remove(&mut map, "enable_tts_command")).as_bool()),
+ friend_source_flags: try!(remove(&mut map, "friend_source_flags").and_then(FriendSourceFlags::decode)),
+ inline_attachment_media: req!(try!(remove(&mut map, "inline_attachment_media")).as_bool()),
+ inline_embed_media: req!(try!(remove(&mut map, "inline_embed_media")).as_bool()),
+ locale: try!(remove(&mut map, "locale").and_then(into_string)),
+ message_display_compact: req!(try!(remove(&mut map, "message_display_compact")).as_bool()),
+ render_embeds: req!(try!(remove(&mut map, "render_embeds")).as_bool()),
+ restricted_guilds: try!(remove(&mut map, "restricted_guilds").and_then(|v| decode_array(v, GuildId::decode))),
+ show_current_game: req!(try!(remove(&mut map, "show_current_game")).as_bool()),
+ theme: try!(remove(&mut map, "theme").and_then(into_string)),
+ }).map(Some)
+ }
+}
diff --git a/src/model/utils.rs b/src/model/utils.rs
new file mode 100644
index 0000000..4ad97bf
--- /dev/null
+++ b/src/model/utils.rs
@@ -0,0 +1,313 @@
+use std::collections::{BTreeMap, HashMap};
+use super::permissions::{self, Permissions};
+use super::{
+ Channel,
+ ChannelId,
+ Emoji,
+ EmojiId,
+ Member,
+ Presence,
+ PublicChannel,
+ ReadState,
+ Relationship,
+ Role,
+ RoleId,
+ User,
+ UserId,
+ VoiceState,
+};
+use ::client::STATE;
+use ::prelude::*;
+use ::utils::{decode_array, into_array};
+
+#[macro_escape]
+macro_rules! missing {
+ (@ $name:expr, $json:ident, $value:expr) => {
+ (Ok($value), warn_field($name, $json)).0
+ };
+ ($json:ident, $ty:ident $(::$ext:ident)* ( $($value:expr),*$(,)* ) ) => {
+ (Ok($ty$(::$ext)* ( $($value),* )), warn_field(stringify!($ty$(::$ext)*), $json)).0
+ };
+ ($json:ident, $ty:ident $(::$ext:ident)* { $($name:ident: $value:expr),*$(,)* } ) => {
+ (Ok($ty$(::$ext)* { $($name: $value),* }), warn_field(stringify!($ty$(::$ext)*), $json)).0
+ };
+}
+
+#[macro_escape]
+macro_rules! req {
+ ($opt:expr) => {
+ try!($opt.ok_or(Error::Decode(concat!("Type mismatch in model:", line!(), ": ", stringify!($opt)), Value::Null)))
+ }
+}
+
+pub fn decode_emojis(value: Value) -> Result<HashMap<EmojiId, Emoji>> {
+ let mut emojis = HashMap::new();
+
+ for emoji in try!(decode_array(value, Emoji::decode)) {
+ emojis.insert(emoji.id, emoji);
+ }
+
+ Ok(emojis)
+}
+
+pub fn decode_experiments(value: Value) -> Result<Vec<Vec<u64>>> {
+ let array = match value {
+ Value::Array(v) => v,
+ value => return Err(Error::Decode("Expected experiment array", value)),
+ };
+
+ let mut experiments: Vec<Vec<u64>> = vec![];
+
+ for arr in array {
+ let arr = match arr {
+ Value::Array(v) => v,
+ value => return Err(Error::Decode("Expected experiment's array", value)),
+ };
+
+ let mut items: Vec<u64> = vec![];
+
+ for item in arr {
+ items.push(match item {
+ Value::I64(v) => v as u64,
+ Value::U64(v) => v,
+ value => return Err(Error::Decode("Expected experiment u64", value)),
+ });
+ }
+
+ experiments.push(items);
+ }
+
+ Ok(experiments)
+}
+
+pub fn decode_id(value: Value) -> Result<u64> {
+ match value {
+ Value::U64(num) => Ok(num),
+ Value::I64(num) => Ok(num as u64),
+ Value::String(text) => match text.parse::<u64>() {
+ Ok(num) => Ok(num),
+ Err(_) => Err(Error::Decode("Expected numeric ID",
+ Value::String(text)))
+ },
+ value => Err(Error::Decode("Expected numeric ID", value))
+ }
+}
+
+pub fn decode_members(value: Value) -> Result<HashMap<UserId, Member>> {
+ let mut members = HashMap::new();
+
+ for member in try!(decode_array(value, Member::decode)) {
+ members.insert(member.user.id, member);
+ }
+
+ Ok(members)
+}
+
+// Clippy's lint is incorrect here and will result in invalid code.
+#[allow(or_fun_call)]
+pub fn decode_notes(value: Value) -> Result<HashMap<UserId, String>> {
+ let mut notes = HashMap::new();
+
+ for (key, value) in into_map(value).unwrap_or(BTreeMap::default()) {
+ let id = UserId(try!(key.parse::<u64>()
+ .map_err(|_| Error::Decode("Invalid user id in notes",
+ Value::String(key)))));
+
+ notes.insert(id, try!(into_string(value)));
+ }
+
+ Ok(notes)
+}
+
+pub fn decode_presences(value: Value) -> Result<HashMap<UserId, Presence>> {
+ let mut presences = HashMap::new();
+
+ for presence in try!(decode_array(value, Presence::decode)) {
+ presences.insert(presence.user_id, presence);
+ }
+
+ Ok(presences)
+}
+
+pub fn decode_private_channels(value: Value)
+ -> Result<HashMap<ChannelId, Channel>> {
+ let mut private_channels = HashMap::new();
+
+ for private_channel in try!(decode_array(value, Channel::decode)) {
+ let id = match private_channel {
+ Channel::Group(ref group) => group.channel_id,
+ Channel::Private(ref channel) => channel.id,
+ Channel::Public(_) => unreachable!("Public private channel decode"),
+ };
+
+ private_channels.insert(id, private_channel);
+ }
+
+ Ok(private_channels)
+}
+
+pub fn decode_public_channels(value: Value)
+ -> Result<HashMap<ChannelId, PublicChannel>> {
+ let mut public_channels = HashMap::new();
+
+ for public_channel in try!(decode_array(value, PublicChannel::decode)) {
+ public_channels.insert(public_channel.id, public_channel);
+ }
+
+ Ok(public_channels)
+}
+
+pub fn decode_read_states(value: Value)
+ -> Result<HashMap<ChannelId, ReadState>> {
+ let mut read_states = HashMap::new();
+
+ for read_state in try!(decode_array(value, ReadState::decode)) {
+ read_states.insert(read_state.id, read_state);
+ }
+
+ Ok(read_states)
+}
+
+pub fn decode_relationships(value: Value)
+ -> Result<HashMap<UserId, Relationship>> {
+ let mut relationships = HashMap::new();
+
+ for relationship in try!(decode_array(value, Relationship::decode)) {
+ relationships.insert(relationship.id, relationship);
+ }
+
+ Ok(relationships)
+}
+
+pub fn decode_roles(value: Value) -> Result<HashMap<RoleId, Role>> {
+ let mut roles = HashMap::new();
+
+ for role in try!(decode_array(value, Role::decode)) {
+ roles.insert(role.id, role);
+ }
+
+ Ok(roles)
+}
+
+pub fn decode_shards(value: Value) -> Result<[u8; 2]> {
+ let array = try!(into_array(value));
+
+ Ok([
+ req!(try!(array.get(0)
+ .ok_or(Error::Client(ClientError::InvalidShards))).as_u64()) as u8,
+ req!(try!(array.get(1)
+ .ok_or(Error::Client(ClientError::InvalidShards))).as_u64()) as u8,
+ ])
+}
+
+pub fn decode_users(value: Value) -> Result<HashMap<UserId, User>> {
+ let mut users = HashMap::new();
+
+ for user in try!(decode_array(value, User::decode)) {
+ users.insert(user.id, user);
+ }
+
+ Ok(users)
+}
+
+pub fn decode_voice_states(value: Value)
+ -> Result<HashMap<UserId, VoiceState>> {
+ let mut voice_states = HashMap::new();
+
+ for voice_state in try!(decode_array(value, VoiceState::decode)) {
+ voice_states.insert(voice_state.user_id, voice_state);
+ }
+
+ Ok(voice_states)
+}
+
+pub fn into_string(value: Value) -> Result<String> {
+ match value {
+ Value::String(s) => Ok(s),
+ Value::U64(v) => Ok(v.to_string()),
+ Value::I64(v) => Ok(v.to_string()),
+ value => Err(Error::Decode("Expected string", value)),
+ }
+}
+
+pub fn into_map(value: Value) -> Result<BTreeMap<String, Value>> {
+ match value {
+ Value::Object(m) => Ok(m),
+ value => Err(Error::Decode("Expected object", value)),
+ }
+}
+
+pub fn into_u64(value: Value) -> Result<u64> {
+ match value {
+ Value::I64(v) => Ok(v as u64),
+ Value::String(v) => match v.parse::<u64>() {
+ Ok(v) => Ok(v),
+ Err(_why) => Err(Error::Decode("Expected valid u64", Value::String(v))),
+ },
+ Value::U64(v) => Ok(v),
+ value => Err(Error::Decode("Expected u64", value)),
+ }
+}
+
+pub fn opt<T, F: FnOnce(Value) -> Result<T>>(map: &mut BTreeMap<String, Value>, key: &str, f: F) -> Result<Option<T>> {
+ match map.remove(key) {
+ None | Some(Value::Null) => Ok(None),
+ Some(val) => f(val).map(Some),
+ }
+}
+
+pub fn parse_discriminator(value: Value) -> Result<u16> {
+ match value {
+ Value::I64(v) => Ok(v as u16),
+ Value::U64(v) => Ok(v as u16),
+ Value::String(s) => match s.parse::<u16>() {
+ Ok(v) => Ok(v),
+ Err(_why) => Err(Error::Decode("Error parsing discriminator as u16",
+ Value::String(s))),
+ },
+ value => Err(Error::Decode("Expected string or u64", value)),
+ }
+}
+
+pub fn remove(map: &mut BTreeMap<String, Value>, key: &str) -> Result<Value> {
+ map.remove(key).ok_or_else(|| {
+ Error::Decode("Unexpected absent key", Value::String(key.into()))
+ })
+}
+
+#[doc(hidden)]
+pub fn user_has_perms(channel_id: ChannelId,
+ mut permissions: Permissions)
+ -> Result<bool> {
+ let state = STATE.lock().unwrap();
+ let current_user = &state.user;
+
+ let channel = match state.find_channel(channel_id) {
+ Some(channel) => channel,
+ None => return Err(Error::Client(ClientError::ItemMissing)),
+ };
+
+ let guild_id = match channel {
+ Channel::Group(_) | Channel::Private(_) => {
+ return Ok(permissions == permissions::MANAGE_MESSAGES);
+ },
+ Channel::Public(channel) => channel.guild_id,
+ };
+
+ let guild = match state.find_guild(guild_id) {
+ Some(guild) => guild,
+ None => return Err(Error::Client(ClientError::ItemMissing)),
+ };
+
+ let perms = guild.permissions_for(channel_id, current_user.id);
+
+ permissions.remove(perms);
+
+ Ok(permissions.is_empty())
+}
+
+pub fn warn_field(name: &str, map: BTreeMap<String, Value>) {
+ if !map.is_empty() {
+ debug!("Unhandled keys: {} has {:?}", name, Value::Object(map))
+ }
+}
diff --git a/src/model/voice.rs b/src/model/voice.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/model/voice.rs