diff options
| author | Austin Hellyer <[email protected]> | 2016-09-19 09:00:03 -0700 |
|---|---|---|
| committer | Austin Hellyer <[email protected]> | 2016-10-18 11:14:27 -0700 |
| commit | 8fc8c81403c3daa187ba96a7d488a64db21463bf (patch) | |
| tree | 81bc4890c28b08ce806f69084617066bce863c2d /src/model | |
| download | serenity-8fc8c81403c3daa187ba96a7d488a64db21463bf.tar.xz serenity-8fc8c81403c3daa187ba96a7d488a64db21463bf.zip | |
Initial commit
Diffstat (limited to 'src/model')
| -rw-r--r-- | src/model/channel.rs | 605 | ||||
| -rw-r--r-- | src/model/gateway.rs | 784 | ||||
| -rw-r--r-- | src/model/guild.rs | 933 | ||||
| -rw-r--r-- | src/model/id.rs | 176 | ||||
| -rw-r--r-- | src/model/invite.rs | 47 | ||||
| -rw-r--r-- | src/model/misc.rs | 95 | ||||
| -rw-r--r-- | src/model/mod.rs | 140 | ||||
| -rw-r--r-- | src/model/permissions.rs | 446 | ||||
| -rw-r--r-- | src/model/user.rs | 146 | ||||
| -rw-r--r-- | src/model/utils.rs | 313 | ||||
| -rw-r--r-- | src/model/voice.rs | 0 |
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 |