diff options
Diffstat (limited to 'src')
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)) +} |