aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/builder.rs508
-rw-r--r--src/client/connection.rs571
-rw-r--r--src/client/context.rs822
-rw-r--r--src/client/dispatch.rs657
-rw-r--r--src/client/event_store.rs73
-rw-r--r--src/client/http.rs684
-rw-r--r--src/client/login_type.rs5
-rw-r--r--src/client/mod.rs973
-rw-r--r--src/client/ratelimiting.rs184
-rw-r--r--src/constants.rs9
-rw-r--r--src/error.rs106
-rw-r--r--src/ext/framework/command.rs7
-rw-r--r--src/ext/framework/configuration.rs48
-rw-r--r--src/ext/framework/mod.rs122
-rw-r--r--src/ext/mod.rs13
-rw-r--r--src/ext/state/mod.rs729
-rw-r--r--src/ext/voice/mod.rs0
-rw-r--r--src/lib.rs104
-rw-r--r--src/model/channel.rs605
-rw-r--r--src/model/gateway.rs784
-rw-r--r--src/model/guild.rs933
-rw-r--r--src/model/id.rs176
-rw-r--r--src/model/invite.rs47
-rw-r--r--src/model/misc.rs95
-rw-r--r--src/model/mod.rs140
-rw-r--r--src/model/permissions.rs446
-rw-r--r--src/model/user.rs146
-rw-r--r--src/model/utils.rs313
-rw-r--r--src/model/voice.rs0
-rw-r--r--src/prelude.rs9
-rw-r--r--src/utils/colour.rs126
-rw-r--r--src/utils/message_builder.rs97
-rw-r--r--src/utils/mod.rs140
33 files changed, 9672 insertions, 0 deletions
diff --git a/src/builder.rs b/src/builder.rs
new file mode 100644
index 0000000..bf228c5
--- /dev/null
+++ b/src/builder.rs
@@ -0,0 +1,508 @@
+//! A set of builders used to make using methods on certain structs simpler to
+//! use.
+//!
+//! These are used when not all parameters are required, all parameters are
+//! optional, and/or sane default values for required parameters can be applied
+//! by a builder.
+
+use serde_json::builder::ObjectBuilder;
+use serde_json::Value;
+use std::collections::BTreeMap;
+use std::default::Default;
+use ::model::{
+ ChannelId,
+ MessageId,
+ Permissions,
+ Region,
+ RoleId,
+ Role,
+ VerificationLevel,
+ permissions,
+};
+
+/// A builder to create a [`RichInvite`] for use via [`Context::create_invite`].
+///
+/// This is a structured and cleaner way of creating an invite, as all
+/// parameters are optional.
+///
+/// # Examples
+///
+/// Create an invite with a max age of 3600 seconds and 10 max uses:
+///
+/// ```rust,ignore
+/// // assuming a `client` has been bound
+/// client.on_message(|context, message| {
+/// if message.content == "!invite" {
+/// let invite = context.create_invite(message.channel_id, |i| i
+/// .max_age(3600)
+/// .max_uses(10));
+/// }
+/// });
+/// ```
+///
+/// [`Context::create_invite`]: ../client/struct.Context.html#method.create_invite
+/// [`RichInvite`]: ../model/struct.Invite.html
+pub struct CreateInvite(pub ObjectBuilder);
+
+impl CreateInvite {
+ /// The duration that the invite will be valid for.
+ ///
+ /// Set to `0` for an invite which does not expire after an amount of time.
+ ///
+ /// Defaults to `86400`, or 24 hours.
+ pub fn max_age(self, max_age: u64) -> Self {
+ CreateInvite(self.0.insert("max_age", max_age))
+ }
+
+ /// The number of uses that the invite will be valid for.
+ ///
+ /// Set to `0` for an invite which does not expire after a number of uses.
+ ///
+ /// Defaults to `0`.
+ pub fn max_uses(self, max_uses: u64) -> Self {
+ CreateInvite(self.0.insert("max_uses", max_uses))
+ }
+
+ /// Whether an invite grants a temporary membership.
+ ///
+ /// Defaults to `false`.
+ pub fn temporary(self, temporary: bool) -> Self {
+ CreateInvite(self.0.insert("temporary", temporary))
+ }
+
+ /// Whether or not to try to reuse a similar invite.
+ ///
+ /// Defaults to `false`.
+ pub fn unique(self, unique: bool) -> Self {
+ CreateInvite(self.0.insert("unique", unique))
+ }
+}
+
+impl Default for CreateInvite {
+ fn default() -> CreateInvite {
+ CreateInvite(ObjectBuilder::new().insert("validate", Value::Null))
+ }
+}
+
+/// A builer to create or edit a [`Role`] for use via a number of model and
+/// context methods.
+///
+/// These are:
+///
+/// - [`Context::create_role`]
+/// - [`Context::edit_role`]
+/// - [`LiveGuild::create_role`]
+/// - [`Role::edit`]
+///
+/// Defaults are provided for each parameter on role creation.
+///
+/// # Examples
+///
+/// Create a hoisted, mentionable role named "a test role":
+///
+/// ```rust,ignore
+/// // assuming you are in a `context` and a `guild_id` has been bound
+/// let role = context.create_role(guild_id, |r| r
+/// .hoist(true)
+/// .mentionable(true)
+/// .name("a test role"));
+/// ```
+///
+/// [`Context::create_role`]: ../client/struct.Context.html#method.create_role
+/// [`Context::edit_role`]: ../client/struct.Context.html#method.edit_role
+/// [`LiveGuild::create_role`]: ../model/struct.LiveGuild.html#method.create_role
+/// [`Role`]: ../model/struct.Role.html
+/// [`Role::edit`]: ../model/struct.Role.html#method.edit
+pub struct EditRole(pub ObjectBuilder);
+
+impl EditRole {
+ /// Creates a new builder with the values of the given [`Role`].
+ pub fn new(role: &Role) -> Self {
+ EditRole(ObjectBuilder::new()
+ .insert("color", role.colour)
+ .insert("hoist", role.hoist)
+ .insert("managed", role.managed)
+ .insert("mentionable", role.mentionable)
+ .insert("name", &role.name)
+ .insert("permissions", role.permissions.bits())
+ .insert("position", role.position))
+ }
+
+ /// Sets the colour of the role.
+ pub fn colour(self, colour: u64) -> Self {
+ EditRole(self.0.insert("color", colour))
+ }
+
+ /// Whether or not to hoist the role above lower-positioned role in the user
+ /// list.
+ pub fn hoist(self, hoist: bool) -> Self {
+ EditRole(self.0.insert("hoist", hoist))
+ }
+
+ /// Whether or not to make the role mentionable, notifying its users.
+ pub fn mentionable(self, mentionable: bool) -> Self {
+ EditRole(self.0.insert("mentionable", mentionable))
+ }
+
+ /// The name of the role to set.
+ pub fn name(self, name: &str) -> Self {
+ EditRole(self.0.insert("name", name))
+ }
+
+ /// The set of permissions to assign the role.
+ pub fn permissions(self, permissions: Permissions) -> Self {
+ EditRole(self.0.insert("permissions", permissions.bits()))
+ }
+
+ /// The position to assign the role in the role list. This correlates to the
+ /// role's position in the user list.
+ pub fn position(self, position: u8) -> Self {
+ EditRole(self.0.insert("position", position))
+ }
+}
+
+impl Default for EditRole {
+ /// Creates a builder with default parameters.
+ ///
+ /// The defaults are:
+ ///
+ /// - **color**: 10070709
+ /// - **hoist**: false
+ /// - **mentionable**: false
+ /// - **name**: new role
+ /// - **permissions**: the [general permissions set]
+ /// - **position**: 1
+ ///
+ /// [general permissions set]: ../model/permissions/fn.general.html
+ fn default() -> EditRole {
+ EditRole(ObjectBuilder::new()
+ .insert("color", 10070709)
+ .insert("hoist", false)
+ .insert("mentionable", false)
+ .insert("name", String::from("new role"))
+ .insert("permissions", permissions::general().bits())
+ .insert("position", 1))
+ }
+}
+
+/// A builder to edit a [`PublicChannel`] for use via one of a couple methods.
+///
+/// These methods are:
+///
+/// - [`Context::edit_channel`]
+/// - [`PublicChannel::edit`]
+///
+/// Defaults are not directly provided by the builder itself.
+///
+/// # Examples
+///
+/// Edit a channel, providing a new name and topic:
+///
+/// ```rust,ignore
+/// // assuming a channel has already been bound
+/// if let Err(why) = channel::edit(|c| c.name("new name").topic("a test topic")) {
+/// // properly handle the error
+/// }
+/// ```
+///
+/// [`Context::edit_channel`]: ../client/struct.Context.html#method.edit_channel
+/// [`PublicChannel`]: ../model/struct.PublicChannel.html
+/// [`PublicChannel::edit`]: ../model/struct.PublicChannel.html#method.edit
+pub struct EditChannel(pub ObjectBuilder);
+
+impl EditChannel {
+ /// The bitrate of the channel in bits.
+ ///
+ /// This is for [voice] channels only.
+ ///
+ /// [voice]: ../model/enum.ChannelType.html#Voice.v
+ pub fn bitrate(self, bitrate: u64) -> Self {
+ EditChannel(self.0.insert("bitrate", bitrate))
+ }
+
+ /// The name of the channel.
+ ///
+ /// Must be between 2 and 100 characters long.
+ pub fn name(self, name: &str) -> Self {
+ EditChannel(self.0.insert("name", name))
+ }
+
+ /// The position of the channel in the channel list.
+ pub fn position(self, position: u64) -> Self {
+ EditChannel(self.0.insert("position", position))
+ }
+
+ /// The topic of the channel. Can be empty.
+ ///
+ /// Must be between 0 and 1024 characters long.
+ ///
+ /// This is for [text] channels only.
+ ///
+ /// [text]: ../model/enum.ChannelType.html#Text.v
+ pub fn topic(self, topic: &str) -> Self {
+ EditChannel(self.0.insert("topic", topic))
+ }
+
+ /// The number of users that may be in the channel simultaneously.
+ ///
+ /// This is for [voice] channels only.
+ ///
+ /// [voice]: ../model/enum.ChannelType.html#Voice.v
+ pub fn user_limit(self, user_limit: u64) -> Self {
+ EditChannel(self.0.insert("user_limit", user_limit))
+ }
+}
+
+impl Default for EditChannel {
+ /// Creates a builder with no default parameters.
+ fn default() -> EditChannel {
+ EditChannel(ObjectBuilder::new())
+ }
+}
+
+pub struct EditGuild(pub ObjectBuilder);
+
+impl EditGuild {
+ pub fn afk_channel<C: Into<ChannelId>>(self, channel: Option<C>) -> Self {
+ EditGuild(match channel {
+ Some(channel) => self.0.insert("afk_channel_id", channel.into().0),
+ None => self.0.insert("afk-channel_id", Value::Null),
+ })
+ }
+
+ pub fn afk_timeout(self, timeout: u64) -> Self {
+ EditGuild(self.0.insert("afk_timeout", timeout))
+ }
+
+ pub fn icon(self, icon: Option<&str>) -> Self {
+ EditGuild(self.0
+ .insert("icon",
+ icon.map_or_else(|| Value::Null,
+ |x| Value::String(x.to_owned()))))
+ }
+
+ pub fn name(self, name: &str) -> Self {
+ EditGuild(self.0.insert("name", name))
+ }
+
+ pub fn region(self, region: Region) -> Self {
+ EditGuild(self.0.insert("region", region.name()))
+ }
+
+ pub fn splash(self, splash: Option<&str>) -> Self {
+ EditGuild(self.0
+ .insert("splash",
+ splash.map_or_else(|| Value::Null,
+ |x| Value::String(x.to_owned()))))
+ }
+
+ pub fn verification_level<V>(self, verification_level: V) -> Self
+ where V: Into<VerificationLevel> {
+ EditGuild(self.0.insert("verification_level",
+ verification_level.into().num()))
+ }
+}
+
+impl Default for EditGuild {
+ fn default() -> EditGuild {
+ EditGuild(ObjectBuilder::new())
+ }
+}
+
+pub struct EditMember(pub ObjectBuilder);
+
+impl EditMember {
+ pub fn deafen(self, deafen: bool) -> Self {
+ EditMember(self.0.insert("deaf", deafen))
+ }
+
+ pub fn mute(self, mute: bool) -> Self {
+ EditMember(self.0.insert("mute", mute))
+ }
+
+ pub fn nickname(self, nickname: &str) -> Self {
+ EditMember(self.0.insert("nick", nickname))
+ }
+
+ pub fn roles(self, roles: &[RoleId]) -> Self {
+ EditMember(self.0
+ .insert_array("roles",
+ |a| roles.iter().fold(a, |a, id| a.push(id.0))))
+ }
+
+ pub fn voice_channel<C: Into<ChannelId>>(self, channel_id: C) -> Self {
+ EditMember(self.0.insert("channel_id", channel_id.into().0))
+ }
+}
+
+impl Default for EditMember {
+ fn default() -> EditMember {
+ EditMember(ObjectBuilder::new())
+ }
+}
+
+pub struct EditProfile(pub ObjectBuilder);
+
+impl EditProfile {
+ /// Sets the avatar of the current user. `None` can be passed to remove an
+ /// avatar.
+ ///
+ /// A base64-encoded string is accepted as the avatar content.
+ ///
+ /// # Examples
+ ///
+ /// A utility method - [`utils::read_message`] - is provided to read an
+ /// image from a file and return its contents in base64-encoded form:
+ ///
+ /// ```rust,ignore
+ /// use serenity::utils;
+ ///
+ /// // assuming you are in a context
+ ///
+ /// let base64 = match utils::read_image("./my_image.jpg") {
+ /// Ok(base64) => base64,
+ /// Err(why) => {
+ /// println!("Error reading image: {:?}", why);
+ ///
+ /// return;
+ /// },
+ /// };
+ ///
+ /// let _ = context.edit_profile(|profile| {
+ /// profile.avatar(Some(base64))
+ /// });
+ /// ```
+ ///
+ /// [`utils::read_image`]: ../utils/fn.read_image.html
+ pub fn avatar(self, icon: Option<&str>) -> Self {
+ EditProfile(self.0
+ .insert("avatar",
+ icon.map_or_else(|| Value::Null,
+ |x| Value::String(x.to_owned()))))
+ }
+
+ /// Modifies the current user's email address.
+ ///
+ /// Note that when modifying the email address, the current password must
+ /// also be [provided].
+ ///
+ /// No validation is performed on this by the library.
+ ///
+ /// **Note**: This can only be used by user accounts.
+ ///
+ /// [provided]: #method.password
+ pub fn email(self, email: &str) -> Self {
+ EditProfile(self.0.insert("email", email))
+ }
+
+ /// Modifies the current user's password.
+ ///
+ /// Note that when modifying the password, the current password must also be
+ /// [provided].
+ ///
+ /// [provided]: #method.password
+ pub fn new_password(self, new_password: &str) -> Self {
+ EditProfile(self.0.insert("new_password", new_password))
+ }
+
+ /// Used for providing the current password as verification when
+ /// [modifying the password] or [modifying the associated email address].
+ ///
+ /// [modifying the password]: #method.new_password
+ /// [modifying the associated email address]: #method.email
+ pub fn password(self, password: &str) -> Self {
+ EditProfile(self.0.insert("password", password))
+ }
+
+ /// Modifies the current user's username.
+ ///
+ /// When modifying the username, if another user has the same _new_ username
+ /// and current discriminator, a new unique discriminator will be assigned.
+ /// If there are no available discriminators with the requested username,
+ /// an error will occur.
+ pub fn username(self, username: &str) -> Self {
+ EditProfile(self.0.insert("username", username))
+ }
+}
+
+impl Default for EditProfile {
+ fn default() -> EditProfile {
+ EditProfile(ObjectBuilder::new())
+ }
+}
+
+/// Builds a request for a request to the API to retrieve messages.
+///
+/// This can have 2 different sets of parameters. The first set is around where
+/// to get the messages:
+///
+/// - `after`
+/// - `around`
+/// - `before`
+/// - `most_recent`
+///
+/// These can not be mixed, and the first in the list alphabetically will be
+/// used. If one is not specified, `most_recent` will be used.
+///
+/// The fourth parameter is to specify the number of messages to retrieve. This
+/// does not _need_ to be called and defaults to a value of 50.
+///
+/// This should be used only for retrieving messages; see
+/// `client::Client::get_messages` for examples.
+pub struct GetMessages(pub BTreeMap<String, u64>);
+
+impl GetMessages {
+ /// Indicates to retrieve the messages after a specific message, given by
+ /// its Id.
+ pub fn after<M: Into<MessageId>>(mut self, message_id: M) -> Self {
+ self.0.insert("after".to_owned(), message_id.into().0);
+
+ self
+ }
+
+ /// Indicates to retrieve the messages _around_ a specific message in either
+ /// direction (before+after) the given message.
+ pub fn around<M: Into<MessageId>>(mut self, message_id: M) -> Self {
+ self.0.insert("around".to_owned(), message_id.into().0);
+
+ self
+ }
+
+ /// Indicates to retrieve the messages before a specific message, given by
+ /// its Id.
+ pub fn before<M: Into<MessageId>>(mut self, message_id: M) -> Self {
+ self.0.insert("before".to_owned(), message_id.into().0);
+
+ self
+ }
+
+ /// The maximum number of messages to retrieve for the query.
+ ///
+ /// If this is not specified, a default value of 50 is used.
+ ///
+ /// **Note**: This field is capped to 100 messages due to a Discord
+ /// limitation. If an amount larger than 100 is supplied, it will be
+ /// reduced.
+ pub fn limit(mut self, limit: u64) -> Self {
+ self.0.insert("limit".to_owned(), if limit > 100 {
+ 100
+ } else {
+ limit
+ });
+
+ self
+ }
+
+ /// This is a function that is here for completeness. You do not need to
+ /// call this - except to clear previous calls to `after`, `around`, and
+ /// `before` - as it is the default value.
+ pub fn most_recent(self) -> Self {
+ self
+ }
+}
+
+impl Default for GetMessages {
+ fn default() -> GetMessages {
+ GetMessages(BTreeMap::default())
+ }
+}
diff --git a/src/client/connection.rs b/src/client/connection.rs
new file mode 100644
index 0000000..f321cfc
--- /dev/null
+++ b/src/client/connection.rs
@@ -0,0 +1,571 @@
+use flate2::read::ZlibDecoder;
+use serde_json::builder::ObjectBuilder;
+use serde_json;
+use std::fmt::{self, Display};
+use std::net::Shutdown;
+use std::sync::mpsc::{
+ self,
+ Receiver as MpscReceiver,
+ Sender as MpscSender,
+ TryRecvError
+};
+use std::thread::{self, Builder as ThreadBuilder};
+use std::time::Duration as StdDuration;
+use std::{env, mem};
+use super::login_type::LoginType;
+use super::Client;
+use time::{self, Duration};
+use websocket::client::request::Url as RequestUrl;
+use websocket::client::{Client as WsClient, Sender, Receiver};
+use websocket::message::{Message as WsMessage, Type as WsType};
+use websocket::stream::WebSocketStream;
+use websocket::ws::receiver::Receiver as WsReceiver;
+use websocket::ws::sender::Sender as WsSender;
+use ::{constants};
+use ::model::*;
+use ::prelude::*;
+
+enum Status {
+ SendMessage(Value),
+ Sequence(u64),
+ ChangeInterval(u64),
+ ChangeSender(Sender<WebSocketStream>),
+}
+
+#[derive(Clone, Debug)]
+pub enum ConnectionError {
+ /// The connection closed
+ Closed(Option<u16>, String),
+ /// Expected a Hello during a handshake
+ ExpectedHello,
+ /// Expected a Ready or an InvalidateSession
+ InvalidHandshake,
+}
+
+impl Display for ConnectionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ ConnectionError::Closed(s, ref v) => {
+ f.write_str(&format!("Connection closed {:?}: {:?}", s, v))
+ },
+ ConnectionError::ExpectedHello => {
+ f.write_str("Expected Hello during handshake")
+ },
+ ConnectionError::InvalidHandshake => {
+ f.write_str("Expected Ready or InvalidateSession")
+ },
+ }
+ }
+}
+
+/// A connection is a handler for a websocket connection to Discord's gateway.
+/// The connection allows for sending and receiving messages over the websocket,
+/// such as setting the active game, reconnecting, syncing guilds, and more.
+///
+/// # Sharding
+///
+/// Sharding is a method to split portions of bots into separate processes. This
+/// is an enforced strategy by Discord once a bot reaches a certain number of
+/// guilds (2500). Once this number is reached, a bot must be sharded in a way
+/// that only 2500 guilds maximum may be allocated per shard.
+///
+/// The "recommended" number of guilds per shard is _around_ 1000. Sharding can
+/// be useful for splitting processes across separate servers. Often you may
+/// want some or all shards to be in the same process, allowing for a shared
+/// State. This is possible through this library.
+///
+/// See [Discord's documentation][docs] for more information.
+///
+/// If you are not using a bot account or do not require sharding - such as for
+/// a small bot - then use [`Client::start`].
+///
+/// There are a few methods of sharding available:
+///
+/// - [`Client::start_autosharded`]: retrieves the number of shards Discord
+/// recommends using from the API, and then automatically starts that number of
+/// shards.
+/// - [`Client::start_shard`]: starts a single shard for use in the instance,
+/// handled by the instance of the Client. Use this if you only want 1 shard
+/// handled by this instance.
+/// - [`Client::start_shards`]: starts all shards in this instance. This is best
+/// for when you want a completely shared State.
+/// - [`Client::start_shard_range`]: start a range of shards within this
+/// instance. This should be used when you, for example, want to split 10 shards
+/// across 3 instances.
+///
+/// **Note**: User accounts can not shard. Use [`Client::start`].
+///
+/// [`Client::start`]: struct.Client.html#method.start
+/// [`Client::start_auosharded`]: struct.Client.html#method.start_autosharded
+/// [`Client::start_shard`]: struct.Client.html#method.start_shard
+/// [`Client::start_shard_range`]: struct.Client.html#method.start_shard_range
+/// [`Client::start_shards`]: struct.Client.html#method.start_shards
+/// [docs]: https://discordapp.com/developers/docs/topics/gateway#sharding
+pub struct Connection {
+ keepalive_channel: MpscSender<Status>,
+ last_sequence: u64,
+ login_type: LoginType,
+ receiver: Receiver<WebSocketStream>,
+ session_id: Option<String>,
+ shard_info: Option<[u8; 2]>,
+ token: String,
+ ws_url: String,
+}
+
+impl Connection {
+ pub fn new(base_url: &str,
+ token: &str,
+ shard_info: Option<[u8; 2]>,
+ login_type: LoginType)
+ -> Result<(Connection, ReadyEvent)> {
+ let url = try!(build_gateway_url(base_url));
+
+ let response = try!(try!(WsClient::connect(url)).send());
+ try!(response.validate());
+
+ let (mut sender, mut receiver) = response.begin().split();
+
+ let identification = identify(token, shard_info);
+ try!(sender.send_json(&identification));
+
+ let heartbeat_interval = match try!(receiver.recv_json(GatewayEvent::decode)) {
+ GatewayEvent::Hello(interval) => interval,
+ other => {
+ debug!("Unexpected event during connection start: {:?}", other);
+
+ return Err(Error::Connection(ConnectionError::ExpectedHello));
+ },
+ };
+
+ let (tx, rx) = mpsc::channel();
+ try!(ThreadBuilder::new()
+ .name("serenity keepalive".into())
+ .spawn(move || keepalive(heartbeat_interval, sender, rx)));
+
+ // Parse READY
+ let event = try!(receiver.recv_json(GatewayEvent::decode));
+ let (ready, sequence) = try!(parse_ready(event,
+ &tx,
+ &mut receiver,
+ identification));
+
+ Ok((Connection {
+ keepalive_channel: tx,
+ last_sequence: sequence,
+ login_type: login_type,
+ receiver: receiver,
+ token: token.to_owned(),
+ session_id: Some(ready.ready.session_id.clone()),
+ shard_info: shard_info,
+ ws_url: base_url.to_owned(),
+ }, ready))
+ }
+
+ pub fn shard_info(&self) -> Option<[u8; 2]> {
+ self.shard_info
+ }
+
+ pub fn set_game(&self, game: Option<Game>) {
+ let msg = ObjectBuilder::new()
+ .insert("op", 3)
+ .insert_object("d", move |mut object| {
+ object = object.insert("idle_since", Value::Null);
+
+ match game {
+ Some(game) => {
+ object.insert_object("game", move |o|
+ o.insert("name", game.name))
+ },
+ None => object.insert("game", Value::Null),
+ }
+ })
+ .build();
+ let _ = self.keepalive_channel.send(Status::SendMessage(msg));
+ }
+
+ pub fn receive(&mut self) -> Result<Event> {
+ match self.receiver.recv_json(GatewayEvent::decode) {
+ Ok(GatewayEvent::Dispatch(sequence, event)) => {
+ self.last_sequence = sequence;
+
+ let _ = self.keepalive_channel.send(Status::Sequence(sequence));
+
+ if let Event::Resumed(ref ev) = event {
+ let _ = self.keepalive_channel.send(Status::ChangeInterval(ev.heartbeat_interval));
+ }
+
+ Ok(event)
+ },
+ Ok(GatewayEvent::Heartbeat(sequence)) => {
+ let map = ObjectBuilder::new()
+ .insert("d", sequence)
+ .insert("op", 1)
+ .build();
+ let _ = self.keepalive_channel.send(Status::SendMessage(map));
+
+ self.receive()
+ },
+ Ok(GatewayEvent::HeartbeatAck) => {
+ self.receive()
+ },
+ Ok(GatewayEvent::Hello(interval)) => {
+ let _ = self.keepalive_channel.send(Status::ChangeInterval(interval));
+
+ self.receive()
+ },
+ Ok(GatewayEvent::InvalidateSession) => {
+ self.session_id = None;
+
+ let status = Status::SendMessage(identify(&self.token,
+ self.shard_info));
+
+ let _ = self.keepalive_channel.send(status);
+
+ self.receive()
+ },
+ Ok(GatewayEvent::Reconnect) => {
+ self.reconnect()
+ },
+ Err(Error::Connection(ConnectionError::Closed(num, message))) => {
+ warn!("Closing with {:?}: {:?}", num, message);
+
+ // Attempt to resume if the following was not received:
+ //
+ // - 1000: Close.
+ //
+ // Otherwise, fallback to reconnecting.
+ if num != Some(1000) {
+ if let Some(session_id) = self.session_id.clone() {
+ match self.resume(session_id) {
+ Ok(event) => return Ok(event),
+ Err(why) => debug!("Err resuming: {:?}", why),
+ }
+ }
+ }
+
+ self.reconnect()
+ },
+ Err(Error::WebSocket(why)) => {
+ warn!("Websocket error: {:?}", why);
+ info!("Reconnecting");
+
+ // Attempt to resume if the following was not received:
+ //
+ // - InvalidateSession.
+ //
+ // Otherwise, fallback to reconnecting.
+ if let Some(session_id) = self.session_id.clone() {
+ match self.resume(session_id) {
+ Ok(event) => return Ok(event),
+ Err(why) => debug!("Err resuming: {:?}", why),
+ }
+ }
+
+ self.reconnect()
+ },
+ Err(error) => Err(error),
+ }
+ }
+
+ fn reconnect(&mut self) -> Result<Event> {
+ debug!("Reconnecting");
+
+ // Take a few attempts at reconnecting; otherwise fall back to
+ // re-instantiating the connection.
+ for _ in 0..3 {
+ let connection = Connection::new(&self.ws_url,
+ &self.token,
+ self.shard_info,
+ self.login_type);
+
+ if let Ok((connection, ready)) = connection {
+ try!(mem::replace(self, connection).shutdown());
+
+ self.session_id = Some(ready.ready.session_id.clone());
+
+ return Ok(Event::Ready(ready));
+ }
+
+ thread::sleep(StdDuration::from_secs(1));
+ }
+
+ // If all else fails: get a new endpoint.
+ //
+ // A bit of complexity here: instantiate a temporary instance of a
+ // Client. This client _does not_ replace the current client(s) that the
+ // user has. This client will then connect to gateway. This new
+ // connection will be used to replace _this_ connection.
+ let (connection, ready) = {
+ let mut client = Client::login_raw(&self.token.clone(),
+ self.login_type);
+
+ try!(client.boot_connection(self.shard_info))
+ };
+
+ // Replace this connection with a new one, and shutdown the now-old
+ // connection.
+ try!(mem::replace(self, connection).shutdown());
+
+ self.session_id = Some(ready.ready.session_id.clone());
+
+ Ok(Event::Ready(ready))
+ }
+
+ fn resume(&mut self, session_id: String) -> Result<Event> {
+ try!(self.receiver.get_mut().get_mut().shutdown(Shutdown::Both));
+ let url = try!(build_gateway_url(&self.ws_url));
+
+ let response = try!(try!(WsClient::connect(url)).send());
+ try!(response.validate());
+
+ let (mut sender, mut receiver) = response.begin().split();
+
+ try!(sender.send_json(&ObjectBuilder::new()
+ .insert_object("d", |o| o
+ .insert("session_id", session_id)
+ .insert("seq", self.last_sequence)
+ .insert("token", &self.token)
+ )
+ .insert("op", 6)
+ .build()));
+
+ let first_event;
+
+ loop {
+ match try!(receiver.recv_json(GatewayEvent::decode)) {
+ GatewayEvent::Dispatch(seq, event) => {
+ if let Event::Ready(ref event) = event {
+ self.session_id = Some(event.ready.session_id.clone());
+ }
+
+ self.last_sequence = seq;
+ first_event = event;
+
+ break;
+ },
+ GatewayEvent::InvalidateSession => {
+ try!(sender.send_json(&identify(&self.token, self.shard_info)));
+ }
+ other => {
+ debug!("Unexpected event: {:?}", other);
+
+ return Err(Error::Connection(ConnectionError::InvalidHandshake));
+ }
+ }
+ }
+
+ self.receiver = receiver;
+ let _ = self.keepalive_channel.send(Status::ChangeSender(sender));
+
+ Ok(first_event)
+ }
+
+ pub fn shutdown(mut self) -> Result<()> {
+ try!(self.receiver
+ .get_mut()
+ .get_mut()
+ .shutdown(Shutdown::Both));
+
+ Ok(())
+ }
+
+ pub fn sync_guilds(&self, guild_ids: &[GuildId]) {
+ let msg = ObjectBuilder::new()
+ .insert("op", 12)
+ .insert_array("d", |a| guild_ids.iter().fold(a, |a, s| a.push(s.0)))
+ .build();
+
+ let _ = self.keepalive_channel.send(Status::SendMessage(msg));
+ }
+
+ pub fn sync_calls(&self, channels: &[ChannelId]) {
+ for &channel in channels {
+ let msg = ObjectBuilder::new()
+ .insert("op", 13)
+ .insert_object("d", |obj| obj
+ .insert("channel_id", channel.0)
+ )
+ .build();
+
+ let _ = self.keepalive_channel.send(Status::SendMessage(msg));
+ }
+ }
+}
+
+trait ReceiverExt {
+ fn recv_json<F, T>(&mut self, decode: F) -> Result<T>
+ where F: FnOnce(Value) -> Result<T>;
+}
+
+trait SenderExt {
+ fn send_json(&mut self, value: &Value) -> Result<()>;
+}
+
+impl ReceiverExt for Receiver<WebSocketStream> {
+ fn recv_json<F, T>(&mut self, decode: F) -> Result<T> where F: FnOnce(Value) -> Result<T> {
+ let message: WsMessage = try!(self.recv_message());
+
+ if message.opcode == WsType::Close {
+ let representation = String::from_utf8_lossy(&message.payload)
+ .into_owned();
+
+ Err(Error::Connection(ConnectionError::Closed(message.cd_status_code,
+ representation)))
+ } else if message.opcode == WsType::Binary || message.opcode == WsType::Text {
+ let json: Value = if message.opcode == WsType::Binary {
+ try!(serde_json::from_reader(ZlibDecoder::new(&message.payload[..])))
+ } else {
+ try!(serde_json::from_reader(&message.payload[..]))
+ };
+
+ decode(json).map_err(|err| {
+ warn!("Error decoding: {}",
+ String::from_utf8_lossy(&message.payload));
+
+ err
+ })
+ } else {
+ let representation = String::from_utf8_lossy(&message.payload)
+ .into_owned();
+
+ Err(Error::Connection(ConnectionError::Closed(None,
+ representation)))
+ }
+ }
+}
+
+impl SenderExt for Sender<WebSocketStream> {
+ fn send_json(&mut self, value: &Value) -> Result<()> {
+ serde_json::to_string(value)
+ .map(WsMessage::text)
+ .map_err(Error::from)
+ .and_then(|m| self.send_message(&m).map_err(Error::from))
+ }
+}
+
+fn parse_ready(event: GatewayEvent,
+ tx: &MpscSender<Status>,
+ receiver: &mut Receiver<WebSocketStream>,
+ identification: Value)
+ -> Result<(ReadyEvent, u64)> {
+ match event {
+ GatewayEvent::Dispatch(seq, Event::Ready(event)) => {
+ Ok((event, seq))
+ },
+ GatewayEvent::InvalidateSession => {
+ debug!("Session invalidation");
+
+ let _ = tx.send(Status::SendMessage(identification));
+
+ match try!(receiver.recv_json(GatewayEvent::decode)) {
+ GatewayEvent::Dispatch(seq, Event::Ready(event)) => {
+ Ok((event, seq))
+ },
+ other => {
+ debug!("Unexpected event: {:?}", other);
+
+ Err(Error::Connection(ConnectionError::InvalidHandshake))
+ },
+ }
+ },
+ other => {
+ debug!("Unexpected event: {:?}", other);
+
+ Err(Error::Connection(ConnectionError::InvalidHandshake))
+ },
+ }
+}
+
+fn identify(token: &str, shard_info: Option<[u8; 2]>) -> serde_json::Value {
+ ObjectBuilder::new()
+ .insert("op", 2)
+ .insert_object("d", |mut object| {
+ object = identify_compression(object)
+ .insert("large_threshold", 250) // max value
+ .insert_object("properties", |object| object
+ .insert("$browser", "Feature-full and ergonomic discord rust library")
+ .insert("$device", "serenity")
+ .insert("$os", env::consts::OS)
+ .insert("$referrer", "")
+ .insert("$referring_domain", "")
+ )
+ .insert("token", token)
+ .insert("v", constants::GATEWAY_VERSION);
+
+ if let Some(shard_info) = shard_info {
+ object = object
+ .insert_array("shard", |array| array
+ .push(shard_info[0])
+ .push(shard_info[1]));
+ }
+
+ object
+ })
+ .build()
+}
+
+#[cfg(not(feature = "debug"))]
+fn identify_compression(object: ObjectBuilder) -> ObjectBuilder {
+ object.insert("compression", true)
+}
+
+#[cfg(feature = "debug")]
+fn identify_compression(object: ObjectBuilder) -> ObjectBuilder {
+ object.insert("compression", false)
+}
+
+fn build_gateway_url(base: &str) -> Result<::websocket::client::request::Url> {
+ RequestUrl::parse(&format!("{}?v={}", base, constants::GATEWAY_VERSION))
+ .map_err(|_| Error::Client(ClientError::Gateway))
+}
+
+fn keepalive(interval: u64,
+ mut sender: Sender<WebSocketStream>,
+ channel: MpscReceiver<Status>) {
+ let mut base_interval = Duration::milliseconds(interval as i64);
+ let mut next_tick = time::get_time() + base_interval;
+
+ let mut last_sequence = 0;
+
+ 'outer: loop {
+ thread::sleep(StdDuration::from_millis(100));
+
+ loop {
+ match channel.try_recv() {
+ Ok(Status::ChangeInterval(interval)) => {
+ base_interval = Duration::milliseconds(interval as i64);
+ },
+ Ok(Status::ChangeSender(new_sender)) => {
+ sender = new_sender;
+ },
+ Ok(Status::SendMessage(val)) => {
+ match sender.send_json(&val) {
+ Ok(()) => {},
+ Err(e) => warn!("Err sending message: {:?}", e),
+ }
+ },
+ Ok(Status::Sequence(seq)) => {
+ last_sequence = seq;
+ },
+ Err(TryRecvError::Empty) => break,
+ Err(TryRecvError::Disconnected) => break 'outer,
+ }
+ }
+
+ if time::get_time() >= next_tick {
+ next_tick = next_tick + base_interval;
+
+ let map = ObjectBuilder::new()
+ .insert("d", last_sequence)
+ .insert("op", 1)
+ .build();
+
+ match sender.send_json(&map) {
+ Ok(()) => {},
+ Err(e) => warn!("Error sending gateway keeaplive: {:?}", e)
+ }
+ }
+ }
+
+ let _ = sender.get_mut().shutdown(Shutdown::Both);
+}
diff --git a/src/client/context.rs b/src/client/context.rs
new file mode 100644
index 0000000..6f28a75
--- /dev/null
+++ b/src/client/context.rs
@@ -0,0 +1,822 @@
+use serde_json::builder::ObjectBuilder;
+use std::collections::HashMap;
+use std::io::Read;
+use std::sync::{Arc, Mutex};
+use super::connection::Connection;
+use super::{STATE, http};
+use super::login_type::LoginType;
+use ::builder::{
+ CreateInvite,
+ EditChannel,
+ EditGuild,
+ EditMember,
+ EditProfile,
+ EditRole,
+ GetMessages
+};
+use ::model::*;
+use ::prelude::*;
+use ::utils;
+
+#[derive(Clone)]
+pub struct Context {
+ channel_id: Option<ChannelId>,
+ pub connection: Arc<Mutex<Connection>>,
+ login_type: LoginType,
+}
+
+impl Context {
+ /// Create a new Context to be passed to an event handler.
+ #[doc(hidden)]
+ pub fn new(channel_id: Option<ChannelId>,
+ connection: Arc<Mutex<Connection>>,
+ login_type: LoginType) -> Context {
+ Context {
+ channel_id: channel_id,
+ connection: connection,
+ login_type: login_type,
+ }
+ }
+
+ pub fn accept_invite(&self, invite: &str) -> Result<Invite> {
+ let code = utils::parse_invite(invite);
+
+ http::accept_invite(code)
+ }
+
+ /// This is an alias of [ack_message](#method.ack_message).
+ pub fn ack<C, M>(&self, channel_id: C, message_id: M) -> Result<()>
+ where C: Into<ChannelId>, M: Into<MessageId> {
+ self.ack_message(channel_id.into(), message_id.into())
+ }
+
+ /// Mark a message as being read in a channel. This will mark up to the
+ /// given message as read. Any messages created after that message will not
+ /// be marked as read.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [ClientError::InvalidOperationAsBot](../enum.ClientError.html#InvalidOperationAsUser.v)
+ /// if this is a bot.
+ pub fn ack_message<C, M>(&self, channel_id: C, message_id: M) -> Result<()>
+ where C: Into<ChannelId>, M: Into<MessageId> {
+ if self.login_type == LoginType::User {
+ return Err(Error::Client(ClientError::InvalidOperationAsUser))
+ }
+
+ http::ack_message(channel_id.into().0, message_id.into().0)
+ }
+
+ /// This is an alias of [ban](#method.ban).
+ pub fn ban<G, U>(&self, guild_id: G, user_id: U, delete_message_days: u8)
+ -> Result<()> where G: Into<GuildId>, U: Into<UserId> {
+ self.ban_user(guild_id.into(), user_id.into(), delete_message_days)
+ }
+
+ /// Ban a user from a guild, removing their messages sent in the last X
+ /// number of days.
+ ///
+ /// 0 days is equivilant to not removing any messages. Up to 7 days' worth
+ /// of messages may be deleted.
+ ///
+ /// Requires that you have the
+ /// [Ban Members](../model/permissions/constant.BAN_MEMBERS.html)
+ /// permission.
+ ///
+ /// # Examples
+ ///
+ /// Ban the user that sent a message for 7 days:
+ ///
+ /// ```rust,ignore
+ /// context.ban_user(context.guild_id, context.message.author, 7);
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [ClientError::DeleteMessageDaysAmount](./enum.ClientError.html#DeleteMessageDaysAmount.v)
+ /// if the number of days given is over the maximum allowed.
+ pub fn ban_user<G, U>(&self, guild_id: G, user_id: U, delete_message_days: u8)
+ -> Result<()> where G: Into<GuildId>, U: Into<UserId> {
+ if delete_message_days > 7 {
+ return Err(Error::Client(ClientError::DeleteMessageDaysAmount(delete_message_days)));
+ }
+
+ http::ban_user(guild_id.into().0, user_id.into().0, delete_message_days)
+ }
+
+ /// Broadcast that you are typing to a channel for the next 5 seconds.
+ ///
+ /// After 5 seconds, another request must be made to continue broadcasting
+ /// that you are typing.
+ ///
+ /// This should rarely be used for bots, and should likely only be used for
+ /// signifying that a long-running command is still being executed.
+ ///
+ /// # Examples
+ ///
+ /// ```rust,ignore
+ /// context.broadcast_typing(context.channel_id);
+ /// ```
+ pub fn broadcast_typing<C>(&self, channel_id: C) -> Result<()>
+ where C: Into<ChannelId> {
+ http::broadcast_typing(channel_id.into().0)
+ }
+
+ /// Creates a [PublicChannel](../model/struct.PublicChannel.html) in the
+ /// given [Guild](../model/struct.Guild.html).
+ ///
+ /// Requires that you have the
+ /// [Manage Channels](../model/permissions/constant.MANAGE_CHANNELS.html)
+ /// permission.
+ ///
+ /// # Examples
+ ///
+ /// Create a voice channel in a guild with the name "test":
+ ///
+ /// ```rust,ignore
+ /// use serenity::model::ChannelType;
+ ///
+ /// context.create_channel(context.guild_id, "test", ChannelType::Voice);
+ /// ```
+ pub fn create_channel<G>(&self, guild_id: G, name: &str, kind: ChannelType)
+ -> Result<Channel> where G: Into<GuildId> {
+ let map = ObjectBuilder::new()
+ .insert("name", name)
+ .insert("type", kind.name())
+ .build();
+
+ http::create_channel(guild_id.into().0, map)
+ }
+
+ pub fn create_emoji<G>(&self, guild_id: G, name: &str, image: &str)
+ -> Result<Emoji> where G: Into<GuildId> {
+ let map = ObjectBuilder::new()
+ .insert("name", name)
+ .insert("image", image)
+ .build();
+
+ http::create_emoji(guild_id.into().0, map)
+ }
+
+ /// Creates a [Guild](../model/struct.Guild.html) with the data provided.
+ ///
+ /// # Examples
+ ///
+ /// Create a guild called "test" in the US West region with no icon:
+ ///
+ /// ```rust,ignore
+ /// use serenity::model::Region;
+ ///
+ /// context.create_guild("test", Region::UsWest, None);
+ /// ```
+ pub fn create_guild(&self, name: &str, region: Region, icon: Option<&str>)
+ -> Result<Guild> {
+ let map = ObjectBuilder::new()
+ .insert("icon", icon)
+ .insert("name", name)
+ .insert("region", region.name())
+ .build();
+
+ http::create_guild(map)
+ }
+
+ pub fn create_integration<G, I>(&self,
+ guild_id: G,
+ integration_id: I,
+ kind: &str)
+ -> Result<()> where G: Into<GuildId>,
+ I: Into<IntegrationId> {
+ let integration_id = integration_id.into();
+ let map = ObjectBuilder::new()
+ .insert("id", integration_id.0)
+ .insert("type", kind)
+ .build();
+
+ http::create_guild_integration(guild_id.into().0, integration_id.0, map)
+ }
+
+ pub fn create_invite<C, F>(&self, channel_id: C, f: F) -> Result<RichInvite>
+ where C: Into<ChannelId>, F: FnOnce(CreateInvite) -> CreateInvite {
+ let map = f(CreateInvite::default()).0.build();
+
+ http::create_invite(channel_id.into().0, map)
+ }
+
+ pub fn create_permission<C>(&self,
+ channel_id: C,
+ target: PermissionOverwrite)
+ -> Result<()> where C: Into<ChannelId> {
+ let (id, kind) = match target.kind {
+ PermissionOverwriteType::Member(id) => (id.0, "member"),
+ PermissionOverwriteType::Role(id) => (id.0, "role"),
+ };
+
+ let map = ObjectBuilder::new()
+ .insert("allow", target.allow.bits())
+ .insert("deny", target.deny.bits())
+ .insert("id", id)
+ .insert("type", kind)
+ .build();
+
+ http::create_permission(channel_id.into().0, id, map)
+ }
+
+ pub fn create_private_channel<U>(&self, user_id: U)
+ -> Result<PrivateChannel> where U: Into<UserId> {
+ let map = ObjectBuilder::new()
+ .insert("recipient_id", user_id.into().0)
+ .build();
+
+ http::create_private_channel(map)
+ }
+
+ pub fn create_role<F, G>(&self, guild_id: G, f: F) -> Result<Role>
+ where F: FnOnce(EditRole) -> EditRole, G: Into<GuildId> {
+ let id = guild_id.into().0;
+
+ // The API only allows creating an empty role.
+ let role = try!(http::create_role(id));
+ let map = f(EditRole::default()).0.build();
+
+ http::edit_role(id, role.id.0, map)
+ }
+
+ /// Deletes a [Channel](../model/enum.Channel.html) based on the id given.
+ ///
+ /// If the channel being deleted is a
+ /// [PublicChannel](../model/struct.PublicChannel.html) (a guild's channel),
+ /// then the
+ /// [Manage Channels](../model/permissions/constant.MANAGE_CHANNELS.html)
+ /// permission is required.
+ pub fn delete_channel<C>(&self, channel_id: C) -> Result<Channel>
+ where C: Into<ChannelId> {
+ http::delete_channel(channel_id.into().0)
+ }
+
+ pub fn delete_emoji<E, G>(&self, guild_id: G, emoji_id: E) -> Result<()>
+ where E: Into<EmojiId>, G: Into<GuildId> {
+ http::delete_emoji(guild_id.into().0, emoji_id.into().0)
+ }
+
+ /// Deletes a [Guild](../model/struct.Guild.html). You must be the guild
+ /// owner to be able to delete the guild.
+ pub fn delete_guild<G: Into<GuildId>>(&self, guild_id: G) -> Result<Guild> {
+ http::delete_guild(guild_id.into().0)
+ }
+
+ pub fn delete_integration<G, I>(&self, guild_id: G, integration_id: I)
+ -> Result<()> where G: Into<GuildId>, I: Into<IntegrationId> {
+ http::delete_guild_integration(guild_id.into().0,
+ integration_id.into().0)
+ }
+
+ /*
+ pub fn delete_invite(&self, invite: &str) -> Result<Invite> {
+ let code = utils::parse_invite(invite);
+
+ http::delete_invite(code)
+ }
+ */
+
+ /// Deletes a [Message](../model/struct.Message.html) given its ID.
+ ///
+ /// # Examples
+ ///
+ /// Deleting a message that was received by its ID:
+ ///
+ /// ```rust,ignore
+ /// context.delete_message(context.message.id);
+ /// ```
+ pub fn delete_message<C, M>(&self, channel_id: C, message_id: M)
+ -> Result<()> where C: Into<ChannelId>, M: Into<MessageId> {
+ http::delete_message(channel_id.into().0, message_id.into().0)
+ }
+
+ pub fn delete_messages<C>(&self, channel_id: C, message_ids: &[MessageId])
+ -> Result<()> where C: Into<ChannelId> {
+ if self.login_type == LoginType::User {
+ 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(channel_id.into().0, map)
+ }
+
+ pub fn delete_note<U: Into<UserId>>(&self, user_id: U) -> Result<()> {
+ let map = ObjectBuilder::new()
+ .insert("note", "")
+ .build();
+
+ http::edit_note(user_id.into().0, map)
+ }
+
+ pub fn delete_permission<C>(&self,
+ channel_id: C,
+ permission_type: PermissionOverwriteType)
+ -> Result<()> where C: Into<ChannelId> {
+ let id = match permission_type {
+ PermissionOverwriteType::Member(id) => id.0,
+ PermissionOverwriteType::Role(id) => id.0,
+ };
+
+ http::delete_permission(channel_id.into().0, id)
+ }
+
+ pub fn delete_role<G, R>(&self, guild_id: G, role_id: R) -> Result<()>
+ where G: Into<GuildId>, R: Into<RoleId> {
+ http::delete_role(guild_id.into().0, role_id.into().0)
+ }
+
+ /// Sends a message to a user through a direct message channel. This is a
+ /// channel that can only be accessed by you and the recipient.
+ ///
+ /// # Examples
+ ///
+ /// There are three ways to send a direct message to someone, the first being
+ /// an unrelated, although equally helpful method.
+ ///
+ /// Sending a message via a [User](../../model/struct.User.html):
+ ///
+ /// ```rust,ignore
+ /// context.message.author.dm("Hello!");
+ /// ```
+ ///
+ /// Sending a message to a PrivateChannel:
+ ///
+ /// ```rust,ignore
+ /// let private_channel = context.create_private_channel(context.message.author.id);
+ ///
+ /// context.direct_message(private_channel, "Test!");
+ /// ```
+ ///
+ /// Sending a message to a PrivateChannel given its ID:
+ ///
+ /// ```rust,ignore
+ /// let private_channel = context.create_private_channel(context.message.author.id);
+ ///
+ /// context.direct_message(private_channel.id, "Test!");
+ /// ```
+ pub fn direct_message<C>(&self, target_id: C, content: &str)
+ -> Result<Message> where C: Into<ChannelId> {
+ self.send_message(target_id.into(), content, "", false)
+ }
+
+ /// This is an alias of [direct_message](#method.direct_message).
+ pub fn dm<C: Into<ChannelId>>(&self, target_id: C, content: &str)
+ -> Result<Message> {
+ self.direct_message(target_id.into(), content)
+ }
+
+ pub fn edit_channel<C, F>(&self, channel_id: C, f: F)
+ -> Result<PublicChannel> where C: Into<ChannelId>,
+ F: FnOnce(EditChannel) -> EditChannel {
+ let channel_id = channel_id.into();
+
+ let map = match try!(self.get_channel(channel_id)) {
+ Channel::Public(channel) => {
+ let map = ObjectBuilder::new()
+ .insert("name", channel.name)
+ .insert("position", channel.position);
+
+ match channel.kind {
+ ChannelType::Text => map.insert("topic", channel.topic),
+ ChannelType::Voice => {
+ map.insert("bitrate", channel.bitrate)
+ .insert("user_limit", channel.user_limit)
+ },
+ kind => return Err(Error::Client(ClientError::UnexpectedChannelType(kind))),
+ }
+ },
+ Channel::Private(channel) => {
+ return Err(Error::Client(ClientError::UnexpectedChannelType(channel.kind)));
+ },
+ Channel::Group(_group) => {
+ return Err(Error::Client(ClientError::UnexpectedChannelType(ChannelType::Group)));
+ },
+ };
+
+ let edited = f(EditChannel(map)).0.build();
+
+ http::edit_channel(channel_id.0, edited)
+ }
+
+ pub fn edit_emoji<E, G>(&self, guild_id: G, emoji_id: E, name: &str)
+ -> Result<Emoji> where E: Into<EmojiId>, G: Into<GuildId> {
+ let map = ObjectBuilder::new()
+ .insert("name", name)
+ .build();
+
+ http::edit_emoji(guild_id.into().0, emoji_id.into().0, map)
+ }
+
+ pub fn edit_guild<F, G>(&self, guild_id: G, f: F) -> Result<Guild>
+ where F: FnOnce(EditGuild) -> EditGuild, G: Into<GuildId> {
+ let map = f(EditGuild::default()).0.build();
+
+ http::edit_guild(guild_id.into().0, map)
+ }
+
+ pub fn edit_member<F, G, U>(&self, guild_id: G, user_id: U, f: F)
+ -> Result<()> where F: FnOnce(EditMember) -> EditMember,
+ G: Into<GuildId>,
+ U: Into<UserId> {
+ let map = f(EditMember::default()).0.build();
+
+ http::edit_member(guild_id.into().0, user_id.into().0, map)
+ }
+
+ pub fn edit_profile<F: FnOnce(EditProfile) -> EditProfile>(&mut self, f: F)
+ -> Result<CurrentUser> {
+ let user = try!(http::get_current_user());
+
+ let mut map = ObjectBuilder::new()
+ .insert("avatar", user.avatar)
+ .insert("username", user.name);
+
+ if let Some(email) = user.email.as_ref() {
+ map = map.insert("email", email);
+ }
+
+ let edited = f(EditProfile(map)).0.build();
+
+ http::edit_profile(edited)
+ }
+
+ pub fn edit_role<F, G, R>(&self, guild_id: G, role_id: R, f: F)
+ -> Result<Role> where F: FnOnce(EditRole) -> EditRole,
+ G: Into<GuildId>,
+ R: Into<GuildId> {
+ let guild_id = guild_id.into();
+ let role_id = role_id.into();
+
+ let map = {
+ let state = STATE.lock().unwrap();
+
+ let role = if let Some(role) = {
+ state.find_role(guild_id.0, role_id.0)
+ } {
+ role
+ } else {
+ return Err(Error::Client(ClientError::RecordNotFound));
+ };
+
+ f(EditRole::new(role)).0.build()
+ };
+
+ http::edit_role(guild_id.0, role_id.0, map)
+ }
+
+ pub fn edit_message<C, M>(&self, channel_id: C, message_id: M, text: &str)
+ -> Result<Message> where C: Into<ChannelId>, M: Into<MessageId> {
+ let map = ObjectBuilder::new()
+ .insert("content", text)
+ .build();
+
+ http::edit_message(channel_id.into().0, message_id.into().0, map)
+ }
+
+ pub fn edit_note<U: Into<UserId>>(&self, user_id: U, note: &str)
+ -> Result<()> {
+ let map = ObjectBuilder::new()
+ .insert("note", note)
+ .build();
+
+ http::edit_note(user_id.into().0, map)
+ }
+
+ pub fn get_application_info(&self) -> Result<CurrentApplicationInfo> {
+ http::get_application_info()
+ }
+
+ pub fn get_applications(&self) -> Result<Vec<ApplicationInfo>> {
+ http::get_applications()
+ }
+
+ pub fn get_bans<G: Into<GuildId>>(&self, guild_id: G) -> Result<Vec<Ban>> {
+ http::get_bans(guild_id.into().0)
+ }
+
+ pub fn get_channel_invites<C: Into<ChannelId>>(&self, channel_id: C)
+ -> Result<Vec<RichInvite>> {
+ http::get_channel_invites(channel_id.into().0)
+ }
+
+ pub fn get_channel<C>(&self, channel_id: C) -> Result<Channel>
+ where C: Into<ChannelId> {
+ let channel_id = channel_id.into();
+
+ if let Some(channel) = STATE.lock().unwrap().find_channel(channel_id) {
+ return Ok(channel.clone())
+ }
+
+ http::get_channel(channel_id.0)
+ }
+
+ pub fn get_channels<G>(&self, guild_id: G)
+ -> Result<HashMap<ChannelId, PublicChannel>> where G: Into<GuildId> {
+ let guild_id = guild_id.into();
+
+ {
+ let state = STATE.lock().unwrap();
+
+ if let Some(guild) = state.find_guild(guild_id) {
+ return Ok(guild.channels.clone());
+ }
+ }
+
+ let mut channels = HashMap::new();
+
+ for channel in try!(http::get_channels(guild_id.0)) {
+ channels.insert(channel.id, channel);
+ }
+
+ Ok(channels)
+ }
+
+ pub fn get_emoji<E, G>(&self, guild_id: G, emoji_id: E) -> Result<Emoji>
+ where E: Into<EmojiId>, G: Into<GuildId> {
+ http::get_emoji(guild_id.into().0, emoji_id.into().0)
+ }
+
+ pub fn get_emojis<G: Into<GuildId>>(&self, guild_id: G)
+ -> Result<Vec<Emoji>> {
+ http::get_emojis(guild_id.into().0)
+ }
+
+ pub fn get_guild<G: Into<GuildId>>(&self, guild_id: G) -> Result<Guild> {
+ http::get_guild(guild_id.into().0)
+ }
+
+ pub fn get_guild_invites<G>(&self, guild_id: G) -> Result<Vec<RichInvite>>
+ where G: Into<GuildId> {
+ http::get_guild_invites(guild_id.into().0)
+ }
+
+ pub fn get_guild_prune_count<G>(&self, guild_id: G, days: u16)
+ -> Result<GuildPrune> where G: Into<GuildId> {
+ let map = ObjectBuilder::new()
+ .insert("days", days)
+ .build();
+
+ http::get_guild_prune_count(guild_id.into().0, map)
+ }
+
+ pub fn get_guilds(&self) -> Result<Vec<GuildInfo>> {
+ http::get_guilds()
+ }
+
+ pub fn get_integrations<G: Into<GuildId>>(&self, guild_id: G)
+ -> Result<Vec<Integration>> {
+ http::get_guild_integrations(guild_id.into().0)
+ }
+
+ pub fn get_invite(&self, invite: &str) -> Result<Invite> {
+ let code = utils::parse_invite(invite);
+
+ http::get_invite(code)
+ }
+
+ pub fn get_member<G, U>(&self, guild_id: G, user_id: U) -> Result<Member>
+ where G: Into<GuildId>, U: Into<UserId> {
+ let guild_id = guild_id.into();
+ let user_id = user_id.into();
+
+ {
+ let state = STATE.lock().unwrap();
+
+ if let Some(member) = state.find_member(guild_id, user_id) {
+ return Ok(member.clone());
+ }
+ }
+
+ http::get_member(guild_id.0, user_id.0)
+ }
+
+ /// Retrieves a single [Message](../../model/struct.Message.html) from a
+ /// [Channel](../../model/struct.Channel.html).
+ ///
+ /// Requires the
+ /// [Read Message History](../../model/permissions/constant.READ_MESSAGE_HISTORY.html)
+ /// permission.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [ClientError::InvalidOperationAsUser](../enum.ClientError.html#InvalidOperationAsUser.v)
+ /// if this is a user.
+ pub fn get_message<C, M>(&self, channel_id: C, message_id: M)
+ -> Result<Message> where C: Into<ChannelId>, M: Into<MessageId> {
+ if self.login_type == LoginType::User {
+ return Err(Error::Client(ClientError::InvalidOperationAsUser))
+ }
+
+ http::get_message(channel_id.into().0, message_id.into().0)
+ }
+
+ pub fn get_messages<C, F>(&self, channel_id: C, f: F) -> Result<Vec<Message>>
+ where C: Into<ChannelId>, F: FnOnce(GetMessages) -> GetMessages {
+ let query = {
+ let mut map = f(GetMessages::default()).0;
+ let mut query = format!("?limit={}",
+ map.remove("limit").unwrap_or(50));
+
+ if let Some(after) = map.remove("after") {
+ query.push_str("&after=");
+ query.push_str(&after.to_string());
+ }
+
+ if let Some(around) = map.remove("around") {
+ query.push_str("&around=");
+ query.push_str(&around.to_string());
+ }
+
+ if let Some(before) = map.remove("before") {
+ query.push_str("&before=");
+ query.push_str(&before.to_string());
+ }
+
+ query
+ };
+
+ http::get_messages(channel_id.into().0, &query)
+ }
+
+ pub fn get_voice_regions(&self) -> Result<Vec<VoiceRegion>> {
+ http::get_voice_regions()
+ }
+
+ /// Kicks a [Member](../../model/struct.Member.html) from the specified
+ /// [Guild](../../model/struct.Guild.html) if they are in it.
+ ///
+ /// Requires the
+ /// [Kick Members](../../model/permissions/constant.KICK_MEMBERS.html)
+ /// permission.
+ pub fn kick_member<G, U>(&self, guild_id: G, user_id: U) -> Result<()>
+ where G: Into<GuildId>, U: Into<UserId> {
+ http::kick_member(guild_id.into().0, user_id.into().0)
+ }
+
+ pub fn leave_guild<G: Into<GuildId>>(&self, guild_id: G) -> Result<Guild> {
+ http::leave_guild(guild_id.into().0)
+ }
+
+ pub fn move_member<C, G, U>(&self, guild_id: G, user_id: U, channel_id: C)
+ -> Result<()> where C: Into<ChannelId>,
+ G: Into<ChannelId>,
+ U: Into<ChannelId> {
+ let map = ObjectBuilder::new()
+ .insert("channel_id", channel_id.into().0)
+ .build();
+
+ http::edit_member(guild_id.into().0, user_id.into().0, map)
+ }
+
+ /// This is an alias of [get_pins](#method.get_pins).
+ pub fn pins<C>(&self, channel_id: C) -> Result<Vec<Message>>
+ where C: Into<ChannelId> {
+ self.get_pins(channel_id.into())
+ }
+
+ /// Retrieves the list of [Message](../../model/struct.Message.html)s which
+ /// are pinned to the specified [Channel](../../model/enum.Channel.html).
+ pub fn get_pins<C>(&self, channel_id: C) -> Result<Vec<Message>>
+ where C: Into<ChannelId> {
+ http::get_pins(channel_id.into().0)
+ }
+
+ /// This is an alias of [pin_message](#method.pin_message).
+ pub fn pin<C, M>(&self, channel_id: C, message_id: M) -> Result<()>
+ where C: Into<ChannelId>, M: Into<MessageId> {
+ self.pin_message(channel_id.into(), message_id.into())
+ }
+
+ pub fn pin_message<C, M>(&self, channel_id: C, message_id: M) -> Result<()>
+ where C: Into<ChannelId>, M: Into<MessageId> {
+ http::pin_message(channel_id.into().0, message_id.into().0)
+ }
+
+ /// This is an alias of [direct_message](#method.direct_message).
+ pub fn pm<C: Into<ChannelId>>(&self, target_id: C, content: &str)
+ -> Result<Message> {
+ self.direct_message(target_id.into(), content)
+ }
+
+ /// Unbans a [User](../../model/struct.User.html) from a guild.
+ ///
+ /// Requires the
+ /// [Ban Members](../../model/permissions/constant.BAN_MEMBERS.html)
+ /// permission.
+ pub fn remove_ban<G, U>(&self, guild_id: G, user_id: U) -> Result<()>
+ where G: Into<GuildId>, U: Into<UserId> {
+ http::remove_ban(guild_id.into().0, user_id.into().0)
+ }
+
+ /// Sends a message with just the given message content in the channel that
+ /// a message was received from.
+ ///
+ /// **Note**: This will only work when a Message is received.
+ ///
+ /// # Errors
+ ///
+ /// Returns a
+ /// [ClientError::NoChannelId](../enum.ClientError.html#NoChannelId) when
+ /// there is no [ChannelId](../../models/struct.ChannelId.html) directly
+ /// available.
+ pub fn say(&self, text: &str) -> Result<Message> {
+ if let Some(channel_id) = self.channel_id {
+ self.send_message(channel_id, text, "", false)
+ } else {
+ Err(Error::Client(ClientError::NoChannelId))
+ }
+ }
+
+ /// This is an alias of [send_message](#method.send_message).
+ pub fn send<C>(&self, channel_id: C, content: &str, nonce: &str, tts: bool)
+ -> Result<Message> where C: Into<ChannelId> {
+ self.send_message(channel_id.into(),
+ content,
+ nonce,
+ tts)
+ }
+
+ pub fn send_file<C, R>(&self,
+ channel_id: C,
+ content: &str,
+ file: R,
+ filename: &str)
+ -> Result<Message> where C: Into<ChannelId>,
+ R: Read {
+ http::send_file(channel_id.into().0, content, file, filename)
+ }
+
+ /// Sends a message to a [Channel](../../model/enum.Channel.html).
+ ///
+ /// Note that often a nonce is not required and can be omitted in most
+ /// situations.
+ ///
+ /// # Example
+ ///
+ /// ```rust,ignore
+ /// let _ = context.send_message(message.channel_id, "Hello!", "", false);
+ /// ```
+ pub fn send_message<C>(&self, channel_id: C, content: &str, nonce: &str, tts: bool)
+ -> Result<Message> where C: Into<ChannelId> {
+ let map = ObjectBuilder::new()
+ .insert("content", content)
+ .insert("nonce", nonce)
+ .insert("tts", tts)
+ .build();
+
+ http::send_message(channel_id.into().0, map)
+ }
+
+ pub fn set_game(&self, game: Option<Game>) {
+ self.connection.lock().unwrap().set_game(game)
+ }
+
+ pub fn start_guild_prune<G>(&self, guild_id: G, days: u16)
+ -> Result<GuildPrune> where G: Into<GuildId> {
+ let map = ObjectBuilder::new()
+ .insert("days", days)
+ .build();
+
+ http::start_guild_prune(guild_id.into().0, map)
+ }
+
+ pub fn start_integration_sync<G, I>(&self, guild_id: G, integration_id: I)
+ -> Result<()> where G: Into<GuildId>, I: Into<IntegrationId> {
+ http::start_integration_sync(guild_id.into().0, integration_id.into().0)
+ }
+
+ /// This is an alias of [broadcast_typing](#method.broadcast_typing).
+ pub fn typing<C>(&self, channel_id: C) -> Result<()>
+ where C: Into<ChannelId> {
+ self.broadcast_typing(channel_id.into().0)
+ }
+
+ /// This is an alias of [remove_ban](#method.remove_ban).
+ pub fn unban<G, U>(&self, guild_id: G, user_id: U) -> Result<()>
+ where G: Into<GuildId>, U: Into<UserId> {
+ self.remove_ban(guild_id.into().0, user_id.into().0)
+ }
+
+ /// This is an alias of [unpin_message](#method.unpin_message).
+ pub fn unpin<C, M>(&self, channel_id: C, message_id: M) -> Result<()>
+ where C: Into<ChannelId>, M: Into<MessageId> {
+ self.unpin_message(channel_id.into().0, message_id.into().0)
+ }
+
+ pub fn unpin_message<C, M>(&self, channel_id: C, message_id: M)
+ -> Result<()> where C: Into<ChannelId>, M: Into<MessageId> {
+ http::unpin_message(channel_id.into().0, message_id.into().0)
+ }
+}
diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs
new file mode 100644
index 0000000..5e206ee
--- /dev/null
+++ b/src/client/dispatch.rs
@@ -0,0 +1,657 @@
+use std::sync::{Arc, Mutex};
+use std::{mem, thread};
+use super::event_store::EventStore;
+use super::login_type::LoginType;
+use super::{STATE, Connection, Context};
+use ::ext::framework::Framework;
+use ::model::{ChannelId, Event, Message};
+use ::prelude::*;
+
+macro_rules! handler {
+ ($field:ident, $event_store:ident) => {
+ $event_store.lock()
+ .unwrap()
+ .$field
+ .as_ref()
+ .cloned()
+ }
+}
+
+macro_rules! update {
+ ($method:ident, $event:expr) => {
+ STATE.lock().unwrap().$method(&$event);
+ }
+}
+
+fn context(channel_id: Option<ChannelId>,
+ conn: Arc<Mutex<Connection>>,
+ login_type: LoginType) -> Context {
+ Context::new(channel_id, conn, login_type)
+}
+
+#[allow(cyclomatic_complexity)]
+pub fn dispatch(event: Result<Event>,
+ conn: Arc<Mutex<Connection>>,
+ framework: Arc<Mutex<Framework>>,
+ login_type: LoginType,
+ event_store: Arc<Mutex<EventStore>>) {
+ match event {
+ Ok(Event::CallCreate(event)) => {
+ if let Some(ref handler) = handler!(on_call_create, event_store) {
+ update!(update_with_call_create, event);
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.call);
+ });
+ } else {
+ update!(update_with_call_create, event);
+ }
+ },
+ Ok(Event::CallDelete(event)) => {
+ if let Some(ref handler) = handler!(on_call_delete, event_store) {
+ let call = STATE
+ .lock()
+ .unwrap()
+ .calls
+ .remove(&event.channel_id);
+ update!(update_with_call_delete, event);
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, call);
+ });
+ } else {
+ update!(update_with_call_delete, event);
+ }
+ },
+ Ok(Event::CallUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_call_update, event_store) {
+ let before = STATE
+ .lock()
+ .unwrap()
+ .calls
+ .get(&event.channel_id)
+ .cloned();
+ update!(update_with_call_update, event);
+ let after = STATE
+ .lock()
+ .unwrap()
+ .calls
+ .get(&event.channel_id)
+ .cloned();
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before, after);
+ });
+ } else {
+ update!(update_with_call_update, event);
+ }
+ },
+ Ok(Event::ChannelCreate(event)) => {
+ if let Some(ref handler) = handler!(on_channel_create, event_store) {
+ update!(update_with_channel_create, event);
+ let context = context(Some(event.channel.id()),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel);
+ });
+ } else {
+ update!(update_with_channel_create, event);
+ }
+ },
+ Ok(Event::ChannelDelete(event)) => {
+ if let Some(ref handler) = handler!(on_channel_delete, event_store) {
+ update!(update_with_channel_delete, event);
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel);
+ });
+ } else {
+ update!(update_with_channel_delete, event);
+ }
+ },
+ Ok(Event::ChannelPinsAck(event)) => {
+ if let Some(ref handler) = handler!(on_channel_pins_ack, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::ChannelPinsUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_channel_pins_update, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::ChannelRecipientAdd(event)) => {
+ update!(update_with_channel_recipient_add, event);
+
+ if let Some(ref handler) = handler!(on_channel_recipient_addition, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel_id, event.user);
+ });
+ }
+ },
+ Ok(Event::ChannelRecipientRemove(event)) => {
+ update!(update_with_channel_recipient_remove, event);
+
+ if let Some(ref handler) = handler!(on_channel_recipient_removal, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel_id, event.user);
+ });
+ }
+ },
+ Ok(Event::ChannelUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_channel_update, event_store) {
+ let before = STATE.lock()
+ .unwrap()
+ .find_channel(event.channel.id());
+ update!(update_with_channel_update, event);
+ let context = context(Some(event.channel.id()),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before, event.channel);
+ });
+ } else {
+ update!(update_with_channel_update, event);
+ }
+ },
+ Ok(Event::GuildBanAdd(event)) => {
+ if let Some(ref handler) = handler!(on_guild_ban_addition, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.user);
+ });
+ }
+ },
+ Ok(Event::GuildBanRemove(event)) => {
+ if let Some(ref handler) = handler!(on_guild_ban_removal, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.user);
+ });
+ }
+ },
+ Ok(Event::GuildCreate(event)) => {
+ update!(update_with_guild_create, event);
+
+ if let Some(ref handler) = handler!(on_guild_create, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild);
+ });
+ }
+ },
+ Ok(Event::GuildDelete(event)) => {
+ update!(update_with_guild_delete, event);
+
+ if let Some(ref handler) = handler!(on_guild_delete, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild);
+ });
+ }
+ },
+ Ok(Event::GuildEmojisUpdate(event)) => {
+ update!(update_with_guild_emojis_update, event);
+
+ if let Some(ref handler) = handler!(on_guild_emojis_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::GuildIntegrationsUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_guild_integrations_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::GuildMemberAdd(event)) => {
+ update!(update_with_guild_member_add, event);
+
+ if let Some(ref handler) = handler!(on_guild_member_addition, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.member);
+ });
+ }
+ },
+ Ok(Event::GuildMemberRemove(event)) => {
+ update!(update_with_guild_member_remove, event);
+
+ if let Some(ref handler) = handler!(on_guild_member_removal, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.user);
+ });
+ }
+ },
+ Ok(Event::GuildMemberUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_guild_member_update, event_store) {
+ let before = STATE.lock()
+ .unwrap()
+ .guilds
+ .get_mut(&event.guild_id)
+ .map(|mut guild| {
+ guild.members.remove(&event.user.id)
+ }).and_then(|x| match x {
+ Some(x) => Some(x),
+ _ => None,
+ });
+ update!(update_with_guild_member_update, event);
+
+ // This is safe, as the update would have created the member
+ // if it did not exist. Thus, there _should_ be no way that this
+ // could fail under any circumstance.
+ let after = STATE.lock()
+ .unwrap()
+ .find_member(event.guild_id, event.user.id)
+ .unwrap()
+ .clone();
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before, after);
+ });
+ }
+ },
+ Ok(Event::GuildMembersChunk(event)) => {
+ update!(update_with_guild_members_chunk, event);
+
+ if let Some(ref handler) = handler!(on_guild_members_chunk, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.members);
+ });
+ }
+ },
+ Ok(Event::GuildRoleCreate(event)) => {
+ update!(update_with_guild_role_create, event);
+
+ if let Some(ref handler) = handler!(on_guild_role_create, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.role);
+ });
+ }
+ },
+ Ok(Event::GuildRoleDelete(event)) => {
+ update!(update_with_guild_role_delete, event);
+
+ if let Some(ref handler) = handler!(on_guild_role_delete, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.role_id);
+ });
+ }
+ },
+ Ok(Event::GuildRoleUpdate(event)) => {
+ update!(update_with_guild_role_update, event);
+
+ if let Some(ref handler) = handler!(on_guild_role_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id, event.role);
+ });
+ }
+ },
+ Ok(Event::GuildSync(event)) => {
+ if let Some(ref handler) = handler!(on_guild_sync, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::GuildUnavailable(event)) => {
+ update!(update_with_guild_unavailable, event);
+
+ if let Some(ref handler) = handler!(on_guild_unavailable, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.guild_id);
+ });
+ }
+ },
+ Ok(Event::GuildUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_guild_update, event_store) {
+ let before = STATE.lock()
+ .unwrap()
+ .guilds
+ .get(&event.guild.id)
+ .cloned();
+ update!(update_with_guild_update, event);
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before, event.guild);
+ });
+ } else {
+ update!(update_with_guild_update, event);
+ }
+ }
+ Ok(Event::MessageAck(event)) => {
+ if let Some(ref handler) = handler!(on_message_ack, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel_id, event.message_id);
+ });
+ }
+ },
+ Ok(Event::MessageCreate(event)) => {
+ let context = context(Some(event.message.channel_id),
+ conn,
+ login_type);
+
+ if framework.lock().unwrap().initialized {
+ dispatch_message(context.clone(),
+ event.message.clone(),
+ event_store);
+
+ framework.lock().unwrap().dispatch(context, event.message);
+ } else {
+ dispatch_message(context, event.message, event_store);
+ }
+ },
+ Ok(Event::MessageDeleteBulk(event)) => {
+ if let Some(ref handler) = handler!(on_message_delete_bulk, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel_id, event.ids);
+ });
+ }
+ },
+ Ok(Event::MessageDelete(event)) => {
+ if let Some(ref handler) = handler!(on_message_delete, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.channel_id, event.message_id);
+ });
+ }
+ },
+ Ok(Event::MessageUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_message_update, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::PresencesReplace(event)) => {
+ update!(update_with_presences_replace, event);
+
+ if let Some(handler) = handler!(on_presence_replace, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.presences);
+ });
+ }
+ },
+ Ok(Event::PresenceUpdate(event)) => {
+ update!(update_with_presence_update, event);
+
+ if let Some(handler) = handler!(on_presence_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::Ready(event)) => {
+ if let Some(ref handler) = handler!(on_ready, event_store) {
+ update!(update_with_ready, event);
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.ready);
+ });
+ } else {
+ update!(update_with_ready, event);
+ }
+ },
+ Ok(Event::RelationshipAdd(event)) => {
+ update!(update_with_relationship_add, event);
+
+ if let Some(ref handler) = handler!(on_relationship_addition, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.relationship);
+ });
+ }
+ },
+ Ok(Event::RelationshipRemove(event)) => {
+ update!(update_with_relationship_remove, event);
+
+ if let Some(ref handler) = handler!(on_relationship_removal, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.user_id, event.kind);
+ });
+ }
+ },
+ Ok(Event::Resumed(event)) => {
+ if let Some(ref handler) = handler!(on_resume, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::TypingStart(event)) => {
+ if let Some(ref handler) = handler!(on_typing_start, event_store) {
+ let context = context(Some(event.channel_id),
+ conn,
+ login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::Unknown(event)) => {
+ if let Some(ref handler) = handler!(on_unknown, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.kind, event.value);
+ });
+ }
+ },
+ Ok(Event::UserGuildSettingsUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_user_guild_settings_update, event_store) {
+ let before = STATE.lock()
+ .unwrap()
+ .guild_settings
+ .remove(&event.settings.guild_id);
+ update!(update_with_user_guild_settings_update, event);
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before, event.settings);
+ });
+ }
+ },
+ Ok(Event::UserNoteUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_note_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event.user_id, event.note);
+ });
+ }
+ },
+ Ok(Event::UserSettingsUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_user_settings_update, event_store) {
+ let before = STATE.lock().unwrap().settings.clone();
+ update!(update_with_user_settings_update, event);
+ let after = STATE.lock().unwrap().settings.clone();
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before.unwrap(), after.unwrap());
+ });
+ } else {
+ update!(update_with_user_settings_update, event);
+ }
+ },
+ Ok(Event::UserUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_user_update, event_store) {
+ // This is equivilant to performing a
+ // `update_with_voice_state_update`, and will be more efficient.
+ let before = {
+ let mut state = STATE.lock().unwrap();
+
+ mem::replace(&mut state.user, event.current_user.clone())
+ };
+
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, before, event.current_user);
+ });
+ }
+ },
+ Ok(Event::VoiceServerUpdate(event)) => {
+ if let Some(ref handler) = handler!(on_voice_server_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Ok(Event::VoiceStateUpdate(event)) => {
+ update!(update_with_voice_state_update, event);
+
+ if let Some(ref handler) = handler!(on_voice_state_update, event_store) {
+ let context = context(None, conn, login_type);
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, event);
+ });
+ }
+ },
+ Err(_why) => {},
+ }
+}
+
+fn dispatch_message(context: Context,
+ message: Message,
+ event_store: Arc<Mutex<EventStore>>) {
+ if let Some(ref handler) = handler!(on_message, event_store) {
+ let handler = handler.clone();
+
+ thread::spawn(move || {
+ (handler)(context, message);
+ });
+ }
+}
diff --git a/src/client/event_store.rs b/src/client/event_store.rs
new file mode 100644
index 0000000..4bd8459
--- /dev/null
+++ b/src/client/event_store.rs
@@ -0,0 +1,73 @@
+use serde_json::Value;
+use std::collections::{BTreeMap, HashMap};
+use std::sync::Arc;
+use super::context::Context;
+use ::model::*;
+
+// This should use type macros when stable receives the type macro
+// stabilization patch.
+//
+// This implementation should be:
+//
+// ```rust,ignore
+// macro_rules! efn {
+// ($def:ty) => {
+// Option<Arc<Box<$def> + Send + Sync + 'static>>
+// }
+// }
+// ```
+//
+// Where each field will look like:
+//
+// ```rust,ignore
+// pub something: efn!(Fn(Context, ...)),
+// ```
+#[allow(type_complexity)]
+#[derive(Default)]
+pub struct EventStore {
+ pub on_call_create: Option<Arc<Fn(Context, Call) + Send + Sync + 'static>>,
+ pub on_call_delete: Option<Arc<Fn(Context, Option<Call>) + Send + Sync + 'static>>,
+ pub on_call_update: Option<Arc<Fn(Context, Option<Call>, Option<Call>) + Send + Sync + 'static>>,
+ pub on_channel_create: Option<Arc<Fn(Context, Channel) + Send + Sync + 'static>>,
+ pub on_channel_delete: Option<Arc<Fn(Context, Channel) + Send + Sync + 'static>>,
+ pub on_channel_pins_ack: Option<Arc<Fn(Context, ChannelPinsAckEvent) + Send + Sync + 'static>>,
+ pub on_channel_pins_update: Option<Arc<Fn(Context, ChannelPinsUpdateEvent) + Send + Sync + 'static>>,
+ pub on_channel_recipient_addition: Option<Arc<Fn(Context, ChannelId, User) + Send + Sync + 'static>>,
+ pub on_channel_recipient_removal: Option<Arc<Fn(Context, ChannelId, User) + Send + Sync + 'static>>,
+ pub on_channel_update: Option<Arc<Fn(Context, Option<Channel>, Channel) + Send + Sync + 'static>>,
+ pub on_guild_ban_addition: Option<Arc<Fn(Context, GuildId, User) + Send + Sync + 'static>>,
+ pub on_guild_ban_removal: Option<Arc<Fn(Context, GuildId, User) + Send + Sync + 'static>>,
+ pub on_guild_create: Option<Arc<Fn(Context, LiveGuild) + Send + Sync + 'static>>,
+ pub on_guild_delete: Option<Arc<Fn(Context, Guild) + Send + Sync + 'static>>,
+ pub on_guild_emojis_update: Option<Arc<Fn(Context, GuildEmojisUpdateEvent) + Send + Sync + 'static>>,
+ pub on_guild_integrations_update: Option<Arc<Fn(Context, GuildIntegrationsUpdateEvent) + Send + Sync + 'static>>,
+ pub on_guild_member_addition: Option<Arc<Fn(Context, GuildId, Member) + Send + Sync + 'static>>,
+ pub on_guild_member_removal: Option<Arc<Fn(Context, GuildId, User) + Send + Sync + 'static>>,
+ pub on_guild_member_update: Option<Arc<Fn(Context, Option<Member>, Member) + Send + Sync + 'static>>,
+ pub on_guild_members_chunk: Option<Arc<Fn(Context, GuildId, HashMap<UserId, Member>) + Send + Sync + 'static>>,
+ pub on_guild_role_create: Option<Arc<Fn(Context, GuildId, Role) + Send + Sync + 'static>>,
+ pub on_guild_role_delete: Option<Arc<Fn(Context, GuildId, RoleId) + Send + Sync + 'static>>,
+ pub on_guild_role_update: Option<Arc<Fn(Context, GuildId, Role) + Send + Sync + 'static>>,
+ pub on_guild_sync: Option<Arc<Fn(Context, GuildSyncEvent) + Send + Sync + 'static>>,
+ pub on_guild_unavailable: Option<Arc<Fn(Context, GuildId) + Send + Sync + 'static>>,
+ pub on_guild_update: Option<Arc<Fn(Context, Option<LiveGuild>, Guild) + Send + Sync + 'static>>,
+ pub on_message: Option<Arc<Fn(Context, Message) + Send + Sync + 'static>>,
+ pub on_message_ack: Option<Arc<Fn(Context, ChannelId, Option<MessageId>) + Send + Sync + 'static>>,
+ pub on_message_delete: Option<Arc<Fn(Context, ChannelId, MessageId) + Send + Sync + 'static>>,
+ pub on_message_delete_bulk: Option<Arc<Fn(Context, ChannelId, Vec<MessageId>) + Send + Sync + 'static>>,
+ pub on_message_update: Option<Arc<Fn(Context, MessageUpdateEvent) + Send + Sync + 'static>>,
+ pub on_note_update: Option<Arc<Fn(Context, UserId, String) + Send + Sync + 'static>>,
+ pub on_presence_replace: Option<Arc<Fn(Context, Vec<Presence>) + Send + Sync + 'static>>,
+ pub on_presence_update: Option<Arc<Fn(Context, PresenceUpdateEvent) + Send + Sync + 'static>>,
+ pub on_ready: Option<Arc<Fn(Context, Ready) + Send + Sync + 'static>>,
+ pub on_relationship_addition: Option<Arc<Fn(Context, Relationship) + Send + Sync + 'static>>,
+ pub on_relationship_removal: Option<Arc<Fn(Context, UserId, RelationshipType) + Send + Sync + 'static>>,
+ pub on_resume: Option<Arc<Fn(Context, ResumedEvent) + Send + Sync + 'static>>,
+ pub on_typing_start: Option<Arc<Fn(Context, TypingStartEvent) + Send + Sync + 'static>>,
+ pub on_unknown: Option<Arc<Fn(Context, String, BTreeMap<String, Value>) + Send + Sync + 'static>>,
+ pub on_user_guild_settings_update: Option<Arc<Fn(Context, Option<UserGuildSettings>, UserGuildSettings) + Send + Sync + 'static>>,
+ pub on_user_update: Option<Arc<Fn(Context, CurrentUser, CurrentUser) + Send + Sync + 'static>>,
+ pub on_user_settings_update: Option<Arc<Fn(Context, UserSettings, UserSettings) + Send + Sync + 'static>>,
+ pub on_voice_state_update: Option<Arc<Fn(Context, VoiceStateUpdateEvent) + Send + Sync + 'static>>,
+ pub on_voice_server_update: Option<Arc<Fn(Context, VoiceServerUpdateEvent) + Send + Sync + 'static>>,
+}
diff --git a/src/client/http.rs b/src/client/http.rs
new file mode 100644
index 0000000..3421359
--- /dev/null
+++ b/src/client/http.rs
@@ -0,0 +1,684 @@
+//! The HTTP module which provides functions for performing requests to
+//! endpoints in Discord's API.
+//!
+//! An important function of the REST API is ratelimiting. Requests to endpoints
+//! are ratelimited to prevent spam, and once ratelimited Discord will stop
+//! performing requests. The library implements protection to pre-emptively
+//! ratelimit, to ensure that no wasted requests are made.
+//!
+//! The HTTP module comprises of two types of requests:
+//!
+//! - REST API requests, which require an authorization token;
+//! - Other requests, which do not require an authorization token.
+//!
+//! The former require a [`Client`] to have logged in, while the latter may be
+//! made regardless of any other usage of the library.
+//!
+//! If a request spuriously fails, it will be retried once.
+//!
+//! [`Client`]: ../struct.Client.html
+
+use hyper::client::{
+ Client as HyperClient,
+ RequestBuilder,
+ Response as HyperResponse,
+ Request,
+};
+use hyper::method::Method;
+use hyper::status::StatusCode;
+use hyper::{Error as HyperError, Result as HyperResult, Url, header};
+use multipart::client::Multipart;
+use serde_json;
+use std::default::Default;
+use std::io::{ErrorKind as IoErrorKind, Read};
+use std::sync::{Arc, Mutex};
+use super::ratelimiting::{self, Route};
+use ::constants;
+use ::model::*;
+use ::prelude::*;
+use ::utils::decode_array;
+
+lazy_static! {
+ static ref TOKEN: Arc<Mutex<String>> = Arc::new(Mutex::new(String::default()));
+}
+
+#[doc(hidden)]
+pub fn set_token(token: &str) {
+ TOKEN.lock().unwrap().clone_from(&token.to_owned());
+}
+
+pub fn accept_invite(code: &str) -> Result<Invite> {
+ let response = request!(Route::InvitesCode, post, "/invite/{}", code);
+
+ Invite::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn ack_message(channel_id: u64, message_id: u64) -> Result<()> {
+ verify(204, request!(Route::None,
+ post,
+ "/channels/{}/messages/{}/ack",
+ channel_id,
+ message_id))
+}
+
+pub fn add_group_recipient(group_id: u64, user_id: u64)
+ -> Result<()> {
+ verify(204, request!(Route::None,
+ put,
+ "/channels/{}/recipients/{}",
+ group_id,
+ user_id))
+}
+
+pub fn ban_user(guild_id: u64, user_id: u64, delete_message_days: u8)
+ -> Result<()> {
+ verify(204, request!(Route::GuildsIdBansUserId,
+ put,
+ "/guilds/{}/bans/{}?delete_message_days={}",
+ guild_id,
+ user_id,
+ delete_message_days))
+}
+
+pub fn broadcast_typing(channel_id: u64) -> Result<()> {
+ verify(204, request!(Route::ChannelsIdTyping,
+ post,
+ "/channels/{}/typing",
+ channel_id))
+}
+
+pub fn create_channel(guild_id: u64, map: Value) -> Result<Channel> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsIdChannels,
+ post(body),
+ "/guilds/{}/channels",
+ guild_id);
+
+ Channel::decode(try!(serde_json::from_reader(response)))
+}
+
+
+pub fn create_emoji(guild_id: u64, map: Value)
+ -> Result<Emoji> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsIdEmojis,
+ post(body),
+ "/guilds/{}/emojis",
+ guild_id);
+
+ Emoji::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn create_guild(map: Value) -> Result<Guild> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::Guilds, post(body), "/guilds");
+
+ Guild::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn create_guild_integration(
+ guild_id: u64,
+ integration_id: u64,
+ map: Value) -> Result<()> {
+ let body = try!(serde_json::to_string(&map));
+
+ verify(204, request!(Route::GuildsIdIntegrations,
+ post(body),
+ "/guilds/{}/integrations/{}",
+ guild_id,
+ integration_id))
+}
+
+pub fn create_invite(channel_id: u64, map: Value)
+ -> Result<RichInvite> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::ChannelsIdInvites,
+ post(body),
+ "/channels/{}/invites",
+ channel_id);
+
+ RichInvite::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn create_permission(channel_id: u64, target_id: u64, map: Value)
+ -> Result<()> {
+ let body = try!(serde_json::to_string(&map));
+
+ verify(204, request!(Route::ChannelsIdPermissionsOverwriteId,
+ put(body),
+ "/channels/{}/permissions/{}",
+ channel_id,
+ target_id))
+}
+
+pub fn create_private_channel(map: Value)
+ -> Result<PrivateChannel> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::UsersMeChannels,
+ post(body),
+ "/users/@me/channels");
+
+ PrivateChannel::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn create_role(guild_id: u64) -> Result<Role> {
+ let body = String::from("{}");
+ let response = request!(Route::GuildsIdRoles,
+ post(body),
+ "/guilds/{}/roles",
+ guild_id);
+
+ Role::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn delete_channel(guild_id: u64) -> Result<Channel> {
+ let response = request!(Route::ChannelsId,
+ delete,
+ "/channels/{}",
+ guild_id);
+
+ Channel::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn delete_emoji(guild_id: u64, emoji_id: u64) -> Result<()> {
+ verify(204, request!(Route::GuildsIdEmojisId,
+ delete,
+ "/guilds/{}/emojis/{}",
+ guild_id,
+ emoji_id))
+}
+
+pub fn delete_guild(guild_id: u64) -> Result<Guild> {
+ let response = request!(Route::GuildsId, delete, "/guilds/{}", guild_id);
+
+ Guild::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn delete_guild_integration(guild_id: u64, integration_id: u64)
+ -> Result<()> {
+ verify(204, request!(Route::GuildsIdIntegrationsId,
+ delete,
+ "/guilds/{}/integrations/{}",
+ guild_id,
+ integration_id))
+}
+
+pub fn delete_invite(code: &str) -> Result<Invite> {
+ let response = request!(Route::InvitesCode, delete, "/invite/{}", code);
+
+ Invite::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn delete_message(channel_id: u64, message_id: u64)
+ -> Result<()> {
+ verify(204, request!(Route::ChannelsIdMessagesId,
+ delete,
+ "/channels/{}/messages/{}",
+ channel_id,
+ message_id))
+}
+
+pub fn delete_messages(channel_id: u64, map: Value) -> Result<()> {
+ let body = try!(serde_json::to_string(&map));
+
+ verify(204, request!(Route::ChannelsIdMessagesBulkDelete,
+ post(body),
+ "/channels/{}/messages/bulk_delete",
+ channel_id))
+}
+
+pub fn delete_permission(channel_id: u64, target_id: u64)
+ -> Result<()> {
+ verify(204, request!(Route::ChannelsIdPermissionsOverwriteId,
+ delete,
+ "/channels/{}/permissions/{}",
+ channel_id,
+ target_id))
+}
+
+pub fn delete_role(guild_id: u64, role_id: u64) -> Result<()> {
+ verify(204, request!(Route::GuildsIdRolesId,
+ delete,
+ "/guilds/{}/roles/{}",
+ guild_id,
+ role_id))
+}
+
+pub fn edit_channel(channel_id: u64, map: Value)
+ -> Result<PublicChannel> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::ChannelsId,
+ patch(body),
+ "/channels/{}",
+ channel_id);
+
+ PublicChannel::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn edit_emoji(guild_id: u64, emoji_id: u64, map: Value)
+ -> Result<Emoji> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsIdEmojisId,
+ patch(body),
+ "/guilds/{}/emojis/{}",
+ guild_id,
+ emoji_id);
+
+ Emoji::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn edit_guild(guild_id: u64, map: Value) -> Result<Guild> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsId,
+ patch(body),
+ "/guilds/{}",
+ guild_id);
+
+ Guild::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn edit_member(guild_id: u64, user_id: u64, map: Value)
+ -> Result<()> {
+ let body = try!(serde_json::to_string(&map));
+
+ verify(204, request!(Route::GuildsIdMembersId,
+ patch(body),
+ "/guilds/{}/members/{}",
+ guild_id,
+ user_id))
+}
+
+pub fn edit_message(channel_id: u64,
+ message_id: u64,
+ map: Value)
+ -> Result<Message> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::ChannelsIdMessagesId,
+ patch(body),
+ "/channels/{}/messages/{}",
+ channel_id,
+ message_id);
+
+ Message::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn edit_note(user_id: u64, map: Value) -> Result<()> {
+ let body = try!(serde_json::to_string(&map));
+
+ verify(204, request!(Route::None,
+ put(body),
+ "/users/@me/notes/{}",
+ user_id))
+}
+
+pub fn edit_profile(map: Value) -> Result<CurrentUser> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::UsersMe, patch(body), "/users/@me");
+
+ CurrentUser::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn edit_role(guild_id: u64, role_id: u64, map: Value)
+ -> Result<Role> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsIdRolesId,
+ patch(body),
+ "/guilds/{}/roles/{}",
+ guild_id,
+ role_id);
+
+ Role::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_application_info() -> Result<CurrentApplicationInfo> {
+ let response = request!(Route::None, get, "/oauth2/applications/@me");
+
+ CurrentApplicationInfo::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_applications() -> Result<Vec<ApplicationInfo>> {
+ let response = request!(Route::None, get, "/oauth2/applications");
+ let decoded = try!(serde_json::from_reader(response));
+
+ decode_array(decoded, ApplicationInfo::decode)
+}
+
+pub fn get_bans(guild_id: u64) -> Result<Vec<Ban>> {
+ let response = request!(Route::GuildsIdBans,
+ get,
+ "/guilds/{}/bans",
+ guild_id);
+
+ decode_array(try!(serde_json::from_reader(response)), Ban::decode)
+}
+
+pub fn get_bot_gateway() -> Result<BotGateway> {
+ let response = request!(Route::Gateway, get, "/gateway/bot");
+
+ BotGateway::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_channel_invites(channel_id: u64)
+ -> Result<Vec<RichInvite>> {
+ let response = request!(Route::ChannelsIdInvites,
+ get,
+ "/channels/{}/invites",
+ channel_id);
+
+ decode_array(try!(serde_json::from_reader(response)),
+ RichInvite::decode)
+}
+
+pub fn get_channel(channel_id: u64) -> Result<Channel> {
+ let response = request!(Route::ChannelsId, get, "/channels/{}", channel_id);
+
+ Channel::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_channels(guild_id: u64) -> Result<Vec<PublicChannel>> {
+ let response = request!(Route::ChannelsId,
+ get,
+ "/guilds/{}/channels",
+ guild_id);
+
+ decode_array(try!(serde_json::from_reader(response)),
+ PublicChannel::decode)
+}
+
+pub fn get_current_user() -> Result<CurrentUser> {
+ let response = request!(Route::UsersMe, get, "/users/@me");
+
+ CurrentUser::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_gateway() -> Result<Gateway> {
+ let response = request!(Route::Gateway, get, "/gateway");
+
+ Gateway::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_emoji(guild_id: u64, emoji_id: u64) -> Result<Emoji> {
+ let response = request!(Route::GuildsIdEmojisId,
+ get,
+ "/guilds/{}/emojis/{}",
+ guild_id,
+ emoji_id);
+
+ Emoji::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_emojis(guild_id: u64) -> Result<Vec<Emoji>> {
+ let response = request!(Route::GuildsIdEmojis,
+ get,
+ "/guilds/{}/emojis",
+ guild_id);
+
+ decode_array(try!(serde_json::from_reader(response)), Emoji::decode)
+}
+
+pub fn get_guild(guild_id: u64) -> Result<Guild> {
+ let response = request!(Route::GuildsId, get, "/guilds/{}", guild_id);
+
+ Guild::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_guild_integrations(guild_id: u64)
+ -> Result<Vec<Integration>> {
+ let response = request!(Route::GuildsIdIntegrations,
+ get,
+ "/guilds/{}/integrations",
+ guild_id);
+
+ decode_array(try!(serde_json::from_reader(response)), Integration::decode)
+}
+
+pub fn get_guild_invites(guild_id: u64) -> Result<Vec<RichInvite>> {
+ let response = request!(Route::GuildsIdInvites,
+ get,
+ "/guilds/{}/invites",
+ guild_id);
+
+ decode_array(try!(serde_json::from_reader(response)),
+ RichInvite::decode)
+}
+
+pub fn get_guild_prune_count(guild_id: u64, map: Value)
+ -> Result<GuildPrune> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsIdPrune,
+ get(body),
+ "/guilds/{}/prune",
+ guild_id);
+
+ GuildPrune::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_guilds() -> Result<Vec<GuildInfo>> {
+ let response = request!(Route::GuildsId, get, "/users/@me/guilds");
+
+ decode_array(try!(serde_json::from_reader(response)), GuildInfo::decode)
+}
+
+pub fn get_invite(code: &str) -> Result<Invite> {
+ let invite = ::utils::parse_invite(code);
+ let response = request!(Route::InvitesCode, get, "/invite/{}", invite);
+
+ Invite::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_member(guild_id: u64, user_id: u64) -> Result<Member> {
+ let response = request!(Route::GuildsIdMembersId,
+ get,
+ "/guilds/{}/members/{}",
+ guild_id,
+ user_id);
+
+ Member::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_message(channel_id: u64, message_id: u64)
+ -> Result<Message> {
+ let response = request!(Route::ChannelsIdMessagesId,
+ get,
+ "/channels/{}/messages/{}",
+ channel_id,
+ message_id);
+
+ Message::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_messages(channel_id: u64, query: &str)
+ -> Result<Vec<Message>> {
+ let url = format!(api_concat!("/channels/{}/messages{}"),
+ channel_id,
+ query);
+ let client = HyperClient::new();
+ let response = try!(request(Route::ChannelsIdMessages,
+ || client.get(&url)));
+
+ decode_array(try!(serde_json::from_reader(response)), Message::decode)
+}
+
+pub fn get_pins(channel_id: u64) -> Result<Vec<Message>> {
+ let response = request!(Route::ChannelsIdPins,
+ get,
+ "/channels/{}/pins",
+ channel_id);
+
+ decode_array(try!(serde_json::from_reader(response)), Message::decode)
+}
+
+pub fn get_user(user_id: u64) -> Result<CurrentUser> {
+ let response = request!(Route::UsersId, get, "/users/{}", user_id);
+
+ CurrentUser::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn get_voice_regions() -> Result<Vec<VoiceRegion>> {
+ let response = request!(Route::VoiceRegions, get, "/voice/regions");
+
+ decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode)
+}
+
+pub fn kick_member(guild_id: u64, user_id: u64) -> Result<()> {
+ verify(204, request!(Route::GuildsIdMembersId,
+ delete,
+ "/guilds/{}/members/{}",
+ guild_id,
+ user_id))
+}
+
+pub fn leave_group(guild_id: u64) -> Result<Group> {
+ let response = request!(Route::None,
+ delete,
+ "/channels/{}",
+ guild_id);
+
+ Group::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn leave_guild(guild_id: u64) -> Result<Guild> {
+ let response = request!(Route::GuildsId, delete, "/guilds/{}", guild_id);
+
+ Guild::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn logout(map: Value) -> Result<()> {
+ let body = try!(serde_json::to_string(&map));
+
+ verify(204, request!(Route::None, post(body), "/auth/logout"))
+}
+
+pub fn remove_group_recipient(group_id: u64, user_id: u64)
+ -> Result<()> {
+ verify(204, request!(Route::None,
+ delete,
+ "/channels/{}/recipients/{}",
+ group_id,
+ user_id))
+}
+
+pub fn send_file<R: Read>(channel_id: u64,
+ content: &str,
+ mut file: R,
+ filename: &str)
+ -> Result<Message> {
+ let uri = format!(api_concat!("/channels/{}/messages"), channel_id);
+ let url = match Url::parse(&uri) {
+ Ok(url) => url,
+ Err(_why) => return Err(Error::Url(uri)),
+ };
+
+ let mut request = try!(Request::new(Method::Post, url));
+ request.headers_mut().set(header::Authorization(TOKEN.lock().unwrap().clone()));
+ request.headers_mut()
+ .set(header::UserAgent(constants::USER_AGENT.to_owned()));
+
+ let mut request = try!(Multipart::from_request(request));
+ try!(request.write_text("content", content));
+ try!(request.write_stream("file", &mut file, Some(&filename), None));
+
+ Message::decode(try!(serde_json::from_reader(try!(request.send()))))
+}
+
+pub fn send_message(channel_id: u64, map: Value) -> Result<Message> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::ChannelsIdMessages,
+ post(body),
+ "/channels/{}/messages",
+ channel_id);
+
+ Message::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn pin_message(channel_id: u64, message_id: u64) -> Result<()> {
+ verify(204, request!(Route::ChannelsIdPinsMessageId,
+ put,
+ "/channels/{}/pins/{}",
+ channel_id,
+ message_id))
+}
+
+pub fn remove_ban(guild_id: u64, user_id: u64) -> Result<()> {
+ verify(204, request!(Route::GuildsIdBansUserId,
+ delete,
+ "/guilds/{}/bans/{}",
+ guild_id,
+ user_id))
+}
+
+pub fn start_guild_prune(guild_id: u64, map: Value)
+ -> Result<GuildPrune> {
+ let body = try!(serde_json::to_string(&map));
+ let response = request!(Route::GuildsIdPrune,
+ post(body),
+ "/guilds/{}/prune",
+ guild_id);
+
+ GuildPrune::decode(try!(serde_json::from_reader(response)))
+}
+
+pub fn start_integration_sync(guild_id: u64, integration_id: u64)
+ -> Result<()> {
+ verify(204, request!(Route::GuildsIdIntegrationsId,
+ post,
+ "/guilds/{}/integrations/{}",
+ guild_id,
+ integration_id))
+}
+
+pub fn unpin_message(channel_id: u64, message_id: u64) -> Result<()> {
+ verify(204, request!(Route::ChannelsIdPinsMessageId,
+ delete,
+ "/channels/{}/pins/{}",
+ channel_id,
+ message_id))
+}
+
+fn request<'a, F>(route: Route, f: F) -> Result<HyperResponse>
+ where F: Fn() -> RequestBuilder<'a> {
+ ratelimiting::perform(route, || f()
+ .header(header::Authorization(TOKEN.lock().unwrap().clone()))
+ .header(header::ContentType::json()))
+}
+
+#[doc(hidden)]
+pub fn retry<'a, F>(f: F) -> HyperResult<HyperResponse>
+ where F: Fn() -> RequestBuilder<'a> {
+ let req = || f()
+ .header(header::UserAgent(constants::USER_AGENT.to_owned()))
+ .send();
+
+ match req() {
+ Err(HyperError::Io(ref io))
+ if io.kind() == IoErrorKind::ConnectionAborted => req(),
+ other => other,
+ }
+}
+
+fn verify(expected_status_code: u16,
+ mut response: HyperResponse)
+ -> Result<()> {
+ let expected_status = match expected_status_code {
+ 204 => StatusCode::NoContent,
+ 401 => StatusCode::Unauthorized,
+ _ => {
+ let client_error = ClientError::UnknownStatus(expected_status_code);
+
+ return Err(Error::Client(client_error));
+ },
+ };
+
+ if response.status == expected_status {
+ return Ok(());
+ }
+
+ debug!("Expected {}, got {}", expected_status_code, response.status);
+
+ let mut s = String::default();
+ try!(response.read_to_string(&mut s));
+
+ debug!("Content: {}", s);
+
+ Err(Error::Client(ClientError::UnexpectedStatusCode(response.status)))
+}
diff --git a/src/client/login_type.rs b/src/client/login_type.rs
new file mode 100644
index 0000000..e62f19a
--- /dev/null
+++ b/src/client/login_type.rs
@@ -0,0 +1,5 @@
+#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug, Ord, PartialOrd)]
+pub enum LoginType {
+ Bot,
+ User,
+}
diff --git a/src/client/mod.rs b/src/client/mod.rs
new file mode 100644
index 0000000..8e291fb
--- /dev/null
+++ b/src/client/mod.rs
@@ -0,0 +1,973 @@
+//! The Client contains information about a single bot or user's "session" with
+//! Discord. Event handers and starting the connection are handled directly via
+//! the client. In addition, the [http module] and [`State`] are also
+//! automatically handled by the Client module for you.
+//!
+//! A [`Context`] is provided for every handler. The
+//! context is an ergonomic way of accessing the lower-level Http struct's
+//! methods.
+//!
+//! The Http struct is the lower-level method of accessing the Discord REST API.
+//! Realistically there should be little reason to use this yourself, as the
+//! Context will do this for you. A possible use case of using the Http struct
+//! is if you do not have a state for purposes such as low memory requirements.
+//!
+//! Creating a Client instance and adding a handler on every message
+//! receive, acting as a "ping-pong" bot is simple:
+//!
+//! ```rust,ignore
+//! use serenity::Client;
+//!
+//! let client = Client::login_bot("my token here");
+//!
+//! client.on_message(|context, message| {
+//! if message.content == "!ping" {
+//! context.say("Pong!");
+//! }
+//! });
+//!
+//! client.start();
+//! ```
+//!
+//! [`Context`]: struct.Context.html
+//! [`State`]: ext/state/index.html
+//! [http module]: client/http/index.html
+
+pub mod http;
+
+mod connection;
+mod context;
+mod dispatch;
+mod event_store;
+mod login_type;
+mod ratelimiting;
+
+pub use self::connection::{Connection, ConnectionError};
+pub use self::context::Context;
+
+use hyper::status::StatusCode;
+use self::dispatch::dispatch;
+use self::event_store::EventStore;
+use self::login_type::LoginType;
+use serde_json::builder::ObjectBuilder;
+use std::collections::{BTreeMap, HashMap};
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::Duration;
+use ::model::*;
+use ::prelude::*;
+use ::ext::framework::Framework;
+use ::ext::state::State;
+
+lazy_static! {
+ /// The STATE is a mutable lazily-initialized static binding. It can be
+ /// accessed across any function and in any context.
+ ///
+ /// This [`State`] instance is updated for every event received, so you do
+ /// not need to maintain your own state.
+ ///
+ /// See the [state module documentation] for more details.
+ ///
+ /// # Examples
+ ///
+ /// Retrieve the [current user][`CurrentUser`]'s Id:
+ ///
+ /// ```rust,ignore
+ /// use serenity::client::STATE;
+ ///
+ /// println!("{}", STATE.lock().unwrap().user.id);
+ /// ```
+ ///
+ /// [`CurrentUser`]: ../model/struct.CurrentUser.html
+ /// [`State`]: ../ext/state/struct.State.html
+ /// [state module documentation]: ../ext/state/index.html
+ pub static ref STATE: Arc<Mutex<State>> = Arc::new(Mutex::new(State::default()));
+}
+
+/// An error returned from the [`Client`] or the [`Context`], or model instance.
+///
+/// This is always wrapped within the library's generic [`Error::Client`]
+/// variant.
+///
+/// # Examples
+///
+/// Matching an [`Error`] with this variant may look something like the
+/// following for the [`Context::ban_user`] method:
+///
+/// ```rust,ignore
+/// use serenity::client::ClientError;
+/// use serenity::Error;
+///
+/// match context.ban_user(context.guild_id, context.message.author, 8) {
+/// Ok(()) => {
+/// // Ban successful.
+/// },
+/// Err(Error::Client(ClientError::DeleteMessageDaysAmount(amount)) => {
+/// println!("Tried deleting {} days' worth of messages", amount);
+/// },
+/// Err(why) => {
+/// println!("Unexpected error: {:?}", why);
+/// },
+/// }
+/// ```
+///
+/// [`Client`]: struct.Client.html
+/// [`Context`]: struct.Context.html
+/// [`Context::ban_user`]: struct.Context.html#method.ban_user
+/// [`Error::Client`]: ../enum.Error.html#Client.v
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub enum ClientError {
+ /// When attempting to delete below or above the minimum and maximum allowed
+ /// number of messages.
+ BulkDeleteAmount,
+ /// When the connection being retrieved from within the Client could not be
+ /// found after being inserted into the Client's internal vector of
+ /// [`Connection`]s.
+ ///
+ /// This can be returned from one of the options for starting one or
+ /// multiple connections.
+ ///
+ /// **This should never be received.**
+ ///
+ /// [`Connection`]: struct.Connection.html
+ ConnectionUnknown,
+ /// When attempting to delete a number of days' worth of messages that is
+ /// not allowed.
+ DeleteMessageDaysAmount(u8),
+ /// When there was an error retrieving the gateway URI from the REST API.
+ Gateway,
+ /// An indication that a [guild][`LiveGuild`] could not be found by
+ /// [Id][`GuildId`] in the [`State`].
+ ///
+ /// [`GuildId`]: ../model/struct.GuildId.html
+ /// [`LiveGuild`]: ../model/struct.LiveGuild.html
+ /// [`State`]: ../ext/state/struct.State.html
+ GuildNotFound,
+ /// When attempting to perform an action which is only available to user
+ /// accounts.
+ InvalidOperationAsBot,
+ /// When attempting to perform an action which is only available to bot
+ /// accounts.
+ InvalidOperationAsUser,
+ /// Indicates that you do not have the required permissions to perform an
+ /// operation.
+ ///
+ /// The provided [`Permission`]s is the set of required permissions
+ /// required.
+ ///
+ /// [`Permission`]: ../model/permissions/struct.Permissions.html
+ InvalidPermissions(Permissions),
+ /// An indicator that the shard data received from the gateway is invalid.
+ InvalidShards,
+ /// When the token provided is invalid. This is returned when validating a
+ /// token through the [`validate_token`] function.
+ ///
+ /// [`validate_token`]: fn.validate_token.html
+ InvalidToken,
+ /// An indicator that the [current user] can not perform an action.
+ ///
+ /// [current user]: ../model/struct.CurrentUser.html
+ InvalidUser,
+ /// An indicator that an item is missing from the [`State`], and the action
+ /// can not be continued.
+ ///
+ /// [`State`]: ../ext/state/struct.State.html
+ ItemMissing,
+ /// When attempting to use a [`Context`] helper method which requires a
+ /// contextual [`ChannelId`], but the current context is not appropriate for
+ /// the action.
+ ///
+ /// [`ChannelId`]: ../model/struct.ChannelId.html
+ /// [`Context`]: struct.Context.html
+ NoChannelId,
+ /// When the decoding of a ratelimit header could not be properly decoded
+ /// into an `i64`.
+ RateLimitI64,
+ /// When the decoding of a ratelimit header could not be properly decoded
+ /// from UTF-8.
+ RateLimitUtf8,
+ /// When attempting to find a required record from the State could not be
+ /// found. This is required in methods such as
+ /// [Context::edit_role](struct.Context.html#method.edit_role).
+ RecordNotFound,
+ /// When a function such as [`Context::edit_channel`] did not expect the
+ /// received [`ChannelType`].
+ ///
+ /// [`ChannelType`]: ../model/enum.ChannelType.html
+ /// [`Context::edit_channel`]: struct.Context.html#method.edit_channel
+ UnexpectedChannelType(ChannelType),
+ /// When a status code was unexpectedly received for a request's status.
+ UnexpectedStatusCode(StatusCode),
+ /// When a status is received, but the verification to ensure the response
+ /// is valid does not recognize the status.
+ UnknownStatus(u16),
+}
+
+pub struct Client {
+ pub connections: Vec<Arc<Mutex<Connection>>>,
+ event_store: Arc<Mutex<EventStore>>,
+ framework: Arc<Mutex<Framework>>,
+ login_type: LoginType,
+ token: String,
+}
+
+#[allow(type_complexity)]
+impl Client {
+ /// Creates a Client for a bot.
+ pub fn login_bot(bot_token: &str) -> Client {
+ let token = format!("Bot {}", bot_token);
+
+ login(&token, LoginType::Bot)
+ }
+ /// Create an instance from "raw values"
+ #[doc(hidden)]
+ pub fn login_raw(token: &str, login_type: LoginType) -> Client {
+ login(&token.to_owned(), login_type)
+ }
+
+ /// Creates a Client for a user.
+ pub fn login_user(user_token: &str) -> Client {
+ login(&user_token.to_owned(), LoginType::User)
+ }
+
+ /// Logout from the Discord API. This theoretically is supposed to
+ /// invalidate the current token, but currently does not do anything. This
+ /// is an issue on Discord's side.
+ ///
+ /// **Note**: This can only be used by users.
+ pub fn logout(self) -> Result<()> {
+ if self.login_type == LoginType::Bot {
+ return Err(Error::Client(ClientError::InvalidOperationAsBot));
+ }
+
+ let map = ObjectBuilder::new()
+ .insert("provider", Value::Null)
+ .insert("token", Value::Null)
+ .build();
+
+ http::logout(map)
+ }
+
+ /// Sets a framework to be used with the client. All message events will be
+ /// passed through the framework _after_ being passed to the [`on_message`]
+ /// event handler.
+ ///
+ /// See the [framework module-level documentation][framework docs] for more
+ /// information on usage.
+ ///
+ /// [`on_message`]: #method.on_message
+ /// [framework docs]: ../ext/framework/index.html
+ pub fn with_framework<F>(&mut self, f: F)
+ where F: FnOnce(Framework) -> Framework + Send + Sync + 'static {
+ self.framework = Arc::new(Mutex::new(f(Framework::default())));
+ }
+
+ /// Establish the connection and start listening for events.
+ ///
+ /// This will start receiving events in a loop and start dispatching the
+ /// events to your registered handlers.
+ ///
+ /// Note that this should be used only for users and for bots which are in
+ /// less than 2500 guilds. If you have a reason for sharding and/or are in
+ /// more than 2500 guilds, use one of these depending on your use case:
+ ///
+ /// Refer to the [module-level documentation][connection docs] for more
+ /// information on effectively using sharding.
+ ///
+ /// [connection docs]: struct.Connection.html#sharding
+ pub fn start(&mut self) -> Result<()> {
+ self.start_connection(None)
+ }
+
+ /// Establish the connection(s) and start listening for events.
+ ///
+ /// This will start receiving events in a loop and start dispatching the
+ /// events to your registered handlers.
+ ///
+ /// This will retrieve an automatically determined number of shards to use
+ /// from the API - determined by Discord - and then open a number of shards
+ /// equivilant to that amount.
+ ///
+ /// Refer to the [module-level documentation][connection docs] for more
+ /// information on effectively using sharding.
+ ///
+ /// [connection docs]: struct.Connection.html#sharding
+ pub fn start_autosharded(&mut self) -> Result<()> {
+ let res = try!(http::get_bot_gateway());
+
+ self.start_connection(Some([0, res.shards as u8 - 1, res.shards as u8]))
+ }
+
+ /// Establish a sharded connection and start listening for events.
+ ///
+ /// This will start receiving events and dispatch them to your registered
+ /// handlers.
+ ///
+ /// This will create a single shard by ID. If using one shard per process,
+ /// you will need to start other processes with the other shard IDs in some
+ /// way.
+ ///
+ /// Refer to the [module-level documentation][connection docs] for more
+ /// information on effectively using sharding.
+ ///
+ /// [connection docs]: struct.Connection.html#sharding
+ pub fn start_shard(&mut self, shard: u8, shards: u8) -> Result<()> {
+ self.start_connection(Some([shard, shard, shards]))
+ }
+
+ /// Establish sharded connections and start listening for events.
+ ///
+ /// This will start receiving events and dispatch them to your registered
+ /// handlers.
+ ///
+ /// This will create and handle all shards within this single process. If
+ /// you only need to start a single shard within the process, or a range of
+ /// shards, use [`start_shard`] or [`start_shard_range`], respectively.
+ ///
+ /// Refer to the [module-level documentation][connection docs] for more
+ /// information on effectively using sharding.
+ ///
+ /// [`start_shard`]: #method.start_shard
+ /// [`start_shard_range`]: #method.start_shards
+ /// [connection docs]: struct.Connection.html#sharding
+ pub fn start_shards(&mut self, total_shards: u8) -> Result<()> {
+ self.start_connection(Some([0, total_shards - 1, total_shards]))
+ }
+
+ /// Establish a range of sharded connections and start listening for events.
+ ///
+ /// This will start receiving events and dispatch them to your registered
+ /// handlers.
+ ///
+ /// This will create and handle all shards within a given range within this
+ /// single process. If you only need to start a single shard within the
+ /// process, or all shards within the process, use [`start_shard`] or
+ /// [`start_shards`], respectively.
+ ///
+ /// Refer to the [module-level documentation][connection docs] for more
+ /// information on effectively using sharding.
+ ///
+ /// # Examples
+ ///
+ /// For a bot using a total of 10 shards, initialize shards 4 through 7:
+ ///
+ /// ```rust,ignore
+ /// // assumes a `client` has already been initialized
+ /// let _ = client.start_shard_range([4, 7], 10);
+ /// ```
+ ///
+ /// [`start_shard`]: #method.start_shard
+ /// [`start_shards`]: #method.start_shards
+ /// [connection docs]: struct.Connection.html#sharding
+ pub fn start_shard_range(&mut self, range: [u8; 2], total_shards: u8)
+ -> Result<()> {
+ self.start_connection(Some([range[0], range[1], total_shards]))
+ }
+
+ /// Attaches a handler for when a [`CallCreate`] is received.
+ ///
+ /// [`CallCreate`]: ../model/enum.Event.html#CallCreate.v
+ pub fn on_call_create<F>(&mut self, handler: F)
+ where F: Fn(Context, Call) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_call_create = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`CallDelete`] is received.
+ ///
+ /// [`CallDelete`]: ../model/enum.Event.html#CallDelete.v
+ pub fn on_call_delete<F>(&mut self, handler: F)
+ where F: Fn(Context, Option<Call>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_call_delete = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`CallUpdate`] is received.
+ ///
+ /// [`CallUpdate`]: ../model/enum.Event.html#CallUpdate.v
+ pub fn on_call_update<F>(&mut self, handler: F)
+ where F: Fn(Context, Option<Call>, Option<Call>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_call_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelCreate`] is received.
+ ///
+ /// [`ChannelCreate`]: ../model/enum.Event.html#ChannelCreate.v
+ pub fn on_channel_create<F>(&mut self, handler: F)
+ where F: Fn(Context, Channel) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_create = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelDelete`] is received.
+ ///
+ /// [`ChannelDelete`]: ../model/enum.Event.html#ChannelDelete.v
+ pub fn on_channel_delete<F>(&mut self, handler: F)
+ where F: Fn(Context, Channel) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_delete = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelPinsAck`] is received.
+ ///
+ /// [`ChannelPinsAck`]: ../model/enum.Event.html#ChannelPinsAck.v
+ pub fn on_channel_pins_ack<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelPinsAckEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_pins_ack = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelPinsUpdate`] is received.
+ ///
+ /// [`ChannelPinsUpdate`]: ../model/enum.Event.html#ChannelPinsUpdate.v
+ pub fn on_channel_pins_update<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelPinsUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_pins_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelUpdate`] is received.
+ ///
+ /// [`ChannelUpdate`]: ../model/enum.Event.html#ChannelUpdate.v
+ pub fn on_channel_update<F>(&mut self, handler: F)
+ where F: Fn(Context, Option<Channel>, Channel) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildCreate`] is received.
+ ///
+ /// [`GuildCreate`]: ../model/enum.Event.html#GuildCreate.v
+ pub fn on_guild_create<F>(&mut self, handler: F)
+ where F: Fn(Context, LiveGuild) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_create = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuilDelete`] is received.
+ ///
+ /// [`GuilDelete`]: ../model/enum.Event.html#GuilDelete.v
+ pub fn on_guild_delete<F>(&mut self, handler: F)
+ where F: Fn(Context, Guild) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_delete = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildEmojisUpdate`] is received.
+ ///
+ /// [`GuildEmojisUpdate`]: ../model/enum.Event.html#GuildEmojisUpdate.v
+ pub fn on_guild_emojis_update<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildEmojisUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_emojis_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildIntegrationsUpdate`] is received.
+ ///
+ /// [`GuildIntegrationsUpdate`]: ../model/enum.Event.html#GuildIntegrationsUpdate.v
+ pub fn on_guild_integrations_update<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildIntegrationsUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_integrations_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildMemberAdd`] is received.
+ ///
+ /// [`GuildMemberAdd`]: ../model/enum.Event.html#GuildMemberAdd.v
+ pub fn on_guild_member_add<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, Member) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_member_addition = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildMemberRemove`] is received.
+ ///
+ /// [`GuildMemberRemove`]: ../model/enum.Event.html#GuildMemberRemove.v
+ pub fn on_guild_member_remove<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, User) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_member_removal = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildMemberUpdate`] is received.
+ ///
+ /// [`GuildMemberUpdate`]: ../model/enum.Event.html#GuildMemberUpdate.v
+ pub fn on_guild_member_update<F>(&mut self, handler: F)
+ where F: Fn(Context, Option<Member>, Member) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_member_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildMembersChunk`] is received.
+ ///
+ /// [`GuildMembersChunk`]: ../model/enum.Event.html#GuildMembersChunk.v
+ pub fn on_guild_members_chunk<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, HashMap<UserId, Member>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_members_chunk = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildRoleCreate`] is received.
+ ///
+ /// [`GuildRoleCreate`]: ../model/enum.Event.html#GuildRoleCreate.v
+ pub fn on_guild_role_create<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, Role) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_role_create = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildRoleDelete`] is received.
+ ///
+ /// [`GuildRoleDelete`]: ../model/enum.Event.html#GuildRoleDelete.v
+ pub fn on_guild_role_delete<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, RoleId) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_role_delete = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildRoleUpdate`] is received.
+ ///
+ /// [`GuildRoleUpdate`]: ../model/enum.Event.html#GuildRoleUpdate.v
+ pub fn on_guild_role_update<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, Role) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_role_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildRoleSync`] is received.
+ ///
+ /// [`GuildRoleSync`]: ../model/enum.Event.html#GuildRoleSync.v
+ pub fn on_guild_sync<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildSyncEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_sync = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildUnavailable`] is received.
+ ///
+ /// [`GuildUnavailable`]: ../model/enum.Event.html#GuildUnavailable.v
+ pub fn on_guild_unavailable<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_unavailable = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildUpdate`] is received.
+ ///
+ /// [`GuildUpdate`]: ../model/enum.Event.html#GuildUpdate.v
+ pub fn on_guild_update<F>(&mut self, handler: F)
+ where F: Fn(Context, Option<LiveGuild>, Guild) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildBan`] is received.
+ ///
+ /// [`GuildBan`]: ../model/enum.Event.html#GuildBan.v
+ pub fn on_member_ban<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, User) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_ban_addition = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`GuildUnban`] is received.
+ ///
+ /// [`GuildUnban`]: ../model/enum.Event.html#GuildUnban.v
+ pub fn on_member_unban<F>(&mut self, handler: F)
+ where F: Fn(Context, GuildId, User) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_guild_ban_removal = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`MessageCreate`] is received.
+ ///
+ /// [`MessageCreate`]: ../model/enum.Event.html#MessageCreate.v
+ pub fn on_message<F>(&mut self, handler: F)
+ where F: Fn(Context, Message) + Send + Sync + 'static {
+
+ self.event_store.lock()
+ .unwrap()
+ .on_message = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`MessageAck`] is received.
+ ///
+ /// [`MessageAck`]: ../model/enum.Event.html#MessageAck.v
+ pub fn on_message_ack<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelId, Option<MessageId>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_message_ack = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`MessageDelete`] is received.
+ ///
+ /// [`MessageDelete`]: ../model/enum.Event.html#MessageDelete.v
+ pub fn on_message_delete<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelId, MessageId) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_message_delete = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`MessageDeleteBulk`] is received.
+ ///
+ /// [`MessageDeleteBulk`]: ../model/enum.Event.html#MessageDeleteBulk.v
+ pub fn on_message_delete_bulk<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelId, Vec<MessageId>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_message_delete_bulk = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`MessageUpdate`] is received.
+ ///
+ /// [`MessageUpdate`]: ../model/enum.Event.html#MessageUpdate.v
+ pub fn on_message_update<F>(&mut self, handler: F)
+ where F: Fn(Context, MessageUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_message_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`UserNoteUpdate`] is received.
+ ///
+ /// [`UserNoteUpdate`]: ../model/enum.Event.html#UserNoteUpdate.v
+ pub fn on_note_update<F>(&mut self, handler: F)
+ where F: Fn(Context, UserId, String) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_note_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`PresencesReplace`] is received.
+ ///
+ /// [`PresencesReplace`]: ../model/enum.Event.html#PresencesReplace.v
+ pub fn on_presence_replace<F>(&mut self, handler: F)
+ where F: Fn(Context, Vec<Presence>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_presence_replace = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`PresenceUpdate`] is received.
+ ///
+ /// [`PresenceUpdate`]: ../model/enum.Event.html#PresenceUpdate.v
+ pub fn on_presence_update<F>(&mut self, handler: F)
+ where F: Fn(Context, PresenceUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_presence_update = Some(Arc::new(handler));
+ }
+
+ /// Register an event to be called whenever a Ready event is received.
+ ///
+ /// Registering a handler for the ready event is good for noting when your
+ /// bot has established a connection to the gateway.
+ ///
+ /// **Note**: The Ready event is not guarenteed to be the first event you
+ /// will receive by Discord. Do not actively rely on it.
+ ///
+ /// # Examples
+ ///
+ /// Print the [current user][`CurrentUser`]'s name on ready:
+ ///
+ /// ```rust,ignore
+ /// // assuming a `client` has been bound
+ /// client.on_ready(|_context, ready| {
+ /// println!("{} is connected", ready.user.name);
+ /// });
+ /// ```
+ ///
+ /// [`CurrentUser`]: ../model/struct.CurrentUser.html
+ pub fn on_ready<F>(&mut self, handler: F)
+ where F: Fn(Context, Ready) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_ready = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelRecipientAdd`] is received.
+ ///
+ /// [`ChannelRecipientAdd`]: ../model/enum.Event.html#ChannelRecipientAdd.v
+ pub fn on_recipient_add<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelId, User) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_recipient_addition = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`ChannelRecipientRemove`] is received.
+ ///
+ /// [`ChannelRecipientRemove`]: ../model/enum.Event.html#ChannelRecipientRemove.v
+ pub fn on_recipient_remove<F>(&mut self, handler: F)
+ where F: Fn(Context, ChannelId, User) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_channel_recipient_removal = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`RelationshipAdd`] is received.
+ ///
+ /// [`RelationshipAdd`]: ../model/enum.Event.html#RelationshipAdd.v
+ pub fn on_relationship_add<F>(&mut self, handler: F)
+ where F: Fn(Context, Relationship) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_relationship_addition = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`RelationshipRemove`] is received.
+ ///
+ /// [`RelationshipRemove`]: ../model/enum.Event.html#RelationshipRemove.v
+ pub fn on_relationship_remove<F>(&mut self, handler: F)
+ where F: Fn(Context, UserId, RelationshipType) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_relationship_removal = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`Resumed`] is received.
+ ///
+ /// [`Resumed`]: ../model/enum.Event.html#Resumed.v
+ pub fn on_resume<F>(&mut self, handler: F)
+ where F: Fn(Context, ResumedEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_resume = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`TypingStart`] is received.
+ ///
+ /// [`TypingStart`]: ../model/enum.Event.html#TypingStart.v
+ pub fn on_typing_start<F>(&mut self, handler: F)
+ where F: Fn(Context, TypingStartEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_typing_start = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when an [`Unknown`] is received.
+ ///
+ /// [`Unknown`]: ../model/enum.Event.html#Unknown.v
+ pub fn on_unknown<F>(&mut self, handler: F)
+ where F: Fn(Context, String, BTreeMap<String, Value>) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_unknown = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`UserGuildSettingsUpdate`] is received.
+ ///
+ /// [`UserGuildSettingsUpdate`]: ../model/enum.Event.html#UserGuildSettingsUpdate.v
+ pub fn on_user_guild_settings_update<F>(&mut self, handler: F)
+ where F: Fn(Context, Option<UserGuildSettings>, UserGuildSettings) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_user_guild_settings_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`UserUpdate`] is received.
+ ///
+ /// [`UserUpdate`]: ../model/enum.Event.html#UserUpdate.v
+ pub fn on_user_update<F>(&mut self, handler: F)
+ where F: Fn(Context, CurrentUser, CurrentUser) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_user_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`UserSettingsUpdate`] is received.
+ ///
+ /// [`UserSettingsUpdate`]: ../model/enum.Event.html#UserSettingsUpdate.v
+ pub fn on_user_settings_update<F>(&mut self, handler: F)
+ where F: Fn(Context, UserSettings, UserSettings) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_user_settings_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`VoiceStateUpdate`] is received.
+ ///
+ /// [`VoiceStateUpdate`]: ../model/enum.Event.html#VoiceStateUpdate.v
+ pub fn on_voice_state_update<F>(&mut self, handler: F)
+ where F: Fn(Context, VoiceStateUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_voice_state_update = Some(Arc::new(handler));
+ }
+
+ /// Attaches a handler for when a [`VoiceServerUpdate`] is received.
+ ///
+ /// [`VoiceServerUpdate`]: ../model/enum.Event.html#VoiceServerUpdate.v
+ pub fn on_voice_server_update<F>(&mut self, handler: F)
+ where F: Fn(Context, VoiceServerUpdateEvent) + Send + Sync + 'static {
+ self.event_store.lock()
+ .unwrap()
+ .on_voice_server_update = Some(Arc::new(handler));
+ }
+
+ // Shard data layout is:
+ // 0: first shard number to initialize
+ // 1: shard number to initialize up to and including
+ // 2: total number of shards the bot is sharding for
+ //
+ // Not all shards need to be initialized in this process.
+ fn start_connection(&mut self, shard_data: Option<[u8; 3]>) -> Result<()> {
+ let gateway_url = try!(http::get_gateway()).url;
+
+ for i in 0..shard_data.map_or(1, |x| x[1] + 1) {
+ let connection = Connection::new(&gateway_url,
+ &self.token,
+ shard_data.map(|s| [i, s[2]]),
+ self.login_type);
+ match connection {
+ Ok((connection, ready)) => {
+ self.connections.push(Arc::new(Mutex::new(connection)));
+
+ STATE.lock()
+ .unwrap()
+ .update_with_ready(&ready);
+
+ match self.connections.last() {
+ Some(connection) => {
+ dispatch(Ok(Event::Ready(ready)),
+ connection.clone(),
+ self.framework.clone(),
+ self.login_type,
+ self.event_store.clone());
+
+ let connection_clone = connection.clone();
+ let event_store = self.event_store.clone();
+ let framework = self.framework.clone();
+ let login_type = self.login_type;
+ thread::spawn(move || {
+ handle_connection(connection_clone,
+ framework,
+ login_type,
+ event_store);
+ });
+ },
+ None => return Err(Error::Client(ClientError::ConnectionUnknown)),
+ }
+ },
+ Err(why) => return Err(why),
+ }
+ }
+
+ // How to avoid the problem while still working on other parts of the
+ // library 101
+ loop {
+ thread::sleep(Duration::from_secs(1));
+ }
+ }
+
+ // Boot up a new connection. This is used primarily in the scenario of
+ // re-instantiating a connection in the reconnect logic in another
+ // Connection.
+ #[doc(hidden)]
+ pub fn boot_connection(&mut self,
+ shard_info: Option<[u8; 2]>)
+ -> Result<(Connection, ReadyEvent)> {
+ let gateway_url = try!(http::get_gateway()).url;
+
+ Connection::new(&gateway_url, &self.token, shard_info, self.login_type)
+ }
+}
+
+fn handle_connection(connection: Arc<Mutex<Connection>>,
+ framework: Arc<Mutex<Framework>>,
+ login_type: LoginType,
+ event_store: Arc<Mutex<EventStore>>) {
+ loop {
+ let event = {
+ let mut connection = connection.lock().unwrap();
+
+ connection.receive()
+ };
+
+ dispatch(event,
+ connection.clone(),
+ framework.clone(),
+ login_type,
+ event_store.clone());
+ }
+}
+
+fn login(token: &str, login_type: LoginType) -> Client {
+ let token = token.to_owned();
+
+ http::set_token(&token);
+
+ Client {
+ connections: Vec::default(),
+ event_store: Arc::new(Mutex::new(EventStore::default())),
+ framework: Arc::new(Mutex::new(Framework::default())),
+ login_type: login_type,
+ token: token.to_owned(),
+ }
+}
+
+/// Validates that a token is likely in a valid format.
+///
+/// This performs the following checks on a given token:
+///
+/// - At least one character long;
+/// - Contains 3 parts (split by the period char `'.'`);
+/// - The second part of the token is at least 6 characters long;
+/// - The token does not contain any whitespace prior to or after the token.
+///
+/// # Errors
+///
+/// Returns a
+/// [ClientError::InvalidToken](enum.ClientError.html#InvalidToken.v) when one
+/// of the above checks fail. The type of failure is not specified.
+pub fn validate_token(token: &str) -> Result<()> {
+ if token.is_empty() {
+ return Err(Error::Client(ClientError::InvalidToken));
+ }
+
+ let parts: Vec<&str> = token.split('.').collect();
+
+ // Check that the token has a total of 3 parts.
+ if parts.len() != 3 {
+ return Err(Error::Client(ClientError::InvalidToken));
+ }
+
+ // Check that the second part is at least 6 characters long.
+ if parts.get(1).unwrap().len() < 6 {
+ return Err(Error::Client(ClientError::InvalidToken));
+ }
+
+ // Check that there is no whitespace before/after the token.
+ if token.trim() != token {
+ return Err(Error::Client(ClientError::InvalidToken));
+ }
+
+ Ok(())
+}
diff --git a/src/client/ratelimiting.rs b/src/client/ratelimiting.rs
new file mode 100644
index 0000000..f5f7cdb
--- /dev/null
+++ b/src/client/ratelimiting.rs
@@ -0,0 +1,184 @@
+use hyper::client::{RequestBuilder, Response};
+use hyper::header::Headers;
+use hyper::status::StatusCode;
+use std::collections::HashMap;
+use std::str;
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::Duration;
+use super::http;
+use time;
+use ::prelude::*;
+
+lazy_static! {
+ static ref GLOBAL: Arc<Mutex<RateLimit>> = Arc::new(Mutex::new(RateLimit::default()));
+ static ref ROUTES: Arc<Mutex<HashMap<Route, RateLimit>>> = Arc::new(Mutex::new(HashMap::default()));
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub enum Route {
+ ChannelsId,
+ ChannelsIdInvites,
+ ChannelsIdMessages,
+ ChannelsIdMessagesBulkDelete,
+ ChannelsIdMessagesId,
+ ChannelsIdPermissionsOverwriteId,
+ ChannelsIdPins,
+ ChannelsIdPinsMessageId,
+ ChannelsIdTyping,
+ Gateway,
+ Global,
+ Guilds,
+ GuildsId,
+ GuildsIdBans,
+ GuildsIdBansUserId,
+ GuildsIdChannels,
+ GuildsIdEmbed,
+ GuildsIdEmojis,
+ GuildsIdEmojisId,
+ GuildsIdIntegrations,
+ GuildsIdIntegrationsId,
+ GuildsIdIntegrationsIdSync,
+ GuildsIdInvites,
+ GuildsIdMembers,
+ GuildsIdMembersId,
+ GuildsIdPrune,
+ GuildsIdRegions,
+ GuildsIdRoles,
+ GuildsIdRolesId,
+ InvitesCode,
+ Users,
+ UsersId,
+ UsersMe,
+ UsersMeChannels,
+ USersMeConnections,
+ UsersMeGuilds,
+ UsersMeGuildsId,
+ VoiceRegions,
+ None,
+}
+
+pub fn perform<'a, F>(route: Route, f: F) -> Result<Response>
+ where F: Fn() -> RequestBuilder<'a> {
+ // Keeping the global lock poisoned here for the duration of the function
+ // will ensure that requests are synchronous, which will further ensure
+ // that 429s are _never_ hit.
+ //
+ // This would otherwise cause the potential for 429s to be hit while
+ // requests are open.
+ let mut global = GLOBAL.lock().expect("global route lock poisoned");
+
+ loop {
+ // Perform pre-checking here:
+ //
+ // - get the route's relevant rate
+ // - sleep if that route's already rate-limited until the end of the
+ // 'reset' time;
+ // - get the global rate;
+ // - sleep if there is 0 remaining
+ // - then, perform the request
+ global.pre_hook();
+
+ if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) {
+ route.pre_hook();
+ }
+
+ let response = try!(http::retry(&f));
+
+ // Check if the request got ratelimited by checking for status 429,
+ // and if so, sleep for the value of the header 'retry-after' -
+ // which is in milliseconds - and then `continue` to try again
+ //
+ // If it didn't ratelimit, subtract one from the RateLimit's
+ // 'remaining'
+ //
+ // Update the 'reset' with the value of the 'x-ratelimit-reset'
+ // header
+ //
+ // It _may_ be possible for the limit to be raised at any time,
+ // so check if it did from the value of the 'x-ratelimit-limit'
+ // header. If the limit was 5 and is now 7, add 2 to the 'remaining'
+
+ let redo = if response.headers.get_raw("x-ratelimit-global").is_some() {
+ global.post_hook(&response)
+ } else {
+ ROUTES.lock()
+ .expect("routes poisoned")
+ .entry(route)
+ .or_insert_with(RateLimit::default)
+ .post_hook(&response)
+ };
+
+ if redo.unwrap_or(false) {
+ continue;
+ }
+
+ return Ok(response);
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+pub struct RateLimit {
+ limit: i64,
+ remaining: i64,
+ reset: i64,
+}
+
+impl RateLimit {
+ pub fn pre_hook(&mut self) {
+ if self.limit == 0 {
+ return;
+ }
+
+ let diff = (self.reset - time::get_time().sec) as u64;
+
+ if self.remaining == 0 {
+ let delay = (diff * 1000) + 500;
+
+ debug!("Pre-emptive ratelimit for {:?}ms", delay);
+ thread::sleep(Duration::from_millis(delay));
+
+ return;
+ }
+
+ self.remaining -= 1;
+ }
+
+ pub fn post_hook(&mut self, response: &Response) -> Result<bool> {
+ if let Some(limit) = try!(get_header(&response.headers, "x-ratelimit-limit")) {
+ self.limit = limit;
+ }
+
+ if let Some(remaining) = try!(get_header(&response.headers, "x-ratelimit-remaining")) {
+ self.remaining = remaining;
+ }
+
+ if let Some(reset) = try!(get_header(&response.headers, "x-ratelimit-reset")) {
+ self.reset = reset;
+ }
+
+ Ok(if response.status != StatusCode::TooManyRequests {
+ false
+ } else if let Some(retry_after) = try!(get_header(&response.headers, "retry-after")) {
+ debug!("Ratelimited: {:?}ms", retry_after);
+ thread::sleep(Duration::from_millis(retry_after as u64));
+
+ true
+ } else {
+ false
+ })
+ }
+}
+
+fn get_header(headers: &Headers, header: &str) -> Result<Option<i64>> {
+ match headers.get_raw(header) {
+ Some(header) => match str::from_utf8(&header[0]) {
+ Ok(v) => match v.parse::<i64>() {
+ Ok(v) => Ok(Some(v)),
+ Err(_why) => Err(Error::Client(ClientError::RateLimitI64)),
+ },
+ Err(_why) => Err(Error::Client(ClientError::RateLimitUtf8)),
+ },
+ None => Ok(None),
+ }
+}
diff --git a/src/constants.rs b/src/constants.rs
new file mode 100644
index 0000000..8140fca
--- /dev/null
+++ b/src/constants.rs
@@ -0,0 +1,9 @@
+/// The base URI for the API.
+pub const API_BASE: &'static str = "https://discordapp.com/api/v6";
+/// The gateway version used by the library. The gateway URI is retrieved via
+/// the REST API.
+pub const GATEWAY_VERSION: u8 = 6;
+/// The [UserAgent] sent along with every request.
+///
+/// [UserAgent]: ../hyper/header/struct.UserAgent.html
+pub const USER_AGENT: &'static str = concat!("DiscordBot (https://github.com/zeyla/serenity, ", env!("CARGO_PKG_VERSION"), ")");
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..065958b
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,106 @@
+use std::io::Error as IoError;
+use std::error::Error as StdError;
+use std::fmt::{self, Display};
+use hyper::Error as HyperError;
+use serde_json::Error as JsonError;
+use serde_json::Value;
+use websocket::result::WebSocketError;
+use ::client::{ClientError, ConnectionError};
+
+/// The common result type between most library functions.
+pub type Result<T> = ::std::result::Result<T, Error>;
+
+/// A common error enum returned by most of the library's functionality within a
+/// [`Result`].
+///
+/// The most common error types, the [`ClientError`] and [`ConnectionError`]
+/// enums, are both wrapped around this in the form of the [`Client`] and
+/// [`Connection`] variants.
+///
+/// [`Client`]: #Client.v
+/// [`ClientError`]: client/enum.ClientError.html
+/// [`Connection`]: #Connection.v
+/// [`ConnectionError`]: client/enum.ConnectionError.html
+/// [`Result`]: type.Result.html
+#[derive(Debug)]
+pub enum Error {
+ /// An Http or Client error.
+ Client(ClientError),
+ /// An error with the WebSocket connection.
+ Connection(ConnectionError),
+ /// An error while decoding a payload.
+ Decode(&'static str, Value),
+ /// An error from the `hyper` crate.
+ Hyper(HyperError),
+ /// An `std::io` error.
+ Io(IoError),
+ /// An error from the `serde_json` crate.
+ Json(JsonError),
+ /// Some other error.
+ Other(&'static str),
+ /// An error from the `url` crate.
+ Url(String),
+ /// An error from the `rust-websocket` crate.
+ WebSocket(WebSocketError),
+}
+
+impl From<IoError> for Error {
+ fn from(err: IoError) -> Error {
+ Error::Io(err)
+ }
+}
+
+impl From<HyperError> for Error {
+ fn from(err: HyperError) -> Error {
+ Error::Hyper(err)
+ }
+}
+
+impl From<JsonError> for Error {
+ fn from(err: JsonError) -> Error {
+ Error::Json(err)
+ }
+}
+
+impl From<WebSocketError> for Error {
+ fn from(err: WebSocketError) -> Error {
+ Error::WebSocket(err)
+ }
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ Error::Hyper(ref inner) => inner.fmt(f),
+ Error::Json(ref inner) => inner.fmt(f),
+ Error::WebSocket(ref inner) => inner.fmt(f),
+ Error::Io(ref inner) => inner.fmt(f),
+ _ => f.write_str(self.description()),
+ }
+ }
+}
+
+impl StdError for Error {
+ fn description(&self) -> &str {
+ match *self {
+ Error::Client(_) => "Client refused a request",
+ Error::Connection(ref _inner) => "Connection error",
+ Error::Decode(msg, _) | Error::Other(msg) => msg,
+ Error::Hyper(ref inner) => inner.description(),
+ Error::Io(ref inner) => inner.description(),
+ Error::Json(ref inner) => inner.description(),
+ Error::Url(ref inner) => inner,
+ Error::WebSocket(ref inner) => inner.description(),
+ }
+ }
+
+ fn cause(&self) -> Option<&StdError> {
+ match *self {
+ Error::Hyper(ref inner) => Some(inner),
+ Error::Json(ref inner) => Some(inner),
+ Error::WebSocket(ref inner) => Some(inner),
+ Error::Io(ref inner) => Some(inner),
+ _ => None,
+ }
+ }
+}
diff --git a/src/ext/framework/command.rs b/src/ext/framework/command.rs
new file mode 100644
index 0000000..31b2520
--- /dev/null
+++ b/src/ext/framework/command.rs
@@ -0,0 +1,7 @@
+use std::sync::Arc;
+use ::client::Context;
+use ::model::Message;
+
+pub type Command = Fn(Context, Message) + Send + Sync;
+#[doc(hidden)]
+pub type InternalCommand = Arc<Command>;
diff --git a/src/ext/framework/configuration.rs b/src/ext/framework/configuration.rs
new file mode 100644
index 0000000..cd68c69
--- /dev/null
+++ b/src/ext/framework/configuration.rs
@@ -0,0 +1,48 @@
+use std::default::Default;
+use ::client::http;
+
+pub struct Configuration {
+ pub depth: usize,
+ pub on_mention: Option<Vec<String>>,
+ pub prefix: Option<String>,
+}
+
+impl Configuration {
+ /// The default depth of the message to check for commands. Defaults to 5.
+ pub fn depth(mut self, depth: u8) -> Self {
+ self.depth = depth as usize;
+
+ self
+ }
+
+ pub fn on_mention(mut self, on_mention: bool) -> Self {
+ if !on_mention {
+ return self;
+ }
+
+ if let Ok(current_user) = http::get_current_user() {
+ self.on_mention = Some(vec![
+ format!("<@{}>", current_user.id), // Regular mention
+ format!("<@!{}>", current_user.id), // Nickname mention
+ ]);
+ }
+
+ self
+ }
+
+ pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
+ self.prefix = Some(prefix.into());
+
+ self
+ }
+}
+
+impl Default for Configuration {
+ fn default() -> Configuration {
+ Configuration {
+ depth: 5,
+ on_mention: None,
+ prefix: None,
+ }
+ }
+}
diff --git a/src/ext/framework/mod.rs b/src/ext/framework/mod.rs
new file mode 100644
index 0000000..3021d5b
--- /dev/null
+++ b/src/ext/framework/mod.rs
@@ -0,0 +1,122 @@
+mod command;
+mod configuration;
+
+pub use self::command::Command;
+pub use self::configuration::Configuration;
+
+use self::command::InternalCommand;
+use std::collections::HashMap;
+use std::sync::Arc;
+use std::thread;
+use ::client::Context;
+use ::model::Message;
+
+#[allow(type_complexity)]
+#[derive(Default)]
+pub struct Framework {
+ configuration: Configuration,
+ commands: HashMap<String, InternalCommand>,
+ checks: HashMap<String, Arc<Fn(&Context, &Message) -> bool + Send + Sync + 'static>>,
+ pub initialized: bool,
+}
+
+impl Framework {
+ pub fn configure<F>(mut self, f: F) -> Self
+ where F: FnOnce(Configuration) -> Configuration {
+ self.configuration = f(self.configuration);
+ self.initialized = true;
+
+ self
+ }
+
+ #[doc(hidden)]
+ pub fn dispatch(&mut self, context: Context, message: Message) {
+ // Determine the point at which the prefix ends, and the command starts.
+ let pos = if let Some(ref prefix) = self.configuration.prefix {
+ let mut mention_ends = None;
+
+ if let Some(ref mentions) = self.configuration.on_mention {
+ for mention in mentions {
+ if !message.content.starts_with(&mention[..]) {
+ continue;
+ }
+
+ mention_ends = Some(mention.len() + 1);
+
+ break;
+ }
+ }
+
+ if let Some(mention_ends) = mention_ends {
+ mention_ends
+ } else if !message.content.starts_with(prefix) {
+ return;
+ } else {
+ prefix.len()
+ }
+ } else {
+ 0
+ };
+
+ // Ensure that the message length is at least longer than the prefix
+ // length. There's no point in checking further ahead if there's nothing
+ // to check.
+ if message.content.len() <= pos {
+ return;
+ }
+
+ let mut built = String::new();
+
+ for i in 0..self.configuration.depth {
+ if i > 0 {
+ built.push(' ');
+ }
+
+ built.push_str(match {
+ message.content
+ .split_at(pos)
+ .1
+ .split_whitespace()
+ .collect::<Vec<&str>>()
+ .get(i)
+ } {
+ Some(piece) => piece,
+ None => return,
+ });
+
+ if let Some(command) = self.commands.get(&built) {
+ if let Some(check) = self.checks.get(&built) {
+ if !(check)(&context, &message) {
+ return;
+ }
+ }
+
+ let command = command.clone();
+
+ thread::spawn(move || {
+ (command)(context, message)
+ });
+
+ return;
+ }
+ }
+ }
+
+ pub fn on<F, S>(mut self, command_name: S, f: F) -> Self
+ where F: Fn(Context, Message) + Send + Sync + 'static,
+ S: Into<String> {
+ self.commands.insert(command_name.into(), Arc::new(f));
+ self.initialized = true;
+
+ self
+ }
+
+ pub fn set_check<F, S>(mut self, command: S, check: F) -> Self
+ where F: Fn(&Context, &Message) -> bool + Send + Sync + 'static,
+ S: Into<String> {
+ self.checks.insert(command.into(), Arc::new(check));
+ self.initialized = true;
+
+ self
+ }
+}
diff --git a/src/ext/mod.rs b/src/ext/mod.rs
new file mode 100644
index 0000000..92fda62
--- /dev/null
+++ b/src/ext/mod.rs
@@ -0,0 +1,13 @@
+//! The set of extensions is functionality that is not required for a
+//! [`Client`] and/or [`Connection`] to properly function.
+//!
+//! These are flagged behind feature-gates and can be enabled and disabled.
+//!
+//! See each extension's module-level documentation for more information.
+//!
+//! [`Client`]: ../client/struct.Client.html
+//! [`Connection`]: ../client/struct.Connection.html
+
+pub mod framework;
+pub mod state;
+pub mod voice;
diff --git a/src/ext/state/mod.rs b/src/ext/state/mod.rs
new file mode 100644
index 0000000..d4c250d
--- /dev/null
+++ b/src/ext/state/mod.rs
@@ -0,0 +1,729 @@
+use std::collections::hash_map::Entry;
+use std::collections::HashMap;
+use std::default::Default;
+use ::model::*;
+
+/// Known state composed from received events.
+#[derive(Debug, Clone)]
+pub struct State {
+ pub calls: HashMap<ChannelId, Call>,
+ pub groups: HashMap<ChannelId, Group>,
+ /// Settings specific to a guild.
+ ///
+ /// This will always be empty for bot accounts.
+ pub guild_settings: HashMap<Option<GuildId>, UserGuildSettings>,
+ pub guilds: HashMap<GuildId, LiveGuild>,
+ /// A list of notes that a user has made for individual users.
+ ///
+ /// This will always be empty for bot accounts.
+ pub notes: HashMap<UserId, String>,
+ pub presences: HashMap<UserId, Presence>,
+ pub private_channels: HashMap<ChannelId, PrivateChannel>,
+ pub relationships: HashMap<UserId, Relationship>,
+ /// Account-specific settings for a user account.
+ pub settings: Option<UserSettings>,
+ pub unavailable_guilds: Vec<GuildId>,
+ pub user: CurrentUser,
+}
+
+impl State {
+ pub fn unknown_members(&self) -> u64 {
+ let mut total = 0;
+
+ for guild in self.guilds.values() {
+ let members = guild.members.len() as u64;
+
+ if guild.member_count > members {
+ total += guild.member_count - members;
+ } else if guild.member_count < members {
+ warn!("Inconsistent member count for {:?}: {} < {}",
+ guild.id,
+ guild.member_count,
+ members);
+ }
+ }
+
+ total
+ }
+
+ pub fn all_private_channels(&self) -> Vec<ChannelId> {
+ self.groups
+ .keys()
+ .cloned()
+ .chain(self.private_channels.keys().cloned())
+ .collect()
+ }
+
+ pub fn all_guilds(&self) -> Vec<GuildId> {
+ self.guilds
+ .values()
+ .map(|s| s.id)
+ .chain(self.unavailable_guilds.iter().cloned())
+ .collect()
+ }
+
+ #[doc(hidden)]
+ pub fn __download_members(&mut self) -> Vec<GuildId> {
+ self.guilds
+ .values_mut()
+ .filter(|guild| guild.large)
+ .map(|ref mut guild| {
+ guild.members.clear();
+
+ guild.id
+ })
+ .collect::<Vec<_>>()
+ }
+
+ pub fn find_call<C: Into<ChannelId>>(&self, group_id: C) -> Option<&Call> {
+ self.calls.get(&group_id.into())
+ }
+
+ pub fn find_channel<C: Into<ChannelId>>(&self, id: C) -> Option<Channel> {
+ let id = id.into();
+
+ for guild in self.guilds.values() {
+ for channel in guild.channels.values() {
+ if channel.id == id {
+ return Some(Channel::Public(channel.clone()));
+ }
+ }
+ }
+
+ None
+ }
+
+ pub fn find_guild<G: Into<GuildId>>(&self, id: G) -> Option<&LiveGuild> {
+ self.guilds.get(&id.into())
+ }
+
+ pub fn find_group<C: Into<ChannelId>>(&self, id: C) -> Option<&Group> {
+ self.groups.get(&id.into())
+ }
+
+ pub fn find_member<G, U>(&self, guild_id: G, user_id: U)
+ -> Option<&Member> where G: Into<GuildId>, U: Into<UserId> {
+ self.guilds
+ .get(&guild_id.into())
+ .map(|guild| {
+ guild.members.get(&user_id.into())
+ }).and_then(|x| match x {
+ Some(x) => Some(x),
+ _ => None,
+ })
+ }
+
+ pub fn find_role<G, R>(&self, guild_id: G, role_id: R) -> Option<&Role>
+ where G: Into<GuildId>, R: Into<RoleId> {
+ if let Some(guild) = self.guilds.get(&guild_id.into()) {
+ guild.roles.get(&role_id.into())
+ } else {
+ None
+ }
+ }
+
+ /// Update the state according to the changes described in the given event.
+ #[allow(cyclomatic_complexity)]
+ #[allow(unneeded_field_pattern)]
+ pub fn update(&mut self, event: &Event) {
+ match *event {
+ Event::CallCreate(ref event) => {
+ self.update_with_call_create(event);
+ },
+ Event::CallUpdate(ref event) => {
+ self.update_with_call_update(event);
+ },
+ Event::CallDelete(ref event) => {
+ self.update_with_call_delete(event);
+ },
+ Event::ChannelCreate(ref event) => {
+ self.update_with_channel_create(event);
+ },
+ Event::ChannelDelete(ref event) => {
+ self.update_with_channel_delete(event);
+ },
+ Event::ChannelPinsUpdate(ref event) => {
+ self.update_with_channel_pins_update(event);
+ },
+ Event::ChannelRecipientAdd(ref event) => {
+ self.update_with_channel_recipient_add(event);
+ },
+ Event::ChannelRecipientRemove(ref event) => {
+ self.update_with_channel_recipient_remove(event);
+ },
+ Event::ChannelUpdate(ref event) => {
+ self.update_with_channel_update(event);
+ },
+ Event::GuildCreate(ref event) => {
+ self.update_with_guild_create(event);
+ },
+ Event::GuildDelete(ref event) => {
+ self.update_with_guild_delete(event);
+ },
+ Event::GuildEmojisUpdate(ref event) => {
+ self.update_with_guild_emojis_update(event);
+ },
+ Event::GuildMemberAdd(ref event) => {
+ self.update_with_guild_member_add(event);
+ },
+ Event::GuildMemberRemove(ref event) => {
+ self.update_with_guild_member_remove(event);
+ },
+ Event::GuildMemberUpdate(ref event) => {
+ self.update_with_guild_member_update(event);
+ },
+ Event::GuildMembersChunk(ref event) => {
+ self.update_with_guild_members_chunk(event);
+ },
+ Event::GuildRoleCreate(ref event) => {
+ self.update_with_guild_role_create(event);
+ },
+ Event::GuildRoleDelete(ref event) => {
+ self.update_with_guild_role_delete(event);
+ },
+ Event::GuildRoleUpdate(ref event) => {
+ self.update_with_guild_role_update(event);
+ },
+ Event::GuildSync(ref event) => {
+ self.update_with_guild_sync(event);
+ },
+ Event::GuildUnavailable(ref event) => {
+ self.update_with_guild_unavailable(event);
+ },
+ Event::GuildUpdate(ref event) => {
+ self.update_with_guild_update(event);
+ },
+ Event::PresencesReplace(ref event) => {
+ self.update_with_presences_replace(event);
+ },
+ Event::PresenceUpdate(ref event) => {
+ self.update_with_presence_update(event);
+ },
+ Event::Ready(ref event) => {
+ self.update_with_ready(event);
+ },
+ Event::RelationshipAdd(ref event) => {
+ self.update_with_relationship_add(event);
+ },
+ Event::RelationshipRemove(ref event) => {
+ self.update_with_relationship_remove(event);
+ },
+ Event::UserGuildSettingsUpdate(ref event) => {
+ self.update_with_user_guild_settings_update(event);
+ },
+ Event::UserNoteUpdate(ref event) => {
+ self.update_with_user_note_update(event);
+ },
+ Event::UserSettingsUpdate(ref event) => {
+ self.update_with_user_settings_update(event);
+ },
+ Event::UserUpdate(ref event) => {
+ self.update_with_user_update(event);
+ },
+ Event::VoiceStateUpdate(ref event) => {
+ self.update_with_voice_state_update(event);
+ },
+ _ => {},
+ }
+ }
+
+ pub fn update_with_call_create(&mut self, event: &CallCreateEvent) {
+ match self.calls.entry(event.call.channel_id) {
+ Entry::Vacant(e) => {
+ e.insert(event.call.clone());
+ },
+ Entry::Occupied(mut e) => {
+ e.get_mut().clone_from(&event.call);
+ },
+ }
+ }
+
+ pub fn update_with_call_delete(&mut self, event: &CallDeleteEvent) {
+ self.calls.remove(&event.channel_id);
+ }
+
+ pub fn update_with_call_update(&mut self, event: &CallUpdateEvent) {
+ self.calls
+ .get_mut(&event.channel_id)
+ .map(|call| {
+ call.region.clone_from(&event.region);
+ call.ringing.clone_from(&event.ringing);
+ });
+ }
+
+ pub fn update_with_channel_create(&mut self, event: &ChannelCreateEvent) {
+ match event.channel {
+ Channel::Group(ref group) => {
+ self.groups.insert(group.channel_id, group.clone());
+ },
+ Channel::Private(ref channel) => {
+ self.private_channels.insert(channel.id, channel.clone());
+ },
+ Channel::Public(ref channel) => {
+ self.guilds
+ .get_mut(&channel.guild_id)
+ .map(|guild| guild.channels.insert(channel.id,
+ channel.clone()));
+ },
+ }
+ }
+
+ pub fn update_with_channel_delete(&mut self, event: &ChannelDeleteEvent) {
+ match event.channel {
+ Channel::Group(ref group) => {
+ self.groups.remove(&group.channel_id);
+ },
+ Channel::Private(ref channel) => {
+ self.private_channels.remove(&channel.id);
+ },
+ Channel::Public(ref channel) => {
+ self.guilds
+ .get_mut(&channel.guild_id)
+ .map(|guild| guild.channels.remove(&channel.id));
+ },
+ }
+ }
+
+ pub fn update_with_channel_pins_update(&mut self,
+ event: &ChannelPinsUpdateEvent) {
+ if let Some(channel) = self.private_channels.get_mut(&event.channel_id) {
+ channel.last_pin_timestamp = event.last_pin_timestamp.clone();
+
+ return;
+ }
+
+ if let Some(group) = self.groups.get_mut(&event.channel_id) {
+ group.last_pin_timestamp = event.last_pin_timestamp.clone();
+
+ return;
+ }
+
+ // Guild searching is last because it is expensive
+ // in comparison to private channel and group searching.
+ for guild in self.guilds.values_mut() {
+ for channel in guild.channels.values_mut() {
+ if channel.id == event.channel_id {
+ channel.last_pin_timestamp = event.last_pin_timestamp.clone();
+
+ return;
+ }
+ }
+ }
+ }
+
+ pub fn update_with_channel_recipient_add(&mut self,
+ event: &ChannelRecipientAddEvent) {
+ self.groups
+ .get_mut(&event.channel_id)
+ .map(|group| group.recipients.insert(event.user.id,
+ event.user.clone()));
+ }
+
+ pub fn update_with_channel_recipient_remove(&mut self,
+ event: &ChannelRecipientRemoveEvent) {
+ self.groups
+ .get_mut(&event.channel_id)
+ .map(|group| group.recipients.remove(&event.user.id));
+ }
+
+ pub fn update_with_channel_update(&mut self, event: &ChannelUpdateEvent) {
+ match event.channel {
+ Channel::Group(ref group) => {
+ match self.groups.entry(group.channel_id) {
+ Entry::Vacant(e) => {
+ e.insert(group.clone());
+ },
+ Entry::Occupied(mut e) => {
+ let dest = e.get_mut();
+ if group.recipients.is_empty() {
+ // if the update omits the recipient list, preserve it
+ let recipients = ::std::mem::replace(&mut dest.recipients, HashMap::new());
+ dest.clone_from(group);
+ dest.recipients = recipients;
+ } else {
+ dest.clone_from(group);
+ }
+ },
+ }
+ },
+ Channel::Private(ref channel) => {
+ self.private_channels
+ .get_mut(&channel.id)
+ .map(|chan| chan.clone_from(channel));
+ },
+ Channel::Public(ref channel) => {
+ self.guilds
+ .get_mut(&channel.guild_id)
+ .map(|guild| guild.channels
+ .insert(channel.id, channel.clone()));
+ },
+ }
+ }
+
+ pub fn update_with_guild_create(&mut self, event: &GuildCreateEvent) {
+ self.guilds.insert(event.guild.id, event.guild.clone());
+ }
+
+ pub fn update_with_guild_delete(&mut self, event: &GuildDeleteEvent) {
+ self.guilds.remove(&event.guild.id);
+ }
+
+ pub fn update_with_guild_emojis_update(&mut self,
+ event: &GuildEmojisUpdateEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| guild.emojis.extend(event.emojis.clone()));
+ }
+
+ pub fn update_with_guild_member_add(&mut self,
+ event: &GuildMemberAddEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| {
+ guild.member_count += 1;
+ guild.members.insert(event.member.user.id,
+ event.member.clone());
+ });
+ }
+
+ pub fn update_with_guild_member_remove(&mut self,
+ event: &GuildMemberRemoveEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| {
+ guild.member_count -= 1;
+ guild.members.remove(&event.user.id);
+ });
+ }
+
+ pub fn update_with_guild_member_update(&mut self,
+ event: &GuildMemberUpdateEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| {
+ let mut found = false;
+
+ if let Some(member) = guild.members.get_mut(&event.user.id) {
+ member.nick.clone_from(&event.nick);
+ member.roles.clone_from(&event.roles);
+ member.user.clone_from(&event.user);
+
+ found = true;
+ }
+
+ if !found {
+ guild.members.insert(event.user.id, Member {
+ deaf: false,
+ joined_at: String::default(),
+ mute: false,
+ nick: event.nick.clone(),
+ roles: event.roles.clone(),
+ user: event.user.clone(),
+ });
+ }
+ });
+ }
+
+ pub fn update_with_guild_members_chunk(&mut self,
+ event: &GuildMembersChunkEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| guild.members.extend(event.members.clone()));
+ }
+
+ pub fn update_with_guild_role_create(&mut self,
+ event: &GuildRoleCreateEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| guild.roles.insert(event.role.id, event.role.clone()));
+ }
+
+ pub fn update_with_guild_role_delete(&mut self,
+ event: &GuildRoleDeleteEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| guild.roles.remove(&event.role_id));
+ }
+
+ pub fn update_with_guild_role_update(&mut self,
+ event: &GuildRoleUpdateEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| guild.roles
+ .get_mut(&event.role.id)
+ .map(|role| role.clone_from(&event.role)));
+ }
+
+ pub fn update_with_guild_sync(&mut self, event: &GuildSyncEvent) {
+ self.guilds
+ .get_mut(&event.guild_id)
+ .map(|guild| {
+ guild.large = event.large;
+ guild.members.clone_from(&event.members);
+ guild.presences.clone_from(&event.presences);
+ });
+ }
+
+ pub fn update_with_guild_unavailable(&mut self,
+ event: &GuildUnavailableEvent) {
+ if !self.unavailable_guilds.contains(&event.guild_id) {
+ self.unavailable_guilds.push(event.guild_id);
+ }
+ }
+
+ pub fn update_with_guild_update(&mut self, event: &GuildUpdateEvent) {
+ self.guilds
+ .get_mut(&event.guild.id)
+ .map(|guild| {
+ // todo: embed
+ guild.afk_timeout = event.guild.afk_timeout;
+ guild.afk_channel_id.clone_from(&event.guild.afk_channel_id);
+ guild.icon.clone_from(&event.guild.icon);
+ guild.name.clone_from(&event.guild.name);
+ guild.owner_id.clone_from(&event.guild.owner_id);
+ guild.region.clone_from(&event.guild.region);
+ guild.roles.clone_from(&event.guild.roles);
+ guild.verification_level = event.guild.verification_level;
+ });
+ }
+
+ pub fn update_with_presences_replace(&mut self, event: &PresencesReplaceEvent) {
+ self.presences.clone_from(&{
+ let mut p = HashMap::default();
+
+ for presence in &event.presences {
+ p.insert(presence.user_id, presence.clone());
+ }
+
+ p
+ });
+ }
+
+ pub fn update_with_presence_update(&mut self, event: &PresenceUpdateEvent) {
+ if let Some(guild_id) = event.guild_id {
+ if let Some(guild) = self.guilds.get_mut(&guild_id) {
+ // If the user was modified, update the member list
+ if let Some(user) = event.presence.user.as_ref() {
+ guild.members
+ .get_mut(&user.id)
+ .map(|member| member.user.clone_from(user));
+ }
+
+ update_presence(&mut guild.presences, &event.presence);
+ }
+ }
+ }
+
+ pub fn update_with_ready(&mut self, ready: &ReadyEvent) {
+ let ready = ready.ready.clone();
+
+ for guild in ready.guilds {
+ match guild {
+ PossibleGuild::Offline(guild_id) => {
+ self.unavailable_guilds.push(guild_id);
+ }
+ PossibleGuild::Online(guild) => {
+ self.guilds.insert(guild.id, guild);
+ },
+ }
+ }
+
+ self.unavailable_guilds.sort();
+ self.unavailable_guilds.dedup();
+
+ // The private channels sent in the READY contains both the actual
+ // private channels, and the groups.
+ for (channel_id, channel) in ready.private_channels {
+ match channel {
+ Channel::Group(group) => {
+ self.groups.insert(channel_id, group);
+ },
+ Channel::Private(channel) => {
+ self.private_channels.insert(channel_id, channel);
+ },
+ Channel::Public(_) => {},
+ }
+ }
+
+ for guild in ready.user_guild_settings.unwrap_or_default() {
+ self.guild_settings.insert(guild.guild_id, guild);
+ }
+
+ for (user_id, presence) in ready.presences {
+ self.presences.insert(user_id, presence);
+ }
+
+ for (user_id, relationship) in ready.relationships {
+ self.relationships.insert(user_id, relationship);
+ }
+
+ self.notes.extend(ready.notes);
+
+ self.settings = ready.user_settings;
+ self.user = ready.user;
+ }
+
+ pub fn update_with_relationship_add(&mut self, event: &RelationshipAddEvent) {
+ self.relationships.insert(event.relationship.id,
+ event.relationship.clone());
+ }
+
+ pub fn update_with_relationship_remove(&mut self,
+ event: &RelationshipRemoveEvent) {
+ self.relationships.remove(&event.user_id);
+ }
+
+ pub fn update_with_user_guild_settings_update(&mut self,
+ event: &UserGuildSettingsUpdateEvent) {
+ self.guild_settings
+ .get_mut(&event.settings.guild_id)
+ .map(|guild_setting| guild_setting.clone_from(&event.settings));
+ }
+
+ pub fn update_with_user_note_update(&mut self,
+ event: &UserNoteUpdateEvent) {
+ if event.note.is_empty() {
+ self.notes.remove(&event.user_id);
+ } else {
+ self.notes.insert(event.user_id, event.note.clone());
+ }
+ }
+
+ pub fn update_with_user_settings_update(&mut self,
+ event: &UserSettingsUpdateEvent) {
+ self.settings
+ .as_mut()
+ .map(|settings| {
+ opt_modify(&mut settings.enable_tts_command, &event.enable_tts_command);
+ opt_modify(&mut settings.inline_attachment_media, &event.inline_attachment_media);
+ opt_modify(&mut settings.inline_embed_media, &event.inline_embed_media);
+ opt_modify(&mut settings.locale, &event.locale);
+ opt_modify(&mut settings.message_display_compact, &event.message_display_compact);
+ opt_modify(&mut settings.render_embeds, &event.render_embeds);
+ opt_modify(&mut settings.show_current_game, &event.show_current_game);
+ opt_modify(&mut settings.theme, &event.theme);
+ opt_modify(&mut settings.convert_emoticons, &event.convert_emoticons);
+ opt_modify(&mut settings.friend_source_flags, &event.friend_source_flags);
+ });
+ }
+
+ pub fn update_with_user_update(&mut self, event: &UserUpdateEvent) {
+ self.user = event.current_user.clone();
+ }
+
+ pub fn update_with_voice_state_update(&mut self,
+ event: &VoiceStateUpdateEvent) {
+ if let Some(guild_id) = event.guild_id {
+ if let Some(guild) = self.guilds.get_mut(&guild_id) {
+ if !event.voice_state.channel_id.is_some() {
+ // Remove the user from the voice state list
+ guild.voice_states.remove(&event.voice_state.user_id);
+ } else {
+ // Update or add to the voice state list
+ {
+ let finding = guild.voice_states
+ .get_mut(&event.voice_state.user_id);
+
+ if let Some(srv_state) = finding {
+ srv_state.clone_from(&event.voice_state);
+
+ return;
+ }
+ }
+
+ guild.voice_states.insert(event.voice_state.user_id,
+ event.voice_state.clone());
+ }
+ }
+
+ return;
+ }
+
+ if let Some(channel) = event.voice_state.channel_id {
+ // channel id available, insert voice state
+ if let Some(call) = self.calls.get_mut(&channel) {
+ {
+ let finding = call.voice_states
+ .get_mut(&event.voice_state.user_id);
+
+ if let Some(grp_state) = finding {
+ grp_state.clone_from(&event.voice_state);
+
+ return;
+ }
+ }
+
+ call.voice_states.insert(event.voice_state.user_id,
+ event.voice_state.clone());
+ }
+ } else {
+ // delete this user from any group call containing them
+ for call in self.calls.values_mut() {
+ call.voice_states.remove(&event.voice_state.user_id);
+ }
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> State {
+ State {
+ calls: HashMap::default(),
+ groups: HashMap::default(),
+ guild_settings: HashMap::default(),
+ guilds: HashMap::default(),
+ notes: HashMap::default(),
+ presences: HashMap::default(),
+ private_channels: HashMap::default(),
+ relationships: HashMap::default(),
+ settings: None,
+ unavailable_guilds: Vec::default(),
+ user: CurrentUser {
+ avatar: None,
+ bot: false,
+ discriminator: 0,
+ email: None,
+ id: UserId(0),
+ mfa_enabled: false,
+ mobile: None,
+ name: String::default(),
+ verified: false,
+ }
+ }
+ }
+}
+
+fn update_presence(presences: &mut HashMap<UserId, Presence>,
+ presence: &Presence) {
+ if presence.status == OnlineStatus::Offline {
+ // Remove the user from the presence list
+ presences.remove(&presence.user_id);
+ } else {
+ // Update or add to the presence list
+ if let Some(ref mut guild_presence) = presences.get(&presence.user_id) {
+ if presence.user.is_none() {
+ guild_presence.clone_from(&presence);
+ }
+
+ return;
+ }
+ presences.insert(presence.user_id, presence.clone());
+ }
+}
+
+/// A reference to a private channel, public channel, or group.
+#[derive(Debug, Clone, Copy)]
+pub enum ChannelRef<'a> {
+ /// A private channel
+ Private(&'a PrivateChannel),
+ /// A group channel
+ Group(&'a Group),
+ /// A public channel and its guild
+ Public(&'a LiveGuild, &'a PublicChannel),
+}
+
+fn opt_modify<T: Clone>(dest: &mut T, src: &Option<T>) {
+ if let Some(val) = src.as_ref() {
+ dest.clone_from(val);
+ }
+}
diff --git a/src/ext/voice/mod.rs b/src/ext/voice/mod.rs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/ext/voice/mod.rs
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..2e75622
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,104 @@
+//! Serenity is an ergonomic and high-level Rust library for the Discord API.
+//!
+//! View the [examples] on how to make and structure a bot.
+//!
+//! Serenity supports both bot and user login via the use of [`Client::login_bot`]
+//! and [`Client::login_user`].
+//!
+//! You may also check your tokens prior to login via the use of
+//! [`validate_token`].
+//!
+//! Once logged in, you may add handlers to your client to dispatch [`Event`]s,
+//! such as [`Client::on_message`]. This will cause your handler to be called
+//! when a [`Event::MessageCreate`] is received. Each handler is given a
+//! [`Context`], giving information about the event. See the
+//! [client's module-level documentation].
+//!
+//! The [`Connection`] is transparently handled by the library, removing
+//! unnecessary complexity. Sharded connections are automatically handled for
+//! you. See the [Connection's documentation][`Connection`] for more
+//! information.
+//!
+//! A [`State`] is also provided for you. This will be updated automatically for
+//! you as data is received from the Discord API via events. When calling a
+//! method on a [`Context`], the state will first be searched for relevant data
+//! to avoid unnecessary HTTP requests to the Discord API. For more information,
+//! see the [state's module-level documentation][state docs].
+//!
+//! Note that - although this documentation will try to be as up-to-date and
+//! accurate as possible - Discord hosts [official documentation][docs]. If you
+//! need to be sure that some information piece is accurate, refer to their
+//! docs.
+//!
+//! # Dependencies
+//!
+//! Serenity requires the following dependencies:
+//!
+//! - openssl
+//!
+//! # Example Bot
+//!
+//! A basic ping-pong bot looks like:
+//!
+//! ```rust,ignore
+//! extern crate serenity;
+//!
+//! use serenity::Client;
+//!
+//! fn main() {
+//! // Login with a bot token from the environment
+//! let client = Client::login_bot(env::var("DISCORD_TOKEN").expect("token"));
+//! client.with_framework(|c| c
+//! .prefix("~") // set the bot's prefix to '~'
+//! .on("ping", |_context, message| drop(message.reply("Pong!"))));
+//!
+//! let _ = client.start(); // start listening for events by starting a connection
+//! }
+//! ```
+//!
+//! [`Client::login_bot`]: client/struct.Client.html#method.login_bot
+//! [`Client::login_user`]: client/struct.Client.html#method.login_user
+//! [`Client::on_message`]: client/struct.Client.html#method.on_message
+//! [`validate_token`]: client/fn.validate_token.html
+//! [`Connection`]: client/struct.Connection.html
+//! [`Context`]: client/struct.Context.html
+//! [`Event`]: model/enum.Event.html
+//! [`Event::MessageCreate`]: model/enum.Event.html#MessageCreate.v
+//! [`State`]: ext/state/struct.State.html
+//! [client's module-level documentation]: client/index.html
+//! [docs]: https://discordapp.com/developers/docs/intro
+//! [examples]: https://github.com/zeyla/serenity.rs/tree/master/examples
+//! [state docs]: ext/state/index.html
+#![allow(doc_markdown, unknown_lints)]
+#![allow(dead_code)]
+
+#[macro_use]
+extern crate bitflags;
+#[macro_use]
+extern crate lazy_static;
+#[macro_use]
+extern crate log;
+
+extern crate base64;
+extern crate byteorder;
+extern crate flate2;
+extern crate hyper;
+extern crate multipart;
+extern crate serde_json;
+extern crate time;
+extern crate websocket;
+
+#[macro_use]
+pub mod utils;
+
+pub mod builder;
+pub mod client;
+pub mod ext;
+pub mod model;
+
+mod constants;
+mod error;
+mod prelude;
+
+pub use client::Client;
+pub use error::{Error, Result};
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
diff --git a/src/prelude.rs b/src/prelude.rs
new file mode 100644
index 0000000..45906bf
--- /dev/null
+++ b/src/prelude.rs
@@ -0,0 +1,9 @@
+//! These prelude re-exports are a set of exports that are commonly used from
+//! within the library.
+//!
+//! These are not publicly re-exported to the end user, and must stay as a
+//! private module.
+
+pub use serde_json::Value;
+pub use ::error::{Error, Result};
+pub use ::client::ClientError;
diff --git a/src/utils/colour.rs b/src/utils/colour.rs
new file mode 100644
index 0000000..7bb7bfd
--- /dev/null
+++ b/src/utils/colour.rs
@@ -0,0 +1,126 @@
+use std::default::Default;
+
+macro_rules! colour {
+ ($struct_:ident; $($name:ident, $val:expr;)*) => {
+ impl $struct_ {
+ $(
+ pub fn $name() -> Colour {
+ Colour::new($val)
+ }
+ )*
+ }
+ }
+}
+
+/// A utility struct to help with working with the basic representation of a
+/// colour. This is particularly useful when working with a [`Role`]'s colour,
+/// as the API works with an integer value instead of an RGB value.
+///
+/// Instances can be created by using the struct's associated functions. These
+/// produce values equivilant to those found in the official client's colour
+/// picker.
+///
+/// # Examples
+///
+/// Passing in a role's colour, and then retrieving its green component
+/// via [`get_g`]:
+///
+/// ```rust,ignore
+/// use serenity::utils::Colour;
+///
+/// // assuming a `role` has already been bound
+///
+/// let colour = Colour::new(role.colour);
+/// let green = colour.get_g();
+///
+/// println!("The green component is: {}", green);
+/// ```
+///
+/// Creating an instance with the [`dark_teal`] value:
+///
+/// ```rust,ignore
+/// use serenity::utils::Colour;
+///
+/// let colour = Colour::dark_teal();
+/// ```
+///
+/// [`Role`]: ../model/struct.Role.html
+/// [`dark_teal`]: #method.dark_teal
+/// [`get_g`]: #method.get_g
+pub struct Colour {
+ /// The raw inner integer value of this Colour. This is worked with to
+ /// generate values such as the red component value.
+ pub value: u32,
+}
+
+impl Colour {
+ /// Generates a new Colour with the given integer value set.
+ pub fn new(value: u32) -> Colour {
+ Colour {
+ value: value,
+ }
+ }
+
+ /// Returns the red RGB component of this Colour.
+ pub fn get_r(&self) -> u8 {
+ ((self.value >> 16) & 255) as u8
+ }
+
+ /// Returns the green RGB component of this Colour.
+ pub fn get_g(&self) -> u8 {
+ ((self.value >> 8) & 255) as u8
+ }
+
+ /// Returns the blue RGB component of this Colour.
+ pub fn get_b(&self) -> u8 {
+ (self.value & 255) as u8
+ }
+
+ /// Returns a tuple of the red, green, and blue components of this Colour.
+ pub fn get_tuple(&self) -> (u8, u8, u8) {
+ (self.get_r(), self.get_g(), self.get_b())
+ }
+}
+
+impl From<u32> for Colour {
+ fn from(value: u32) -> Colour {
+ Colour::new(value)
+ }
+}
+
+impl From<u64> for Colour {
+ fn from(value: u64) -> Colour {
+ Colour::new(value as u32)
+ }
+}
+
+colour! {
+ Colour;
+ blue, 0x3498db;
+ dark_blue, 0x206694;
+ dark_green, 0x1f8b4c;
+ dark_gold, 0xc27c0e;
+ dark_grey, 0x607d8b;
+ dark_magenta, 0xad1457;
+ dark_orange, 0xa84300;
+ dark_purple, 0x71368a;
+ dark_red, 0x992d22;
+ dark_teal, 0x11806a;
+ darker_grey, 0x546e7a;
+ gold, 0xf1c40f;
+ light_grey, 0x979c9f;
+ lighter_grey, 0x95a5a6;
+ magenta, 0xe91e63;
+ orange, 0xe67e22;
+ purple, 0x9b59b6;
+ red, 0xe74c3c;
+ teal, 0x1abc9c;
+}
+
+impl Default for Colour {
+ fn default() -> Colour {
+ Colour {
+ value: 0,
+ }
+ }
+}
diff --git a/src/utils/message_builder.rs b/src/utils/message_builder.rs
new file mode 100644
index 0000000..a24fd2d
--- /dev/null
+++ b/src/utils/message_builder.rs
@@ -0,0 +1,97 @@
+use std::default::Default;
+use std::fmt;
+use ::model::{ChannelId, Emoji, Mentionable, RoleId, UserId};
+
+/// The Message Builder is an ergonomic utility to easily build a message,
+/// by adding text and mentioning mentionable structs.
+///
+/// The finalized value can be accessed via `.build()` or the inner value.
+///
+/// # Examples
+///
+/// Build a message, mentioning a user and an emoji:
+///
+/// ```rust,ignore
+/// use serenity::utils::MessageBuilder;
+///
+/// let content = MessageBuilder::new()
+/// .push("You sent a message, ")
+/// .mention(user)
+/// .push("! ");
+/// .mention(emoji)
+/// .build();
+/// ```
+pub struct MessageBuilder(pub String);
+
+impl MessageBuilder {
+ /// Creates a new, empty-content builder.
+ pub fn new() -> MessageBuilder {
+ MessageBuilder::default()
+ }
+
+ /// Pulls the inner value out of the builder. This is equivilant to simply
+ /// retrieving the value.
+ pub fn build(self) -> String {
+ self.0
+ }
+
+ /// Mentions the channel in the built message.
+ pub fn channel<C: Into<ChannelId>>(mut self, channel: C) -> Self {
+ self.0.push_str(&format!("{}", channel.into()));
+
+ self
+ }
+
+ /// Uses and displays the given emoji in the built message.
+ pub fn emoji(mut self, emoji: Emoji) -> Self {
+ self.0.push_str(&format!("{}", emoji));
+
+ self
+ }
+
+ /// Mentions something that implements the
+ /// [Mentionable](../model/trait.Mentionable.html) trait.
+ pub fn mention<M: Mentionable>(mut self, item: M) -> Self {
+ self.0.push_str(&item.mention());
+
+ self
+ }
+
+ /// Pushes a string to the internal message content.
+ ///
+ /// Note that this does not mutate either the given data or the internal
+ /// message content in anyway prior to appending the given content to the
+ /// internal message.
+ pub fn push(mut self, content: &str) -> Self {
+ self.0.push_str(content);
+
+ self
+ }
+
+
+ /// Mentions the role in the built message.
+ pub fn role<R: Into<RoleId>>(mut self, role: R) -> Self {
+ self.0.push_str(&format!("{}", role.into()));
+
+ self
+ }
+
+ /// Mentions the user in the built message.
+ pub fn user<U: Into<UserId>>(mut self, user: U) -> Self {
+ self.0.push_str(&format!("{}", user.into()));
+
+ self
+ }
+}
+
+impl fmt::Display for MessageBuilder {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ fmt::Display::fmt(&self.0, f)
+ }
+}
+
+impl Default for MessageBuilder {
+ fn default() -> MessageBuilder {
+ MessageBuilder(String::default())
+ }
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
new file mode 100644
index 0000000..a2dbf6f
--- /dev/null
+++ b/src/utils/mod.rs
@@ -0,0 +1,140 @@
+
+//! A set of utilities to help with common use cases that are not required to
+//! fully use the library.
+
+use base64;
+use std::ffi::OsStr;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+use ::prelude::*;
+
+mod colour;
+mod message_builder;
+
+pub use self::colour::Colour;
+pub use self::message_builder::MessageBuilder;
+
+macro_rules! cdn_concat {
+ ($e:expr) => {
+ concat!("https://cdn.discordapp.com", $e)
+ }
+}
+macro_rules! api {
+ ($e:expr) => {
+ concat!("https://discordapp.com/api/v6", $e)
+ };
+ ($e:expr, $($rest:tt)*) => {
+ format!(api!($e), $($rest)*)
+ };
+}
+
+macro_rules! api_concat {
+ ($e:expr) => {
+ concat!("https://discordapp.com/api/v6", $e)
+ }
+}
+macro_rules! status_concat {
+ ($e:expr) => {
+ concat!("https://status.discordapp.com/api/v2", $e)
+ }
+}
+
+#[doc(hidden)]
+pub fn decode_array<T, F: Fn(Value) -> Result<T>>(value: Value, f: F) -> Result<Vec<T>> {
+ into_array(value).and_then(|x| x.into_iter().map(f).collect())
+}
+
+#[doc(hidden)]
+pub fn into_array(value: Value) -> Result<Vec<Value>> {
+ match value {
+ Value::Array(v) => Ok(v),
+ value => Err(Error::Decode("Expected array", value)),
+ }
+}
+
+macro_rules! request {
+ ($route:path, $method:ident($body:expr), $url:expr, $($rest:tt)*) => {{
+ let client = HyperClient::new();
+ try!(request($route, || client
+ .$method(&format!(api!($url), $($rest)*))
+ .body(&$body)))
+ }};
+ ($route:path, $method:ident($body:expr), $url:expr) => {{
+ let client = HyperClient::new();
+ try!(request($route, || client
+ .$method(api!($url))
+ .body(&$body)))
+ }};
+ ($route:path, $method:ident, $url:expr, $($rest:tt)*) => {{
+ let client = HyperClient::new();
+ try!(request($route, || client
+ .$method(&format!(api!($url), $($rest)*))))
+ }};
+ ($route:path, $method:ident, $url:expr) => {{
+ let client = HyperClient::new();
+ try!(request($route, || client
+ .$method(api_concat!($url))))
+ }};
+}
+
+/// Retrieves the "code" part of an [invite][`RichInvite`] out of a URL.
+///
+/// # Examples
+/// Retrieving the code from the URL `https://discord.gg/0cDvIgU2voY8RSYL`:
+///
+/// ```rust
+/// use serenity::utils;
+///
+/// assert!(utils::parse_invite("https://discord.gg/0cDvIgU2voY8RSYL") == "0cDvIgU2voY8RSYL");
+/// ```
+///
+/// [`RichInvite`]: ../model/struct.RichInvite.html
+pub fn parse_invite(code: &str) -> &str {
+ if code.starts_with("https://discord.gg/") {
+ &code[19..]
+ } else if code.starts_with("http://discord.gg/") {
+ &code[18..]
+ } else if code.starts_with("discord.gg/") {
+ &code[11..]
+ } else {
+ code
+ }
+}
+
+/// Reads an image from a path and encodes it into base64.
+///
+/// This can be used for methods like [`EditProfile::avatar`].
+///
+/// # Examples
+///
+/// Reads an image located at `./cat.png` into a base64-encoded string:
+///
+/// ```rust,ignore
+/// use serenity::utils;
+///
+/// let image = match utils::read_image("./cat.png") {
+/// Ok(image) => image,
+/// Err(why) => {
+/// // properly handle the error
+/// },
+/// };
+/// ```
+///
+/// [`EditProfile::avatar`]: ../builder/struct.EditProfile.html#method.avatar
+pub fn read_image<P: AsRef<Path>>(path: P) -> Result<String> {
+ let path = path.as_ref();
+
+ let mut v = Vec::default();
+ let mut f = try!(File::open(path));
+ let _ = f.read_to_end(&mut v);
+
+ let b64 = base64::encode(&v);
+ let ext = if path.extension() == Some(OsStr::new("png")) {
+ "png"
+ } else {
+ "jpg"
+ };
+
+ Ok(format!("data:image/{};base64,{}", ext, b64))
+}