diff options
81 files changed, 2964 insertions, 1871 deletions
@@ -8,11 +8,12 @@ license = "ISC" name = "serenity" readme = "README.md" repository = "https://github.com/zeyla/serenity.git" -version = "0.4.3" +version = "0.5.0" [dependencies] bitflags = "^1.0" log = "~0.3" +parking_lot = "^0.4" serde = "^1.0" serde_derive = "^1.0" serde_json = "^1.0" @@ -23,7 +24,7 @@ version = "~0.7" [dependencies.byteorder] optional = true -version = "1.1" +version = "^1.0" [dependencies.chrono] features = ["serde"] @@ -59,10 +60,6 @@ version = "0.1" optional = true version = "0.2" -[dependencies.parking_lot] -optional = true -version = "0.4" - [dependencies.sodiumoxide] default-features = false optional = true @@ -104,7 +101,6 @@ client = [ "gateway", "http", "lazy_static", - "parking_lot", "threadpool", "typemap", ] diff --git a/examples/01_basic_ping_bot/src/main.rs b/examples/01_basic_ping_bot/src/main.rs index 4c2f35c..a900cc2 100644 --- a/examples/01_basic_ping_bot/src/main.rs +++ b/examples/01_basic_ping_bot/src/main.rs @@ -12,7 +12,7 @@ impl EventHandler for Handler { // // Event handlers are dispatched through multi-threading, and so multiple // of a single event can be dispatched simultaneously. - fn on_message(&self, _: Context, msg: Message) { + fn message(&self, _: Context, msg: Message) { if msg.content == "!ping" { // Sending a message can fail, due to a network error, an // authentication error, or lack of permissions to post in the @@ -30,7 +30,7 @@ impl EventHandler for Handler { // private channels, and more. // // In this case, just print what the current user's username is. - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } @@ -43,7 +43,7 @@ fn main() { // Create a new instance of the Client, logging in as a bot. This will // automatically prepend your bot token with "Bot ", which is a requirement // by Discord for bot users. - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); // Finally, start a single shard, and start listening to events. // diff --git a/examples/02_transparent_guild_sharding/src/main.rs b/examples/02_transparent_guild_sharding/src/main.rs index 55b20c1..c1a5bbb 100644 --- a/examples/02_transparent_guild_sharding/src/main.rs +++ b/examples/02_transparent_guild_sharding/src/main.rs @@ -25,24 +25,17 @@ use std::env; struct Handler; impl EventHandler for Handler { - fn on_message(&self, ctx: Context, msg: Message) { + fn message(&self, ctx: Context, msg: Message) { if msg.content == "!ping" { - // The current shard needs to be unlocked so it can be read from, as - // multiple threads may otherwise attempt to read from or mutate it - // concurrently. - { - let shard = ctx.shard.lock(); - let shard_info = shard.shard_info(); - println!("Shard {}", shard_info[0]); - } + println!("Shard {}", ctx.shard_id); if let Err(why) = msg.channel_id.say("Pong!") { println!("Error sending message: {:?}", why); } - } + } } - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } @@ -52,7 +45,7 @@ fn main() { // Configure the client with your Discord bot token in the environment. let token = env::var("DISCORD_TOKEN") .expect("Expected a token in the environment"); - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); // The total number of shards to use. The "current shard number" of a // shard - that is, the shard it is assigned to - is indexed at 0, diff --git a/examples/03_struct_utilities/src/main.rs b/examples/03_struct_utilities/src/main.rs index e08cf00..4aafb07 100644 --- a/examples/03_struct_utilities/src/main.rs +++ b/examples/03_struct_utilities/src/main.rs @@ -7,7 +7,7 @@ use std::env; struct Handler; impl EventHandler for Handler { - fn on_message(&self, _: Context, msg: Message) { + fn message(&self, _: Context, msg: Message) { if msg.content == "!messageme" { // If the `methods` feature is enabled, then model structs will // have a lot of useful methods implemented, to avoid using an @@ -23,7 +23,7 @@ impl EventHandler for Handler { } } - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } @@ -32,7 +32,7 @@ fn main() { // Configure the client with your Discord bot token in the environment. let token = env::var("DISCORD_TOKEN") .expect("Expected a token in the environment"); - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); if let Err(why) = client.start() { println!("Client error: {:?}", why); diff --git a/examples/04_message_builder/src/main.rs b/examples/04_message_builder/src/main.rs index 713224b..1ecd302 100644 --- a/examples/04_message_builder/src/main.rs +++ b/examples/04_message_builder/src/main.rs @@ -8,7 +8,7 @@ use std::env; struct Handler; impl EventHandler for Handler { - fn on_message(&self, _: Context, msg: Message) { + fn message(&self, _: Context, msg: Message) { if msg.content == "!ping" { let channel = match msg.channel_id.get() { Ok(channel) => channel, @@ -37,7 +37,7 @@ impl EventHandler for Handler { } } - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } @@ -45,7 +45,7 @@ fn main() { // Configure the client with your Discord bot token in the environment. let token = env::var("DISCORD_TOKEN") .expect("Expected a token in the environment"); - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); if let Err(why) = client.start() { println!("Client error: {:?}", why); diff --git a/examples/05_command_framework/src/main.rs b/examples/05_command_framework/src/main.rs index 437c44e..02a4f41 100644 --- a/examples/05_command_framework/src/main.rs +++ b/examples/05_command_framework/src/main.rs @@ -31,7 +31,7 @@ impl Key for CommandCounter { struct Handler; impl EventHandler for Handler { - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } @@ -41,7 +41,7 @@ fn main() { let token = env::var("DISCORD_TOKEN").expect( "Expected a token in the environment", ); - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); { let mut data = client.data.lock(); @@ -199,7 +199,7 @@ command!(about_role(_ctx, msg, args) { if let Some(guild) = msg.guild() { // `role_by_name()` allows us to attempt attaining a reference to a role // via its name. - if let Some(role) = guild.read().unwrap().role_by_name(&potential_role_name) { + if let Some(role) = guild.read().role_by_name(&potential_role_name) { if let Err(why) = msg.channel_id.say(&format!("Role-ID: {}", role.id)) { println!("Error sending message: {:?}", why); } diff --git a/examples/06_voice/src/main.rs b/examples/06_voice/src/main.rs index 8de2c3b..64738a3 100644 --- a/examples/06_voice/src/main.rs +++ b/examples/06_voice/src/main.rs @@ -21,7 +21,7 @@ use std::env; struct Handler; impl EventHandler for Handler { - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { println!("{} is connected!", ready.user.name); } } @@ -30,7 +30,7 @@ fn main() { // Configure the client with your Discord bot token in the environment. let token = env::var("DISCORD_TOKEN") .expect("Expected a token in the environment"); - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); client.with_framework(StandardFramework::new() .configure(|c| c @@ -49,8 +49,8 @@ fn main() { } command!(deafen(ctx, msg) { - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Groups and DMs not supported")); @@ -95,8 +95,8 @@ command!(join(ctx, msg, args) { }, }; - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Groups and DMs not supported")); @@ -111,8 +111,8 @@ command!(join(ctx, msg, args) { }); command!(leave(ctx, msg) { - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Groups and DMs not supported")); @@ -133,8 +133,8 @@ command!(leave(ctx, msg) { }); command!(mute(ctx, msg) { - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Groups and DMs not supported")); @@ -182,8 +182,8 @@ command!(play(ctx, msg, args) { return Ok(()); } - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Error finding channel info")); @@ -212,8 +212,8 @@ command!(play(ctx, msg, args) { }); command!(undeafen(ctx, msg) { - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Error finding channel info")); @@ -231,8 +231,8 @@ command!(undeafen(ctx, msg) { }); command!(unmute(ctx, msg) { - let guild_id = match CACHE.read().unwrap().guild_channel(msg.channel_id) { - Some(channel) => channel.read().unwrap().guild_id, + let guild_id = match CACHE.read().guild_channel(msg.channel_id) { + Some(channel) => channel.read().guild_id, None => { check_msg(msg.channel_id.say("Error finding channel info")); diff --git a/examples/07_sample_bot_structure/src/main.rs b/examples/07_sample_bot_structure/src/main.rs index 37f6c98..4c0ff8b 100644 --- a/examples/07_sample_bot_structure/src/main.rs +++ b/examples/07_sample_bot_structure/src/main.rs @@ -48,7 +48,10 @@ fn main() { // `RUST_LOG` to debug`. env_logger::init().expect("Failed to initialize env_logger"); - let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); + let token = env::var("DISCORD_TOKEN") + .expect("Expected a token in the environment"); + + let mut client = Client::new(&token, Handler).expect("Err creating client"); let owners = match http::get_current_application_info() { Ok(info) => { diff --git a/examples/08_env_logging/src/main.rs b/examples/08_env_logging/src/main.rs index db34037..ca6af8f 100644 --- a/examples/08_env_logging/src/main.rs +++ b/examples/08_env_logging/src/main.rs @@ -11,12 +11,12 @@ use std::env; struct Handler; impl EventHandler for Handler { - fn on_ready(&self, _: Context, ready: Ready) { + fn ready(&self, _: Context, ready: Ready) { // Log at the INFO level. This is a macro from the `log` crate. info!("{} is connected!", ready.user.name); } - fn on_resume(&self, _: Context, resume: ResumedEvent) { + fn resume(&self, _: Context, resume: ResumedEvent) { // Log at the DEBUG level. // // In this example, this will not show up in the logs because DEBUG is @@ -37,7 +37,7 @@ fn main() { let token = env::var("DISCORD_TOKEN") .expect("Expected a token in the environment"); - let mut client = Client::new(&token, Handler); + let mut client = Client::new(&token, Handler).expect("Err creating client"); if let Err(why) = client.start() { error!("Client error: {:?}", why); diff --git a/src/builder/create_embed.rs b/src/builder/create_embed.rs index b38984c..7c4d3d6 100644 --- a/src/builder/create_embed.rs +++ b/src/builder/create_embed.rs @@ -17,10 +17,12 @@ use chrono::{DateTime, TimeZone}; use serde_json::Value; +use std::collections::HashMap; use std::default::Default; use std::fmt::Display; use internal::prelude::*; use model::Embed; +use utils; #[cfg(feature = "utils")] use utils::Colour; @@ -37,7 +39,7 @@ use utils::Colour; /// [`Embed`]: ../model/struct.Embed.html /// [`ExecuteWebhook::embeds`]: struct.ExecuteWebhook.html#method.embeds #[derive(Clone, Debug)] -pub struct CreateEmbed(pub Map<String, Value>); +pub struct CreateEmbed(pub HashMap<&'static str, Value>); impl CreateEmbed { /// Set the author of the embed. @@ -48,9 +50,9 @@ impl CreateEmbed { /// [`CreateEmbedAuthor`]: struct.CreateEmbedAuthor.html pub fn author<F>(mut self, f: F) -> Self where F: FnOnce(CreateEmbedAuthor) -> CreateEmbedAuthor { - let author = f(CreateEmbedAuthor::default()).0; + let map = utils::hashmap_to_json_map(f(CreateEmbedAuthor::default()).0); - self.0.insert("author".to_string(), Value::Object(author)); + self.0.insert("author", Value::Object(map)); CreateEmbed(self.0) } @@ -68,7 +70,7 @@ impl CreateEmbed { #[cfg(feature = "utils")] pub fn colour<C: Into<Colour>>(mut self, colour: C) -> Self { self.0.insert( - "color".to_string(), + "color", Value::Number(Number::from(u64::from(colour.into().0))), ); @@ -88,7 +90,7 @@ impl CreateEmbed { #[cfg(not(feature = "utils"))] pub fn colour(mut self, colour: u32) -> Self { self.0 - .insert("color".to_string(), Value::Number(Number::from(colour))); + .insert("color", Value::Number(Number::from(colour))); CreateEmbed(self.0) } @@ -98,7 +100,7 @@ impl CreateEmbed { /// **Note**: This can't be longer than 2048 characters. pub fn description<D: Display>(mut self, description: D) -> Self { self.0.insert( - "description".to_string(), + "description", Value::String(format!("{}", description)), ); @@ -115,54 +117,35 @@ impl CreateEmbed { /// name and 1024 in a field value and a field is inline by default. /// /// [`CreateEmbedField`]: struct.CreateEmbedField.html - pub fn field<F>(mut self, f: F) -> Self - where F: FnOnce(CreateEmbedField) -> CreateEmbedField { - let field = f(CreateEmbedField::default()).0; - + pub fn field<T, U>(mut self, name: T, value: U, inline: bool) -> Self + where T: Into<String>, U: Into<String> { { - let key = "fields".to_string(); - - let entry = self.0.remove(&key).unwrap_or_else(|| Value::Array(vec![])); - let mut arr = match entry { - Value::Array(inner) => inner, - _ => { - // The type of `entry` should always be a `Value::Array`. - // - // Theoretically this never happens, but you never know. - // - // In the event that it does, just return the current value. - return CreateEmbed(self.0); - }, - }; - arr.push(Value::Object(field)); - - self.0.insert("fields".to_string(), Value::Array(arr)); + let entry = self.0 + .entry("fields") + .or_insert_with(|| Value::Array(vec![])); + + if let Value::Array(ref mut inner) = *entry { + inner.push(json!({ + "inline": inline, + "name": name.into(), + "value": value.into(), + })); + } } - CreateEmbed(self.0) + self } /// Adds multiple fields at once. - pub fn fields<It: IntoIterator<Item=CreateEmbedField>>(mut self, fields: It) -> Self { - let fields = fields - .into_iter() - .map(|m| Value::Object(m.0)) - .collect::<Vec<Value>>(); - - { - let key = "fields".to_string(); - - let entry = self.0.remove(&key).unwrap_or_else(|| Value::Array(vec![])); - let mut arr = match entry { - Value::Array(inner) => inner, - _ => return CreateEmbed(self.0), - }; - arr.extend(fields); - - self.0.insert("fields".to_string(), Value::Array(arr)); + pub fn fields<T, U, It>(mut self, fields: It) -> Self + where It: IntoIterator<Item=(T, U, bool)>, + T: Into<String>, + U: Into<String> { + for field in fields { + self = self.field(field.0.into(), field.1.into(), field.2); } - CreateEmbed(self.0) + self } /// Set the footer of the embed. @@ -174,8 +157,9 @@ impl CreateEmbed { pub fn footer<F>(mut self, f: F) -> Self where F: FnOnce(CreateEmbedFooter) -> CreateEmbedFooter { let footer = f(CreateEmbedFooter::default()).0; + let map = utils::hashmap_to_json_map(footer); - self.0.insert("footer".to_string(), Value::Object(footer)); + self.0.insert("footer", Value::Object(map)); CreateEmbed(self.0) } @@ -186,7 +170,7 @@ impl CreateEmbed { "url": url.to_string() }); - self.0.insert("image".to_string(), image); + self.0.insert("image", image); CreateEmbed(self.0) } @@ -197,7 +181,7 @@ impl CreateEmbed { "url": url.to_string(), }); - self.0.insert("thumbnail".to_string(), thumbnail); + self.0.insert("thumbnail", thumbnail); CreateEmbed(self.0) } @@ -223,8 +207,9 @@ impl CreateEmbed { /// # use serenity::model::*; /// # /// struct Handler; + /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { + /// fn message(&self, _: Context, msg: Message) { /// if msg.content == "~embed" { /// let _ = msg.channel_id.send_message(|m| m /// .embed(|e| e @@ -234,7 +219,9 @@ impl CreateEmbed { /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// Creating a join-log: @@ -246,23 +233,30 @@ impl CreateEmbed { /// # use serenity::model::*; /// # /// struct Handler; + /// /// impl EventHandler for Handler { +<<<<<<< HEAD + /// fn guild_member_addition(&self, _: Context, guild_id: GuildId, member: Member) { + /// use serenity::client::CACHE; + /// let cache = CACHE.read(); +======= /// fn on_guild_member_addition(&self, _: Context, guild_id: GuildId, member: Member) { /// use serenity::CACHE; /// let cache = CACHE.read().unwrap(); +>>>>>>> v0.4.3 /// /// if let Some(guild) = cache.guild(guild_id) { - /// let guild = guild.read().unwrap(); + /// let guild = guild.read(); /// /// let channel_search = guild /// .channels /// .values() - /// .find(|c| c.read().unwrap().name == "join-log"); + /// .find(|c| c.read().name == "join-log"); /// /// if let Some(channel) = channel_search { - /// let user = member.user.read().unwrap(); + /// let user = member.user.read(); /// - /// let _ = channel.read().unwrap().send_message(|m| m + /// let _ = channel.read().send_message(|m| m /// .embed(|e| { /// let mut e = e /// .author(|a| a.icon_url(&user.face()).name(&user.name)) @@ -279,11 +273,13 @@ impl CreateEmbed { /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` pub fn timestamp<T: Into<Timestamp>>(mut self, timestamp: T) -> Self { self.0 - .insert("timestamp".to_string(), Value::String(timestamp.into().ts)); + .insert("timestamp", Value::String(timestamp.into().ts)); CreateEmbed(self.0) } @@ -291,7 +287,7 @@ impl CreateEmbed { /// Set the title of the embed. pub fn title<D: Display>(mut self, title: D) -> Self { self.0 - .insert("title".to_string(), Value::String(format!("{}", title))); + .insert("title", Value::String(format!("{}", title))); CreateEmbed(self.0) } @@ -299,7 +295,7 @@ impl CreateEmbed { /// Set the URL to direct to when clicking on the title. pub fn url(mut self, url: &str) -> Self { self.0 - .insert("url".to_string(), Value::String(url.to_string())); + .insert("url", Value::String(url.to_string())); CreateEmbed(self.0) } @@ -318,8 +314,8 @@ impl CreateEmbed { impl Default for CreateEmbed { /// Creates a builder with default values, setting the `type` to `rich`. fn default() -> CreateEmbed { - let mut map = Map::new(); - map.insert("type".to_string(), Value::String("rich".to_string())); + let mut map = HashMap::new(); + map.insert("type", Value::String("rich".to_string())); CreateEmbed(map) } @@ -353,9 +349,7 @@ impl From<Embed> for CreateEmbed { } for field in embed.fields { - b = b.field(move |f| { - f.inline(field.inline).name(&field.name).value(&field.value) - }); + b = b.field(field.name, field.value, field.inline); } if let Some(image) = embed.image { @@ -391,81 +385,31 @@ impl From<Embed> for CreateEmbed { /// [`CreateEmbed::author`]: struct.CreateEmbed.html#method.author /// [`name`]: #method.name #[derive(Clone, Debug, Default)] -pub struct CreateEmbedAuthor(pub Map<String, Value>); +pub struct CreateEmbedAuthor(pub HashMap<&'static str, Value>); impl CreateEmbedAuthor { /// Set the URL of the author's icon. pub fn icon_url(mut self, icon_url: &str) -> Self { - self.0 - .insert("icon_url".to_string(), Value::String(icon_url.to_string())); + self.0.insert("icon_url", Value::String(icon_url.to_string())); self } /// Set the author's name. pub fn name(mut self, name: &str) -> Self { - self.0 - .insert("name".to_string(), Value::String(name.to_string())); + self.0.insert("name", Value::String(name.to_string())); self } /// Set the author's URL. pub fn url(mut self, url: &str) -> Self { - self.0 - .insert("url".to_string(), Value::String(url.to_string())); + self.0.insert("url", Value::String(url.to_string())); self } } -/// A builder to create a fake [`Embed`] object's field, for use with the -/// [`CreateEmbed::field`] method. -/// -/// This does not require any field be set. `inline` is set to `true` by -/// default. -/// -/// [`Embed`]: ../model/struct.Embed.html -/// [`CreateEmbed::field`]: struct.CreateEmbed.html#method.field -#[derive(Clone, Debug)] -pub struct CreateEmbedField(pub Map<String, Value>); - -impl CreateEmbedField { - /// Set whether the field is inlined. Set to true by default. - pub fn inline(mut self, inline: bool) -> Self { - self.0.insert("inline".to_string(), Value::Bool(inline)); - - self - } - - /// Set the field's name. It can't be longer than 256 characters. - pub fn name<D: Display>(mut self, name: D) -> Self { - self.0 - .insert("name".to_string(), Value::String(format!("{}", name))); - - self - } - - /// Set the field's value. It can't be longer than 1024 characters. - pub fn value<D: Display>(mut self, value: D) -> Self { - self.0 - .insert("value".to_string(), Value::String(format!("{}", value))); - - self - } -} - -impl Default for CreateEmbedField { - /// Creates a builder with default values, setting the value of `inline` to - /// `true`. - fn default() -> CreateEmbedField { - let mut map = Map::new(); - map.insert("inline".to_string(), Value::Bool(true)); - - CreateEmbedField(map) - } -} - /// A builder to create a fake [`Embed`] object's footer, for use with the /// [`CreateEmbed::footer`] method. /// @@ -474,21 +418,19 @@ impl Default for CreateEmbedField { /// [`Embed`]: ../model/struct.Embed.html /// [`CreateEmbed::footer`]: struct.CreateEmbed.html#method.footer #[derive(Clone, Debug, Default)] -pub struct CreateEmbedFooter(pub Map<String, Value>); +pub struct CreateEmbedFooter(pub HashMap<&'static str, Value>); impl CreateEmbedFooter { /// Set the icon URL's value. This only supports HTTP(S). pub fn icon_url(mut self, icon_url: &str) -> Self { - self.0 - .insert("icon_url".to_string(), Value::String(icon_url.to_string())); + self.0.insert("icon_url", Value::String(icon_url.to_string())); self } /// Set the footer's text. pub fn text<D: Display>(mut self, text: D) -> Self { - self.0 - .insert("text".to_string(), Value::String(format!("{}", text))); + self.0.insert("text", Value::String(format!("{}", text))); self } diff --git a/src/builder/create_invite.rs b/src/builder/create_invite.rs index 645d401..0b05b66 100644 --- a/src/builder/create_invite.rs +++ b/src/builder/create_invite.rs @@ -1,4 +1,5 @@ use serde_json::Value; +use std::collections::HashMap; use std::default::Default; use internal::prelude::*; @@ -21,7 +22,7 @@ use internal::prelude::*; /// fn on_message(&self, _: Context, msg: Message) { /// use serenity::CACHE; /// if msg.content == "!createinvite" { -/// let channel = match CACHE.read().unwrap().guild_channel(msg.channel_id) { +/// let channel = match CACHE.read().guild_channel(msg.channel_id) { /// Some(channel) => channel, /// None => { /// let _ = msg.channel_id.say("Error creating invite"); @@ -30,7 +31,7 @@ use internal::prelude::*; /// }, /// }; /// -/// let reader = channel.read().unwrap(); +/// let reader = channel.read(); /// /// let invite = match reader.create_invite(|i| i.max_age(3600).max_uses(10)) { /// Ok(invite) => invite, @@ -52,13 +53,16 @@ use internal::prelude::*; /// } /// } /// } -/// let mut client = Client::new("token", Handler); client.start().unwrap(); +/// +/// let mut client = Client::new("token", Handler).unwrap(); +/// +/// client.start().unwrap(); /// ``` /// /// [`GuildChannel::create_invite`]: ../model/struct.GuildChannel.html#method.create_invite /// [`RichInvite`]: ../model/struct.Invite.html #[derive(Clone, Debug)] -pub struct CreateInvite(pub JsonMap); +pub struct CreateInvite(pub HashMap<&'static str, Value>); impl CreateInvite { /// The duration that the invite will be valid for. @@ -77,8 +81,8 @@ impl CreateInvite { /// # use std::error::Error; /// # /// # fn try_main() -> Result<(), Box<Error>> { - /// # let channel = CACHE.read().unwrap().guild_channel(81384788765712384).unwrap(); - /// # let channel = channel.read().unwrap(); + /// # let channel = CACHE.read().guild_channel(81384788765712384).unwrap(); + /// # let channel = channel.read(); /// # /// let invite = channel.create_invite(|i| i.max_age(3600))?; /// # Ok(()) @@ -89,8 +93,7 @@ impl CreateInvite { /// # } /// ``` pub fn max_age(mut self, max_age: u64) -> Self { - self.0 - .insert("max_age".to_string(), Value::Number(Number::from(max_age))); + self.0.insert("max_age", Value::Number(Number::from(max_age))); self } @@ -111,8 +114,8 @@ impl CreateInvite { /// # use std::error::Error; /// # /// # fn try_main() -> Result<(), Box<Error>> { - /// # let channel = CACHE.read().unwrap().guild_channel(81384788765712384).unwrap(); - /// # let channel = channel.read().unwrap(); + /// # let channel = CACHE.read().guild_channel(81384788765712384).unwrap(); + /// # let channel = channel.read(); /// # /// let invite = channel.create_invite(|i| i.max_uses(5))?; /// # Ok(()) @@ -123,8 +126,7 @@ impl CreateInvite { /// # } /// ``` pub fn max_uses(mut self, max_uses: u64) -> Self { - self.0 - .insert("max_uses".to_string(), Value::Number(Number::from(max_uses))); + self.0.insert("max_uses", Value::Number(Number::from(max_uses))); self } @@ -143,8 +145,8 @@ impl CreateInvite { /// # use std::error::Error; /// # /// # fn try_main() -> Result<(), Box<Error>> { - /// # let channel = CACHE.read().unwrap().guild_channel(81384788765712384).unwrap(); - /// # let channel = channel.read().unwrap(); + /// # let channel = CACHE.read().guild_channel(81384788765712384).unwrap(); + /// # let channel = channel.read(); /// # /// let invite = channel.create_invite(|i| i.temporary(true))?; /// # Ok(()) @@ -155,8 +157,7 @@ impl CreateInvite { /// # } /// ``` pub fn temporary(mut self, temporary: bool) -> Self { - self.0 - .insert("temporary".to_string(), Value::Bool(temporary)); + self.0.insert("temporary", Value::Bool(temporary)); self } @@ -175,8 +176,8 @@ impl CreateInvite { /// # use std::error::Error; /// # /// # fn try_main() -> Result<(), Box<Error>> { - /// # let channel = CACHE.read().unwrap().guild_channel(81384788765712384).unwrap(); - /// # let channel = channel.read().unwrap(); + /// # let channel = CACHE.read().guild_channel(81384788765712384).unwrap(); + /// # let channel = channel.read(); /// # /// let invite = channel.create_invite(|i| i.unique(true))?; /// # Ok(()) @@ -187,7 +188,7 @@ impl CreateInvite { /// # } /// ``` pub fn unique(mut self, unique: bool) -> Self { - self.0.insert("unique".to_string(), Value::Bool(unique)); + self.0.insert("unique", Value::Bool(unique)); self } @@ -206,8 +207,8 @@ impl Default for CreateInvite { /// let invite_builder = CreateInvite::default(); /// ``` fn default() -> CreateInvite { - let mut map = Map::new(); - map.insert("validate".to_string(), Value::Null); + let mut map = HashMap::new(); + map.insert("validate", Value::Null); CreateInvite(map) } diff --git a/src/builder/create_message.rs b/src/builder/create_message.rs index 821048b..5b3abdb 100644 --- a/src/builder/create_message.rs +++ b/src/builder/create_message.rs @@ -1,6 +1,8 @@ use super::CreateEmbed; use model::ReactionType; use internal::prelude::*; +use utils; +use std::collections::HashMap; use std::fmt::Display; /// A builder to specify the contents of an [`http::send_message`] request, @@ -39,15 +41,14 @@ use std::fmt::Display; /// [`embed`]: #method.embed /// [`http::send_message`]: ../http/fn.send_message.html #[derive(Clone, Debug)] -pub struct CreateMessage(pub Map<String, Value>, pub Option<Vec<ReactionType>>); +pub struct CreateMessage(pub HashMap<&'static str, Value>, pub Option<Vec<ReactionType>>); impl CreateMessage { /// Set the content of the message. /// /// **Note**: Message contents must be under 2000 unicode code points. pub fn content<D: Display>(mut self, content: D) -> Self { - self.0 - .insert("content".to_string(), Value::String(format!("{}", content))); + self.0.insert("content", Value::String(format!("{}", content))); CreateMessage(self.0, self.1) } @@ -55,9 +56,10 @@ impl CreateMessage { /// Set an embed for the message. pub fn embed<F>(mut self, f: F) -> Self where F: FnOnce(CreateEmbed) -> CreateEmbed { - let embed = Value::Object(f(CreateEmbed::default()).0); + let map = utils::hashmap_to_json_map(f(CreateEmbed::default()).0); + let embed = Value::Object(map); - self.0.insert("embed".to_string(), embed); + self.0.insert("embed", embed); CreateMessage(self.0, self.1) } @@ -68,7 +70,7 @@ impl CreateMessage { /// /// Defaults to `false`. pub fn tts(mut self, tts: bool) -> Self { - self.0.insert("tts".to_string(), Value::Bool(tts)); + self.0.insert("tts", Value::Bool(tts)); CreateMessage(self.0, self.1) } @@ -88,8 +90,8 @@ impl Default for CreateMessage { /// [`Message`]: ../model/struct.Message.html /// [`tts`]: #method.tts fn default() -> CreateMessage { - let mut map = Map::default(); - map.insert("tts".to_string(), Value::Bool(false)); + let mut map = HashMap::default(); + map.insert("tts", Value::Bool(false)); CreateMessage(map, None) } diff --git a/src/builder/edit_channel.rs b/src/builder/edit_channel.rs index 1ed1551..96455e2 100644 --- a/src/builder/edit_channel.rs +++ b/src/builder/edit_channel.rs @@ -1,4 +1,5 @@ use internal::prelude::*; +use std::collections::HashMap; /// A builder to edit a [`GuildChannel`] for use via [`GuildChannel::edit`] /// @@ -18,7 +19,7 @@ use internal::prelude::*; /// [`GuildChannel`]: ../model/struct.GuildChannel.html /// [`GuildChannel::edit`]: ../model/struct.GuildChannel.html#method.edit #[derive(Clone, Debug, Default)] -pub struct EditChannel(pub JsonMap); +pub struct EditChannel(pub HashMap<&'static str, Value>); impl EditChannel { /// The bitrate of the channel in bits. @@ -27,8 +28,7 @@ impl EditChannel { /// /// [voice]: ../model/enum.ChannelType.html#variant.Voice pub fn bitrate(mut self, bitrate: u64) -> Self { - self.0 - .insert("bitrate".to_string(), Value::Number(Number::from(bitrate))); + self.0.insert("bitrate", Value::Number(Number::from(bitrate))); self } @@ -37,16 +37,14 @@ impl EditChannel { /// /// Must be between 2 and 100 characters long. pub fn name(mut self, name: &str) -> Self { - self.0 - .insert("name".to_string(), Value::String(name.to_string())); + self.0.insert("name", Value::String(name.to_string())); self } /// The position of the channel in the channel list. pub fn position(mut self, position: u64) -> Self { - self.0 - .insert("position".to_string(), Value::Number(Number::from(position))); + self.0.insert("position", Value::Number(Number::from(position))); self } @@ -59,8 +57,7 @@ impl EditChannel { /// /// [text]: ../model/enum.ChannelType.html#variant.Text pub fn topic(mut self, topic: &str) -> Self { - self.0 - .insert("topic".to_string(), Value::String(topic.to_string())); + self.0.insert("topic", Value::String(topic.to_string())); self } @@ -71,10 +68,7 @@ impl EditChannel { /// /// [voice]: ../model/enum.ChannelType.html#variant.Voice pub fn user_limit(mut self, user_limit: u64) -> Self { - self.0.insert( - "user_limit".to_string(), - Value::Number(Number::from(user_limit)), - ); + self.0.insert("user_limit", Value::Number(Number::from(user_limit))); self } diff --git a/src/builder/edit_guild.rs b/src/builder/edit_guild.rs index 0719305..45969fe 100644 --- a/src/builder/edit_guild.rs +++ b/src/builder/edit_guild.rs @@ -1,5 +1,6 @@ use internal::prelude::*; use model::{ChannelId, Region, UserId, VerificationLevel}; +use std::collections::HashMap; /// A builder to optionally edit certain fields of a [`Guild`]. This is meant /// for usage with [`Guild::edit`]. @@ -11,7 +12,7 @@ use model::{ChannelId, Region, UserId, VerificationLevel}; /// [`Guild`]: ../model/struct.Guild.html /// [Manage Guild]: ../model/permissions/constant.MANAGE_GUILD.html #[derive(Clone, Debug, Default)] -pub struct EditGuild(pub Map<String, Value>); +pub struct EditGuild(pub HashMap<&'static str, Value>); impl EditGuild { /// Set the "AFK voice channel" that users are to move to if they have been @@ -24,7 +25,7 @@ impl EditGuild { /// [`afk_timeout`]: #method.afk_timeout pub fn afk_channel<C: Into<ChannelId>>(mut self, channel: Option<C>) -> Self { self.0.insert( - "afk_channel_id".to_string(), + "afk_channel_id", match channel { Some(channel) => Value::Number(Number::from(channel.into().0)), None => Value::Null, @@ -40,7 +41,7 @@ impl EditGuild { /// [`afk_channel`]: #method.afk_channel pub fn afk_timeout(mut self, timeout: u64) -> Self { self.0.insert( - "afk_timeout".to_string(), + "afk_timeout", Value::Number(Number::from(timeout)), ); @@ -78,7 +79,7 @@ impl EditGuild { /// [`utils::read_image`]: ../utils/fn.read_image.html pub fn icon(mut self, icon: Option<&str>) -> Self { self.0.insert( - "icon".to_string(), + "icon", icon.map_or_else(|| Value::Null, |x| Value::String(x.to_string())), ); @@ -89,8 +90,7 @@ impl EditGuild { /// /// **Note**: Must be between (and including) 2-100 chracters. pub fn name(mut self, name: &str) -> Self { - self.0 - .insert("name".to_string(), Value::String(name.to_string())); + self.0.insert("name", Value::String(name.to_string())); self } @@ -99,10 +99,8 @@ impl EditGuild { /// /// **Note**: The current user must be the owner of the guild. pub fn owner<U: Into<UserId>>(mut self, user_id: U) -> Self { - self.0.insert( - "owner_id".to_string(), - Value::Number(Number::from(user_id.into().0)), - ); + let id = Value::Number(Number::from(user_id.into().0)); + self.0.insert("owner_id", id); self } @@ -134,8 +132,7 @@ impl EditGuild { /// /// [`Region::UsWest`]: ../model/enum.Region.html#variant.UsWest pub fn region(mut self, region: Region) -> Self { - self.0 - .insert("region".to_string(), Value::String(region.name().to_string())); + self.0.insert("region", Value::String(region.name().to_string())); self } @@ -149,8 +146,7 @@ impl EditGuild { /// [`features`]: ../model/struct.LiveGuild.html#structfield.features pub fn splash(mut self, splash: Option<&str>) -> Self { let splash = splash.map_or(Value::Null, |x| Value::String(x.to_string())); - - self.0.insert("splash".to_string(), splash); + self.0.insert("splash", splash); self } @@ -189,7 +185,7 @@ impl EditGuild { where V: Into<VerificationLevel> { let num = Value::Number(Number::from(verification_level.into().num())); - self.0.insert("verification_level".to_string(), num); + self.0.insert("verification_level", num); self } diff --git a/src/builder/edit_member.rs b/src/builder/edit_member.rs index 560d241..5a399f5 100644 --- a/src/builder/edit_member.rs +++ b/src/builder/edit_member.rs @@ -1,5 +1,6 @@ use model::{ChannelId, RoleId}; use internal::prelude::*; +use std::collections::HashMap; /// A builder which edits the properties of a [`Member`], to be used in /// conjunction with [`Member::edit`]. @@ -7,7 +8,7 @@ use internal::prelude::*; /// [`Member`]: ../model/struct.Member.html /// [`Member::edit`]: ../model/struct.Member.html#method.edit #[derive(Clone, Debug, Default)] -pub struct EditMember(pub JsonMap); +pub struct EditMember(pub HashMap<&'static str, Value>); impl EditMember { /// Whether to deafen the member. @@ -16,7 +17,7 @@ impl EditMember { /// /// [Deafen Members]: ../model/permissions/constant.DEAFEN_MEMBERS.html pub fn deafen(mut self, deafen: bool) -> Self { - self.0.insert("deaf".to_string(), Value::Bool(deafen)); + self.0.insert("deaf", Value::Bool(deafen)); self } @@ -27,7 +28,7 @@ impl EditMember { /// /// [Mute Members]: ../model/permissions/constant.MUTE_MEMBERS.html pub fn mute(mut self, mute: bool) -> Self { - self.0.insert("mute".to_string(), Value::Bool(mute)); + self.0.insert("mute", Value::Bool(mute)); self } @@ -39,8 +40,7 @@ impl EditMember { /// /// [Manage Nicknames]: ../model/permissions/constant.MANAGE_NICKNAMES.html pub fn nickname(mut self, nickname: &str) -> Self { - self.0 - .insert("nick".to_string(), Value::String(nickname.to_string())); + self.0.insert("nick", Value::String(nickname.to_string())); self } @@ -56,7 +56,7 @@ impl EditMember { .map(|x| Value::Number(Number::from(x.as_ref().0))) .collect(); - self.0.insert("roles".to_string(), Value::Array(role_ids)); + self.0.insert("roles", Value::Array(role_ids)); self } @@ -67,10 +67,8 @@ impl EditMember { /// /// [Move Members]: ../model/permissions/constant.MOVE_MEMBERS.html pub fn voice_channel<C: Into<ChannelId>>(mut self, channel_id: C) -> Self { - self.0.insert( - "channel_id".to_string(), - Value::Number(Number::from(channel_id.into().0)), - ); + let num = Value::Number(Number::from(channel_id.into().0)); + self.0.insert("channel_id", num); self } diff --git a/src/builder/edit_profile.rs b/src/builder/edit_profile.rs index ae0bbd5..09d3337 100644 --- a/src/builder/edit_profile.rs +++ b/src/builder/edit_profile.rs @@ -1,11 +1,12 @@ use internal::prelude::*; +use std::collections::HashMap; /// A builder to edit the current user's settings, to be used in conjunction /// with [`CurrentUser::edit`]. /// /// [`CurrentUser::edit`]: ../model/struct.CurrentUser.html#method.edit #[derive(Clone, Debug, Default)] -pub struct EditProfile(pub JsonMap); +pub struct EditProfile(pub HashMap<&'static str, Value>); impl EditProfile { /// Sets the avatar of the current user. `None` can be passed to remove an @@ -23,8 +24,9 @@ impl EditProfile { /// # use serenity::model::*; /// # /// # struct Handler; + /// /// # impl EventHandler for Handler { - /// # fn on_message(&self, context: Context, _: Message) { + /// # fn message(&self, context: Context, _: Message) { /// use serenity::utils; /// /// // assuming a `context` has been bound @@ -37,15 +39,16 @@ impl EditProfile { /// }); /// # } /// } - - /// # let mut client = Client::new("token", Handler); client.start().unwrap(); + /// # + /// # let mut client = Client::new("token", Handler).unwrap(); + /// # + /// # client.start().unwrap(); /// ``` /// /// [`utils::read_image`]: ../fn.read_image.html pub fn avatar(mut self, avatar: Option<&str>) -> Self { let avatar = avatar.map_or(Value::Null, |x| Value::String(x.to_string())); - - self.0.insert("avatar".to_string(), avatar); + self.0.insert("avatar", avatar); self } @@ -61,8 +64,7 @@ impl EditProfile { /// /// [provided]: #method.password pub fn email(mut self, email: &str) -> Self { - self.0 - .insert("email".to_string(), Value::String(email.to_string())); + self.0.insert("email", Value::String(email.to_string())); self } @@ -74,10 +76,7 @@ impl EditProfile { /// /// [provided]: #method.password pub fn new_password(mut self, new_password: &str) -> Self { - self.0.insert( - "new_password".to_string(), - Value::String(new_password.to_string()), - ); + self.0.insert("new_password", Value::String(new_password.to_string())); self } @@ -88,8 +87,7 @@ impl EditProfile { /// [modifying the password]: #method.new_password /// [modifying the associated email address]: #method.email pub fn password(mut self, password: &str) -> Self { - self.0 - .insert("password".to_string(), Value::String(password.to_string())); + self.0.insert("password", Value::String(password.to_string())); self } @@ -101,8 +99,7 @@ impl EditProfile { /// If there are no available discriminators with the requested username, /// an error will occur. pub fn username(mut self, username: &str) -> Self { - self.0 - .insert("username".to_string(), Value::String(username.to_string())); + self.0.insert("username", Value::String(username.to_string())); self } diff --git a/src/builder/edit_role.rs b/src/builder/edit_role.rs index 0ef1b35..0906329 100644 --- a/src/builder/edit_role.rs +++ b/src/builder/edit_role.rs @@ -1,6 +1,7 @@ use std::default::Default; use internal::prelude::*; use model::{permissions, Permissions, Role}; +use std::collections::HashMap; /// A builer to create or edit a [`Role`] for use via a number of model methods. /// @@ -39,48 +40,38 @@ use model::{permissions, Permissions, Role}; /// [`Role`]: ../model/struct.Role.html /// [`Role::edit`]: ../model/struct.Role.html#method.edit #[derive(Clone, Debug)] -pub struct EditRole(pub JsonMap); +pub struct EditRole(pub HashMap<&'static str, Value>); impl EditRole { /// Creates a new builder with the values of the given [`Role`]. /// /// [`Role`]: ../model/struct.Role.html pub fn new(role: &Role) -> Self { - let mut map = Map::new(); + let mut map = HashMap::new(); #[cfg(feature = "utils")] { - map.insert( - "color".to_string(), - Value::Number(Number::from(role.colour.0)), - ); + map.insert("color", Value::Number(Number::from(role.colour.0))); } #[cfg(not(feature = "utils"))] { - map.insert("color".to_string(), Value::Number(Number::from(role.colour))); + map.insert("color", Value::Number(Number::from(role.colour))); } - map.insert("hoist".to_string(), Value::Bool(role.hoist)); - map.insert("managed".to_string(), Value::Bool(role.managed)); - map.insert("mentionable".to_string(), Value::Bool(role.mentionable)); - map.insert("name".to_string(), Value::String(role.name.clone())); - map.insert( - "permissions".to_string(), - Value::Number(Number::from(role.permissions.bits())), - ); - map.insert( - "position".to_string(), - Value::Number(Number::from(role.position)), - ); + map.insert("hoist", Value::Bool(role.hoist)); + map.insert("managed", Value::Bool(role.managed)); + map.insert("mentionable", Value::Bool(role.mentionable)); + map.insert("name", Value::String(role.name.clone())); + map.insert("permissions",Value::Number(Number::from(role.permissions.bits()))); + map.insert("position", Value::Number(Number::from(role.position))); EditRole(map) } /// Sets the colour of the role. pub fn colour(mut self, colour: u64) -> Self { - self.0 - .insert("color".to_string(), Value::Number(Number::from(colour))); + self.0.insert("color", Value::Number(Number::from(colour))); self } @@ -88,15 +79,14 @@ impl EditRole { /// Whether or not to hoist the role above lower-positioned role in the user /// list. pub fn hoist(mut self, hoist: bool) -> Self { - self.0.insert("hoist".to_string(), Value::Bool(hoist)); + self.0.insert("hoist", Value::Bool(hoist)); self } /// Whether or not to make the role mentionable, notifying its users. pub fn mentionable(mut self, mentionable: bool) -> Self { - self.0 - .insert("mentionable".to_string(), Value::Bool(mentionable)); + self.0.insert("mentionable", Value::Bool(mentionable)); self } @@ -104,17 +94,14 @@ impl EditRole { /// The name of the role to set. pub fn name(mut self, name: &str) -> Self { self.0 - .insert("name".to_string(), Value::String(name.to_string())); + .insert("name", Value::String(name.to_string())); self } /// The set of permissions to assign the role. pub fn permissions(mut self, permissions: Permissions) -> Self { - self.0.insert( - "permissions".to_string(), - Value::Number(Number::from(permissions.bits())), - ); + self.0.insert("permissions", Value::Number(Number::from(permissions.bits()))); self } @@ -122,8 +109,7 @@ impl EditRole { /// The position to assign the role in the role list. This correlates to the /// role's position in the user list. pub fn position(mut self, position: u8) -> Self { - self.0 - .insert("position".to_string(), Value::Number(Number::from(position))); + self.0.insert("position", Value::Number(Number::from(position))); self } @@ -143,15 +129,15 @@ impl Default for EditRole { /// /// [general permissions set]: ../model/permissions/constant.PRESET_GENERAL.html fn default() -> EditRole { - let mut map = Map::new(); + let mut map = HashMap::new(); let permissions = Number::from(permissions::PRESET_GENERAL.bits()); - map.insert("color".to_string(), Value::Number(Number::from(10_070_709))); - map.insert("hoist".to_string(), Value::Bool(false)); - map.insert("mentionable".to_string(), Value::Bool(false)); - map.insert("name".to_string(), Value::String("new role".to_string())); - map.insert("permissions".to_string(), Value::Number(permissions)); - map.insert("position".to_string(), Value::Number(Number::from(1))); + map.insert("color", Value::Number(Number::from(10_070_709))); + map.insert("hoist", Value::Bool(false)); + map.insert("mentionable", Value::Bool(false)); + map.insert("name", Value::String("new role".to_string())); + map.insert("permissions", Value::Number(permissions)); + map.insert("position", Value::Number(Number::from(1))); EditRole(map) } diff --git a/src/builder/execute_webhook.rs b/src/builder/execute_webhook.rs index ba1668e..6276da1 100644 --- a/src/builder/execute_webhook.rs +++ b/src/builder/execute_webhook.rs @@ -1,6 +1,6 @@ use serde_json::Value; +use std::collections::HashMap; use std::default::Default; -use internal::prelude::*; /// A builder to create the inner content of a [`Webhook`]'s execution. /// @@ -35,14 +35,8 @@ use internal::prelude::*; /// .title("Rust Resources") /// .description("A few resources to help with learning Rust") /// .colour(0xDEA584) -/// .field(|f| f -/// .inline(false) -/// .name("The Rust Book") -/// .value("A comprehensive resource for all topics related to Rust")) -/// .field(|f| f -/// .inline(false) -/// .name("Rust by Example") -/// .value("A collection of Rust examples on topics, useable in-browser"))); +/// .field("The Rust Book", "A comprehensive resource for Rust.", false) +/// .field("Rust by Example", "A collection of Rust examples", false)); /// /// let _ = webhook.execute(false, |w| w /// .content("Here's some information on Rust:") @@ -53,7 +47,7 @@ use internal::prelude::*; /// [`Webhook::execute`]: ../model/struct.Webhook.html#method.execute /// [`execute_webhook`]: ../http/fn.execute_webhook.html #[derive(Clone, Debug)] -pub struct ExecuteWebhook(pub JsonMap); +pub struct ExecuteWebhook(pub HashMap<&'static str, Value>); impl ExecuteWebhook { /// Override the default avatar of the webhook with an image URL. @@ -74,10 +68,7 @@ impl ExecuteWebhook { /// .content("Here's a webhook")); /// ``` pub fn avatar_url(mut self, avatar_url: &str) -> Self { - self.0.insert( - "avatar_url".to_string(), - Value::String(avatar_url.to_string()), - ); + self.0.insert("avatar_url", Value::String(avatar_url.to_string())); self } @@ -103,8 +94,7 @@ impl ExecuteWebhook { /// /// [`embeds`]: #method.embeds pub fn content(mut self, content: &str) -> Self { - self.0 - .insert("content".to_string(), Value::String(content.to_string())); + self.0.insert("content", Value::String(content.to_string())); self } @@ -123,7 +113,7 @@ impl ExecuteWebhook { /// [`Webhook::execute`]: ../model/struct.Webhook.html#method.execute /// [struct-level documentation]: #examples pub fn embeds(mut self, embeds: Vec<Value>) -> Self { - self.0.insert("embeds".to_string(), Value::Array(embeds)); + self.0.insert("embeds", Value::Array(embeds)); self } @@ -144,7 +134,7 @@ impl ExecuteWebhook { /// } /// ``` pub fn tts(mut self, tts: bool) -> Self { - self.0.insert("tts".to_string(), Value::Bool(tts)); + self.0.insert("tts", Value::Bool(tts)); self } @@ -165,8 +155,7 @@ impl ExecuteWebhook { /// } /// ``` pub fn username(mut self, username: &str) -> Self { - self.0 - .insert("username".to_string(), Value::String(username.to_string())); + self.0.insert("username", Value::String(username.to_string())); self } @@ -190,8 +179,8 @@ impl Default for ExecuteWebhook { /// [`Webhook`]: ../model/struct.Webhook.html /// [`tts`]: #method.tts fn default() -> ExecuteWebhook { - let mut map = Map::new(); - map.insert("tts".to_string(), Value::Bool(false)); + let mut map = HashMap::new(); + map.insert("tts", Value::Bool(false)); ExecuteWebhook(map) } diff --git a/src/builder/get_messages.rs b/src/builder/get_messages.rs index 71af9e5..e59584f 100644 --- a/src/builder/get_messages.rs +++ b/src/builder/get_messages.rs @@ -1,5 +1,5 @@ -use std::collections::BTreeMap; use model::MessageId; +use std::collections::HashMap; /// Builds a request for a request to the API to retrieve messages. /// @@ -50,13 +50,13 @@ use model::MessageId; /// /// [`GuildChannel::messages`]: ../model/struct.GuildChannel.html#method.messages #[derive(Clone, Debug, Default)] -pub struct GetMessages(pub BTreeMap<String, u64>); +pub struct GetMessages(pub HashMap<&'static str, 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_string(), message_id.into().0); + self.0.insert("after", message_id.into().0); self } @@ -64,7 +64,7 @@ impl GetMessages { /// 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_string(), message_id.into().0); + self.0.insert("around", message_id.into().0); self } @@ -72,7 +72,7 @@ impl GetMessages { /// 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_string(), message_id.into().0); + self.0.insert("before", message_id.into().0); self } @@ -86,7 +86,7 @@ impl GetMessages { /// reduced. pub fn limit(mut self, limit: u64) -> Self { self.0 - .insert("limit".to_string(), if limit > 100 { 100 } else { limit }); + .insert("limit", if limit > 100 { 100 } else { limit }); self } diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 496d7e1..3c59c0d 100644 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -16,7 +16,7 @@ mod edit_role; mod execute_webhook; mod get_messages; -pub use self::create_embed::{CreateEmbed, CreateEmbedAuthor, CreateEmbedField, CreateEmbedFooter}; +pub use self::create_embed::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter}; pub use self::create_invite::CreateInvite; pub use self::create_message::CreateMessage; pub use self::edit_channel::EditChannel; diff --git a/src/cache/mod.rs b/src/cache/mod.rs index a663a79..e23e17e 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -41,10 +41,12 @@ //! [`Role`]: ../model/struct.Role.html //! [`CACHE`]: ../struct.CACHE.html //! [`http`]: ../http/index.html + +use parking_lot::RwLock; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::default::Default; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use model::*; mod cache_update; @@ -178,7 +180,7 @@ impl Cache { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_ready(&self, ctx: Context, _: Ready) { + /// fn ready(&self, ctx: Context, _: Ready) { /// // Wait some time for guilds to be received. /// // /// // You should keep track of this in a better fashion by tracking how @@ -190,11 +192,13 @@ impl Cache { /// // seconds. /// thread::sleep(Duration::from_secs(5)); /// - /// println!("{} unknown members", CACHE.read().unwrap().unknown_members()); + /// println!("{} unknown members", CACHE.read().unknown_members()); /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// # } /// # /// # #[cfg(not(feature = "client"))] @@ -208,7 +212,7 @@ impl Cache { let mut total = 0; for guild in self.guilds.values() { - let guild = guild.read().unwrap(); + let guild = guild.read(); let members = guild.members.len() as u64; @@ -233,7 +237,7 @@ impl Cache { /// ```rust,no_run /// use serenity::CACHE; /// - /// let amount = CACHE.read().unwrap().all_private_channels().len(); + /// let amount = CACHE.read().all_private_channels().len(); /// /// println!("There are {} private channels", amount); /// ``` @@ -346,10 +350,8 @@ impl Cache { /// # fn try_main() -> Result<(), Box<Error>> { /// use serenity::CACHE; /// - /// let cache = CACHE.read()?; - /// - /// if let Some(guild) = cache.guild(7) { - /// println!("Guild name: {}", guild.read().unwrap().name); + /// if let Some(guild) = CACHE.read().guild(7) { + /// println!("Guild name: {}", guild.read().name); /// } /// # Ok(()) /// # } @@ -385,8 +387,8 @@ impl Cache { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, message: Message) { - /// let cache = CACHE.read().unwrap(); + /// fn message(&self, ctx: Context, message: Message) { + /// let cache = CACHE.read(); /// /// let channel = match cache.guild_channel(message.channel_id) { /// Some(channel) => channel, @@ -402,7 +404,9 @@ impl Cache { /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// # } /// # /// # #[cfg(not(feature = "client"))] @@ -437,10 +441,8 @@ impl Cache { /// # fn try_main() -> Result<(), Box<Error>> { /// use serenity::CACHE; /// - /// let cache = CACHE.read()?; - /// - /// if let Some(group) = cache.group(7) { - /// println!("Owner Id: {}", group.read().unwrap().owner_id); + /// if let Some(group) = CACHE.read().group(7) { + /// println!("Owner Id: {}", group.read().owner_id); /// } /// # Ok(()) /// # } @@ -468,7 +470,7 @@ impl Cache { /// ```rust,ignore /// use serenity::CACHE; /// - /// let cache = CACHE.read().unwrap(); + /// let cache = CACHE.read(); /// let member = { /// let channel = match cache.guild_channel(message.channel_id) { /// Some(channel) => channel, @@ -502,7 +504,7 @@ impl Cache { pub fn 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()).and_then(|guild| { - guild.read().unwrap().members.get(&user_id.into()).cloned() + guild.read().members.get(&user_id.into()).cloned() }) } @@ -563,9 +565,7 @@ impl Cache { /// # fn try_main() -> Result<(), Box<Error>> { /// use serenity::CACHE; /// - /// let cache = CACHE.read()?; - /// - /// if let Some(role) = cache.role(7, 77) { + /// if let Some(role) = CACHE.read().role(7, 77) { /// println!("Role with Id 77 is called {}", role.name); /// } /// # Ok(()) @@ -579,7 +579,7 @@ impl Cache { where G: Into<GuildId>, R: Into<RoleId> { self.guilds .get(&guild_id.into()) - .and_then(|g| g.read().unwrap().roles.get(&role_id.into()).cloned()) + .and_then(|g| g.read().roles.get(&role_id.into()).cloned()) } /// Retrieves a `User` from the cache's [`users`] map, if it exists. @@ -600,10 +600,8 @@ impl Cache { /// # fn try_main() -> Result<(), Box<Error>> { /// use serenity::CACHE; /// - /// let cache = CACHE.read()?; - /// - /// if let Some(user) = cache.user(7) { - /// println!("User with Id 7 is currently named {}", user.read().unwrap().name); + /// if let Some(user) = CACHE.read().user(7) { + /// println!("User with Id 7 is currently named {}", user.read().name); /// } /// # Ok(()) /// # } @@ -635,7 +633,7 @@ impl Cache { e.insert(Arc::new(RwLock::new(user.clone()))); }, Entry::Occupied(mut e) => { - e.get_mut().write().unwrap().clone_from(user); + e.get_mut().write().clone_from(user); }, } } diff --git a/src/client/bridge/gateway/mod.rs b/src/client/bridge/gateway/mod.rs index 0b873aa..752b015 100644 --- a/src/client/bridge/gateway/mod.rs +++ b/src/client/bridge/gateway/mod.rs @@ -1,28 +1,113 @@ +//! The client gateway bridge is support essential for the [`client`] module. +//! +//! This is made available for user use if one wishes to be lower-level or avoid +//! the higher functionality of the [`Client`]. +//! +//! Of interest are three pieces: +//! +//! ### [`ShardManager`] +//! +//! The shard manager is responsible for being a clean interface between the +//! user and the shard runners, providing essential functions such as +//! [`ShardManager::shutdown`] to shutdown a shard and [`ShardManager::restart`] +//! to restart a shard. +//! +//! If you are using the `Client`, this is likely the only piece of interest to +//! you. Refer to [its documentation][`ShardManager`] for more information. +//! +//! ### [`ShardQueuer`] +//! +//! The shard queuer is a light wrapper around an mpsc receiver that receives +//! [`ShardManagerMessage`]s. It should be run in its own thread so it can +//! receive messages to start shards in a queue. +//! +//! Refer to [its documentation][`ShardQueuer`] for more information. +//! +//! ### [`ShardRunner`] +//! +//! The shard runner is responsible for actually running a shard and +//! communicating with its respective WebSocket client. +//! +//! It is performs all actions such as sending a presence update over the client +//! and, with the help of the [`Shard`], will be able to determine what to do. +//! This is, for example, whether to reconnect, resume, or identify with the +//! gateway. +//! +//! ### In Conclusion +//! +//! For almost every - if not every - use case, you only need to _possibly_ be +//! concerned about the [`ShardManager`] in this module. +//! +//! [`Client`]: ../../struct.Client.html +//! [`client`]: ../.. +//! [`Shard`]: ../../../gateway/struct.Shard.html +//! [`ShardManager`]: struct.ShardManager.html +//! [`ShardManager::restart`]: struct.ShardManager.html#method.restart +//! [`ShardManager::shutdown`]: struct.ShardManager.html#method.shutdown +//! [`ShardQueuer`]: struct.ShardQueuer.html +//! [`ShardRunner`]: struct.ShardRunner.html + mod shard_manager; +mod shard_manager_monitor; +mod shard_messenger; mod shard_queuer; mod shard_runner; +mod shard_runner_message; pub use self::shard_manager::ShardManager; +pub use self::shard_manager_monitor::ShardManagerMonitor; +pub use self::shard_messenger::ShardMessenger; pub use self::shard_queuer::ShardQueuer; pub use self::shard_runner::ShardRunner; +pub use self::shard_runner_message::ShardRunnerMessage; -use gateway::Shard; -use parking_lot::Mutex; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::sync::mpsc::Sender; -use std::sync::Arc; -type Parked<T> = Arc<Mutex<T>>; -type LockedShard = Parked<Shard>; +/// A message either for a [`ShardManager`] or a [`ShardRunner`]. +/// +/// [`ShardManager`]: struct.ShardManager.html +/// [`ShardRunner`]: struct.ShardRunner.html +pub enum ShardClientMessage { + /// A message intended to be worked with by a [`ShardManager`]. + /// + /// [`ShardManager`]: struct.ShardManager.html + Manager(ShardManagerMessage), + /// A message intended to be worked with by a [`ShardRunner`]. + /// + /// [`ShardRunner`]: struct.ShardRunner.html + Runner(ShardRunnerMessage), +} +/// A message for a [`ShardManager`] relating to an operation with a shard. +/// +/// [`ShardManager`]: struct.ShardManager.html #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub enum ShardManagerMessage { + /// Indicator that a [`ShardManagerMonitor`] should restart a shard. + /// + /// [`ShardManagerMonitor`]: struct.ShardManagerMonitor.html Restart(ShardId), + /// Indicator that a [`ShardManagerMonitor`] should fully shutdown a shard + /// without bringing it back up. + /// + /// [`ShardManagerMonitor`]: struct.ShardManagerMonitor.html Shutdown(ShardId), - #[allow(dead_code)] + /// Indicator that a [`ShardManagerMonitor`] should fully shutdown all shards + /// and end its monitoring process for the [`ShardManager`]. + /// + /// [`ShardManager`]: struct.ShardManager.html + /// [`ShardManagerMonitor`]: struct.ShardManagerMonitor.html ShutdownAll, } +/// A message to be sent to the [`ShardQueuer`]. +/// +/// This should usually be wrapped in a [`ShardClientMessage`]. +/// +/// [`ShardClientMessage`]: enum.ShardClientMessage.html +/// [`ShardQueuer`]: enum.ShardQueuer.html +#[derive(Clone, Debug)] pub enum ShardQueuerMessage { /// Message to start a shard, where the 0-index element is the ID of the /// Shard to start and the 1-index element is the total shards in use. @@ -31,8 +116,8 @@ pub enum ShardQueuerMessage { Shutdown, } -// A light tuplestruct wrapper around a u64 to verify type correctness when -// working with the IDs of shards. +/// A light tuplestruct wrapper around a u64 to verify type correctness when +/// working with the IDs of shards. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct ShardId(pub u64); @@ -42,7 +127,16 @@ impl Display for ShardId { } } +/// Information about a [`ShardRunner`]. +/// +/// The [`ShardId`] is not included because, as it stands, you probably already +/// know the Id if you obtained this. +/// +/// [`ShardId`]: struct.ShardId.html +/// [`ShardRunner`]: struct.ShardRunner.html +#[derive(Debug)] pub struct ShardRunnerInfo { - pub runner_tx: Sender<ShardManagerMessage>, - pub shard: Arc<Mutex<Shard>>, + /// The channel used to communicate with the shard runner, telling it + /// what to do with regards to its status. + pub runner_tx: Sender<ShardClientMessage>, } diff --git a/src/client/bridge/gateway/shard_manager.rs b/src/client/bridge/gateway/shard_manager.rs index dfd4a16..2cf45fd 100644 --- a/src/client/bridge/gateway/shard_manager.rs +++ b/src/client/bridge/gateway/shard_manager.rs @@ -1,13 +1,15 @@ use internal::prelude::*; -use parking_lot::Mutex as ParkingLotMutex; +use parking_lot::Mutex; use std::collections::HashMap; -use std::sync::mpsc::{self, Receiver, Sender}; -use std::sync::{Arc, Mutex}; +use std::sync::mpsc::{self, Sender}; +use std::sync::Arc; use std::thread; use super::super::super::EventHandler; use super::{ + ShardClientMessage, ShardId, ShardManagerMessage, + ShardManagerMonitor, ShardQueuer, ShardQueuerMessage, ShardRunnerInfo, @@ -18,8 +20,80 @@ use typemap::ShareMap; #[cfg(feature = "framework")] use framework::Framework; +/// A manager for handling the status of shards by starting them, restarting +/// them, and stopping them when required. +/// +/// **Note**: The [`Client`] internally uses a shard manager. If you are using a +/// Client, then you do not need to make one of these. +/// +/// # Examples +/// +/// Initialize a shard manager with a framework responsible for shards 0 through +/// 2, of 5 total shards: +/// +/// ```rust,no_run +/// extern crate parking_lot; +/// extern crate serenity; +/// extern crate threadpool; +/// extern crate typemap; +/// +/// # use std::error::Error; +/// # +/// # #[cfg(feature = "framework")] +/// # fn try_main() -> Result<(), Box<Error>> { +/// # +/// use parking_lot::Mutex; +/// use serenity::client::bridge::gateway::ShardManager; +/// use serenity::client::EventHandler; +/// use serenity::http; +/// use std::sync::Arc; +/// use std::env; +/// use threadpool::ThreadPool; +/// use typemap::ShareMap; +/// +/// struct Handler; +/// +/// impl EventHandler for Handler { } +/// +/// let token = env::var("DISCORD_TOKEN")?; +/// http::set_token(&token); +/// let token = Arc::new(Mutex::new(token)); +/// +/// let gateway_url = Arc::new(Mutex::new(http::get_gateway()?.url)); +/// let data = Arc::new(Mutex::new(ShareMap::custom())); +/// let event_handler = Arc::new(Handler); +/// let framework = Arc::new(Mutex::new(None)); +/// let threadpool = ThreadPool::with_name("my threadpool".to_owned(), 5); +/// +/// ShardManager::new( +/// 0, // the shard index to start initiating from +/// 3, // the number of shards to initiate (this initiates 0, 1, and 2) +/// 5, // the total number of shards in use +/// gateway_url, +/// token, +/// data, +/// event_handler, +/// framework, +/// threadpool, +/// ); +/// # Ok(()) +/// # } +/// # +/// # #[cfg(not(feature = "framework"))] +/// # fn try_main() -> Result<(), Box<Error>> { +/// # Ok(()) +/// # } +/// # +/// # fn main() { +/// # try_main().unwrap(); +/// # } +/// ``` +/// +/// [`Client`]: ../../struct.Client.html +#[derive(Debug)] pub struct ShardManager { - pub runners: Arc<ParkingLotMutex<HashMap<ShardId, ShardRunnerInfo>>>, + /// The shard runners currently managed. + runners: Arc<Mutex<HashMap<ShardId, ShardRunnerInfo>>>, /// The index of the first shard to initialize, 0-indexed. shard_index: u64, /// The number of shards to initialize. @@ -27,10 +101,11 @@ pub struct ShardManager { /// The total shards in use, 1-indexed. shard_total: u64, shard_queuer: Sender<ShardQueuerMessage>, - thread_rx: Receiver<ShardManagerMessage>, } impl ShardManager { + /// Creates a new shard manager, returning both the manager and a monitor + /// for usage in a separate thread. #[cfg(feature = "framework")] #[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))] pub fn new<H>( @@ -39,15 +114,15 @@ impl ShardManager { shard_total: u64, ws_url: Arc<Mutex<String>>, token: Arc<Mutex<String>>, - data: Arc<ParkingLotMutex<ShareMap>>, + data: Arc<Mutex<ShareMap>>, event_handler: Arc<H>, framework: Arc<Mutex<Option<Box<Framework + Send>>>>, threadpool: ThreadPool, - ) -> Self where H: EventHandler + Send + Sync + 'static { + ) -> (Arc<Mutex<Self>>, ShardManagerMonitor) where H: EventHandler + Send + Sync + 'static { let (thread_tx, thread_rx) = mpsc::channel(); let (shard_queue_tx, shard_queue_rx) = mpsc::channel(); - let runners = Arc::new(ParkingLotMutex::new(HashMap::new())); + let runners = Arc::new(Mutex::new(HashMap::new())); let mut shard_queuer = ShardQueuer { data: Arc::clone(&data), @@ -66,16 +141,22 @@ impl ShardManager { shard_queuer.run(); }); - Self { + let manager = Arc::new(Mutex::new(Self { shard_queuer: shard_queue_tx, - thread_rx: thread_rx, runners, shard_index, shard_init, shard_total, - } + })); + + (Arc::clone(&manager), ShardManagerMonitor { + rx: thread_rx, + manager, + }) } + /// Creates a new shard manager, returning both the manager and a monitor + /// for usage in a separate thread. #[cfg(not(feature = "framework"))] pub fn new<H>( shard_index: u64, @@ -83,24 +164,24 @@ impl ShardManager { shard_total: u64, ws_url: Arc<Mutex<String>>, token: Arc<Mutex<String>>, - data: Arc<ParkingLotMutex<ShareMap>>, + data: Arc<Mutex<ShareMap>>, event_handler: Arc<H>, threadpool: ThreadPool, - ) -> Self where H: EventHandler + Send + Sync + 'static { + ) -> (Arc<Mutex<Self>>, ShardManagerMonitor) where H: EventHandler + Send + Sync + 'static { let (thread_tx, thread_rx) = mpsc::channel(); let (shard_queue_tx, shard_queue_rx) = mpsc::channel(); - let runners = Arc::new(ParkingLotMutex::new(HashMap::new())); + let runners = Arc::new(Mutex::new(HashMap::new())); let mut shard_queuer = ShardQueuer { - data: data.clone(), - event_handler: event_handler.clone(), + data: Arc::clone(&data), + event_handler: Arc::clone(&event_handler), last_start: None, manager_tx: thread_tx.clone(), - runners: runners.clone(), + runners: Arc::clone(&runners), rx: shard_queue_rx, - token: token.clone(), - ws_url: ws_url.clone(), + token: Arc::clone(&token), + ws_url: Arc::clone(&ws_url), threadpool, }; @@ -108,21 +189,41 @@ impl ShardManager { shard_queuer.run(); }); - Self { + let manager = Arc::new(Mutex::new(Self { shard_queuer: shard_queue_tx, - thread_rx: thread_rx, runners, shard_index, shard_init, shard_total, - } + })); + + (Arc::clone(&manager), ShardManagerMonitor { + rx: thread_rx, + manager, + }) + } + + /// Returns whether the shard manager contains either an active instance of + /// a shard runner responsible for the given ID. + /// + /// If a shard has been queued but has not yet been initiated, then this + /// will return `false`. Consider double-checking [`is_responsible_for`] to + /// determine whether this shard manager is responsible for the given shard. + /// + /// [`is_responsible_for`]: #method.is_responsible_for + pub fn has(&self, shard_id: ShardId) -> bool { + self.runners.lock().contains_key(&shard_id) } + /// Initializes all shards that the manager is responsible for. + /// + /// This will communicate shard boots with the [`ShardQueuer`] so that they + /// are properly queued. + /// + /// [`ShardQueuer`]: struct.ShardQueuer.html pub fn initialize(&mut self) -> Result<()> { let shard_to = self.shard_index + self.shard_init; - debug!("{}, {}", self.shard_index, self.shard_init); - for shard_id in self.shard_index..shard_to { let shard_total = self.shard_total; @@ -132,26 +233,118 @@ impl ShardManager { Ok(()) } - pub fn run(&mut self) { - while let Ok(value) = self.thread_rx.recv() { - match value { - ShardManagerMessage::Restart(shard_id) => self.restart(shard_id), - ShardManagerMessage::Shutdown(shard_id) => self.shutdown(shard_id), - ShardManagerMessage::ShutdownAll => { - self.shutdown_all(); + /// Sets the new sharding information for the manager. + /// + /// This will shutdown all existing shards. + /// + /// This will _not_ instantiate the new shards. + pub fn set_shards(&mut self, index: u64, init: u64, total: u64) { + self.shutdown_all(); + + self.shard_index = index; + self.shard_init = init; + self.shard_total = total; + } - break; - }, + /// Restarts a shard runner. + /// + /// This sends a shutdown signal to a shard's associated [`ShardRunner`], + /// and then queues a initialization of a shard runner for the same shard + /// via the [`ShardQueuer`]. + /// + /// # Examples + /// + /// Creating a client and then restarting a shard by ID: + /// + /// _(note: in reality this precise code doesn't have an effect since the + /// shard would not yet have been initialized via [`initialize`], but the + /// concept is the same)_ + /// + /// ```rust,no_run + /// use serenity::client::bridge::gateway::ShardId; + /// use serenity::client::{Client, EventHandler}; + /// use std::env; + /// + /// struct Handler; + /// + /// impl EventHandler for Handler { } + /// + /// let token = env::var("DISCORD_TOKEN").unwrap(); + /// let mut client = Client::new(&token, Handler).unwrap(); + /// + /// // restart shard ID 7 + /// client.shard_manager.lock().restart(ShardId(7)); + /// ``` + /// + /// [`ShardQueuer`]: struct.ShardQueuer.html + /// [`ShardRunner`]: struct.ShardRunner.html + pub fn restart(&mut self, shard_id: ShardId) { + info!("Restarting shard {}", shard_id); + self.shutdown(shard_id); + + let shard_total = self.shard_total; + + self.boot([shard_id, ShardId(shard_total)]); + } + + /// Returns the [`ShardId`]s of the shards that have been instantiated and + /// currently have a valid [`ShardRunner`]. + /// + /// [`ShardId`]: struct.ShardId.html + /// [`ShardRunner`]: struct.ShardRunner.html + pub fn shards_instantiated(&self) -> Vec<ShardId> { + self.runners.lock().keys().cloned().collect() + } + + /// Attempts to shut down the shard runner by Id. + /// + /// Returns a boolean indicating whether a shard runner was present. This is + /// _not_ necessary an indicator of whether the shard runner was + /// successfully shut down. + /// + /// **Note**: If the receiving end of an mpsc channel - theoretically owned + /// by the shard runner - no longer exists, then the shard runner will not + /// know it should shut down. This _should never happen_. It may already be + /// stopped. + pub fn shutdown(&mut self, shard_id: ShardId) -> bool { + info!("Shutting down shard {}", shard_id); + + if let Some(runner) = self.runners.lock().get(&shard_id) { + let shutdown = ShardManagerMessage::Shutdown(shard_id); + let msg = ShardClientMessage::Manager(shutdown); + + if let Err(why) = runner.runner_tx.send(msg) { + warn!( + "Failed to cleanly shutdown shard {}: {:?}", + shard_id, + why, + ); } } + + self.runners.lock().remove(&shard_id).is_some() } + /// Sends a shutdown message for all shards that the manager is responsible + /// for that are still known to be running. + /// + /// If you only need to shutdown a select number of shards, prefer looping + /// over the [`shutdown`] method. + /// + /// [`shutdown`]: #method.shutdown pub fn shutdown_all(&mut self) { - info!("Shutting down all shards"); let keys = { - self.runners.lock().keys().cloned().collect::<Vec<ShardId>>() + let runners = self.runners.lock(); + + if runners.is_empty() { + return; + } + + runners.keys().cloned().collect::<Vec<_>>() }; + info!("Shutting down all shards"); + for shard_id in keys { self.shutdown(shard_id); } @@ -163,37 +356,19 @@ impl ShardManager { let msg = ShardQueuerMessage::Start(shard_info[0], shard_info[1]); let _ = self.shard_queuer.send(msg); } - - fn restart(&mut self, shard_id: ShardId) { - info!("Restarting shard {}", shard_id); - self.shutdown(shard_id); - - let shard_total = self.shard_total; - - self.boot([shard_id, ShardId(shard_total)]); - } - - fn shutdown(&mut self, shard_id: ShardId) { - if let Some(runner) = self.runners.lock().get(&shard_id) { - let is_shutdown = runner.shard.lock().is_shutdown(); - - if !is_shutdown { - info!("Shutting down shard {}", shard_id); - - let msg = ShardManagerMessage::Shutdown(shard_id); - - if let Err(why) = runner.runner_tx.send(msg) { - warn!("Failed to cleanly shutdown shard {}: {:?}", shard_id, why); - } - } - } - - self.runners.lock().remove(&shard_id); - } } impl Drop for ShardManager { + /// A custom drop implementation to clean up after the manager. + /// + /// This shuts down all active [`ShardRunner`]s and attempts to tell the + /// [`ShardQueuer`] to shutdown. + /// + /// [`ShardQueuer`]: struct.ShardQueuer.html + /// [`ShardRunner`]: struct.ShardRunner.html fn drop(&mut self) { + self.shutdown_all(); + if let Err(why) = self.shard_queuer.send(ShardQueuerMessage::Shutdown) { warn!("Failed to send shutdown to shard queuer: {:?}", why); } diff --git a/src/client/bridge/gateway/shard_manager_monitor.rs b/src/client/bridge/gateway/shard_manager_monitor.rs new file mode 100644 index 0000000..4a64473 --- /dev/null +++ b/src/client/bridge/gateway/shard_manager_monitor.rs @@ -0,0 +1,54 @@ +use parking_lot::Mutex; +use std::sync::mpsc::Receiver; +use std::sync::Arc; +use super::{ShardManager, ShardManagerMessage}; + +/// The shard manager monitor does what it says on the tin -- it monitors the +/// shard manager and performs actions on it as received. +/// +/// The monitor is essentially responsible for running in its own thread and +/// receiving [`ShardManagerMessage`]s, such as whether to shutdown a shard or +/// shutdown everything entirely. +/// +/// [`ShardManagerMessage`]: struct.ShardManagerMessage.html +#[derive(Debug)] +pub struct ShardManagerMonitor { + /// An clone of the Arc to the manager itself. + pub manager: Arc<Mutex<ShardManager>>, + /// The mpsc Receiver channel to receive shard manager messages over. + pub rx: Receiver<ShardManagerMessage>, +} + +impl ShardManagerMonitor { + /// "Runs" the monitor, waiting for messages over the Receiver. + /// + /// This should be called in its own thread due to its blocking, looped + /// nature. + /// + /// This will continue running until either: + /// + /// - a [`ShardManagerMessage::ShutdownAll`] has been received + /// - an error is returned while receiving a message from the + /// channel (probably indicating that the shard manager should stop anyway) + /// + /// [`ShardManagerMessage::ShutdownAll`]: enum.ShardManagerMessage.html#variant.ShutdownAll + pub fn run(&mut self) { + debug!("Starting shard manager worker"); + + while let Ok(value) = self.rx.recv() { + match value { + ShardManagerMessage::Restart(shard_id) => { + self.manager.lock().restart(shard_id); + }, + ShardManagerMessage::Shutdown(shard_id) => { + self.manager.lock().shutdown(shard_id); + }, + ShardManagerMessage::ShutdownAll => { + self.manager.lock().shutdown_all(); + + break; + }, + } + } + } +} diff --git a/src/client/bridge/gateway/shard_messenger.rs b/src/client/bridge/gateway/shard_messenger.rs new file mode 100644 index 0000000..069c838 --- /dev/null +++ b/src/client/bridge/gateway/shard_messenger.rs @@ -0,0 +1,276 @@ +use model::{Game, GuildId, OnlineStatus}; +use super::{ShardClientMessage, ShardRunnerMessage}; +use std::sync::mpsc::{SendError, Sender}; +use websocket::message::OwnedMessage; + +/// A lightweight wrapper around an mpsc sender. +/// +/// This is used to cleanly communicate with a shard's respective +/// [`ShardRunner`]. This can be used for actions such as setting the game via +/// [`set_game`] or shutting down via [`shutdown`]. +/// +/// [`ShardRunner`]: struct.ShardRunner.html +/// [`set_game`]: #method.set_game +/// [`shutdown`]: #method.shutdown +#[derive(Clone, Debug)] +pub struct ShardMessenger { + tx: Sender<ShardClientMessage>, +} + +impl ShardMessenger { + /// Creates a new shard messenger. + /// + /// If you are using the [`Client`], you do not need to do this. + /// + /// [`Client`]: ../../struct.Client.html + #[inline] + pub fn new(tx: Sender<ShardClientMessage>) -> Self { + Self { + tx, + } + } + + /// Requests that one or multiple [`Guild`]s be chunked. + /// + /// This will ask the gateway to start sending member chunks for large + /// guilds (250 members+). If a guild is over 250 members, then a full + /// member list will not be downloaded, and must instead be requested to be + /// sent in "chunks" containing members. + /// + /// Member chunks are sent as the [`Event::GuildMembersChunk`] event. Each + /// chunk only contains a partial amount of the total members. + /// + /// If the `cache` feature is enabled, the cache will automatically be + /// updated with member chunks. + /// + /// # Examples + /// + /// Chunk a single guild by Id, limiting to 2000 [`Member`]s, and not + /// specifying a query parameter: + /// + /// ```rust,no_run + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; + /// # use serenity::client::gateway::Shard; + /// # use std::error::Error; + /// # use std::sync::Arc; + /// # + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1])?; + /// # + /// use serenity::model::GuildId; + /// + /// let guild_ids = vec![GuildId(81384788765712384)]; + /// + /// shard.chunk_guilds(guild_ids, Some(2000), None); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } + /// ``` + /// + /// Chunk a single guild by Id, limiting to 20 members, and specifying a + /// query parameter of `"do"`: + /// + /// ```rust,no_run + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; + /// # use serenity::client::gateway::Shard; + /// # use std::error::Error; + /// # use std::sync::Arc; + /// # + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1])?; + /// # + /// use serenity::model::GuildId; + /// + /// let guild_ids = vec![GuildId(81384788765712384)]; + /// + /// shard.chunk_guilds(guild_ids, Some(20), Some("do")); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } + /// ``` + /// + /// [`Event::GuildMembersChunk`]: + /// ../../model/event/enum.Event.html#variant.GuildMembersChunk + /// [`Guild`]: ../../model/struct.Guild.html + /// [`Member`]: ../../model/struct.Member.html + pub fn chunk_guilds<It>( + &self, + guild_ids: It, + limit: Option<u16>, + query: Option<String>, + ) where It: IntoIterator<Item=GuildId> { + let guilds = guild_ids.into_iter().collect::<Vec<GuildId>>(); + + let _ = self.send(ShardRunnerMessage::ChunkGuilds { + guild_ids: guilds, + limit, + query, + }); + } + + /// Sets the user's current game, if any. + /// + /// Other presence settings are maintained. + /// + /// # Examples + /// + /// Setting the current game to playing `"Heroes of the Storm"`: + /// + /// ```rust,no_run + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; + /// # use serenity::client::gateway::Shard; + /// # use std::error::Error; + /// # use std::sync::Arc; + /// # + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); + /// # + /// use serenity::model::Game; + /// + /// shard.set_game(Some(Game::playing("Heroes of the Storm"))); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } + /// ``` + pub fn set_game(&self, game: Option<Game>) { + let _ = self.send(ShardRunnerMessage::SetGame(game)); + } + + /// Sets the user's full presence information. + /// + /// Consider using the individual setters if you only need to modify one of + /// these. + /// + /// # Examples + /// + /// Set the current user as playing `"Heroes of the Storm"` and being + /// online: + /// + /// ```rust,ignore + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; + /// # use serenity::client::gateway::Shard; + /// # use std::error::Error; + /// # use std::sync::Arc; + /// # + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); + /// # + /// use serenity::model::{Game, OnlineStatus}; + /// + /// shard.set_presence(Some(Game::playing("Heroes of the Storm")), OnlineStatus::Online); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } + /// ``` + pub fn set_presence(&self, game: Option<Game>, mut status: OnlineStatus) { + if status == OnlineStatus::Offline { + status = OnlineStatus::Invisible; + } + + let _ = self.send(ShardRunnerMessage::SetPresence(status, game)); + } + + /// Sets the user's current online status. + /// + /// Note that [`Offline`] is not a valid online status, so it is + /// automatically converted to [`Invisible`]. + /// + /// Other presence settings are maintained. + /// + /// # Examples + /// + /// Setting the current online status for the shard to [`DoNotDisturb`]. + /// + /// ```rust,no_run + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; + /// # use serenity::client::gateway::Shard; + /// # use std::error::Error; + /// # use std::sync::Arc; + /// # + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); + /// # + /// use serenity::model::OnlineStatus; + /// + /// shard.set_status(OnlineStatus::DoNotDisturb); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } + /// ``` + /// + /// [`DoNotDisturb`]: ../../model/enum.OnlineStatus.html#variant.DoNotDisturb + /// [`Invisible`]: ../../model/enum.OnlineStatus.html#variant.Invisible + /// [`Offline`]: ../../model/enum.OnlineStatus.html#variant.Offline + pub fn set_status(&self, mut online_status: OnlineStatus) { + if online_status == OnlineStatus::Offline { + online_status = OnlineStatus::Invisible; + } + + let _ = self.send(ShardRunnerMessage::SetStatus(online_status)); + } + + /// Shuts down the websocket by attempting to cleanly close the + /// connection. + pub fn shutdown_clean(&self) { + let _ = self.send(ShardRunnerMessage::Close(1000, None)); + } + + /// Sends a raw message over the WebSocket. + /// + /// The given message is not mutated in any way, and is sent as-is. + /// + /// You should only use this if you know what you're doing. If you're + /// wanting to, for example, send a presence update, prefer the usage of + /// the [`set_presence`] method. + /// + /// [`set_presence`]: #method.set_presence + pub fn websocket_message(&self, message: OwnedMessage) { + let _ = self.send(ShardRunnerMessage::Message(message)); + } + + #[inline] + fn send(&self, msg: ShardRunnerMessage) + -> Result<(), SendError<ShardClientMessage>> { + self.tx.send(ShardClientMessage::Runner(msg)) + } +} diff --git a/src/client/bridge/gateway/shard_queuer.rs b/src/client/bridge/gateway/shard_queuer.rs index 78118c7..bd02532 100644 --- a/src/client/bridge/gateway/shard_queuer.rs +++ b/src/client/bridge/gateway/shard_queuer.rs @@ -1,9 +1,9 @@ use gateway::Shard; use internal::prelude::*; -use parking_lot::Mutex as ParkingLotMutex; +use parking_lot::Mutex; use std::collections::HashMap; use std::sync::mpsc::{Receiver, Sender}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::thread; use std::time::{Duration, Instant}; use super::super::super::EventHandler; @@ -27,20 +27,69 @@ use framework::Framework; /// blocking nature of the loop itself as well as a 5 second thread sleep /// between shard starts. pub struct ShardQueuer<H: EventHandler + Send + Sync + 'static> { - pub data: Arc<ParkingLotMutex<ShareMap>>, + /// A copy of [`Client::data`] to be given to runners for contextual + /// dispatching. + /// + /// [`Client::data`]: ../../struct.Client.html#structfield.data + pub data: Arc<Mutex<ShareMap>>, + /// A reference to an `EventHandler`, such as the one given to the + /// [`Client`]. + /// + /// [`Client`]: ../../struct.Client.html pub event_handler: Arc<H>, + /// A copy of the framework #[cfg(feature = "framework")] pub framework: Arc<Mutex<Option<Box<Framework + Send>>>>, + /// The instant that a shard was last started. + /// + /// This is used to determine how long to wait between shard IDENTIFYs. pub last_start: Option<Instant>, + /// A copy of the sender channel to communicate with the + /// [`ShardManagerMonitor`]. + /// + /// [`ShardManagerMonitor`]: struct.ShardManagerMonitor.html pub manager_tx: Sender<ShardManagerMessage>, - pub runners: Arc<ParkingLotMutex<HashMap<ShardId, ShardRunnerInfo>>>, + /// A copy of the map of shard runners. + pub runners: Arc<Mutex<HashMap<ShardId, ShardRunnerInfo>>>, + /// A receiver channel for the shard queuer to be told to start shards. pub rx: Receiver<ShardQueuerMessage>, + /// A copy of a threadpool to give shard runners. + /// + /// For example, when using the [`Client`], this will be a copy of + /// [`Client::threadpool`]. + /// + /// [`Client`]: ../../struct.Client.html + /// [`Client::threadpool`]: ../../struct.Client.html#structfield.threadpool pub threadpool: ThreadPool, + /// A copy of the token to connect with. pub token: Arc<Mutex<String>>, + /// A copy of the URI to use to connect to the gateway. pub ws_url: Arc<Mutex<String>>, } impl<H: EventHandler + Send + Sync + 'static> ShardQueuer<H> { + /// Begins the shard queuer loop. + /// + /// This will loop over the internal [`rx`] for [`ShardQueuerMessage`]s, + /// blocking for messages on what to do. + /// + /// If a [`ShardQueuerMessage::Start`] is received, this will: + /// + /// 1. Check how much time has passed since the last shard was started + /// 2. If the amount of time is less than the ratelimit, it will sleep until + /// that time has passed + /// 3. Start the shard by ID + /// + /// If a [`ShardQueuerMessage::Shutdown`] is received, this will return and + /// the loop will be over. + /// + /// **Note**: This should be run in its own thread due to the blocking + /// nature of the loop. + /// + /// [`ShardQueuerMessage`]: enum.ShardQueuerMessage.html + /// [`ShardQueuerMessage::Shutdown`]: enum.ShardQueuerMessage.html#variant.Shutdown + /// [`ShardQueuerMessage::Start`]: enum.ShardQueuerMessage.html#variant.Start + /// [`rx`]: #structfield.rx pub fn run(&mut self) { while let Ok(msg) = self.rx.recv() { match msg { @@ -80,16 +129,16 @@ impl<H: EventHandler + Send + Sync + 'static> ShardQueuer<H> { fn start(&mut self, shard_id: ShardId, shard_total: ShardId) -> Result<()> { let shard_info = [shard_id.0, shard_total.0]; + let shard = Shard::new( Arc::clone(&self.ws_url), Arc::clone(&self.token), shard_info, )?; - let locked = Arc::new(ParkingLotMutex::new(shard)); let mut runner = feature_framework! {{ ShardRunner::new( - Arc::clone(&locked), + shard, self.manager_tx.clone(), Arc::clone(&self.framework), Arc::clone(&self.data), @@ -98,7 +147,7 @@ impl<H: EventHandler + Send + Sync + 'static> ShardQueuer<H> { ) } else { ShardRunner::new( - locked.clone(), + shard, self.manager_tx.clone(), self.data.clone(), self.event_handler.clone(), @@ -108,7 +157,6 @@ impl<H: EventHandler + Send + Sync + 'static> ShardQueuer<H> { let runner_info = ShardRunnerInfo { runner_tx: runner.runner_tx(), - shard: locked, }; thread::spawn(move || { diff --git a/src/client/bridge/gateway/shard_runner.rs b/src/client/bridge/gateway/shard_runner.rs index aa0e064..0ba83ba 100644 --- a/src/client/bridge/gateway/shard_runner.rs +++ b/src/client/bridge/gateway/shard_runner.rs @@ -1,45 +1,52 @@ +use gateway::{Shard, ShardAction}; use internal::prelude::*; use internal::ws_impl::ReceiverExt; use model::event::{Event, GatewayEvent}; -use parking_lot::Mutex as ParkingLotMutex; -use std::sync::mpsc::{self, Receiver, Sender}; +use parking_lot::Mutex; +use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::sync::Arc; use super::super::super::{EventHandler, dispatch}; -use super::{LockedShard, ShardId, ShardManagerMessage}; +use super::{ShardClientMessage, ShardId, ShardManagerMessage, ShardRunnerMessage}; use threadpool::ThreadPool; use typemap::ShareMap; +use websocket::message::{CloseData, OwnedMessage}; use websocket::WebSocketError; #[cfg(feature = "framework")] use framework::Framework; -#[cfg(feature = "framework")] -use std::sync::Mutex; +#[cfg(feature = "voice")] +use internal::ws_impl::SenderExt; +/// A runner for managing a [`Shard`] and its respective WebSocket client. +/// +/// [`Shard`]: ../../../gateway/struct.Shard.html pub struct ShardRunner<H: EventHandler + Send + Sync + 'static> { - data: Arc<ParkingLotMutex<ShareMap>>, + data: Arc<Mutex<ShareMap>>, event_handler: Arc<H>, #[cfg(feature = "framework")] framework: Arc<Mutex<Option<Box<Framework + Send>>>>, manager_tx: Sender<ShardManagerMessage>, - runner_rx: Receiver<ShardManagerMessage>, - runner_tx: Sender<ShardManagerMessage>, - shard: LockedShard, - shard_info: [u64; 2], + // channel to receive messages from the shard manager and dispatches + runner_rx: Receiver<ShardClientMessage>, + // channel to send messages to the shard runner from the shard manager + runner_tx: Sender<ShardClientMessage>, + shard: Shard, threadpool: ThreadPool, } impl<H: EventHandler + Send + Sync + 'static> ShardRunner<H> { + /// Creates a new runner for a Shard. + #[allow(too_many_arguments)] #[cfg(feature = "framework")] pub fn new( - shard: LockedShard, + shard: Shard, manager_tx: Sender<ShardManagerMessage>, framework: Arc<Mutex<Option<Box<Framework + Send>>>>, - data: Arc<ParkingLotMutex<ShareMap>>, + data: Arc<Mutex<ShareMap>>, event_handler: Arc<H>, threadpool: ThreadPool, ) -> Self { let (tx, rx) = mpsc::channel(); - let shard_info = shard.lock().shard_info(); Self { runner_rx: rx, @@ -49,21 +56,20 @@ impl<H: EventHandler + Send + Sync + 'static> ShardRunner<H> { framework, manager_tx, shard, - shard_info, threadpool, } } + /// Creates a new runner for a Shard. #[cfg(not(feature = "framework"))] pub fn new( - shard: LockedShard, + shard: Shard, manager_tx: Sender<ShardManagerMessage>, - data: Arc<ParkingLotMutex<ShareMap>>, + data: Arc<Mutex<ShareMap>>, event_handler: Arc<H>, threadpool: ThreadPool, ) -> Self { let (tx, rx) = mpsc::channel(); - let shard_info = shard.lock().shard_info(); Self { runner_rx: rx, @@ -72,98 +78,276 @@ impl<H: EventHandler + Send + Sync + 'static> ShardRunner<H> { event_handler, manager_tx, shard, - shard_info, threadpool, } } + /// Starts the runner's loop to receive events. + /// + /// This runs a loop that performs the following in each iteration: + /// + /// 1. checks the receiver for [`ShardRunnerMessage`]s, possibly from the + /// [`ShardManager`], and if there is one, acts on it. + /// + /// 2. checks if a heartbeat should be sent to the discord Gateway, and if + /// so, sends one. + /// + /// 3. attempts to retrieve a message from the WebSocket, processing it into + /// a [`GatewayEvent`]. This will block for 100ms before assuming there is + /// no message available. + /// + /// 4. Checks with the [`Shard`] to determine if the gateway event is + /// specifying an action to take (e.g. resuming, reconnecting, heartbeating) + /// and then performs that action, if any. + /// + /// 5. Dispatches the event via the Client. + /// + /// 6. Go back to 1. + /// + /// [`GatewayEvent`]: ../../../model/event/enum.GatewayEvent.html + /// [`Shard`]: ../../../gateway/struct.Shard.html + /// [`ShardManager`]: struct.ShardManager.html + /// [`ShardRunnerMessage`]: enum.ShardRunnerMessage.html pub fn run(&mut self) -> Result<()> { - debug!("[ShardRunner {:?}] Running", self.shard_info); + debug!("[ShardRunner {:?}] Running", self.shard.shard_info()); loop { - { - let mut shard = self.shard.lock(); - let incoming = self.runner_rx.try_recv(); + if !self.recv()? { + return Ok(()); + } - // Check for an incoming message over the runner channel. - // - // If the message is to shutdown, first verify the ID so we know - // for certain this runner is to shutdown. - if let Ok(ShardManagerMessage::Shutdown(id)) = incoming { - if id.0 == self.shard_info[0] { - let _ = shard.shutdown_clean(); + // check heartbeat + if let Err(why) = self.shard.check_heartbeat() { + warn!( + "[ShardRunner {:?}] Error heartbeating: {:?}", + self.shard.shard_info(), + why, + ); + debug!( + "[ShardRunner {:?}] Requesting restart", + self.shard.shard_info(), + ); + + return self.request_restart(); + } - return Ok(()); + #[cfg(feature = "voice")] + { + for message in self.shard.cycle_voice_recv() { + if let Err(why) = self.shard.client.send_json(&message) { + println!("Err sending from voice over WS: {:?}", why); } } + } - if let Err(why) = shard.check_heartbeat() { - error!("Failed to heartbeat and reconnect: {:?}", why); - - return self.request_restart(); - } + let (event, action, successful) = self.recv_event(); - #[cfg(feature = "voice")] - { - shard.cycle_voice_recv(); - } + if let Some(action) = action { + let _ = self.action(action); } - let (event, successful) = self.recv_event(); - if let Some(event) = event { - let data = Arc::clone(&self.data); - let event_handler = Arc::clone(&self.event_handler); - let shard = Arc::clone(&self.shard); - - feature_framework! {{ - let framework = Arc::clone(&self.framework); - - self.threadpool.execute(|| { - dispatch( - event, - shard, - framework, - data, - event_handler, - ); - }); - } else { - self.threadpool.execute(|| { - dispatch( - event, - shard, - data, - event_handler, - ); - }); - }} + self.dispatch(event); } - { - let shard = self.shard.lock(); + if !successful && !self.shard.stage().is_connecting() { + return self.request_restart(); + } + } + } - if !successful && !shard.stage().is_connecting() { - return self.request_restart(); - } + /// Clones the internal copy of the Sender to the shard runner. + pub(super) fn runner_tx(&self) -> Sender<ShardClientMessage> { + self.runner_tx.clone() + } - if shard.is_shutdown() { - return self.request_shutdown(); - } + /// Takes an action that a [`Shard`] has determined should happen and then + /// does it. + /// + /// For example, if the shard says that an Identify message needs to be + /// sent, this will do that. + /// + /// # Errors + /// + /// Returns + fn action(&mut self, action: ShardAction) -> Result<()> { + match action { + ShardAction::Autoreconnect => self.shard.autoreconnect(), + ShardAction::Heartbeat => self.shard.heartbeat(), + ShardAction::Identify => self.shard.identify(), + ShardAction::Reconnect => self.shard.reconnect(), + ShardAction::Resume => self.shard.resume(), + } + } + + // Checks if the ID received to shutdown is equivalent to the ID of the + // shard this runner is responsible. If so, it shuts down the WebSocket + // client. + // + // Returns whether the WebSocket client is still active. + // + // If true, the WebSocket client was _not_ shutdown. If false, it was. + fn checked_shutdown(&mut self, id: ShardId) -> bool { + // First verify the ID so we know for certain this runner is + // to shutdown. + if id.0 != self.shard.shard_info()[0] { + // Not meant for this runner for some reason, don't + // shutdown. + return true; + } + + let close_data = CloseData::new(1000, String::new()); + let msg = OwnedMessage::Close(Some(close_data)); + let _ = self.shard.client.send_message(&msg); + + false + } + + fn dispatch(&self, event: Event) { + let data = Arc::clone(&self.data); + let event_handler = Arc::clone(&self.event_handler); + let runner_tx = self.runner_tx.clone(); + let shard_id = self.shard.shard_info()[0]; + + feature_framework! {{ + let framework = Arc::clone(&self.framework); + + self.threadpool.execute(move || { + dispatch( + event, + framework, + data, + event_handler, + runner_tx, + shard_id, + ); + }); + } else { + self.threadpool.execute(move || { + dispatch( + event, + data, + event_handler, + runner_tx, + shard_id, + ); + }); + }} + } + + // Handles a received value over the shard runner rx channel. + // + // Returns a boolean on whether the shard runner can continue. + // + // This always returns true, except in the case that the shard manager asked + // the runner to shutdown. + fn handle_rx_value(&mut self, value: ShardClientMessage) -> bool { + match value { + ShardClientMessage::Manager(x) => match x { + ShardManagerMessage::Restart(id) | + ShardManagerMessage::Shutdown(id) => { + self.checked_shutdown(id) + }, + ShardManagerMessage::ShutdownAll => { + // This variant should never be received. + warn!( + "[ShardRunner {:?}] Received a ShutdownAll?", + self.shard.shard_info(), + ); + + true + }, } + ShardClientMessage::Runner(x) => match x { + ShardRunnerMessage::ChunkGuilds { guild_ids, limit, query } => { + self.shard.chunk_guilds( + guild_ids, + limit, + query.as_ref().map(String::as_str), + ).is_ok() + }, + ShardRunnerMessage::Close(code, reason) => { + let reason = reason.unwrap_or_else(String::new); + let data = CloseData::new(code, reason); + let msg = OwnedMessage::Close(Some(data)); + + self.shard.client.send_message(&msg).is_ok() + }, + ShardRunnerMessage::Message(msg) => { + self.shard.client.send_message(&msg).is_ok() + }, + ShardRunnerMessage::SetGame(game) => { + // To avoid a clone of `game`, we do a little bit of + // trickery here: + // + // First, we obtain a reference to the current presence of + // the shard, and create a new presence tuple of the new + // game we received over the channel as well as the online + // status that the shard already had. + // + // We then (attempt to) send the websocket message with the + // status update, expressively returning: + // + // - whether the message successfully sent + // - the original game we received over the channel + self.shard.set_game(game); + + self.shard.update_presence().is_ok() + }, + ShardRunnerMessage::SetPresence(status, game) => { + self.shard.set_presence(status, game); + + self.shard.update_presence().is_ok() + }, + ShardRunnerMessage::SetStatus(status) => { + self.shard.set_status(status); + + self.shard.update_presence().is_ok() + }, + }, } } - pub(super) fn runner_tx(&self) -> Sender<ShardManagerMessage> { - self.runner_tx.clone() + // Receives values over the internal shard runner rx channel and handles + // them. + // + // This will loop over values until there is no longer one. + // + // Requests a restart if the sending half of the channel disconnects. This + // should _never_ happen, as the sending half is kept on the runner. + + // Returns whether the shard runner is in a state that can continue. + fn recv(&mut self) -> Result<bool> { + loop { + match self.runner_rx.try_recv() { + Ok(value) => { + if !self.handle_rx_value(value) { + return Ok(false); + } + }, + Err(TryRecvError::Disconnected) => { + warn!( + "[ShardRunner {:?}] Sending half DC; restarting", + self.shard.shard_info(), + ); + + let _ = self.request_restart(); + + return Ok(false); + }, + Err(TryRecvError::Empty) => break, + } + } + + // There are no longer any values available. + + Ok(true) } /// Returns a received event, as well as whether reading the potentially /// present event was successful. - fn recv_event(&mut self) -> (Option<Event>, bool) { - let mut shard = self.shard.lock(); - - let gw_event = match shard.client.recv_json(GatewayEvent::decode) { + fn recv_event(&mut self) -> (Option<Event>, Option<ShardAction>, bool) { + let gw_event = match self.shard.client.recv_json(GatewayEvent::decode) { Err(Error::WebSocket(WebSocketError::IoError(_))) => { // Check that an amount of time at least double the // heartbeat_interval has passed. @@ -171,58 +355,69 @@ impl<H: EventHandler + Send + Sync + 'static> ShardRunner<H> { // If not, continue on trying to receive messages. // // If it has, attempt to auto-reconnect. - let last = shard.last_heartbeat_ack(); - let interval = shard.heartbeat_interval(); - - if let (Some(last_heartbeat_ack), Some(interval)) = (last, interval) { - let seconds_passed = last_heartbeat_ack.elapsed().as_secs(); - let interval_in_secs = interval / 1000; - - if seconds_passed <= interval_in_secs * 2 { - return (None, true); + { + let last = self.shard.last_heartbeat_ack(); + let interval = self.shard.heartbeat_interval(); + + if let (Some(last_heartbeat_ack), Some(interval)) = (last, interval) { + let seconds_passed = last_heartbeat_ack.elapsed().as_secs(); + let interval_in_secs = interval / 1000; + + if seconds_passed <= interval_in_secs * 2 { + return (None, None, true); + } + } else { + return (None, None, true); } - } else { - return (None, true); } debug!("Attempting to auto-reconnect"); - if let Err(why) = shard.autoreconnect() { + if let Err(why) = self.shard.autoreconnect() { error!("Failed to auto-reconnect: {:?}", why); } - return (None, true); + return (None, None, true); }, Err(Error::WebSocket(WebSocketError::NoDataAvailable)) => { // This is hit when the websocket client dies this will be // hit every iteration. - return (None, false); + return (None, None, false); }, other => other, }; let event = match gw_event { Ok(Some(event)) => Ok(event), - Ok(None) => return (None, true), + Ok(None) => return (None, None, true), Err(why) => Err(why), }; - let event = match shard.handle_event(event) { - Ok(Some(event)) => event, - Ok(None) => return (None, true), + let action = match self.shard.handle_event(&event) { + Ok(Some(action)) => Some(action), + Ok(None) => None, Err(why) => { error!("Shard handler received err: {:?}", why); - return (None, true); + return (None, None, true); }, - }; + }; + + let event = match event { + Ok(GatewayEvent::Dispatch(_, event)) => Some(event), + _ => None, + }; - (Some(event), true) + (event, action, true) } fn request_restart(&self) -> Result<()> { - debug!("[ShardRunner {:?}] Requesting restart", self.shard_info); - let msg = ShardManagerMessage::Restart(ShardId(self.shard_info[0])); + debug!( + "[ShardRunner {:?}] Requesting restart", + self.shard.shard_info(), + ); + let shard_id = ShardId(self.shard.shard_info()[0]); + let msg = ShardManagerMessage::Restart(shard_id); let _ = self.manager_tx.send(msg); Ok(()) diff --git a/src/client/bridge/gateway/shard_runner_message.rs b/src/client/bridge/gateway/shard_runner_message.rs new file mode 100644 index 0000000..e6458eb --- /dev/null +++ b/src/client/bridge/gateway/shard_runner_message.rs @@ -0,0 +1,43 @@ +use model::{Game, GuildId, OnlineStatus}; +use websocket::message::OwnedMessage; + +/// A message to send from a shard over a WebSocket. +pub enum ShardRunnerMessage { + /// Indicates that the client is to send a member chunk message. + ChunkGuilds { + /// The IDs of the [`Guild`]s to chunk. + /// + /// [`Guild`]: ../../../model/struct.Guild.html + guild_ids: Vec<GuildId>, + /// The maximum number of members to receive [`GuildMembersChunkEvent`]s + /// for. + /// + /// [`GuildMembersChunkEvent`]: ../../../model/event/GuildMembersChunkEvent.html + limit: Option<u16>, + /// Text to filter members by. + /// + /// For example, a query of `"s"` will cause only [`Member`]s whose + /// usernames start with `"s"` to be chunked. + /// + /// [`Member`]: ../../../model/struct.Member.html + query: Option<String>, + }, + /// Indicates that the client is to close with the given status code and + /// reason. + /// + /// You should rarely - if _ever_ - need this, but the option is available. + /// Prefer to use the [`ShardManager`] to shutdown WebSocket clients if you + /// are intending to send a 1000 close code. + /// + /// [`ShardManager`]: struct.ShardManager.html + Close(u16, Option<String>), + /// Indicates that the client is to send a custom WebSocket message. + Message(OwnedMessage), + /// Indicates that the client is to update the shard's presence's game. + SetGame(Option<Game>), + /// Indicates that the client is to update the shard's presence in its + /// entirity. + SetPresence(OnlineStatus, Option<Game>), + /// Indicates that the client is to update the shard's presence's status. + SetStatus(OnlineStatus), +} diff --git a/src/client/bridge/mod.rs b/src/client/bridge/mod.rs index 4f27526..ea18d73 100644 --- a/src/client/bridge/mod.rs +++ b/src/client/bridge/mod.rs @@ -1 +1,10 @@ +//! A collection of bridged support between the [`client`] module and other +//! modules. +//! +//! **Warning**: You likely _do not_ need to mess with anything in here. Beware. +//! This is lower-level functionality abstracted by the [`Client`]. +//! +//! [`Client`]: ../struct.Client.html +//! [`client`]: ../ + pub mod gateway; diff --git a/src/client/context.rs b/src/client/context.rs index d0a6fc7..cc7ee63 100644 --- a/src/client/context.rs +++ b/src/client/context.rs @@ -1,7 +1,7 @@ -use Result; +use client::bridge::gateway::{ShardClientMessage, ShardMessenger}; +use std::sync::mpsc::Sender; use std::sync::Arc; use typemap::ShareMap; -use gateway::Shard; use model::*; use parking_lot::Mutex; @@ -12,7 +12,9 @@ use internal::prelude::*; #[cfg(feature = "builder")] use builder::EditProfile; #[cfg(feature = "builder")] -use http; +use {Result, http, utils}; +#[cfg(feature = "builder")] +use std::collections::HashMap; /// The context is a general utility struct provided on event dispatches, which /// helps with dealing with the current "context" of the event dispatch. @@ -36,21 +38,23 @@ pub struct Context { /// /// [`Client::data`]: struct.Client.html#structfield.data pub data: Arc<Mutex<ShareMap>>, - /// The associated shard which dispatched the event handler. - /// - /// Note that if you are sharding, in relevant terms, this is the shard - /// which received the event being dispatched. - pub shard: Arc<Mutex<Shard>>, + /// The messenger to communicate with the shard runner. + pub shard: ShardMessenger, + /// The ID of the shard this context is related to. + pub shard_id: u64, } impl Context { /// Create a new Context to be passed to an event handler. - pub(crate) fn new(shard: Arc<Mutex<Shard>>, - data: Arc<Mutex<ShareMap>>) - -> Context { + pub(crate) fn new( + data: Arc<Mutex<ShareMap>>, + runner_tx: Sender<ShardClientMessage>, + shard_id: u64, + ) -> Context { Context { + shard: ShardMessenger::new(runner_tx), + shard_id, data, - shard, } } @@ -69,43 +73,46 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, msg: Message) { + /// fn message(&self, ctx: Context, msg: Message) { /// if msg.content == "!changename" { /// ctx.edit_profile(|e| e.username("Edward Elric")); /// } /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` #[cfg(feature = "builder")] pub fn edit_profile<F: FnOnce(EditProfile) -> EditProfile>(&self, f: F) -> Result<CurrentUser> { - let mut map = Map::new(); + let mut map = HashMap::new(); feature_cache! { { - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); - map.insert("username".to_string(), Value::String(cache.user.name.clone())); + map.insert("username", Value::String(cache.user.name.clone())); if let Some(email) = cache.user.email.as_ref() { - map.insert("email".to_string(), Value::String(email.clone())); + map.insert("email", Value::String(email.clone())); } } else { let user = http::get_current_user()?; - map.insert("username".to_string(), Value::String(user.name.clone())); + map.insert("username", Value::String(user.name.clone())); if let Some(email) = user.email.as_ref() { - map.insert("email".to_string(), Value::String(email.clone())); + map.insert("email", Value::String(email.clone())); } } } - let edited = f(EditProfile(map)).0; + let edited = utils::hashmap_to_json_map(f(EditProfile(map)).0); http::edit_profile(&edited) } + /// Sets the current user as being [`Online`]. This maintains the current /// game. /// @@ -120,19 +127,22 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, msg: Message) { + /// fn message(&self, ctx: Context, msg: Message) { /// if msg.content == "!online" { /// ctx.online(); /// } /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// [`Online`]: ../model/enum.OnlineStatus.html#variant.Online + #[inline] pub fn online(&self) { - let mut shard = self.shard.lock(); - shard.set_status(OnlineStatus::Online); + self.shard.set_status(OnlineStatus::Online); } /// Sets the current user as being [`Idle`]. This maintains the current @@ -149,19 +159,21 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, msg: Message) { + /// fn message(&self, ctx: Context, msg: Message) { /// if msg.content == "!idle" { /// ctx.idle(); /// } /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// [`Idle`]: ../model/enum.OnlineStatus.html#variant.Idle + #[inline] pub fn idle(&self) { - let mut shard = self.shard.lock(); - shard.set_status(OnlineStatus::Idle); + self.shard.set_status(OnlineStatus::Idle); } /// Sets the current user as being [`DoNotDisturb`]. This maintains the @@ -178,19 +190,21 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, msg: Message) { + /// fn message(&self, ctx: Context, msg: Message) { /// if msg.content == "!dnd" { /// ctx.dnd(); /// } /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// [`DoNotDisturb`]: ../model/enum.OnlineStatus.html#variant.DoNotDisturb + #[inline] pub fn dnd(&self) { - let mut shard = self.shard.lock(); - shard.set_status(OnlineStatus::DoNotDisturb); + self.shard.set_status(OnlineStatus::DoNotDisturb); } /// Sets the current user as being [`Invisible`]. This maintains the current @@ -208,19 +222,21 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_ready(&self, ctx: Context, _: Ready) { + /// fn ready(&self, ctx: Context, _: Ready) { /// ctx.invisible(); /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// [`Event::Ready`]: ../model/event/enum.Event.html#variant.Ready /// [`Invisible`]: ../model/enum.OnlineStatus.html#variant.Invisible + #[inline] pub fn invisible(&self) { - let mut shard = self.shard.lock(); - shard.set_status(OnlineStatus::Invisible); + self.shard.set_status(OnlineStatus::Invisible); } /// "Resets" the current user's presence, by setting the game to `None` and @@ -239,20 +255,22 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_resume(&self, ctx: Context, _: ResumedEvent) { + /// fn resume(&self, ctx: Context, _: ResumedEvent) { /// ctx.reset_presence(); /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// [`Event::Resumed`]: ../model/event/enum.Event.html#variant.Resumed /// [`Online`]: ../model/enum.OnlineStatus.html#variant.Online /// [`set_presence`]: #method.set_presence + #[inline] pub fn reset_presence(&self) { - let mut shard = self.shard.lock(); - shard.set_presence(None, OnlineStatus::Online, false) + self.shard.set_presence(None, OnlineStatus::Online); } /// Sets the current game, defaulting to an online status of [`Online`]. @@ -271,8 +289,9 @@ impl Context { /// use serenity::model::Game; /// /// struct Handler; + /// /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, msg: Message) { + /// fn message(&self, ctx: Context, msg: Message) { /// let args = msg.content.splitn(2, ' ').collect::<Vec<&str>>(); /// /// if args.len() < 2 || *unsafe { args.get_unchecked(0) } != "~setgame" { @@ -283,17 +302,19 @@ impl Context { /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// # } - /// # + /// /// # #[cfg(not(feature = "model"))] - /// # fn main() { } + /// # fn main() {} /// ``` /// /// [`Online`]: ../model/enum.OnlineStatus.html#variant.Online + #[inline] pub fn set_game(&self, game: Game) { - let mut shard = self.shard.lock(); - shard.set_presence(Some(game), OnlineStatus::Online, false); + self.shard.set_presence(Some(game), OnlineStatus::Online); } /// Sets the current game, passing in only its name. This will automatically @@ -316,12 +337,13 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_ready(&self, ctx: Context, _: Ready) { + /// fn ready(&self, ctx: Context, _: Ready) { /// ctx.set_game_name("test"); /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// client.start().unwrap(); /// ``` /// /// [`Event::Ready`]: ../model/event/enum.Event.html#variant.Ready @@ -338,8 +360,7 @@ impl Context { url: None, }; - let mut shard = self.shard.lock(); - shard.set_presence(Some(game), OnlineStatus::Online, false); + self.shard.set_presence(Some(game), OnlineStatus::Online); } /// Sets the current user's presence, providing all fields to be passed. @@ -355,13 +376,15 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_ready(&self, ctx: Context, _: Ready) { + /// fn ready(&self, ctx: Context, _: Ready) { /// use serenity::model::OnlineStatus; /// - /// ctx.set_presence(None, OnlineStatus::Idle, false); + /// ctx.set_presence(None, OnlineStatus::Idle); /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// Setting the current user as playing `"Heroes of the Storm"`, while being @@ -374,23 +397,26 @@ impl Context { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_ready(&self, context: Context, _: Ready) { + /// fn ready(&self, context: Context, _: Ready) { /// use serenity::model::{Game, OnlineStatus}; /// /// let game = Game::playing("Heroes of the Storm"); /// let status = OnlineStatus::DoNotDisturb; /// - /// context.set_presence(Some(game), status, false); + /// context.set_presence(Some(game), status); /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// [`DoNotDisturb`]: ../model/enum.OnlineStatus.html#variant.DoNotDisturb /// [`Idle`]: ../model/enum.OnlineStatus.html#variant.Idle - pub fn set_presence(&self, game: Option<Game>, status: OnlineStatus, afk: bool) { - let mut shard = self.shard.lock(); - shard.set_presence(game, status, afk) + #[inline] + pub fn set_presence(&self, game: Option<Game>, status: OnlineStatus) { + self.shard.set_presence(game, status); } /// Disconnects the shard from the websocket, essentially "quiting" it. @@ -399,9 +425,8 @@ impl Context { /// until [`Client::start`] and vice versa are called again. /// /// [`Client::start`]: ./struct.Client.html#method.start - pub fn quit(&self) -> Result<()> { - let mut shard = self.shard.lock(); - - shard.shutdown_clean() + #[inline] + pub fn quit(&self) { + self.shard.shutdown_clean(); } } diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs index 351b4fb..9545a6f 100644 --- a/src/client/dispatch.rs +++ b/src/client/dispatch.rs @@ -1,9 +1,10 @@ use std::sync::Arc; use parking_lot::Mutex; +use super::bridge::gateway::ShardClientMessage; use super::event_handler::EventHandler; use super::Context; +use std::sync::mpsc::Sender; use typemap::ShareMap; -use gateway::Shard; use model::event::Event; use model::{Channel, Message}; @@ -15,8 +16,6 @@ use framework::Framework; use model::GuildId; #[cfg(feature = "cache")] use std::{thread, time}; -#[cfg(feature = "framework")] -use std::sync; #[cfg(feature = "cache")] use super::CACHE; @@ -26,7 +25,7 @@ macro_rules! update { { #[cfg(feature="cache")] { - CACHE.write().unwrap().update(&mut $event) + CACHE.write().update(&mut $event) } } }; @@ -37,44 +36,54 @@ macro_rules! now { () => (Utc::now().time().second() * 1000) } -fn context(conn: Arc<Mutex<Shard>>, data: Arc<Mutex<ShareMap>>) -> Context { - Context::new(conn, data) +fn context( + data: Arc<Mutex<ShareMap>>, + runner_tx: Sender<ShardClientMessage>, + shard_id: u64, +) -> Context { + Context::new(data, runner_tx, shard_id) } #[cfg(feature = "framework")] -pub fn dispatch<H: EventHandler + 'static>(event: Event, - conn: Arc<Mutex<Shard>>, - framework: Arc<sync::Mutex<Option<Box<Framework + Send>>>>, - data: Arc<Mutex<ShareMap>>, - event_handler: Arc<H>) { +pub fn dispatch<H: EventHandler + 'static>( + event: Event, + framework: Arc<Mutex<Option<Box<Framework + Send>>>>, + data: Arc<Mutex<ShareMap>>, + event_handler: Arc<H>, + runner_tx: Sender<ShardClientMessage>, + shard_id: u64, +) { match event { Event::MessageCreate(event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); dispatch_message( context.clone(), event.message.clone(), event_handler, ); - if let Some(ref mut framework) = *framework.lock().unwrap() { + if let Some(ref mut framework) = *framework.lock() { framework.dispatch(context, event.message); } }, - other => handle_event(other, conn, data, event_handler), + other => handle_event(other, data, event_handler, runner_tx, shard_id), } } #[cfg(not(feature = "framework"))] -pub fn dispatch<H: EventHandler + 'static>(event: Event, - conn: Arc<Mutex<Shard>>, - data: Arc<Mutex<ShareMap>>, - event_handler: Arc<H>) { +pub fn dispatch<H: EventHandler + 'static>( + event: Event, + data: Arc<Mutex<ShareMap>>, + event_handler: Arc<H>, + runner_tx: Sender<ShardClientMessage>, + shard_id: u64, +) { match event { Event::MessageCreate(event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); dispatch_message(context, event.message, event_handler); }, - other => handle_event(other, conn, data, event_handler), + other => handle_event(other, data, event_handler, runner_tx, shard_id), } } @@ -89,20 +98,23 @@ fn dispatch_message<H>( message.transform_content(); } - event_handler.on_message(context, message); + event_handler.message(context, message); } #[allow(cyclomatic_complexity, unused_assignments, unused_mut)] -fn handle_event<H: EventHandler + 'static>(event: Event, - conn: Arc<Mutex<Shard>>, - data: Arc<Mutex<ShareMap>>, - event_handler: Arc<H>) { +fn handle_event<H: EventHandler + 'static>( + event: Event, + data: Arc<Mutex<ShareMap>>, + event_handler: Arc<H>, + runner_tx: Sender<ShardClientMessage>, + shard_id: u64, +) { #[cfg(feature = "cache")] let mut last_guild_create_time = now!(); #[cfg(feature = "cache")] let wait_for_guilds = move || -> ::Result<()> { - let unavailable_guilds = CACHE.read().unwrap().unavailable_guilds.len(); + let unavailable_guilds = CACHE.read().unavailable_guilds.len(); while unavailable_guilds != 0 && (now!() < last_guild_create_time + 2000) { thread::sleep(time::Duration::from_millis(500)); @@ -115,84 +127,84 @@ fn handle_event<H: EventHandler + 'static>(event: Event, Event::ChannelCreate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); // This different channel_create dispatching is only due to the fact that // each time the bot receives a dm, this event is also fired. // So in short, only exists to reduce unnecessary clutter. match event.channel { Channel::Private(channel) => { - event_handler.on_private_channel_create(context, channel); + event_handler.private_channel_create(context, channel); }, Channel::Group(_) => {}, Channel::Guild(channel) => { - event_handler.on_channel_create(context, channel); + event_handler.channel_create(context, channel); }, Channel::Category(channel) => { - event_handler.on_category_create(context, channel); + event_handler.category_create(context, channel); }, } }, Event::ChannelDelete(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); match event.channel { Channel::Private(_) | Channel::Group(_) => {}, Channel::Guild(channel) => { - event_handler.on_channel_delete(context, channel); + event_handler.channel_delete(context, channel); }, Channel::Category(channel) => { - event_handler.on_category_delete(context, channel); + event_handler.category_delete(context, channel); }, } }, Event::ChannelPinsUpdate(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_channel_pins_update(context, event); + event_handler.channel_pins_update(context, event); }, Event::ChannelRecipientAdd(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_channel_recipient_addition(context, event.channel_id, event.user); + event_handler.channel_recipient_addition(context, event.channel_id, event.user); }, Event::ChannelRecipientRemove(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_channel_recipient_removal(context, event.channel_id, event.user); + event_handler.channel_recipient_removal(context, event.channel_id, event.user); }, Event::ChannelUpdate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - let before = CACHE.read().unwrap().channel(event.channel.id()); - event_handler.on_channel_update(context, before, event.channel); + let before = CACHE.read().channel(event.channel.id()); + event_handler.channel_update(context, before, event.channel); } else { - event_handler.on_channel_update(context, event.channel); + event_handler.channel_update(context, event.channel); }} }, Event::GuildBanAdd(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_ban_addition(context, event.guild_id, event.user); + event_handler.guild_ban_addition(context, event.guild_id, event.user); }, Event::GuildBanRemove(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_ban_removal(context, event.guild_id, event.user); + event_handler.guild_ban_removal(context, event.guild_id, event.user); }, Event::GuildCreate(mut event) => { #[cfg(feature = "cache")] let _is_new = { - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); !cache.unavailable_guilds.contains(&event.guild.id) }; @@ -203,10 +215,10 @@ fn handle_event<H: EventHandler + 'static>(event: Event, { last_guild_create_time = now!(); - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); if cache.unavailable_guilds.is_empty() { - let context = context(Arc::clone(&conn), Arc::clone(&data)); + let context = context(Arc::clone(&data), runner_tx.clone(), shard_id); let guild_amount = cache .guilds @@ -214,179 +226,177 @@ fn handle_event<H: EventHandler + 'static>(event: Event, .map(|(&id, _)| id) .collect::<Vec<GuildId>>(); - event_handler.on_cached(context, guild_amount); + event_handler.cached(context, guild_amount); } } - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - event_handler.on_guild_create(context, event.guild, _is_new); + event_handler.guild_create(context, event.guild, _is_new); } else { - event_handler.on_guild_create(context, event.guild); + event_handler.guild_create(context, event.guild); }} }, Event::GuildDelete(mut event) => { let _full = update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - event_handler.on_guild_delete(context, event.guild, _full); + event_handler.guild_delete(context, event.guild, _full); } else { - event_handler.on_guild_delete(context, event.guild); + event_handler.guild_delete(context, event.guild); }} }, Event::GuildEmojisUpdate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_emojis_update(context, event.guild_id, event.emojis); + event_handler.guild_emojis_update(context, event.guild_id, event.emojis); }, Event::GuildIntegrationsUpdate(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_integrations_update(context, event.guild_id); + event_handler.guild_integrations_update(context, event.guild_id); }, Event::GuildMemberAdd(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_member_addition(context, event.guild_id, event.member); + event_handler.guild_member_addition(context, event.guild_id, event.member); }, Event::GuildMemberRemove(mut event) => { let _member = update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - event_handler.on_guild_member_removal(context, event.guild_id, event.user, _member); + event_handler.guild_member_removal(context, event.guild_id, event.user, _member); } else { - event_handler.on_guild_member_removal(context, event.guild_id, event.user); + event_handler.guild_member_removal(context, event.guild_id, event.user); }} }, Event::GuildMemberUpdate(mut event) => { let _before = update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ // This is safe to unwrap, as the update would have created // the member if it did not exist. So, there is be _no_ way // that this could fail under any circumstance. let after = CACHE.read() - .unwrap() .member(event.guild_id, event.user.id) .unwrap() .clone(); - event_handler.on_guild_member_update(context, _before, after); + event_handler.guild_member_update(context, _before, after); } else { - event_handler.on_guild_member_update(context, event); + event_handler.guild_member_update(context, event); }} }, Event::GuildMembersChunk(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_members_chunk(context, event.guild_id, event.members); + event_handler.guild_members_chunk(context, event.guild_id, event.members); }, Event::GuildRoleCreate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_role_create(context, event.guild_id, event.role); + event_handler.guild_role_create(context, event.guild_id, event.role); }, Event::GuildRoleDelete(mut event) => { let _role = update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - event_handler.on_guild_role_delete(context, event.guild_id, event.role_id, _role); + event_handler.guild_role_delete(context, event.guild_id, event.role_id, _role); } else { - event_handler.on_guild_role_delete(context, event.guild_id, event.role_id); + event_handler.guild_role_delete(context, event.guild_id, event.role_id); }} }, Event::GuildRoleUpdate(mut event) => { let _before = update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - event_handler.on_guild_role_update(context, event.guild_id, _before, event.role); + event_handler.guild_role_update(context, event.guild_id, _before, event.role); } else { - event_handler.on_guild_role_update(context, event.guild_id, event.role); + event_handler.guild_role_update(context, event.guild_id, event.role); }} }, Event::GuildUnavailable(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_guild_unavailable(context, event.guild_id); + event_handler.guild_unavailable(context, event.guild_id); }, Event::GuildUpdate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ let before = CACHE.read() - .unwrap() .guilds .get(&event.guild.id) .cloned(); - event_handler.on_guild_update(context, before, event.guild); + event_handler.guild_update(context, before, event.guild); } else { - event_handler.on_guild_update(context, event.guild); + event_handler.guild_update(context, event.guild); }} }, // Already handled by the framework check macro Event::MessageCreate(_) => {}, Event::MessageDeleteBulk(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_message_delete_bulk(context, event.channel_id, event.ids); + event_handler.message_delete_bulk(context, event.channel_id, event.ids); }, Event::MessageDelete(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_message_delete(context, event.channel_id, event.message_id); + event_handler.message_delete(context, event.channel_id, event.message_id); }, Event::MessageUpdate(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_message_update(context, event); + event_handler.message_update(context, event); }, Event::PresencesReplace(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_presence_replace(context, event.presences); + event_handler.presence_replace(context, event.presences); }, Event::PresenceUpdate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_presence_update(context, event); + event_handler.presence_update(context, event); }, Event::ReactionAdd(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_reaction_add(context, event.reaction); + event_handler.reaction_add(context, event.reaction); }, Event::ReactionRemove(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_reaction_remove(context, event.reaction); + event_handler.reaction_remove(context, event.reaction); }, Event::ReactionRemoveAll(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_reaction_remove_all(context, event.channel_id, event.message_id); + event_handler.reaction_remove_all(context, event.channel_id, event.message_id); }, Event::Ready(mut event) => { update!(event); @@ -397,58 +407,58 @@ fn handle_event<H: EventHandler + 'static>(event: Event, let _ = wait_for_guilds() .map(move |_| { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_ready(context, event.ready); + event_handler.ready(context, event.ready); }); } else { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_ready(context, event.ready); + event_handler.ready(context, event.ready); } } }, Event::Resumed(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_resume(context, event); + event_handler.resume(context, event); }, Event::TypingStart(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_typing_start(context, event); + event_handler.typing_start(context, event); }, Event::Unknown(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_unknown(context, event.kind, event.value); + event_handler.unknown(context, event.kind, event.value); }, Event::UserUpdate(mut event) => { let _before = update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); feature_cache! {{ - event_handler.on_user_update(context, _before.unwrap(), event.current_user); + event_handler.user_update(context, _before.unwrap(), event.current_user); } else { - event_handler.on_user_update(context, event.current_user); + event_handler.user_update(context, event.current_user); }} }, Event::VoiceServerUpdate(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_voice_server_update(context, event); + event_handler.voice_server_update(context, event); }, Event::VoiceStateUpdate(mut event) => { update!(event); - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_voice_state_update(context, event.guild_id, event.voice_state); + event_handler.voice_state_update(context, event.guild_id, event.voice_state); }, Event::WebhookUpdate(mut event) => { - let context = context(conn, data); + let context = context(data, runner_tx, shard_id); - event_handler.on_webhook_update(context, event.guild_id, event.channel_id); + event_handler.webhook_update(context, event.guild_id, event.channel_id); }, } } diff --git a/src/client/event_handler.rs b/src/client/event_handler.rs index 6e3c78e..5c2f131 100644 --- a/src/client/event_handler.rs +++ b/src/client/event_handler.rs @@ -1,3 +1,4 @@ +use parking_lot::RwLock; use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; @@ -5,77 +6,75 @@ use super::context::Context; use model::event::*; use model::*; -use std::sync::RwLock; - pub trait EventHandler { #[cfg(feature = "cache")] - fn on_cached(&self, _: Context, _: Vec<GuildId>) {} - fn on_channel_create(&self, _: Context, _: Arc<RwLock<GuildChannel>>) {} - fn on_category_create(&self, _: Context, _: Arc<RwLock<ChannelCategory>>) {} - fn on_category_delete(&self, _: Context, _: Arc<RwLock<ChannelCategory>>) {} - fn on_private_channel_create(&self, _: Context, _: Arc<RwLock<PrivateChannel>>) {} - fn on_channel_delete(&self, _: Context, _: Arc<RwLock<GuildChannel>>) {} - fn on_channel_pins_update(&self, _: Context, _: ChannelPinsUpdateEvent) {} - fn on_channel_recipient_addition(&self, _: Context, _: ChannelId, _: User) {} - fn on_channel_recipient_removal(&self, _: Context, _: ChannelId, _: User) {} + fn cached(&self, _: Context, _: Vec<GuildId>) {} + fn channel_create(&self, _: Context, _: Arc<RwLock<GuildChannel>>) {} + fn category_create(&self, _: Context, _: Arc<RwLock<ChannelCategory>>) {} + fn category_delete(&self, _: Context, _: Arc<RwLock<ChannelCategory>>) {} + fn private_channel_create(&self, _: Context, _: Arc<RwLock<PrivateChannel>>) {} + fn channel_delete(&self, _: Context, _: Arc<RwLock<GuildChannel>>) {} + fn channel_pins_update(&self, _: Context, _: ChannelPinsUpdateEvent) {} + fn channel_recipient_addition(&self, _: Context, _: ChannelId, _: User) {} + fn channel_recipient_removal(&self, _: Context, _: ChannelId, _: User) {} #[cfg(feature = "cache")] - fn on_channel_update(&self, _: Context, _: Option<Channel>, _: Channel) {} + fn channel_update(&self, _: Context, _: Option<Channel>, _: Channel) {} #[cfg(not(feature = "cache"))] - fn on_channel_update(&self, _: Context, _: Channel) {} - fn on_guild_ban_addition(&self, _: Context, _: GuildId, _: User) {} - fn on_guild_ban_removal(&self, _: Context, _: GuildId, _: User) {} + fn channel_update(&self, _: Context, _: Channel) {} + fn guild_ban_addition(&self, _: Context, _: GuildId, _: User) {} + fn guild_ban_removal(&self, _: Context, _: GuildId, _: User) {} #[cfg(feature = "cache")] - fn on_guild_create(&self, _: Context, _: Guild, _: bool) {} + fn guild_create(&self, _: Context, _: Guild, _: bool) {} #[cfg(not(feature = "cache"))] - fn on_guild_create(&self, _: Context, _: Guild) {} + fn guild_create(&self, _: Context, _: Guild) {} #[cfg(feature = "cache")] - fn on_guild_delete(&self, _: Context, _: PartialGuild, _: Option<Arc<RwLock<Guild>>>) {} + fn guild_delete(&self, _: Context, _: PartialGuild, _: Option<Arc<RwLock<Guild>>>) {} #[cfg(not(feature = "cache"))] - fn on_guild_delete(&self, _: Context, _: PartialGuild) {} - fn on_guild_emojis_update(&self, _: Context, _: GuildId, _: HashMap<EmojiId, Emoji>) {} - fn on_guild_integrations_update(&self, _: Context, _: GuildId) {} - fn on_guild_member_addition(&self, _: Context, _: GuildId, _: Member) {} + fn guild_delete(&self, _: Context, _: PartialGuild) {} + fn guild_emojis_update(&self, _: Context, _: GuildId, _: HashMap<EmojiId, Emoji>) {} + fn guild_integrations_update(&self, _: Context, _: GuildId) {} + fn guild_member_addition(&self, _: Context, _: GuildId, _: Member) {} #[cfg(feature = "cache")] - fn on_guild_member_removal(&self, _: Context, _: GuildId, _: User, _: Option<Member>) {} + fn guild_member_removal(&self, _: Context, _: GuildId, _: User, _: Option<Member>) {} #[cfg(not(feature = "cache"))] - fn on_guild_member_removal(&self, _: Context, _: GuildId, _: User) {} + fn guild_member_removal(&self, _: Context, _: GuildId, _: User) {} #[cfg(feature = "cache")] - fn on_guild_member_update(&self, _: Context, _: Option<Member>, _: Member) {} + fn guild_member_update(&self, _: Context, _: Option<Member>, _: Member) {} #[cfg(not(feature = "cache"))] - fn on_guild_member_update(&self, _: Context, _: GuildMemberUpdateEvent) {} - fn on_guild_members_chunk(&self, _: Context, _: GuildId, _: HashMap<UserId, Member>) {} - fn on_guild_role_create(&self, _: Context, _: GuildId, _: Role) {} + fn guild_member_update(&self, _: Context, _: GuildMemberUpdateEvent) {} + fn guild_members_chunk(&self, _: Context, _: GuildId, _: HashMap<UserId, Member>) {} + fn guild_role_create(&self, _: Context, _: GuildId, _: Role) {} #[cfg(feature = "cache")] - fn on_guild_role_delete(&self, _: Context, _: GuildId, _: RoleId, _: Option<Role>) {} + fn guild_role_delete(&self, _: Context, _: GuildId, _: RoleId, _: Option<Role>) {} #[cfg(not(feature = "cache"))] - fn on_guild_role_delete(&self, _: Context, _: GuildId, _: RoleId) {} + fn guild_role_delete(&self, _: Context, _: GuildId, _: RoleId) {} #[cfg(feature = "cache")] - fn on_guild_role_update(&self, _: Context, _: GuildId, _: Option<Role>, _: Role) {} + fn guild_role_update(&self, _: Context, _: GuildId, _: Option<Role>, _: Role) {} #[cfg(not(feature = "cache"))] - fn on_guild_role_update(&self, _: Context, _: GuildId, _: Role) {} - fn on_guild_unavailable(&self, _: Context, _: GuildId) {} + fn guild_role_update(&self, _: Context, _: GuildId, _: Role) {} + fn guild_unavailable(&self, _: Context, _: GuildId) {} #[cfg(feature = "cache")] - fn on_guild_update(&self, _: Context, _: Option<Arc<RwLock<Guild>>>, _: PartialGuild) {} + fn guild_update(&self, _: Context, _: Option<Arc<RwLock<Guild>>>, _: PartialGuild) {} #[cfg(not(feature = "cache"))] - fn on_guild_update(&self, _: Context, _: PartialGuild) {} - fn on_message(&self, _: Context, _: Message) {} - fn on_message_delete(&self, _: Context, _: ChannelId, _: MessageId) {} - fn on_message_delete_bulk(&self, _: Context, _: ChannelId, _: Vec<MessageId>) {} - fn on_reaction_add(&self, _: Context, _: Reaction) {} - fn on_reaction_remove(&self, _: Context, _: Reaction) {} - fn on_reaction_remove_all(&self, _: Context, _: ChannelId, _: MessageId) {} - fn on_message_update(&self, _: Context, _: MessageUpdateEvent) {} - fn on_presence_replace(&self, _: Context, _: Vec<Presence>) {} - fn on_presence_update(&self, _: Context, _: PresenceUpdateEvent) {} - fn on_ready(&self, _: Context, _: Ready) {} - fn on_resume(&self, _: Context, _: ResumedEvent) {} - fn on_typing_start(&self, _: Context, _: TypingStartEvent) {} - fn on_unknown(&self, _: Context, _: String, _: Value) {} + fn guild_update(&self, _: Context, _: PartialGuild) {} + fn message(&self, _: Context, _: Message) {} + fn message_delete(&self, _: Context, _: ChannelId, _: MessageId) {} + fn message_delete_bulk(&self, _: Context, _: ChannelId, _: Vec<MessageId>) {} + fn reaction_add(&self, _: Context, _: Reaction) {} + fn reaction_remove(&self, _: Context, _: Reaction) {} + fn reaction_remove_all(&self, _: Context, _: ChannelId, _: MessageId) {} + fn message_update(&self, _: Context, _: MessageUpdateEvent) {} + fn presence_replace(&self, _: Context, _: Vec<Presence>) {} + fn presence_update(&self, _: Context, _: PresenceUpdateEvent) {} + fn ready(&self, _: Context, _: Ready) {} + fn resume(&self, _: Context, _: ResumedEvent) {} + fn typing_start(&self, _: Context, _: TypingStartEvent) {} + fn unknown(&self, _: Context, _: String, _: Value) {} #[cfg(feature = "cache")] - fn on_user_update(&self, _: Context, _: CurrentUser, _: CurrentUser) {} + fn user_update(&self, _: Context, _: CurrentUser, _: CurrentUser) {} #[cfg(not(feature = "cache"))] - fn on_user_update(&self, _: Context, _: CurrentUser) {} - fn on_voice_server_update(&self, _: Context, _: VoiceServerUpdateEvent) {} - fn on_voice_state_update(&self, _: Context, _: Option<GuildId>, _: VoiceState) {} - fn on_webhook_update(&self, _: Context, _: GuildId, _: ChannelId) {} + fn user_update(&self, _: Context, _: CurrentUser) {} + fn voice_server_update(&self, _: Context, _: VoiceServerUpdateEvent) {} + fn voice_state_update(&self, _: Context, _: Option<GuildId>, _: VoiceState) {} + fn webhook_update(&self, _: Context, _: GuildId, _: ChannelId) {} } diff --git a/src/client/mod.rs b/src/client/mod.rs index 6711287..a227920 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -37,13 +37,11 @@ pub use http as rest; #[cfg(feature = "cache")] pub use CACHE; -use self::bridge::gateway::{ShardId, ShardManager, ShardRunnerInfo}; +use self::bridge::gateway::{ShardManager, ShardManagerMonitor}; use self::dispatch::dispatch; -use std::sync::{self, Arc}; +use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering, ATOMIC_BOOL_INIT}; use parking_lot::Mutex; -use std::collections::HashMap; -use std::mem; use threadpool::ThreadPool; use typemap::ShareMap; use http; @@ -103,8 +101,7 @@ impl CloseHandle { /// [`on_message`]: #method.on_message /// [`Event::MessageCreate`]: ../model/event/enum.Event.html#variant.MessageCreate /// [sharding docs]: gateway/index.html#sharding -#[derive(Clone)] -pub struct Client<H: EventHandler + Send + Sync + 'static> { +pub struct Client { /// A ShareMap which requires types to be Send + Sync. This is a map that /// can be safely shared across contexts. /// @@ -191,8 +188,7 @@ pub struct Client<H: EventHandler + Send + Sync + 'static> { /// /// [`Event::Ready`]: ../model/event/enum.Event.html#variant.Ready /// [`on_ready`]: #method.on_ready - event_handler: Arc<H>, - #[cfg(feature = "framework")] framework: Arc<sync::Mutex<Option<Box<Framework + Send>>>>, + #[cfg(feature = "framework")] framework: Arc<Mutex<Option<Box<Framework + Send>>>>, /// A HashMap of all shards instantiated by the Client. /// /// The key is the shard ID and the value is the shard itself. @@ -222,14 +218,14 @@ pub struct Client<H: EventHandler + Send + Sync + 'static> { /// /// impl EventHandler for Handler { } /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler)?; /// - /// let shard_runners = client.shard_runners.clone(); + /// let shard_manager = client.shard_manager.clone(); /// /// thread::spawn(move || { /// loop { /// println!("Shard count instantiated: {}", - /// shard_runners.lock().len()); + /// shard_manager.lock().shards_instantiated().len()); /// /// thread::sleep(Duration::from_millis(5000)); /// } @@ -244,16 +240,26 @@ pub struct Client<H: EventHandler + Send + Sync + 'static> { /// /// [`Client::start_shard`]: #method.start_shard /// [`Client::start_shards`]: #method.start_shards - pub shard_runners: Arc<Mutex<HashMap<ShardId, ShardRunnerInfo>>>, + pub shard_manager: Arc<Mutex<ShardManager>>, + shard_manager_worker: ShardManagerMonitor, /// The threadpool shared by all shards. /// /// Defaults to 5 threads, which should suffice small bots. Consider /// increasing this number as your bot grows. pub threadpool: ThreadPool, - token: Arc<sync::Mutex<String>>, + /// The token in use by the client. + pub token: Arc<Mutex<String>>, + /// URI that the client's shards will use to connect to the gateway. + /// + /// This is likely not important for production usage and is, at best, used + /// for debugging. + /// + /// This is wrapped in an `Arc<Mutex<T>>` so all shards will have an updated + /// value available. + pub ws_uri: Arc<Mutex<String>>, } -impl<H: EventHandler + Send + Sync + 'static> Client<H> { +impl Client { /// Creates a Client for a bot user. /// /// Discord has a requirement of prefixing bot tokens with `"Bot "`, which @@ -275,7 +281,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use std::env; /// /// let token = env::var("DISCORD_TOKEN")?; - /// let client = Client::new(&token, Handler); + /// let client = Client::new(&token, Handler)?; /// # Ok(()) /// # } /// # @@ -283,7 +289,8 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// # try_main().unwrap(); /// # } /// ``` - pub fn new(token: &str, handler: H) -> Self { + pub fn new<H>(token: &str, handler: H) -> Result<Self> + where H: EventHandler + Send + Sync + 'static { let token = if token.starts_with("Bot ") { token.to_string() } else { @@ -291,29 +298,59 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { }; http::set_token(&token); - let locked = Arc::new(sync::Mutex::new(token)); + let locked = Arc::new(Mutex::new(token)); let name = "serenity client".to_owned(); let threadpool = ThreadPool::with_name(name, 5); + let url = Arc::new(Mutex::new(http::get_gateway()?.url)); + let data = Arc::new(Mutex::new(ShareMap::custom())); + let event_handler = Arc::new(handler); + + Ok(feature_framework! {{ + let framework = Arc::new(Mutex::new(None)); + + let (shard_manager, shard_manager_worker) = ShardManager::new( + 0, + 0, + 0, + Arc::clone(&url), + Arc::clone(&locked), + Arc::clone(&data), + Arc::clone(&event_handler), + Arc::clone(&framework), + threadpool.clone(), + ); - feature_framework! {{ Client { - data: Arc::new(Mutex::new(ShareMap::custom())), - event_handler: Arc::new(handler), - framework: Arc::new(sync::Mutex::new(None)), - shard_runners: Arc::new(Mutex::new(HashMap::new())), - threadpool, token: locked, + ws_uri: url, + framework, + data, + shard_manager, + shard_manager_worker, + threadpool, } } else { + let (shard_manager, shard_manager_worker) = ShardManager::new( + 0, + 0, + 0, + Arc::clone(&url), + locked.clone(), + data.clone(), + Arc::clone(&event_handler), + threadpool.clone(), + ); + Client { - data: Arc::new(Mutex::new(ShareMap::custom())), - event_handler: Arc::new(handler), - shard_runners: Arc::new(Mutex::new(HashMap::new())), - threadpool, token: locked, + ws_uri: url, + data, + shard_manager, + shard_manager_worker, + threadpool, } - }} + }}) } /// Sets a framework to be used with the client. All message events will be @@ -340,7 +377,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler)?; /// client.with_framework(StandardFramework::new() /// .configure(|c| c.prefix("~")) /// .command("ping", |c| c.exec_str("Pong!"))); @@ -392,12 +429,11 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// /// impl EventHandler for Handler {} /// - /// /// # fn try_main() -> Result<(), Box<Error>> { /// use serenity::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let mut client = Client::new(&token, Handler).unwrap(); /// client.with_framework(MyFramework { commands: { /// let mut map = HashMap::new(); /// map.insert("ping".to_string(), Box::new(|msg, _| msg.channel_id.say("pong!"))); @@ -417,7 +453,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// [framework docs]: ../framework/index.html #[cfg(feature = "framework")] pub fn with_framework<F: Framework + Send + 'static>(&mut self, f: F) { - self.framework = Arc::new(sync::Mutex::new(Some(Box::new(f)))); + *self.framework.lock() = Some(Box::new(f)); } /// Establish the connection and start listening for events. @@ -447,7 +483,8 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::client::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let token = env::var("DISCORD_TOKEN")?; + /// let mut client = Client::new(&token, Handler).unwrap(); /// /// if let Err(why) = client.start() { /// println!("Err with client: {:?}", why); @@ -462,7 +499,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// /// [gateway docs]: gateway/index.html#sharding pub fn start(&mut self) -> Result<()> { - self.start_connection([0, 0, 1], http::get_gateway()?.url) + self.start_connection([0, 0, 1]) } /// Establish the connection(s) and start listening for events. @@ -492,7 +529,8 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::client::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let token = env::var("DISCORD_TOKEN")?; + /// let mut client = Client::new(&token, Handler).unwrap(); /// /// if let Err(why) = client.start_autosharded() { /// println!("Err with client: {:?}", why); @@ -513,15 +551,13 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown /// [gateway docs]: gateway/index.html#sharding pub fn start_autosharded(&mut self) -> Result<()> { - let mut res = http::get_bot_gateway()?; - - let x = res.shards as u64 - 1; - let y = res.shards as u64; - let url = mem::replace(&mut res.url, String::default()); + let (x, y) = { + let res = http::get_bot_gateway()?; - drop(res); + (res.shards as u64 - 1, res.shards as u64) + }; - self.start_connection([0, x, y], url) + self.start_connection([0, x, y]) } /// Establish a sharded connection and start listening for events. @@ -551,7 +587,8 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::client::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let token = env::var("DISCORD_TOKEN")?; + /// let mut client = Client::new(&token, Handler).unwrap(); /// /// if let Err(why) = client.start_shard(3, 5) { /// println!("Err with client: {:?}", why); @@ -578,7 +615,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::client::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler)?; /// /// if let Err(why) = client.start_shard(0, 1) { /// println!("Err with client: {:?}", why); @@ -601,7 +638,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// [`start_autosharded`]: #method.start_autosharded /// [gateway docs]: gateway/index.html#sharding pub fn start_shard(&mut self, shard: u64, shards: u64) -> Result<()> { - self.start_connection([shard, shard, shards], http::get_gateway()?.url) + self.start_connection([shard, shard, shards]) } /// Establish sharded connections and start listening for events. @@ -631,7 +668,8 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::client::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let token = env::var("DISCORD_TOKEN")?; + /// let mut client = Client::new(&token, Handler).unwrap(); /// /// if let Err(why) = client.start_shards(8) { /// println!("Err with client: {:?}", why); @@ -654,10 +692,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// [`start_shard_range`]: #method.start_shard_range /// [Gateway docs]: gateway/index.html#sharding pub fn start_shards(&mut self, total_shards: u64) -> Result<()> { - self.start_connection( - [0, total_shards - 1, total_shards], - http::get_gateway()?.url, - ) + self.start_connection([0, total_shards - 1, total_shards]) } /// Establish a range of sharded connections and start listening for events. @@ -702,7 +737,8 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// use serenity::client::Client; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler); + /// let token = env::var("DISCORD_TOKEN")?; + /// let mut client = Client::new(&token, Handler).unwrap(); /// /// if let Err(why) = client.start_shard_range([4, 7], 10) { /// println!("Err with client: {:?}", why); @@ -726,7 +762,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { /// [`start_shards`]: #method.start_shards /// [Gateway docs]: gateway/index.html#sharding pub fn start_shard_range(&mut self, range: [u64; 2], total_shards: u64) -> Result<()> { - self.start_connection([range[0], range[1], total_shards], http::get_gateway()?.url) + self.start_connection([range[0], range[1], total_shards]) } /// Returns a thread-safe handle for closing shards. @@ -745,7 +781,7 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { // an error. // // [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown - fn start_connection(&mut self, shard_data: [u64; 3], url: String) -> Result<()> { + fn start_connection(&mut self, shard_data: [u64; 3]) -> Result<()> { HANDLE_STILL.store(true, Ordering::Relaxed); // Update the framework's current user if the feature is enabled. @@ -755,44 +791,42 @@ impl<H: EventHandler + Send + Sync + 'static> Client<H> { { let user = http::get_current_user()?; - if let Some(ref mut framework) = *self.framework.lock().unwrap() { + if let Some(ref mut framework) = *self.framework.lock() { framework.update_current_user(user.id, user.bot); } } - let gateway_url = Arc::new(sync::Mutex::new(url)); + { + let mut manager = self.shard_manager.lock(); + + let init = shard_data[1] - shard_data[0] + 1; - let mut manager = ShardManager::new( - shard_data[0], - shard_data[1] - shard_data[0] + 1, - shard_data[2], - Arc::clone(&gateway_url), - Arc::clone(&self.token), - Arc::clone(&self.data), - Arc::clone(&self.event_handler), - #[cfg(feature = "framework")] - Arc::clone(&self.framework), - self.threadpool.clone(), - ); + manager.set_shards(shard_data[0], init, shard_data[2]); - self.shard_runners = Arc::clone(&manager.runners); + debug!( + "Initializing shard info: {} - {}/{}", + shard_data[0], + init, + shard_data[2], + ); - if let Err(why) = manager.initialize() { - error!("Failed to boot a shard: {:?}", why); - info!("Shutting down all shards"); + if let Err(why) = manager.initialize() { + error!("Failed to boot a shard: {:?}", why); + info!("Shutting down all shards"); - manager.shutdown_all(); + manager.shutdown_all(); - return Err(Error::Client(ClientError::ShardBootFailure)); + return Err(Error::Client(ClientError::ShardBootFailure)); + } } - manager.run(); + self.shard_manager_worker.run(); Err(Error::Client(ClientError::Shutdown)) } } -impl<H: EventHandler + Send + Sync + 'static> Drop for Client<H> { +impl Drop for Client { fn drop(&mut self) { self.close_handle().close(); } } diff --git a/src/framework/standard/args.rs b/src/framework/standard/args.rs index 7fb82e4..fab27a5 100644 --- a/src/framework/standard/args.rs +++ b/src/framework/standard/args.rs @@ -64,7 +64,12 @@ pub struct Args { } impl Args { - pub fn new(message: &str, delimiter: &str) -> Self { + pub fn new(message: &str, possible_delimiters: Vec<String>) -> Self { + let delimiter = possible_delimiters + .iter() + .find(|&d| message.contains(d)) + .map_or(possible_delimiters[0].as_str(), |s| s.as_str()); + let split = if message.trim().is_empty() { Vec::new() } else { @@ -78,14 +83,14 @@ impl Args { } /// Removes the first element, parses it to a specific type if necessary, returns. - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("42 69", " "); - /// + /// + /// let mut args = Args::new("42 69", vec![" ".to_owned()]); + /// /// assert_eq!(args.single::<i32>().unwrap(), 42); /// assert_eq!(args, ["69"]); /// ``` @@ -107,18 +112,18 @@ impl Args { /// /// [`single`]: #method.single /// [`FromStrZc`]: trait.FromStrZc.html - pub fn single_zc<'a, T: FromStrZc<'a> + 'a>(&'a mut self) -> Result<T, T::Err> + pub fn single_zc<'a, T: FromStrZc<'a> + 'a>(&'a mut self) -> Result<T, T::Err> where T::Err: StdError { - + // This is a hack as to mitigate some nasty lifetime errors. // // (Culprit `Vec::remove`s return type) fn get_and_remove(b: &mut Vec<String>) -> Option<&str> { struct GetThenRemove<'a>(&'a mut Vec<String>); - + impl<'a> Drop for GetThenRemove<'a> { fn drop(&mut self) { - if !self.0.is_empty() { + if !self.0.is_empty() { self.0.remove(0); } } @@ -134,16 +139,16 @@ impl Args { /// Like [`single`], but doesn't remove the element. /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let args = Args::new("42 69", " "); - /// + /// + /// let args = Args::new("42 69", vec![" ".to_owned()]); + /// /// assert_eq!(args.single_n::<i32>().unwrap(), 42); /// assert_eq!(args, ["42", "69"]); /// ``` - /// + /// /// [`single`]: #method.single pub fn single_n<T: FromStr>(&self) -> Result<T, T::Err> where T::Err: StdError { @@ -158,14 +163,14 @@ impl Args { } /// Skips if there's a first element, but also returns it. - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("42 69", " "); - /// + /// + /// let mut args = Args::new("42 69", vec![" ".to_owned()]); + /// /// assert_eq!(args.skip().unwrap(), "42"); /// assert_eq!(args, ["69"]); /// ``` @@ -176,13 +181,13 @@ impl Args { /// # Examples /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("42 69 88 99", " "); - /// + /// + /// let mut args = Args::new("42 69 88 99", vec![" ".to_owned()]); + /// /// assert_eq!(*args.skip_for(3).unwrap(), ["42".to_string(), "69".to_string(), "88".to_string()]); /// assert_eq!(args, ["99"]); /// ``` - /// + /// /// [`skip`]: #method.skip pub fn skip_for(&mut self, i: u32) -> Option<Vec<String>> { let mut vec = Vec::with_capacity(i as usize); @@ -195,18 +200,18 @@ impl Args { } /// Like [`single`], but takes quotes into account. - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new(r#""42"#, " "); - /// + /// + /// let mut args = Args::new(r#""42"#, vec![" ".to_owned()]); + /// /// assert_eq!(args.single_quoted::<i32>().unwrap(), 42); /// assert!(args.is_empty()); /// ``` - /// + /// /// [`single`]: #method.single pub fn single_quoted<T: FromStr>(&mut self) -> Result<T, T::Err> where T::Err: StdError { @@ -241,30 +246,30 @@ impl Args { } /// Empty outs the internal vector while parsing (if necessary) and returning them - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("42 69", " "); - /// + /// + /// let mut args = Args::new("42 69", vec![" ".to_owned()]); + /// /// assert_eq!(*args.list::<i32>().unwrap(), [42, 69]); - /// ``` + /// ``` pub fn list<T: FromStr>(mut self) -> Result<Vec<T>, T::Err> where T::Err: StdError { Iter::<T>::new(&mut self).collect() } /// Provides an iterator of items: (`T: FromStr`) `Result<T, T::Err>`. - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("3 4", " "); - /// + /// + /// let mut args = Args::new("3 4", vec![" ".to_owned()]); + /// /// assert_eq!(*args.iter::<i32>().map(|num| num.unwrap().pow(2)).collect::<Vec<_>>(), [9, 16]); /// assert!(args.is_empty()); /// ``` @@ -273,27 +278,27 @@ impl Args { } /// This method is just `internal_vector.join(delimiter)` - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("42 69", " "); - /// + /// + /// let mut args = Args::new("42 69", vec![" ".to_owned()]); + /// /// assert_eq!(args.full(), "42 69"); /// ``` pub fn full(&self) -> String { self.delimiter_split.join(&self.delimiter) } /// Returns the first argument that can be converted and removes it from the list. - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let mut args = Args::new("c47 69", " "); - /// + /// + /// let mut args = Args::new("c47 69", vec![" ".to_owned()]); + /// /// assert_eq!(args.find::<i32>().unwrap(), 69); /// assert_eq!(args, ["c47"]); /// ``` @@ -322,14 +327,14 @@ impl Args { } /// Returns the first argument that can be converted and does not remove it from the list. - /// + /// /// # Examples - /// + /// /// ```rust /// use serenity::framework::standard::Args; - /// - /// let args = Args::new("c47 69", " "); - /// + /// + /// let args = Args::new("c47 69", vec![" ".to_owned()]); + /// /// assert_eq!(args.find_n::<i32>().unwrap(), 69); /// assert_eq!(args, ["c47", "69"]); /// ``` @@ -348,29 +353,29 @@ impl Args { } /// A version of `FromStr` that allows for "zero-copy" parsing. -/// +/// /// # Examples -/// +/// /// ```rust,ignore /// use serenity::framework::standard::{Args, FromStrZc}; /// use std::fmt; -/// +/// /// struct NameDiscrim<'a>(&'a str, Option<&'a str>); -/// +/// /// #[derive(Debug)] /// struct Error(&'static str); -/// +/// /// impl std::error::Error for Error { /// fn description(&self) -> &str { self.0 } /// } -/// +/// /// impl fmt::Display for Error { /// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.0) } /// } -/// +/// /// impl<'a> FromStrZc<'a> for NameDiscrim<'a> { /// type Err = Error; -/// +/// /// fn from_str(s: &'a str) -> Result<NameDiscrim<'a>, Error> { /// let mut it = s.split("#"); /// let name = it.next().ok_or(Error("name must be specified"))?; @@ -378,16 +383,16 @@ impl Args { /// Ok(NameDiscrim(name, discrim)) /// } /// } -/// -/// let mut args = Args::new("abc#1234", " "); +/// +/// let mut args = Args::new("abc#1234", vec![" ".to_owned()]); /// let NameDiscrim(name, discrim) = args.single_zc::<NameDiscrim>().unwrap(); -/// +/// /// assert_eq!(name, "abc"); /// assert_eq!(discrim, Some("1234")); -/// ``` +/// ``` pub trait FromStrZc<'a>: Sized { type Err; - + fn from_str(s: &'a str) -> ::std::result::Result<Self, Self::Err>; } @@ -408,14 +413,14 @@ impl ::std::ops::Deref for Args { impl<'a> PartialEq<[&'a str]> for Args { fn eq(&self, other: &[&str]) -> bool { let mut b = true; - + for (s, o) in self.delimiter_split.iter().zip(other.iter()) { if s != o { b = false; break; } } - + b } } @@ -427,7 +432,7 @@ macro_rules! impl_slices { self.delimiter_split.is_empty() } } - + $( impl<'a> PartialEq<[&'a str; $num]> for Args { fn eq(&self, other: &[&str; $num]) -> bool { diff --git a/src/framework/standard/command.rs b/src/framework/standard/command.rs index b53ed90..be7ec2a 100644 --- a/src/framework/standard/command.rs +++ b/src/framework/standard/command.rs @@ -9,12 +9,9 @@ pub type Check = Fn(&mut Context, &Message, &mut Args, &Arc<Command>) -> bool + Send + Sync + 'static; -pub type Exec = Fn(&mut Context, &Message, Args) -> Result<(), Error> + Send + Sync + 'static; -pub type Help = Fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, Args) - -> Result<(), Error> - + Send - + Sync - + 'static; +pub type Exec = fn(&mut Context, &Message, Args) -> Result<(), Error>; +pub type Help = fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, Args) + -> Result<(), Error>; pub type BeforeHook = Fn(&mut Context, &Message, &str) -> bool + Send + Sync + 'static; pub type AfterHook = Fn(&mut Context, &Message, &str, Result<(), Error>) + Send + Sync + 'static; pub(crate) type InternalCommand = Arc<Command>; @@ -40,8 +37,8 @@ impl<D: fmt::Display> From<D> for Error { /// your commands. pub enum CommandType { StringResponse(String), - Basic(Box<Exec>), - WithCommands(Box<Help>), + Basic(Exec), + WithCommands(Help), } pub struct CommandGroup { @@ -88,14 +85,14 @@ pub struct Command { pub guild_only: bool, /// Whether command can only be used by owners or not. pub owners_only: bool, - pub(crate) aliases: Vec<String>, + /// Other names that can be used to call this command instead. + pub aliases: Vec<String>, } impl Command { - pub fn new<F>(f: F) -> Self - where F: Fn(&mut Context, &Message, Args) -> Result<(), Error> + Send + Sync + 'static { + pub fn new(f: fn(&mut Context, &Message, Args) -> Result<(), Error>) -> Self { Command { - exec: CommandType::Basic(Box::new(f)), + exec: CommandType::Basic(f), ..Command::default() } } @@ -106,7 +103,7 @@ impl Default for Command { Command { aliases: Vec::new(), checks: Vec::default(), - exec: CommandType::Basic(Box::new(|_, _, _| Ok(()))), + exec: CommandType::Basic(|_, _, _| Ok(())), desc: None, usage: None, example: None, diff --git a/src/framework/standard/configuration.rs b/src/framework/standard/configuration.rs index c731d31..6cb5767 100644 --- a/src/framework/standard/configuration.rs +++ b/src/framework/standard/configuration.rs @@ -20,11 +20,13 @@ use model::{GuildId, Message, UserId}; /// struct Handler; /// /// impl EventHandler for Handler {} +/// /// use serenity::Client; /// use std::env; /// use serenity::framework::StandardFramework; /// -/// let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); +/// let token = env::var("DISCORD_BOT_TOKEN").unwrap(); +/// let mut client = Client::new(&token, Handler).unwrap(); /// /// client.with_framework(StandardFramework::new() /// .configure(|c| c.on_mention(true).prefix("~"))); @@ -98,7 +100,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::model::GuildId; /// use serenity::framework::StandardFramework; /// @@ -123,7 +125,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::model::UserId; /// use serenity::framework::StandardFramework; /// @@ -161,7 +163,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::framework::StandardFramework; /// /// let disabled = vec!["ping"].into_iter().map(|x| x.to_string()).collect(); @@ -191,7 +193,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::framework::StandardFramework; /// /// client.with_framework(StandardFramework::new() @@ -276,7 +278,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::model::UserId; /// use serenity::framework::StandardFramework; /// @@ -291,7 +293,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::model::UserId; /// use std::collections::HashSet; /// use serenity::framework::StandardFramework; @@ -320,7 +322,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -345,7 +347,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -369,7 +371,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -394,7 +396,7 @@ impl Configuration { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// diff --git a/src/framework/standard/create_command.rs b/src/framework/standard/create_command.rs index 9df9c82..c1558ce 100644 --- a/src/framework/standard/create_command.rs +++ b/src/framework/standard/create_command.rs @@ -47,7 +47,8 @@ impl CreateCommand { /// use std::env; /// use std::sync::Arc; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); + /// let token = env::var("DISCORD_TOKEN").unwrap(); + /// let mut client = Client::new(&token, Handler).unwrap(); /// /// client.with_framework(StandardFramework::new() /// .configure(|c| c.prefix("~")) @@ -106,9 +107,8 @@ impl CreateCommand { /// See [`exec_str`] if you _only_ need to return a string on command use. /// /// [`exec_str`]: #method.exec_str - pub fn exec<F>(mut self, func: F) -> Self - where F: Fn(&mut Context, &Message, Args) -> Result<(), CommandError> + Send + Sync + 'static { - self.0.exec = CommandType::Basic(Box::new(func)); + pub fn exec(mut self, func: fn(&mut Context, &Message, Args) -> Result<(), CommandError>) -> Self { + self.0.exec = CommandType::Basic(func); self } @@ -117,14 +117,11 @@ impl CreateCommand { /// the internal HashMap of commands, used specifically for creating a help /// command. /// - /// You can return `Err(Custom(string))` if there's an error. - pub fn exec_help<F>(mut self, f: F) -> Self - where F: Fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, Args) - -> Result<(), CommandError> - + Send - + Sync - + 'static { - self.0.exec = CommandType::WithCommands(Box::new(f)); + /// You can return `Err(From::from(string))` if there's an error. + pub fn exec_help(mut self, f: + fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, Args) + -> Result<(), CommandError>) -> Self { + self.0.exec = CommandType::WithCommands(f); self } diff --git a/src/framework/standard/create_group.rs b/src/framework/standard/create_group.rs index 911d2ea..39fbcc6 100644 --- a/src/framework/standard/create_group.rs +++ b/src/framework/standard/create_group.rs @@ -70,14 +70,14 @@ impl CreateGroup { } /// Adds a command to group with simplified API. - /// You can return Err(string) if there's an error. - pub fn on<F>(mut self, command_name: &str, f: F) -> Self - where F: Fn(&mut Context, &Message, Args) -> Result<(), CommandError> + Send + Sync + 'static { + /// You can return Err(From::from(string)) if there's an error. + pub fn on(mut self, name: &str, + f: fn(&mut Context, &Message, Args) -> Result<(), CommandError>) -> Self { let cmd = Arc::new(Command::new(f)); self.0 .commands - .insert(command_name.to_string(), CommandOrAlias::Command(cmd)); + .insert(name.to_string(), CommandOrAlias::Command(cmd)); self } diff --git a/src/framework/standard/help_commands.rs b/src/framework/standard/help_commands.rs index 6b85239..8cbcb21 100644 --- a/src/framework/standard/help_commands.rs +++ b/src/framework/standard/help_commands.rs @@ -55,7 +55,7 @@ fn remove_aliases(cmds: &HashMap<String, CommandOrAlias>) -> HashMap<&String, &I /// and given the required permissions. pub fn has_all_requirements(cmd: &Command, msg: &Message) -> bool { if let Some(guild) = msg.guild() { - let guild = guild.read().unwrap(); + let guild = guild.read(); if let Some(member) = guild.members.get(&msg.author.id) { @@ -69,7 +69,7 @@ pub fn has_all_requirements(cmd: &Command, msg: &Message) -> bool { } } } - false + !cmd.guild_only } /// Posts an embed showing each individual command group and its commands. @@ -83,7 +83,7 @@ pub fn has_all_requirements(cmd: &Command, msg: &Message) -> bool { /// # struct Handler; /// # /// # impl EventHandler for Handler {} -/// # let mut client = Client::new("token", Handler); +/// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::standard::{StandardFramework, help_commands}; /// @@ -155,26 +155,25 @@ pub fn with_embeds(_: &mut Context, } if let Some(ref usage) = command.usage { - embed = embed.field(|f| { - f.name("Usage") - .value(&format!("`{} {}`", command_name, usage)) - }); + let value = format!("`{} {}`", command_name, usage); + + embed = embed.field("Usage", value, true); } if let Some(ref example) = command.example { - embed = embed.field(|f| { - f.name("Sample usage") - .value(&format!("`{} {}`", command_name, example)) - }); + let value = format!("`{} {}`", command_name, example); + + embed = embed.field("Sample usage", value, true); } if group_name != "Ungrouped" { - embed = embed.field(|f| f.name("Group").value(&group_name)); + embed = embed.field("Group", group_name, true); } if !command.aliases.is_empty() { let aliases = command.aliases.join(", "); - embed = embed.field(|f| f.name("Aliases").value(&aliases)); + + embed = embed.field("Aliases", aliases, true); } let available = if command.dm_only { @@ -185,7 +184,7 @@ pub fn with_embeds(_: &mut Context, "In DM and guilds" }; - embed = embed.field(|f| f.name("Available").value(available)); + embed = embed.field("Available", available, true); embed }) @@ -235,7 +234,7 @@ pub fn with_embeds(_: &mut Context, } if has_commands { - e = e.field(|f| f.name(group_name).value(&desc)); + e = e.field(&group_name[..], &desc[..], true); } } e @@ -256,7 +255,7 @@ pub fn with_embeds(_: &mut Context, /// # struct Handler; /// # /// # impl EventHandler for Handler {} -/// # let mut client = Client::new("token", Handler); +/// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::standard::{StandardFramework, help_commands}; /// diff --git a/src/framework/standard/mod.rs b/src/framework/standard/mod.rs index a806817..fdab3a8 100644 --- a/src/framework/standard/mod.rs +++ b/src/framework/standard/mod.rs @@ -218,7 +218,8 @@ impl StandardFramework { /// use serenity::framework::StandardFramework; /// use std::env; /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); + /// let token = env::var("DISCORD_TOKEN").unwrap(); + /// let mut client = Client::new(&token, Handler).unwrap(); /// client.with_framework(StandardFramework::new() /// .configure(|c| c /// .depth(3) @@ -251,7 +252,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -287,7 +288,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -396,7 +397,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -425,7 +426,7 @@ impl StandardFramework { #[cfg(feature = "cache")] fn is_blocked_guild(&self, message: &Message) -> bool { - if let Some(Channel::Guild(channel)) = CACHE.read().unwrap().channel(message.channel_id) { + if let Some(Channel::Guild(channel)) = CACHE.read().channel(message.channel_id) { let guild_id = channel.with(|g| g.guild_id); if self.configuration.blocked_guilds.contains(&guild_id) { return true; @@ -536,7 +537,7 @@ impl StandardFramework { } else { if !command.allowed_roles.is_empty() { if let Some(guild) = message.guild() { - let guild = guild.read().unwrap(); + let guild = guild.read(); if let Some(member) = guild.members.get(&message.author.id) { if let Ok(permissions) = member.permissions() { @@ -592,7 +593,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -603,20 +604,18 @@ impl StandardFramework { /// }); /// # } /// ``` - pub fn on<F, S>(mut self, command_name: S, f: F) -> Self - where F: Fn(&mut Context, &Message, Args) -> Result<(), CommandError> + Send + Sync + 'static, - S: Into<String> { + pub fn on(mut self, name: &str, + f: fn(&mut Context, &Message, Args) + -> Result<(), CommandError>) -> Self { { let ungrouped = self.groups .entry("Ungrouped".to_string()) .or_insert_with(|| Arc::new(CommandGroup::default())); if let Some(ref mut group) = Arc::get_mut(ungrouped) { - let name = command_name.into(); - group .commands - .insert(name, CommandOrAlias::Command(Arc::new(Command::new(f)))); + .insert(name.to_string(), CommandOrAlias::Command(Arc::new(Command::new(f)))); } } @@ -686,7 +685,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -719,7 +718,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// use serenity::framework::standard::DispatchError::{NotEnoughArguments, /// TooManyArguments}; /// use serenity::framework::StandardFramework; @@ -760,7 +759,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -778,7 +777,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -816,7 +815,7 @@ impl StandardFramework { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::framework::StandardFramework; /// @@ -910,13 +909,7 @@ impl Framework for StandardFramework { let mut content = message.content[position..].trim(); content = content[command_length..].trim(); - let delimiter = self.configuration - .delimiters - .iter() - .find(|&d| content.contains(d)) - .map_or(" ", |s| s.as_str()); - - Args::new(content, delimiter) + Args::new(&content, self.configuration.delimiters.clone()) }; if let Some(error) = self.should_fail( diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ed54bca..c2acaac 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -51,9 +51,18 @@ mod error; mod shard; +mod ws_client_ext; pub use self::error::Error as GatewayError; pub use self::shard::Shard; +pub use self::ws_client_ext::WebSocketGatewayClientExt; + +use model::{Game, OnlineStatus}; +use websocket::sync::client::Client; +use websocket::sync::stream::{TcpStream, TlsStream}; + +pub type CurrentPresence = (Option<Game>, OnlineStatus); +pub type WsClient = Client<TlsStream<TcpStream>>; /// Indicates the current connection stage of a [`Shard`]. /// @@ -136,3 +145,11 @@ impl ConnectionStage { } } } + +pub enum ShardAction { + Autoreconnect, + Heartbeat, + Identify, + Reconnect, + Resume, +} diff --git a/src/gateway/shard.rs b/src/gateway/shard.rs index bd60b57..340e2e8 100644 --- a/src/gateway/shard.rs +++ b/src/gateway/shard.rs @@ -1,38 +1,31 @@ -use chrono::Utc; -use serde_json::Value; -use std::env::consts; -use std::io::Write; -use std::net::Shutdown; -use std::sync::{Arc, Mutex}; +use parking_lot::Mutex; +use std::sync::Arc; use std::time::{Duration as StdDuration, Instant}; -use std::thread; -use super::{ConnectionStage, GatewayError}; +use super::{ + ConnectionStage, + CurrentPresence, + ShardAction, + GatewayError, + WsClient, + WebSocketGatewayClientExt, +}; use websocket::client::Url; -use websocket::message::{CloseData, OwnedMessage}; use websocket::stream::sync::AsTcpStream; -use websocket::sync::client::{Client, ClientBuilder}; -use websocket::sync::stream::{TcpStream, TlsStream}; +use websocket::sync::client::ClientBuilder; use websocket::WebSocketError; -use constants::{self, close_codes, OpCode}; +use constants::{self, close_codes}; use internal::prelude::*; -use internal::ws_impl::SenderExt; use model::event::{Event, GatewayEvent}; use model::{Game, GuildId, OnlineStatus}; #[cfg(feature = "voice")] +use serde_json::Value; +#[cfg(feature = "voice")] use std::sync::mpsc::{self, Receiver as MpscReceiver}; -#[cfg(feature = "cache")] -use client::CACHE; #[cfg(feature = "voice")] use voice::Manager as VoiceManager; #[cfg(feature = "voice")] use http; -#[cfg(feature = "cache")] -use utils; - -pub type WsClient = Client<TlsStream<TcpStream>>; - -type CurrentPresence = (Option<Game>, OnlineStatus, bool); /// A Shard is a higher-level handler for a websocket connection to Discord's /// gateway. The shard allows for sending and receiving messages over the @@ -92,7 +85,7 @@ pub struct Shard { /// Whether the shard has permanently shutdown. shutdown: bool, stage: ConnectionStage, - token: Arc<Mutex<String>>, + pub token: Arc<Mutex<String>>, ws_url: Arc<Mutex<String>>, /// The voice connections that this Shard is responsible for. The Shard will /// update the voice connections' states. @@ -111,27 +104,44 @@ impl Shard { /// Instantiating a new Shard manually for a bot with no shards, and /// then listening for events: /// - /// ```rust,ignore + /// ```rust,no_run + /// extern crate parking_lot; + /// extern crate serenity; + /// # + /// # use std::error::Error; + /// # + /// # fn try_main() -> Result<(), Box<Error>> { + /// # + /// use parking_lot::Mutex; /// use serenity::gateway::Shard; /// use serenity::http; /// use std::env; + /// use std::sync::Arc; /// - /// let token = env::var("DISCORD_TOKEN").expect("Token in environment"); + /// let token = Arc::new(Mutex::new(env::var("DISCORD_BOT_TOKEN")?)); /// // retrieve the gateway response, which contains the URL to connect to - /// let gateway = http::get_gateway().expect("Valid gateway response").url; - /// let shard = Shard::new(&gateway, &token, None) - /// .expect("Working shard"); + /// let gateway = Arc::new(Mutex::new(http::get_gateway()?.url)); + /// let shard = Shard::new(gateway, token, [0, 1])?; /// /// // at this point, you can create a `loop`, and receive events and match /// // their variants + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } /// ``` - pub fn new(ws_url: Arc<Mutex<String>>, - token: Arc<Mutex<String>>, - shard_info: [u64; 2]) - -> Result<Shard> { - let client = connecting(&*ws_url.lock().unwrap()); + pub fn new( + ws_url: Arc<Mutex<String>>, + token: Arc<Mutex<String>>, + shard_info: [u64; 2], + ) -> Result<Shard> { + let mut client = connect(&*ws_url.lock())?; - let current_presence = (None, OnlineStatus::Online, false); + let _ = set_client_timeout(&mut client); + + let current_presence = (None, OnlineStatus::Online); let heartbeat_instants = (None, None); let heartbeat_interval = None; let last_heartbeat_acknowledged = true; @@ -146,7 +156,12 @@ impl Shard { let user = http::get_current_user()?; Shard { +<<<<<<< HEAD + manager: VoiceManager::new(tx, user.id), + manager_rx: rx, +======= shutdown: false, +>>>>>>> v0.4.3 client, current_presence, heartbeat_instants, @@ -158,8 +173,6 @@ impl Shard { shard_info, session_id, ws_url, - manager: VoiceManager::new(tx, user.id), - manager_rx: rx, } } else { Shard { @@ -180,6 +193,13 @@ impl Shard { }) } +<<<<<<< HEAD + /// Retrieves the current presence of the shard. + #[inline] + pub fn current_presence(&self) -> &CurrentPresence { + &self.current_presence + } +======= /// Whether the shard has permanently shutdown. /// /// This should normally happen due to manual calling of [`shutdown`] or @@ -215,25 +235,90 @@ impl Shard { /// assert_eq!(shard.shard_info(), [1, 2]); /// ``` pub fn shard_info(&self) -> [u64; 2] { self.shard_info } +>>>>>>> v0.4.3 - /// Sets whether the current user is afk. This helps Discord determine where - /// to send notifications. + /// Retrieves the heartbeat instants of the shard. /// - /// Other presence settings are maintained. - pub fn set_afk(&mut self, afk: bool) { - self.current_presence.2 = afk; + /// This is the time of when a heartbeat was sent and when an + /// acknowledgement was last received. + #[inline] + pub fn heartbeat_instants(&self) -> &(Option<Instant>, Option<Instant>) { + &self.heartbeat_instants + } - self.update_presence(); + /// Retrieves the value of when the last heartbeat was sent. + #[inline] + pub fn last_heartbeat_sent(&self) -> Option<&Instant> { + self.heartbeat_instants.0.as_ref() } - /// Sets the user's current game, if any. + /// Retrieves the value of when the last heartbeat ack was received. + #[inline] + pub fn last_heartbeat_ack(&self) -> Option<&Instant> { + self.heartbeat_instants.1.as_ref() + } + + /// Sends a heartbeat to the gateway with the current sequence. /// - /// Other presence settings are maintained. + /// This sets the last heartbeat time to now, and + /// `last_heartbeat_acknowledged` to `false`. /// - /// # Examples + /// # Errors /// - /// Setting the current game to playing `"Heroes of the Storm"`: +<<<<<<< HEAD + /// Returns [`GatewayError::HeartbeatFailed`] if there was an error sending + /// a heartbeat. /// + /// [`GatewayError::HeartbeatFailed`]: enum.GatewayError.html#variant.HeartbeatFailed + pub fn heartbeat(&mut self) -> Result<()> { + match self.client.send_heartbeat(&self.shard_info, Some(self.seq)) { + Ok(()) => { + self.heartbeat_instants.0 = Some(Instant::now()); + self.last_heartbeat_acknowledged = false; + + Ok(()) + }, + Err(why) => { + match why { + Error::WebSocket(WebSocketError::IoError(err)) => if err.raw_os_error() != Some(32) { + debug!("[Shard {:?}] Err heartbeating: {:?}", + self.shard_info, + err); + }, + other => { + warn!("[Shard {:?}] Other err w/ keepalive: {:?}", + self.shard_info, + other); + }, + } + + Err(Error::Gateway(GatewayError::HeartbeatFailed)) + } + } + } + + #[inline] + pub fn heartbeat_interval(&self) -> Option<&u64> { + self.heartbeat_interval.as_ref() + } + + #[inline] + pub fn last_heartbeat_acknowledged(&self) -> bool { + self.last_heartbeat_acknowledged + } + + #[inline] + pub fn seq(&self) -> u64 { + self.seq + } + + #[inline] + pub fn session_id(&self) -> Option<&String> { + self.session_id.as_ref() + } + + #[inline] +======= /// ```rust,no_run /// # #[cfg(feature = "model")] /// # fn main() { @@ -252,68 +337,66 @@ impl Shard { /// # #[cfg(not(feature = "model"))] /// # fn main() { } /// ``` +>>>>>>> v0.4.3 pub fn set_game(&mut self, game: Option<Game>) { self.current_presence.0 = game; + } - self.update_presence(); + #[inline] + pub fn set_presence(&mut self, status: OnlineStatus, game: Option<Game>) { + self.set_game(game); + self.set_status(status); } - /// Sets the user's current online status. - /// - /// Note that [`Offline`] is not a valid online status, so it is - /// automatically converted to [`Invisible`]. - /// - /// Other presence settings are maintained. - /// - /// # Examples - /// - /// Setting the current online status for the shard to [`DoNotDisturb`]. - /// - /// ```rust,no_run - /// # use serenity::client::gateway::Shard; - /// # use std::sync::{Arc, Mutex}; - /// # - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); - /// # - /// use serenity::model::OnlineStatus; - /// - /// shard.set_status(OnlineStatus::DoNotDisturb); - /// ``` - /// - /// [`DoNotDisturb`]: ../../model/enum.OnlineStatus.html#variant.DoNotDisturb - /// [`Invisible`]: ../../model/enum.OnlineStatus.html#variant.Invisible - /// [`Offline`]: ../../model/enum.OnlineStatus.html#variant.Offline - pub fn set_status(&mut self, online_status: OnlineStatus) { - self.current_presence.1 = match online_status { - OnlineStatus::Offline => OnlineStatus::Invisible, - other => other, - }; + #[inline] + pub fn set_status(&mut self, mut status: OnlineStatus) { + if status == OnlineStatus::Offline { + status = OnlineStatus::Invisible; + } - self.update_presence(); + self.current_presence.1 = status; } - /// Sets the user's full presence information. + /// Retrieves a copy of the current shard information. + /// + /// The first element is the _current_ shard - 0-indexed - while the second + /// element is the _total number_ of shards -- 1-indexed. /// - /// Consider using the individual setters if you only need to modify one of - /// these. + /// For example, if using 3 shards in total, and if this is shard 1, then it + /// can be read as "the second of three shards". /// /// # Examples /// - /// Set the current user as playing `"Heroes of the Storm"`, being online, - /// and not being afk: + /// Retrieving the shard info for the second shard, out of two shards total: /// /// ```rust,no_run +<<<<<<< HEAD + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; +======= /// # #[cfg(feature = "model")] /// # fn main() { +>>>>>>> v0.4.3 /// # use serenity::client::gateway::Shard; - /// # use std::sync::{Arc, Mutex}; + /// # use std::error::Error; + /// # use std::sync::Arc; /// # - /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); /// # - /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); + /// # let shard = Shard::new(mutex.clone(), mutex, [1, 2]).unwrap(); /// # +<<<<<<< HEAD + /// assert_eq!(shard.shard_info(), [1, 2]); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } +======= /// use serenity::model::{Game, OnlineStatus}; /// /// shard.set_presence(Some(Game::playing("Heroes of the Storm")), OnlineStatus::Online, false); @@ -321,16 +404,9 @@ impl Shard { /// # /// # #[cfg(not(feature = "model"))] /// # fn main() { } +>>>>>>> v0.4.3 /// ``` - pub fn set_presence(&mut self, game: Option<Game>, mut status: OnlineStatus, afk: bool) { - if status == OnlineStatus::Offline { - status = OnlineStatus::Invisible; - } - - self.current_presence = (game, status, afk); - - self.update_presence(); - } + pub fn shard_info(&self) -> [u64; 2] { self.shard_info } /// Returns the current connection stage of the shard. pub fn stage(&self) -> ConnectionStage { @@ -362,21 +438,24 @@ impl Shard { /// Returns a `GatewayError::OverloadedShard` if the shard would have too /// many guilds assigned to it. #[allow(cyclomatic_complexity)] - pub(crate) fn handle_event(&mut self, event: Result<GatewayEvent>) -> Result<Option<Event>> { - match event { - Ok(GatewayEvent::Dispatch(seq, event)) => { + pub(crate) fn handle_event(&mut self, event: &Result<GatewayEvent>) + -> Result<Option<ShardAction>> { + match *event { + Ok(GatewayEvent::Dispatch(seq, ref event)) => { if seq > self.seq + 1 { warn!("[Shard {:?}] Heartbeat off; them: {}, us: {}", self.shard_info, seq, self.seq); } - match event { + match *event { Event::Ready(ref ready) => { debug!("[Shard {:?}] Received Ready", self.shard_info); self.session_id = Some(ready.ready.session_id.clone()); self.stage = ConnectionStage::Connected; + /* set_client_timeout(&mut self.client)?; + */ }, Event::Resumed(_) => { info!("[Shard {:?}] Resumed", self.shard_info); @@ -396,7 +475,7 @@ impl Shard { self.seq = seq; - Ok(Some(event)) + Ok(None) }, Ok(GatewayEvent::Heartbeat(s)) => { info!("[Shard {:?}] Received shard heartbeat", self.shard_info); @@ -413,24 +492,18 @@ impl Shard { if self.stage == ConnectionStage::Handshake { self.stage = ConnectionStage::Identifying; - self.identify()?; + return Ok(Some(ShardAction::Identify)); } else { warn!( "[Shard {:?}] Heartbeat during non-Handshake; auto-reconnecting", self.shard_info ); - return self.autoreconnect().and(Ok(None)); + return Ok(Some(ShardAction::Autoreconnect)); } } - let map = json!({ - "d": Value::Null, - "op": OpCode::Heartbeat.num(), - }); - self.client.send_json(&map)?; - - Ok(None) + Ok(Some(ShardAction::Heartbeat)) }, Ok(GatewayEvent::HeartbeatAck) => { self.heartbeat_instants.1 = Some(Instant::now()); @@ -453,30 +526,30 @@ impl Shard { self.heartbeat_interval = Some(interval); } - if self.stage == ConnectionStage::Handshake { - self.identify().and(Ok(None)) + Ok(Some(if self.stage == ConnectionStage::Handshake { + ShardAction::Identify } else { debug!("[Shard {:?}] Received late Hello; autoreconnecting", self.shard_info); - self.autoreconnect().and(Ok(None)) - } + ShardAction::Autoreconnect + })) }, - Ok(GatewayEvent::InvalidateSession) => { + Ok(GatewayEvent::InvalidateSession(resumable)) => { info!( - "[Shard {:?}] Received session invalidation; re-identifying", - self.shard_info + "[Shard {:?}] Received session invalidation", + self.shard_info, ); - self.seq = 0; - self.session_id = None; - - self.identify().and(Ok(None)) + Ok(Some(if resumable { + ShardAction::Resume + } else { + ShardAction::Reconnect + })) }, - Ok(GatewayEvent::Reconnect) => self.reconnect().and(Ok(None)), - Err(Error::Gateway(GatewayError::Closed(data))) => { + Ok(GatewayEvent::Reconnect) => Ok(Some(ShardAction::Reconnect)), + Err(Error::Gateway(GatewayError::Closed(ref data))) => { let num = data.as_ref().map(|d| d.status_code); - let reason = data.map(|d| d.reason); let clean = num == Some(1000); match num { @@ -536,7 +609,7 @@ impl Shard { "[Shard {:?}] Unknown unclean close {}: {:?}", self.shard_info, other, - reason + data.as_ref().map(|d| &d.reason), ); }, _ => {}, @@ -547,14 +620,14 @@ impl Shard { self.session_id.is_some() }).unwrap_or(true); - if resume { - self.resume().or_else(|_| self.reconnect()).and(Ok(None)) + Ok(Some(if resume { + ShardAction::Resume } else { - self.reconnect().and(Ok(None)) - } + ShardAction::Reconnect + })) }, - Err(Error::WebSocket(why)) => { - if let WebSocketError::NoDataAvailable = why { + Err(Error::WebSocket(ref why)) => { + if let WebSocketError::NoDataAvailable = *why { if self.heartbeat_instants.1.is_none() { return Ok(None); } @@ -566,13 +639,64 @@ impl Shard { info!("[Shard {:?}] Will attempt to auto-reconnect", self.shard_info); - self.autoreconnect().and(Ok(None)) + Ok(Some(ShardAction::Autoreconnect)) }, - Err(error) => Err(error), + _ => Ok(None), + } + } + + pub fn check_heartbeat(&mut self) -> Result<()> { + let wait = { + let heartbeat_interval = match self.heartbeat_interval { + Some(heartbeat_interval) => heartbeat_interval, + None => return Ok(()), + }; + + StdDuration::from_secs(heartbeat_interval / 1000) + }; + + // If a duration of time less than the heartbeat_interval has passed, + // then don't perform a keepalive or attempt to reconnect. + if let Some(last_sent) = self.heartbeat_instants.0 { + if last_sent.elapsed() <= wait { + return Ok(()); + } + } + + // If the last heartbeat didn't receive an acknowledgement, then + // auto-reconnect. + if !self.last_heartbeat_acknowledged { + debug!( + "[Shard {:?}] Last heartbeat not acknowledged; re-connecting", + self.shard_info, + ); + + return self.reconnect().map_err(|why| { + warn!( + "[Shard {:?}] Err auto-reconnecting from heartbeat check: {:?}", + self.shard_info, + why, + ); + + why + }); + } + + // Otherwise, we're good to heartbeat. + if let Err(why) = self.heartbeat() { + warn!("[Shard {:?}] Err heartbeating: {:?}", self.shard_info, why); + + self.reconnect() + } else { + trace!("[Shard {:?}] Heartbeated", self.shard_info); + + Ok(()) } } /// Calculates the heartbeat latency between the shard and the gateway. +<<<<<<< HEAD +======= /// /// # Examples /// @@ -608,6 +732,7 @@ impl Shard { /// /// [`Client`]: ../struct.Client.html /// [`EventHandler::on_message`]: ../event_handler/trait.EventHandler.html#method.on_message +>>>>>>> v0.4.3 // Shamelessly stolen from brayzure's commit in eris: // <https://github.com/abalabahaha/eris/commit/0ce296ae9a542bcec0edf1c999ee2d9986bed5a6> pub fn latency(&self) -> Option<StdDuration> { @@ -618,41 +743,79 @@ impl Shard { } } - /// Shuts down the receiver by attempting to cleanly close the - /// connection. - pub fn shutdown_clean(&mut self) -> Result<()> { - { - let data = CloseData { - status_code: 1000, - reason: String::new(), - }; - - let message = OwnedMessage::Close(Some(data)); - - self.client.send_message(&message)?; + #[cfg(feature = "voice")] + fn voice_dispatch(&mut self, event: &Event) { + if let Event::VoiceStateUpdate(ref update) = *event { + if let Some(guild_id) = update.guild_id { + if let Some(handler) = self.manager.get(guild_id) { + handler.update_state(&update.voice_state); + } + } } - let mut stream = self.client.stream_ref().as_tcp(); + if let Event::VoiceServerUpdate(ref update) = *event { + if let Some(guild_id) = update.guild_id { + if let Some(handler) = self.manager.get(guild_id) { + handler.update_server(&update.endpoint, &update.token); + } + } + } + } - stream.flush()?; - stream.shutdown(Shutdown::Both)?; + #[cfg(feature = "voice")] + pub(crate) fn cycle_voice_recv(&mut self) -> Vec<Value> { + let mut messages = vec![]; +<<<<<<< HEAD + while let Ok(v) = self.manager_rx.try_recv() { + messages.push(v); + } +======= self.shutdown = true; debug!("[Shard {:?}] Cleanly shutdown shard", self.shard_info); +>>>>>>> v0.4.3 - Ok(()) + messages } - /// Uncleanly shuts down the receiver by not sending a close code. - pub fn shutdown(&mut self) -> Result<()> { - let mut stream = self.client.stream_ref().as_tcp(); + /// Performs a deterministic reconnect. + /// + /// The type of reconnect is deterministic on whether a [`session_id`]. + /// + /// If the `session_id` still exists, then a RESUME is sent. If not, then + /// an IDENTIFY is sent. + /// + /// Note that, if the shard is already in a stage of + /// [`ConnectionStage::Connecting`], then no action will be performed. + /// + /// [`ConnectionStage::Connecting`]: ../../../gateway/enum.ConnectionStage.html#variant.Connecting + /// [`session_id`]: ../../../gateway/struct.Shard.html#method.session_id + pub fn autoreconnect(&mut self) -> Result<()> { + if self.stage == ConnectionStage::Connecting { + return Ok(()); + } + + if self.session_id().is_some() { + debug!( + "[Shard {:?}] Autoreconnector choosing to resume", + self.shard_info, + ); - stream.flush()?; - stream.shutdown(Shutdown::Both)?; + self.resume() + } else { + debug!( + "[Shard {:?}] Autoreconnector choosing to reconnect", + self.shard_info, + ); +<<<<<<< HEAD + self.reconnect() + } +======= self.shutdown = true; Ok(()) +>>>>>>> v0.4.3 } /// Requests that one or multiple [`Guild`]s be chunked. @@ -674,304 +837,100 @@ impl Shard { /// specifying a query parameter: /// /// ```rust,no_run + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; /// # use serenity::client::gateway::Shard; - /// # use std::sync::{Arc, Mutex}; + /// # use std::error::Error; + /// # use std::sync::Arc; /// # - /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); /// # - /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1])?; /// # /// use serenity::model::GuildId; /// /// let guild_ids = vec![GuildId(81384788765712384)]; /// - /// shard.chunk_guilds(&guild_ids, Some(2000), None); + /// shard.chunk_guilds(guild_ids, Some(2000), None); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } /// ``` /// /// Chunk a single guild by Id, limiting to 20 members, and specifying a /// query parameter of `"do"`: /// /// ```rust,no_run + /// # extern crate parking_lot; + /// # extern crate serenity; + /// # + /// # use parking_lot::Mutex; /// # use serenity::client::gateway::Shard; - /// # use std::sync::{Arc, Mutex}; + /// # use std::error::Error; + /// # use std::sync::Arc; /// # - /// # let mutex = Arc::new(Mutex::new("".to_string())); + /// # fn try_main() -> Result<(), Box<Error>> { + /// # let mutex = Arc::new(Mutex::new("".to_string())); /// # - /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); + /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1])?; /// # /// use serenity::model::GuildId; /// /// let guild_ids = vec![GuildId(81384788765712384)]; /// - /// shard.chunk_guilds(&guild_ids, Some(20), Some("do")); + /// shard.chunk_guilds(guild_ids, Some(20), Some("do")); + /// # Ok(()) + /// # } + /// # + /// # fn main() { + /// # try_main().unwrap(); + /// # } /// ``` /// /// [`Event::GuildMembersChunk`]: /// ../../model/event/enum.Event.html#variant.GuildMembersChunk /// [`Guild`]: ../../model/struct.Guild.html /// [`Member`]: ../../model/struct.Member.html - pub fn chunk_guilds<T: AsRef<GuildId>, It>(&mut self, guild_ids: It, limit: Option<u16>, query: Option<&str>) - where It: IntoIterator<Item=T> { + pub fn chunk_guilds<It>( + &mut self, + guild_ids: It, + limit: Option<u16>, + query: Option<&str>, + ) -> Result<()> where It: IntoIterator<Item=GuildId> { debug!("[Shard {:?}] Requesting member chunks", self.shard_info); - let msg = json!({ - "op": OpCode::GetGuildMembers.num(), - "d": { - "guild_id": guild_ids.into_iter().map(|x| x.as_ref().0).collect::<Vec<u64>>(), - "limit": limit.unwrap_or(0), - "query": query.unwrap_or(""), - }, - }); - - let _ = self.client.send_json(&msg); + self.client.send_chunk_guilds( + guild_ids, + &self.shard_info, + limit, + query, + ) } - /// Calculates the number of guilds that the shard is responsible for. - /// - /// If sharding is not being used (i.e. 1 shard), then the total number of - /// [`Guild`] in the [`Cache`] will be used. - /// - /// **Note**: Requires the `cache` feature be enabled. - /// - /// # Examples - /// - /// Retrieve the number of guilds a shard is responsible for: - /// - /// ```rust,no_run - /// # use serenity::client::gateway::Shard; - /// # use std::sync::{Arc, Mutex}; - /// # - /// # let mutex = Arc::new(Mutex::new("will anyone read this".to_string())); - /// # - /// # let shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); - /// # - /// let info = shard.shard_info(); - /// let guilds = shard.guilds_handled(); - /// - /// println!("Shard {:?} is responsible for {} guilds", info, guilds); - /// ``` - /// - /// [`Cache`]: ../ext/cache/struct.Cache.html - /// [`Guild`]: ../model/struct.Guild.html - #[cfg(feature = "cache")] - pub fn guilds_handled(&self) -> u16 { - let cache = CACHE.read().unwrap(); - - let (shard_id, shard_count) = (self.shard_info[0], self.shard_info[1]); - - cache - .guilds - .keys() - .filter(|guild_id| { - utils::shard_id(guild_id.0, shard_count) == shard_id - }) - .count() as u16 - } - - #[cfg(feature = "voice")] - fn voice_dispatch(&mut self, event: &Event) { - if let Event::VoiceStateUpdate(ref update) = *event { - if let Some(guild_id) = update.guild_id { - if let Some(handler) = self.manager.get(guild_id) { - handler.update_state(&update.voice_state); - } - } - } - - if let Event::VoiceServerUpdate(ref update) = *event { - if let Some(guild_id) = update.guild_id { - if let Some(handler) = self.manager.get(guild_id) { - handler.update_server(&update.endpoint, &update.token); - } - } - } - } - - #[cfg(feature = "voice")] - pub(crate) fn cycle_voice_recv(&mut self) { - if let Ok(v) = self.manager_rx.try_recv() { - if let Err(why) = self.client.send_json(&v) { - warn!("[Shard {:?}] Err sending voice msg: {:?}", - self.shard_info, - why); - } - } - } - - pub(crate) fn heartbeat(&mut self) -> Result<()> { - let map = json!({ - "d": self.seq, - "op": OpCode::Heartbeat.num(), - }); - - trace!("[Shard {:?}] Sending heartbeat d: {}", - self.shard_info, - self.seq); - - match self.client.send_json(&map) { - Ok(_) => { - self.heartbeat_instants.0 = Some(Instant::now()); - self.last_heartbeat_acknowledged = false; - - trace!("[Shard {:?}] Successfully heartbeated", - self.shard_info); - - Ok(()) - }, - Err(why) => { - match why { - Error::WebSocket(WebSocketError::IoError(err)) => if err.raw_os_error() != Some(32) { - debug!("[Shard {:?}] Err heartbeating: {:?}", - self.shard_info, - err); - }, - other => { - warn!("[Shard {:?}] Other err w/ keepalive: {:?}", - self.shard_info, - other); - }, - } - - Err(Error::Gateway(GatewayError::HeartbeatFailed)) - }, - } - } - - pub(crate) fn check_heartbeat(&mut self) -> Result<()> { - let heartbeat_interval = match self.heartbeat_interval { - Some(heartbeat_interval) => heartbeat_interval, - None => return Ok(()), - }; - - let wait = StdDuration::from_secs(heartbeat_interval / 1000); - - // If a duration of time less than the heartbeat_interval has passed, - // then don't perform a keepalive or attempt to reconnect. - if let Some(last_sent) = self.heartbeat_instants.0 { - if last_sent.elapsed() <= wait { - return Ok(()); - } - } - - // If the last heartbeat didn't receive an acknowledgement, then - // auto-reconnect. - if !self.last_heartbeat_acknowledged { - debug!( - "[Shard {:?}] Last heartbeat not acknowledged; re-connecting", - self.shard_info, - ); - - return self.reconnect().map_err(|why| { - warn!( - "[Shard {:?}] Err auto-reconnecting from heartbeat check: {:?}", - self.shard_info, - why, - ); - - why - }); - } - - // Otherwise, we're good to heartbeat. - trace!("[Shard {:?}] Heartbeating", self.shard_info); - - if let Err(why) = self.heartbeat() { - warn!("[Shard {:?}] Err heartbeating: {:?}", self.shard_info, why); - - self.reconnect() - } else { - trace!("[Shard {:?}] Heartbeated", self.shard_info); - self.heartbeat_instants.0 = Some(Instant::now()); - - Ok(()) - } - } - - pub(crate) fn autoreconnect(&mut self) -> Result<()> { - if self.stage == ConnectionStage::Connecting { - return Ok(()); - } - - if self.session_id.is_some() { - debug!("[Shard {:?}] Autoreconnector choosing to resume", - self.shard_info); - - self.resume() - } else { - debug!("[Shard {:?}] Autoreconnector choosing to reconnect", - self.shard_info); - - self.reconnect() - } - } - - /// Retrieves the `heartbeat_interval`. - #[inline] - pub(crate) fn heartbeat_interval(&self) -> Option<u64> { - self.heartbeat_interval - } - - /// Retrieves the value of when the last heartbeat ack was received. - #[inline] - pub(crate) fn last_heartbeat_ack(&self) -> Option<Instant> { - self.heartbeat_instants.1 - } - - fn reconnect(&mut self) -> Result<()> { - info!("[Shard {:?}] Attempting to reconnect", self.shard_info); - self.reset(); - - self.initialize() - } - - // Attempts to send a RESUME message. - // - // # Examples + // Sets the shard as going into identifying stage, which sets: // - // Returns a `GatewayError::NoSessionId` is there is no `session_id`, - // indicating that the shard should instead [`reconnect`]. - // - // [`reconnect`]: #method.reconnect - fn resume(&mut self) -> Result<()> { - debug!("Shard {:?}] Attempting to resume", self.shard_info); - - self.initialize()?; - self.stage = ConnectionStage::Resuming; - - self.send_resume().or_else(|why| { - warn!("[Shard {:?}] Err sending resume: {:?}", - self.shard_info, - why); + // - the time that the last heartbeat sent as being now + // - the `stage` to `Identifying` + pub fn identify(&mut self) -> Result<()> { + self.client.send_identify(&self.shard_info, &self.token.lock())?; - self.reconnect() - }) - } - - fn send_resume(&mut self) -> Result<()> { - let session_id = match self.session_id.clone() { - Some(session_id) => session_id, - None => return Err(Error::Gateway(GatewayError::NoSessionId)), - }; - - debug!("[Shard {:?}] Sending resume; seq: {}", - self.shard_info, - self.seq); + self.heartbeat_instants.0 = Some(Instant::now()); + self.stage = ConnectionStage::Identifying; - self.client.send_json(&json!({ - "op": OpCode::Resume.num(), - "d": { - "session_id": session_id, - "seq": self.seq, - "token": &*self.token.lock().unwrap(), - }, - })) + Ok(()) } /// Initializes a new WebSocket client. /// /// This will set the stage of the shard before and after instantiation of /// the client. - fn initialize(&mut self) -> Result<()> { + pub fn initialize(&mut self) -> Result<WsClient> { debug!("[Shard {:?}] Initializing", self.shard_info); // We need to do two, sort of three things here: @@ -983,81 +942,56 @@ impl Shard { // This is used to accurately assess whether the state of the shard is // accurate when a Hello is received. self.stage = ConnectionStage::Connecting; - self.client = connect(&self.ws_url.lock().unwrap())?; + let mut client = connect(&self.ws_url.lock())?; self.stage = ConnectionStage::Handshake; - Ok(()) - } - - fn identify(&mut self) -> Result<()> { - let identification = json!({ - "op": OpCode::Identify.num(), - "d": { - "compression": true, - "large_threshold": constants::LARGE_THRESHOLD, - "shard": self.shard_info, - "token": &*self.token.lock().unwrap(), - "v": constants::GATEWAY_VERSION, - "properties": { - "$browser": "serenity", - "$device": "serenity", - "$os": consts::OS, - }, - }, - }); - - self.heartbeat_instants.0 = Some(Instant::now()); - self.stage = ConnectionStage::Identifying; + let _ = set_client_timeout(&mut client); - debug!("[Shard {:?}] Identifying", self.shard_info); - - self.client.send_json(&identification) + Ok(client) } - fn reset(&mut self) { + pub fn reset(&mut self) { self.heartbeat_instants = (Some(Instant::now()), None); self.heartbeat_interval = None; self.last_heartbeat_acknowledged = true; + self.session_id = None; self.stage = ConnectionStage::Disconnected; self.seq = 0; } - fn update_presence(&mut self) { - let (ref game, status, afk) = self.current_presence; - let now = Utc::now().timestamp() as u64; - - let msg = json!({ - "op": OpCode::StatusUpdate.num(), - "d": { - "afk": afk, - "since": now, - "status": status.name(), - "game": game.as_ref().map(|x| json!({ - "name": x.name, - "type": x.kind, - "url": x.url, - })), - }, - }); + pub fn resume(&mut self) -> Result<()> { + debug!("Shard {:?}] Attempting to resume", self.shard_info); - debug!("[Shard {:?}] Sending presence update", self.shard_info); + self.client = self.initialize()?; + self.stage = ConnectionStage::Resuming; - if let Err(why) = self.client.send_json(&msg) { - warn!("[Shard {:?}] Err sending presence update: {:?}", - self.shard_info, - why); + match self.session_id.as_ref() { + Some(session_id) => { + self.client.send_resume( + &self.shard_info, + session_id, + &self.seq, + &self.token.lock(), + ) + }, + None => Err(Error::Gateway(GatewayError::NoSessionId)), } + } - #[cfg(feature = "cache")] - { - let mut cache = CACHE.write().unwrap(); - let current_user_id = cache.user.id; + pub fn reconnect(&mut self) -> Result<()> { + info!("[Shard {:?}] Attempting to reconnect", self.shard_info()); - cache.presences.get_mut(¤t_user_id).map(|presence| { - presence.game = game.clone(); - presence.last_modified = Some(now); - }); - } + self.reset(); + self.client = self.initialize()?; + + Ok(()) + } + + pub fn update_presence(&mut self) -> Result<()> { + self.client.send_presence_update( + &self.shard_info, + &self.current_presence, + ) } } @@ -1084,18 +1018,3 @@ fn build_gateway_url(base: &str) -> Result<Url> { Error::Gateway(GatewayError::BuildingUrl) }) } - -/// Tries to connect and upon failure, retries. -fn connecting(uri: &str) -> WsClient { - let waiting_time = 30; - - loop { - match connect(&uri) { - Ok(client) => return client, - Err(why) => { - warn!("Connecting failed: {:?}\n Will retry in {} seconds.", why, waiting_time); - thread::sleep(StdDuration::from_secs(waiting_time)); - }, - }; - } -} diff --git a/src/gateway/ws_client_ext.rs b/src/gateway/ws_client_ext.rs new file mode 100644 index 0000000..873d114 --- /dev/null +++ b/src/gateway/ws_client_ext.rs @@ -0,0 +1,133 @@ +use chrono::Utc; +use constants::{self, OpCode}; +use gateway::{CurrentPresence, WsClient}; +use internal::prelude::*; +use internal::ws_impl::SenderExt; +use model::GuildId; +use std::env::consts; + +pub trait WebSocketGatewayClientExt { + fn send_chunk_guilds<It>( + &mut self, + guild_ids: It, + shard_info: &[u64; 2], + limit: Option<u16>, + query: Option<&str>, + ) -> Result<()> where It: IntoIterator<Item=GuildId>; + + fn send_heartbeat(&mut self, shard_info: &[u64; 2], seq: Option<u64>) + -> Result<()>; + + fn send_identify(&mut self, shard_info: &[u64; 2], token: &str) + -> Result<()>; + + fn send_presence_update( + &mut self, + shard_info: &[u64; 2], + current_presence: &CurrentPresence, + ) -> Result<()>; + + fn send_resume( + &mut self, + shard_info: &[u64; 2], + session_id: &str, + seq: &u64, + token: &str, + ) -> Result<()>; +} + +impl WebSocketGatewayClientExt for WsClient { + fn send_chunk_guilds<It>( + &mut self, + guild_ids: It, + shard_info: &[u64; 2], + limit: Option<u16>, + query: Option<&str>, + ) -> Result<()> where It: IntoIterator<Item=GuildId> { + debug!("[Shard {:?}] Requesting member chunks", shard_info); + + self.send_json(&json!({ + "op": OpCode::GetGuildMembers.num(), + "d": { + "guild_id": guild_ids.into_iter().map(|x| x.as_ref().0).collect::<Vec<u64>>(), + "limit": limit.unwrap_or(0), + "query": query.unwrap_or(""), + }, + })).map_err(From::from) + } + + fn send_heartbeat(&mut self, shard_info: &[u64; 2], seq: Option<u64>) + -> Result<()> { + trace!("[Shard {:?}] Sending heartbeat d: {:?}", shard_info, seq); + + self.send_json(&json!({ + "d": seq, + "op": OpCode::Heartbeat.num(), + })).map_err(From::from) + } + + fn send_identify(&mut self, shard_info: &[u64; 2], token: &str) + -> Result<()> { + debug!("[Shard {:?}] Identifying", shard_info); + + self.send_json(&json!({ + "op": OpCode::Identify.num(), + "d": { + "compression": true, + "large_threshold": constants::LARGE_THRESHOLD, + "shard": shard_info, + "token": token, + "v": constants::GATEWAY_VERSION, + "properties": { + "$browser": "serenity", + "$device": "serenity", + "$os": consts::OS, + }, + }, + })) + } + + fn send_presence_update( + &mut self, + shard_info: &[u64; 2], + current_presence: &CurrentPresence, + ) -> Result<()> { + let &(ref game, ref status) = current_presence; + let now = Utc::now().timestamp() as u64; + + debug!("[Shard {:?}] Sending presence update", shard_info); + + self.send_json(&json!({ + "op": OpCode::StatusUpdate.num(), + "d": { + "afk": false, + "since": now, + "status": status.name(), + "game": game.as_ref().map(|x| json!({ + "name": x.name, + "type": x.kind, + "url": x.url, + })), + }, + })) + } + + fn send_resume( + &mut self, + shard_info: &[u64; 2], + session_id: &str, + seq: &u64, + token: &str, + ) -> Result<()> { + debug!("[Shard {:?}] Sending resume; seq: {}", shard_info, seq); + + self.send_json(&json!({ + "op": OpCode::Resume.num(), + "d": { + "session_id": session_id, + "seq": seq, + "token": token, + }, + })).map_err(From::from) + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs index 099a33c..239c777 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -38,6 +38,7 @@ use hyper::net::HttpsConnector; use hyper::{header, Error as HyperError, Result as HyperResult, Url}; use hyper_native_tls::NativeTlsClient; use multipart::client::Multipart; +use parking_lot::Mutex; use self::ratelimiting::Route; use serde_json; use std::collections::BTreeMap; @@ -46,7 +47,7 @@ use std::fmt::Write as FmtWrite; use std::fs::File; use std::io::{ErrorKind as IoErrorKind, Read}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use constants; use internal::prelude::*; use model::*; @@ -98,7 +99,7 @@ lazy_static! { /// # fn main() { /// # try_main().unwrap(); /// # } -pub fn set_token(token: &str) { TOKEN.lock().unwrap().clone_from(&token.to_string()); } +pub fn set_token(token: &str) { TOKEN.lock().clone_from(&token.to_string()); } /// Adds a [`User`] as a recipient to a [`Group`]. /// @@ -625,7 +626,12 @@ pub fn delete_role(guild_id: u64, role_id: u64) -> Result<()> { pub fn delete_webhook(webhook_id: u64) -> Result<()> { verify( 204, - request!(Route::WebhooksId, delete, "/webhooks/{}", webhook_id), + request!( + Route::WebhooksId(webhook_id), + delete, + "/webhooks/{}", + webhook_id, + ), ) } @@ -788,7 +794,7 @@ pub fn edit_profile(map: &JsonMap) -> Result<CurrentUser> { let mut value = serde_json::from_reader::<HyperResponse, Value>(response)?; if let Some(map) = value.as_object_mut() { - if !TOKEN.lock().unwrap().starts_with("Bot ") { + if !TOKEN.lock().starts_with("Bot ") { if let Some(Value::String(token)) = map.remove("token") { set_token(&token); } @@ -873,7 +879,12 @@ pub fn edit_role_position(guild_id: u64, role_id: u64, position: u64) -> Result< // external crates being incredibly messy and misleading in the end user's view. pub fn edit_webhook(webhook_id: u64, map: &Value) -> Result<Webhook> { let body = map.to_string(); - let response = request!(Route::WebhooksId, patch(body), "/webhooks/{}", webhook_id); + let response = request!( + Route::WebhooksId(webhook_id), + patch(body), + "/webhooks/{}", + webhook_id, + ); serde_json::from_reader::<HyperResponse, Webhook>(response) .map_err(From::from) @@ -1044,12 +1055,20 @@ pub fn get_bans(guild_id: u64) -> Result<Vec<Ban>> { } /// Gets all audit logs in a specific guild. -pub fn get_audit_logs(guild_id: u64) -> Result<AuditLogs> { +pub fn get_audit_logs(guild_id: u64, + action_type: Option<u8>, + user_id: Option<u64>, + before: Option<u64>, + limit: Option<u8>) -> Result<AuditLogs> { let response = request!( Route::GuildsIdAuditLogs(guild_id), get, - "/guilds/{}/audit-logs", - guild_id + "/guilds/{}/audit-logs?user_id={}&action_type={}&before={}&limit={}", + guild_id, + user_id.unwrap_or(0), + action_type.unwrap_or(0), + before.unwrap_or(0), + limit.unwrap_or(50), ); serde_json::from_reader::<HyperResponse, AuditLogs>(response) @@ -1536,7 +1555,12 @@ pub fn get_voice_regions() -> Result<Vec<VoiceRegion>> { /// /// [`get_webhook_with_token`]: fn.get_webhook_with_token.html pub fn get_webhook(webhook_id: u64) -> Result<Webhook> { - let response = request!(Route::WebhooksId, get, "/webhooks/{}", webhook_id); + let response = request!( + Route::WebhooksId(webhook_id), + get, + "/webhooks/{}", + webhook_id, + ); serde_json::from_reader::<HyperResponse, Webhook>(response) .map_err(From::from) @@ -1642,7 +1666,7 @@ pub fn send_files<'a, T, It: IntoIterator<Item=T>>(channel_id: u64, files: It, m let mut request = Request::with_connector(Method::Post, url, &connector)?; request .headers_mut() - .set(header::Authorization(TOKEN.lock().unwrap().clone())); + .set(header::Authorization(TOKEN.lock().clone())); request .headers_mut() .set(header::UserAgent(constants::USER_AGENT.to_string())); @@ -1650,7 +1674,7 @@ pub fn send_files<'a, T, It: IntoIterator<Item=T>>(channel_id: u64, files: It, m let mut request = Multipart::from_request(request)?; let mut file_num = "0".to_string(); - for file in files { + for file in files.into_iter() { match file.into() { AttachmentType::Bytes((mut bytes, filename)) => { request @@ -1801,7 +1825,7 @@ pub fn unpin_message(channel_id: u64, message_id: u64) -> Result<()> { fn request<'a, F>(route: Route, f: F) -> Result<HyperResponse> where F: Fn() -> RequestBuilder<'a> { let response = ratelimiting::perform(route, || { - f().header(header::Authorization(TOKEN.lock().unwrap().clone())) + f().header(header::Authorization(TOKEN.lock().clone())) .header(header::ContentType::json()) })?; diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index 8008616..08dc9ee 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -44,8 +44,9 @@ use chrono::Utc; use hyper::client::{RequestBuilder, Response}; use hyper::header::Headers; use hyper::status::StatusCode; +use parking_lot::Mutex; use std::collections::HashMap; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::time::Duration; use std::{str, thread, i64}; use super::{HttpError, LightMethod}; @@ -80,10 +81,8 @@ lazy_static! { /// ```rust,no_run /// use serenity::http::ratelimiting::{ROUTES, Route}; /// - /// let routes = ROUTES.lock().unwrap(); - /// - /// if let Some(route) = routes.get(&Route::ChannelsId(7)) { - /// println!("Reset time at: {}", route.lock().unwrap().reset); + /// if let Some(route) = ROUTES.lock().get(&Route::ChannelsId(7)) { + /// println!("Reset time at: {}", route.lock().reset); /// } /// ``` /// @@ -338,9 +337,9 @@ pub enum Route { /// Route for the `/voice/regions` path. VoiceRegions, /// Route for the `/webhooks/:webhook_id` path. - WebhooksId, - /// Route where no ratelimit headers are in place (e.g. current application - /// info retrieval). + WebhooksId(u64), + /// Route where no ratelimit headers are in place (i.e. user account-only + /// routes). /// /// This is a special case, in that if the route is `None` then pre- and /// post-hooks are not executed. @@ -352,7 +351,7 @@ pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> loop { // This will block if another thread already has the global // unlocked already (due to receiving an x-ratelimit-global). - let _ = GLOBAL.lock().expect("global route lock poisoned"); + let _ = GLOBAL.lock(); // Perform pre-checking here: // @@ -364,7 +363,6 @@ pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> // - then, perform the request let bucket = Arc::clone(ROUTES .lock() - .expect("routes poisoned") .entry(route) .or_insert_with(|| { Arc::new(Mutex::new(RateLimit { @@ -374,7 +372,7 @@ pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> })) })); - let mut lock = bucket.lock().unwrap(); + let mut lock = bucket.lock(); lock.pre_hook(&route); let response = super::retry(&f)?; @@ -396,7 +394,7 @@ pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> return Ok(response); } else { let redo = if response.headers.get_raw("x-ratelimit-global").is_some() { - let _ = GLOBAL.lock().expect("global route lock poisoned"); + let _ = GLOBAL.lock(); Ok( if let Some(retry_after) = parse_header(&response.headers, "retry-after")? { diff --git a/src/internal/macros.rs b/src/internal/macros.rs index 92a21c1..3bcc5c5 100644 --- a/src/internal/macros.rs +++ b/src/internal/macros.rs @@ -182,6 +182,7 @@ macro_rules! enum_number { } } +#[allow(unused_macros)] macro_rules! try_opt { ($x:expr) => (match $x { Some(v) => v, diff --git a/src/internal/rwlock_ext.rs b/src/internal/rwlock_ext.rs index 8266cdf..6235370 100644 --- a/src/internal/rwlock_ext.rs +++ b/src/internal/rwlock_ext.rs @@ -1,3 +1,4 @@ +use parking_lot::RwLock as ParkingLotRwLock; use std::sync::{Arc, RwLock}; pub trait RwLockExt<T> { @@ -16,3 +17,15 @@ impl<T> RwLockExt<T> for Arc<RwLock<T>> { f(&mut w) } } + +impl<T> RwLockExt<T> for Arc<ParkingLotRwLock<T>> { + fn with<Y, F: Fn(&T) -> Y>(&self, f: F) -> Y { + let r = self.read(); + f(&r) + } + + fn with_mut<Y, F: FnMut(&mut T) -> Y>(&self, mut f: F) -> Y { + let mut w = self.write(); + f(&mut w) + } +} @@ -109,6 +109,7 @@ extern crate serde_json; extern crate lazy_static; extern crate chrono; +extern crate parking_lot; extern crate serde; #[cfg(feature = "base64")] @@ -127,8 +128,6 @@ extern crate multipart; extern crate native_tls; #[cfg(feature = "opus")] extern crate opus; -#[cfg(feature = "parking_lot")] -extern crate parking_lot; #[cfg(feature = "sodiumoxide")] extern crate sodiumoxide; #[cfg(feature = "threadpool")] @@ -174,7 +173,7 @@ pub use client::Client; #[cfg(feature = "cache")] use cache::Cache; #[cfg(feature = "cache")] -use std::sync::RwLock; +use parking_lot::RwLock; #[cfg(feature = "cache")] lazy_static! { @@ -199,16 +198,9 @@ lazy_static! { /// ```rust,ignore /// use serenity::CACHE; /// - /// println!("{}", CACHE.read().unwrap().user.id); + /// println!("{}", CACHE.read().user.id); /// ``` /// - /// By `unwrap()`ing, the thread managing an event dispatch will be blocked - /// until the guard can be opened. - /// - /// If you do not want to block the current thread, you may instead use - /// `RwLock::try_read`. Refer to `RwLock`'s documentation in the stdlib for - /// more information. - /// /// [`CurrentUser`]: model/struct.CurrentUser.html /// [`Cache`]: cache/struct.Cache.html /// [cache module documentation]: cache/index.html diff --git a/src/model/channel/attachment.rs b/src/model/channel/attachment.rs index eac513f..f520cd4 100644 --- a/src/model/channel/attachment.rs +++ b/src/model/channel/attachment.rs @@ -54,7 +54,7 @@ impl Attachment { /// /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, message: Message) { + /// fn message(&self, _: Context, message: Message) { /// for attachment in message.attachments { /// let content = match attachment.download() { /// Ok(content) => content, @@ -86,12 +86,14 @@ impl Attachment { /// } /// } /// - /// fn on_ready(&self, _: Context, ready: Ready) { + /// fn ready(&self, _: Context, ready: Ready) { /// println!("{} is connected!", ready.user.name); /// } /// } /// let token = env::var("DISCORD_TOKEN").expect("token in environment"); - /// let mut client = Client::new(&token, Handler); client.start().unwrap(); + /// let mut client = Client::new(&token, Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// # Errors diff --git a/src/model/channel/channel_category.rs b/src/model/channel/channel_category.rs index f567cee..73c50a7 100644 --- a/src/model/channel/channel_category.rs +++ b/src/model/channel/channel_category.rs @@ -93,20 +93,14 @@ impl ChannelCategory { } } - let mut map = Map::new(); - map.insert("name".to_string(), Value::String(self.name.clone())); - map.insert( - "position".to_string(), - Value::Number(Number::from(self.position)), - ); - map.insert( - "type".to_string(), - Value::String(self.kind.name().to_string()), - ); + let mut map = HashMap::new(); + map.insert("name", Value::String(self.name.clone())); + map.insert("position", Value::Number(Number::from(self.position))); + map.insert("type", Value::String(self.kind.name().to_string())); - let edited = f(EditChannel(map)).0; + let map = serenity_utils::hashmap_to_json_map(f(EditChannel(map)).0); - http::edit_channel(self.id.0, &edited).map(|channel| { + http::edit_channel(self.id.0, &map).map(|channel| { let GuildChannel { id, category_id, diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 2e415dc..158ebcf 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -11,6 +11,8 @@ use builder::{CreateMessage, EditChannel, GetMessages}; use CACHE; #[cfg(feature = "model")] use http::{self, AttachmentType}; +#[cfg(feature = "model")] +use utils; #[cfg(feature = "model")] impl ChannelId { @@ -189,7 +191,9 @@ impl ChannelId { /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html #[inline] pub fn edit<F: FnOnce(EditChannel) -> EditChannel>(&self, f: F) -> Result<GuildChannel> { - http::edit_channel(self.0, &f(EditChannel::default()).0) + let map = utils::hashmap_to_json_map(f(EditChannel::default()).0); + + http::edit_channel(self.0, &map) } /// Edits a [`Message`] in the channel given its Id. @@ -213,9 +217,9 @@ impl ChannelId { /// [`the limit`]: ../builder/struct.CreateMessage.html#method.content pub fn edit_message<F, M>(&self, message_id: M, f: F) -> Result<Message> where F: FnOnce(CreateMessage) -> CreateMessage, M: Into<MessageId> { - let map = f(CreateMessage::default()).0; + let msg = f(CreateMessage::default()); - if let Some(content) = map.get("content") { + if let Some(content) = msg.0.get("content") { if let Value::String(ref content) = *content { if let Some(length_over) = Message::overflow_length(content) { return Err(Error::Model(ModelError::MessageTooLong(length_over))); @@ -223,19 +227,21 @@ impl ChannelId { } } + let map = utils::hashmap_to_json_map(msg.0); + http::edit_message(self.0, message_id.into().0, &Value::Object(map)) } /// Search the cache for the channel with the Id. #[cfg(feature = "cache")] - pub fn find(&self) -> Option<Channel> { CACHE.read().unwrap().channel(*self) } + pub fn find(&self) -> Option<Channel> { CACHE.read().channel(*self) } /// Search the cache for the channel. If it can't be found, the channel is /// requested over REST. pub fn get(&self) -> Result<Channel> { #[cfg(feature = "cache")] { - if let Some(channel) = CACHE.read().unwrap().channel(*self) { + if let Some(channel) = CACHE.read().channel(*self) { return Ok(channel); } } @@ -315,13 +321,13 @@ impl ChannelId { }; Some(match channel { - Guild(channel) => channel.read().unwrap().name().to_string(), - Group(channel) => match channel.read().unwrap().name() { + Guild(channel) => channel.read().name().to_string(), + Group(channel) => match channel.read().name() { Cow::Borrowed(name) => name.to_string(), Cow::Owned(name) => name, }, - Category(category) => category.read().unwrap().name().to_string(), - Private(channel) => channel.read().unwrap().name(), + Category(category) => category.read().name().to_string(), + Private(channel) => channel.read().name(), }) } @@ -445,9 +451,9 @@ impl ChannelId { /// [Send Messages]: permissions/constant.SEND_MESSAGES.html pub fn send_files<'a, F, T, It: IntoIterator<Item=T>>(&self, files: It, f: F) -> Result<Message> where F: FnOnce(CreateMessage) -> CreateMessage, T: Into<AttachmentType<'a>> { - let mut map = f(CreateMessage::default()).0; + let mut msg = f(CreateMessage::default()); - if let Some(content) = map.get("content") { + if let Some(content) = msg.0.get("content") { if let Value::String(ref content) = *content { if let Some(length_over) = Message::overflow_length(content) { return Err(Error::Model(ModelError::MessageTooLong(length_over))); @@ -455,7 +461,8 @@ impl ChannelId { } } - let _ = map.remove("embed"); + let _ = msg.0.remove("embed"); + let map = utils::hashmap_to_json_map(msg.0); http::send_files(self.0, files, map) } @@ -481,14 +488,15 @@ impl ChannelId { /// [Send Messages]: permissions/constant.SEND_MESSAGES.html pub fn send_message<F>(&self, f: F) -> Result<Message> where F: FnOnce(CreateMessage) -> CreateMessage { - let CreateMessage(map, reactions) = f(CreateMessage::default()); + let msg = f(CreateMessage::default()); + let map = utils::hashmap_to_json_map(msg.0); Message::check_content_length(&map)?; Message::check_embed_length(&map)?; let message = http::send_message(self.0, &Value::Object(map))?; - if let Some(reactions) = reactions { + if let Some(reactions) = msg.1 { for reaction in reactions { self.create_reaction(message.id, reaction)?; } diff --git a/src/model/channel/embed.rs b/src/model/channel/embed.rs index 435f706..401f10d 100644 --- a/src/model/channel/embed.rs +++ b/src/model/channel/embed.rs @@ -4,6 +4,8 @@ use utils::Colour; use internal::prelude::*; #[cfg(feature = "model")] use builder::CreateEmbed; +#[cfg(feature = "model")] +use utils; /// Represents a rich embed which allows using richer markdown, multiple fields /// and more. This was heavily inspired by [slack's attachments]. @@ -81,15 +83,14 @@ impl Embed { /// let embed = Embed::fake(|e| e /// .title("Embed title") /// .description("Making a basic embed") - /// .field(|f| f - /// .name("A field") - /// .value("Has some content.") - /// .inline(false))); + /// .field("A field", "Has some content.", false)); /// ``` #[inline] pub fn fake<F>(f: F) -> Value where F: FnOnce(CreateEmbed) -> CreateEmbed { - Value::Object(f(CreateEmbed::default()).0) + let map = utils::hashmap_to_json_map(f(CreateEmbed::default()).0); + + Value::Object(map) } } @@ -123,6 +124,24 @@ pub struct EmbedField { pub value: String, } +impl EmbedField { + /// Creates a new embed field. + /// + /// **Note**: Refer to the [`name`] and [`value`] documentation for maximum + /// lengths. + /// + /// [`name`]: #structfield.name + /// [`value`]: #structfield.value + pub fn new<T, U>(name: T, value: U, inline: bool) -> Self + where T: Into<String>, U: Into<String> { + Self { + name: name.into(), + value: value.into(), + inline, + } + } +} + /// Footer information for an embed. #[derive(Clone, Debug, Deserialize)] pub struct EmbedFooter { diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index b7295fc..76cea47 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -116,7 +116,9 @@ impl GuildChannel { } } - http::create_invite(self.id.0, &f(CreateInvite::default()).0) + let map = serenity_utils::hashmap_to_json_map(f(CreateInvite::default()).0); + + http::create_invite(self.id.0, &map) } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -157,12 +159,12 @@ impl GuildChannel { /// kind: PermissionOverwriteType::Member(user_id), /// }; /// - /// let cache = CACHE.read().unwrap(); + /// let cache = CACHE.read(); /// let channel = cache /// .guild_channel(channel_id) /// .ok_or(ModelError::ItemMissing)?; /// - /// channel.read().unwrap().create_permission(&overwrite)?; + /// channel.read().create_permission(&overwrite)?; /// # Ok(()) /// # } /// # @@ -199,12 +201,12 @@ impl GuildChannel { /// kind: PermissionOverwriteType::Member(user_id), /// }; /// - /// let cache = CACHE.read().unwrap(); + /// let cache = CACHE.read(); /// let channel = cache /// .guild_channel(channel_id) /// .ok_or(ModelError::ItemMissing)?; /// - /// channel.read().unwrap().create_permission(&overwrite)?; + /// channel.read().create_permission(&overwrite)?; /// # Ok(()) /// # } /// # @@ -312,18 +314,12 @@ impl GuildChannel { } } - let mut map = Map::new(); - map.insert("name".to_string(), Value::String(self.name.clone())); - map.insert( - "position".to_string(), - Value::Number(Number::from(self.position)), - ); - map.insert( - "type".to_string(), - Value::String(self.kind.name().to_string()), - ); + let mut map = HashMap::new(); + map.insert("name", Value::String(self.name.clone())); + map.insert("position", Value::Number(Number::from(self.position))); + map.insert("type", Value::String(self.kind.name().to_string())); - let edited = f(EditChannel(map)).0; + let edited = serenity_utils::hashmap_to_json_map(f(EditChannel(map)).0); match http::edit_channel(self.id.0, &edited) { Ok(channel) => { @@ -365,7 +361,7 @@ impl GuildChannel { /// **Note**: Right now this performs a clone of the guild. This will be /// optimized in the future. #[cfg(feature = "cache")] - pub fn guild(&self) -> Option<Arc<RwLock<Guild>>> { CACHE.read().unwrap().guild(self.guild_id) } + pub fn guild(&self) -> Option<Arc<RwLock<Guild>>> { CACHE.read().guild(self.guild_id) } /// Gets all of the channel's invites. /// @@ -435,18 +431,20 @@ impl GuildChannel { /// use serenity::CACHE; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { - /// let channel = match CACHE.read().unwrap().guild_channel(msg.channel_id) { + /// fn message(&self, _: Context, msg: Message) { + /// let channel = match CACHE.read().guild_channel(msg.channel_id) { /// Some(channel) => channel, /// None => return, /// }; /// - /// let permissions = channel.read().unwrap().permissions_for(&msg.author).unwrap(); + /// let permissions = channel.read().permissions_for(&msg.author).unwrap(); /// /// println!("The user's permissions: {:?}", permissions); /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// Check if the current user has the [Attach Files] and [Send Messages] @@ -463,15 +461,15 @@ impl GuildChannel { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { - /// let channel = match CACHE.read().unwrap().guild_channel(msg.channel_id) { + /// fn message(&self, _: Context, msg: Message) { + /// let channel = match CACHE.read().guild_channel(msg.channel_id) { /// Some(channel) => channel, /// None => return, /// }; /// - /// let current_user_id = CACHE.read().unwrap().user.id; + /// let current_user_id = CACHE.read().user.id; /// let permissions = - /// channel.read().unwrap().permissions_for(current_user_id).unwrap(); + /// channel.read().permissions_for(current_user_id).unwrap(); /// /// if !permissions.contains(Permissions::ATTACH_FILES | /// Permissions::SEND_MESSAGES) { @@ -492,7 +490,9 @@ impl GuildChannel { /// } /// } /// - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` /// /// # Errors @@ -512,7 +512,7 @@ impl GuildChannel { pub fn permissions_for<U: Into<UserId>>(&self, user_id: U) -> Result<Permissions> { self.guild() .ok_or_else(|| Error::Model(ModelError::GuildNotFound)) - .map(|g| g.read().unwrap().permissions_in(self.id, user_id)) + .map(|g| g.read().permissions_in(self.id, user_id)) } /// Pins a [`Message`] to the channel. diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index 480c15d..984e274 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -14,6 +14,8 @@ use constants; use CACHE; #[cfg(feature = "model")] use http; +#[cfg(feature = "model")] +use utils as serenity_utils; /// A representation of a message over a guild's text channel, a group, or a /// private channel. @@ -86,7 +88,7 @@ impl Message { /// # struct Handler; /// # /// # impl EventHandler for Handler {} - /// # let mut client = Client::new("token", Handler); + /// # let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::model::Channel; /// use serenity::framework::StandardFramework; @@ -97,12 +99,12 @@ impl Message { /// /// command!(channel_name(_ctx, msg) { /// let _ = match msg.channel() { - /// Some(Channel::Category(c)) => msg.reply(&c.read().unwrap().name), - /// Some(Channel::Group(c)) => msg.reply(&c.read().unwrap().name()), - /// Some(Channel::Guild(c)) => msg.reply(&c.read().unwrap().name), + /// Some(Channel::Category(c)) => msg.reply(&c.read().name), + /// Some(Channel::Group(c)) => msg.reply(&c.read().name()), + /// Some(Channel::Guild(c)) => msg.reply(&c.read().name), /// Some(Channel::Private(c)) => { - /// let channel = c.read().unwrap(); - /// let user = channel.recipient.read().unwrap(); + /// let channel = c.read(); + /// let user = channel.recipient.read(); /// /// msg.reply(&format!("DM with {}", user.name.clone())) /// }, @@ -113,12 +115,12 @@ impl Message { /// ``` #[cfg(feature = "cache")] #[inline] - pub fn channel(&self) -> Option<Channel> { CACHE.read().unwrap().channel(self.channel_id) } + pub fn channel(&self) -> Option<Channel> { CACHE.read().channel(self.channel_id) } /// A util function for determining whether this message was sent by someone else, or the /// bot. #[cfg(all(feature = "cache", feature = "utils"))] - pub fn is_own(&self) -> bool { self.author.id == CACHE.read().unwrap().user.id } + pub fn is_own(&self) -> bool { self.author.id == CACHE.read().user.id } /// Deletes the message. /// @@ -138,7 +140,7 @@ impl Message { #[cfg(feature = "cache")] { let req = Permissions::MANAGE_MESSAGES; - let is_author = self.author.id == CACHE.read().unwrap().user.id; + let is_author = self.author.id == CACHE.read().user.id; let has_perms = utils::user_has_perms(self.channel_id, req)?; if !is_author && !has_perms { @@ -211,7 +213,7 @@ impl Message { where F: FnOnce(CreateMessage) -> CreateMessage { #[cfg(feature = "cache")] { - if self.author.id != CACHE.read().unwrap().user.id { + if self.author.id != CACHE.read().user.id { return Err(Error::Model(ModelError::InvalidUser)); } } @@ -230,7 +232,7 @@ impl Message { builder = builder.tts(true); } - let map = f(builder).0; + let map = serenity_utils::hashmap_to_json_map(f(builder).0); match http::edit_message(self.channel_id.0, self.id.0, &Value::Object(map)) { Ok(edited) => { @@ -335,7 +337,7 @@ impl Message { #[cfg(feature = "cache")] pub fn guild(&self) -> Option<Arc<RwLock<Guild>>> { self.guild_id() - .and_then(|guild_id| CACHE.read().unwrap().guild(guild_id)) + .and_then(|guild_id| CACHE.read().guild(guild_id)) } /// Retrieves the Id of the guild that the message was sent in, if sent in @@ -345,8 +347,8 @@ impl Message { /// cache. #[cfg(feature = "cache")] pub fn guild_id(&self) -> Option<GuildId> { - match CACHE.read().unwrap().channel(self.channel_id) { - Some(Channel::Guild(ch)) => Some(ch.read().unwrap().guild_id), + match CACHE.read().channel(self.channel_id) { + Some(Channel::Guild(ch)) => Some(ch.read().guild_id), _ => None, } } @@ -354,7 +356,7 @@ impl Message { /// True if message was sent using direct messages. #[cfg(feature = "cache")] pub fn is_private(&self) -> bool { - match CACHE.read().unwrap().channel(self.channel_id) { + match CACHE.read().channel(self.channel_id) { Some(Channel::Group(_)) | Some(Channel::Private(_)) => true, _ => false, } diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index 07a73d7..ae46805 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -84,16 +84,16 @@ impl Channel { pub fn delete(&self) -> Result<()> { match *self { Channel::Group(ref group) => { - let _ = group.read().unwrap().leave()?; + let _ = group.read().leave()?; }, Channel::Guild(ref public_channel) => { - let _ = public_channel.read().unwrap().delete()?; + let _ = public_channel.read().delete()?; }, Channel::Private(ref private_channel) => { - let _ = private_channel.read().unwrap().delete()?; + let _ = private_channel.read().delete()?; }, Channel::Category(ref category) => { - category.read().unwrap().delete()?; + category.read().delete()?; }, } @@ -386,15 +386,15 @@ impl Display for Channel { /// [`PrivateChannel`]: struct.PrivateChannel.html fn fmt(&self, f: &mut Formatter) -> FmtResult { match *self { - Channel::Group(ref group) => Display::fmt(&group.read().unwrap().name(), f), - Channel::Guild(ref ch) => Display::fmt(&ch.read().unwrap().id.mention(), f), + Channel::Group(ref group) => Display::fmt(&group.read().name(), f), + Channel::Guild(ref ch) => Display::fmt(&ch.read().id.mention(), f), Channel::Private(ref ch) => { - let channel = ch.read().unwrap(); - let recipient = channel.recipient.read().unwrap(); + let channel = ch.read(); + let recipient = channel.recipient.read(); Display::fmt(&recipient.name, f) }, - Channel::Category(ref category) => Display::fmt(&category.read().unwrap().name, f), + Channel::Category(ref category) => Display::fmt(&category.read().name, f), } } } diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index d6ba781..01b387a 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -279,6 +279,6 @@ impl PrivateChannel { impl Display for PrivateChannel { /// Formats the private channel, displaying the recipient's username. fn fmt(&self, f: &mut Formatter) -> FmtResult { - f.write_str(&self.recipient.read().unwrap().name) + f.write_str(&self.recipient.read().name) } } diff --git a/src/model/channel/reaction.rs b/src/model/channel/reaction.rs index 8edc2e9..1a40ecb 100644 --- a/src/model/channel/reaction.rs +++ b/src/model/channel/reaction.rs @@ -48,7 +48,7 @@ impl Reaction { pub fn delete(&self) -> Result<()> { let user_id = feature_cache! { { - let user = if self.user_id == CACHE.read().unwrap().user.id { + let user = if self.user_id == CACHE.read().user.id { None } else { Some(self.user_id.0) diff --git a/src/model/error.rs b/src/model/error.rs index 454b94b..c2f568b 100644 --- a/src/model/error.rs +++ b/src/model/error.rs @@ -27,7 +27,7 @@ use super::Permissions; /// struct Handler; /// /// impl EventHandler for Handler { -/// fn on_guild_ban_removal(&self, context: Context, guild_id: GuildId, user: User) { +/// fn guild_ban_removal(&self, context: Context, guild_id: GuildId, user: User) { /// // If the user has an even discriminator, don't re-ban them. /// if user.discriminator % 2 == 0 { /// return; @@ -46,8 +46,10 @@ use super::Permissions; /// } /// } /// } -/// let token = env::var("DISCORD_TOKEN")?; -/// let mut client = Client::new(&token, Handler); client.start()?; +/// let token = env::var("DISCORD_BOT_TOKEN")?; +/// let mut client = Client::new(&token, Handler).unwrap(); +/// +/// client.start()?; /// # Ok(()) /// # } /// # diff --git a/src/model/event.rs b/src/model/event.rs index d4148b9..aea05b1 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -61,7 +61,7 @@ impl CacheUpdate for ChannelCreateEvent { let channel_id = group.with_mut(|writer| { for (recipient_id, recipient) in &mut writer.recipients { - cache.update_user_entry(&recipient.read().unwrap()); + cache.update_user_entry(&recipient.read()); *recipient = Arc::clone(&cache.users[recipient_id]); } @@ -110,7 +110,7 @@ impl CacheUpdate for ChannelCreateEvent { }, Channel::Category(ref category) => cache .categories - .insert(category.read().unwrap().id, Arc::clone(category)) + .insert(category.read().id, Arc::clone(category)) .map(Channel::Category), } } @@ -214,7 +214,7 @@ impl CacheUpdate for ChannelRecipientAddEvent { let user = Arc::clone(&cache.users[&self.user.id]); cache.groups.get_mut(&self.channel_id).map(|group| { - group.write().unwrap().recipients.insert(self.user.id, user); + group.write().recipients.insert(self.user.id, user); }); None @@ -263,16 +263,16 @@ impl CacheUpdate for ChannelUpdateEvent { e.insert(Arc::clone(group)); }, Entry::Occupied(mut e) => { - let mut dest = e.get_mut().write().unwrap(); + let mut dest = e.get_mut().write(); if no_recipients { let recipients = mem::replace(&mut dest.recipients, HashMap::new()); - dest.clone_from(&group.read().unwrap()); + dest.clone_from(&group.read()); dest.recipients = recipients; } else { - dest.clone_from(&group.read().unwrap()); + dest.clone_from(&group.read()); } }, } @@ -289,13 +289,13 @@ impl CacheUpdate for ChannelUpdateEvent { Channel::Private(ref channel) => { cache .private_channels - .get_mut(&channel.read().unwrap().id) + .get_mut(&channel.read().id) .map(|private| private.clone_from(channel)); }, Channel::Category(ref category) => { cache .categories - .get_mut(&category.read().unwrap().id) + .get_mut(&category.read().id) .map(|c| c.clone_from(category)); }, } @@ -340,7 +340,7 @@ impl CacheUpdate for GuildCreateEvent { let mut guild = self.guild.clone(); for (user_id, member) in &mut guild.members { - cache.update_user_entry(&member.user.read().unwrap()); + cache.update_user_entry(&member.user.read()); let user = Arc::clone(&cache.users[user_id]); member.user = Arc::clone(&user); @@ -375,7 +375,7 @@ impl CacheUpdate for GuildDeleteEvent { fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { // Remove channel entries for the guild if the guild is found. cache.guilds.remove(&self.guild.id).map(|guild| { - for channel_id in guild.write().unwrap().channels.keys() { + for channel_id in guild.write().channels.keys() { cache.channels.remove(channel_id); } @@ -430,7 +430,7 @@ impl CacheUpdate for GuildMemberAddEvent { fn update(&mut self, cache: &mut Cache) -> Option<()> { let user_id = self.member.user.with(|u| u.id); - cache.update_user_entry(&self.member.user.read().unwrap()); + cache.update_user_entry(&self.member.user.read()); // Always safe due to being inserted above. self.member.user = Arc::clone(&cache.users[&user_id]); @@ -500,7 +500,7 @@ impl CacheUpdate for GuildMemberUpdateEvent { cache.update_user_entry(&self.user); if let Some(guild) = cache.guilds.get_mut(&self.guild_id) { - let mut guild = guild.write().unwrap(); + let mut guild = guild.write(); let mut found = false; @@ -509,7 +509,7 @@ impl CacheUpdate for GuildMemberUpdateEvent { member.nick.clone_from(&self.nick); member.roles.clone_from(&self.roles); - member.user.write().unwrap().clone_from(&self.user); + member.user.write().clone_from(&self.user); found = true; @@ -552,7 +552,7 @@ impl CacheUpdate for GuildMembersChunkEvent { fn update(&mut self, cache: &mut Cache) -> Option<()> { for member in self.members.values() { - cache.update_user_entry(&member.user.read().unwrap()); + cache.update_user_entry(&member.user.read()); } cache.guilds.get_mut(&self.guild_id).map(|guild| { @@ -609,7 +609,6 @@ impl CacheUpdate for GuildRoleCreateEvent { cache.guilds.get_mut(&self.guild_id).map(|guild| { guild .write() - .unwrap() .roles .insert(self.role.id, self.role.clone()) }); @@ -685,7 +684,7 @@ impl CacheUpdate for GuildUpdateEvent { fn update(&mut self, cache: &mut Cache) -> Option<()> { cache.guilds.get_mut(&self.guild.id).map(|guild| { - let mut guild = guild.write().unwrap(); + let mut guild = guild.write(); guild.afk_timeout = self.guild.afk_timeout; guild.afk_channel_id.clone_from(&self.guild.afk_channel_id); @@ -768,13 +767,13 @@ impl CacheUpdate for PresenceUpdateEvent { let user_id = self.presence.user_id; if let Some(user) = self.presence.user.as_mut() { - cache.update_user_entry(&user.read().unwrap()); + cache.update_user_entry(&user.read()); *user = Arc::clone(&cache.users[&user_id]); } if let Some(guild_id) = self.guild_id { if let Some(guild) = cache.guilds.get_mut(&guild_id) { - let mut guild = guild.write().unwrap(); + let mut guild = guild.write(); // If the member went offline, remove them from the presence list. if self.presence.status == OnlineStatus::Offline { @@ -920,7 +919,7 @@ impl CacheUpdate for ReadyEvent { for (user_id, presence) in &mut ready.presences { if let Some(ref user) = presence.user { - cache.update_user_entry(&user.read().unwrap()); + cache.update_user_entry(&user.read()); } presence.user = cache.users.get(user_id).cloned(); @@ -1004,7 +1003,7 @@ impl CacheUpdate for VoiceStateUpdateEvent { fn update(&mut self, cache: &mut Cache) -> Option<()> { if let Some(guild_id) = self.guild_id { if let Some(guild) = cache.guilds.get_mut(&guild_id) { - let mut guild = guild.write().unwrap(); + let mut guild = guild.write(); if self.voice_state.channel_id.is_some() { // Update or add to the voice state list @@ -1062,7 +1061,8 @@ pub enum GatewayEvent { Dispatch(u64, Event), Heartbeat(u64), Reconnect, - InvalidateSession, + /// Whether the session can be resumed. + InvalidateSession(bool), Hello(u64), HeartbeatAck, } @@ -1098,7 +1098,15 @@ impl GatewayEvent { GatewayEvent::Heartbeat(s) }, OpCode::Reconnect => GatewayEvent::Reconnect, - OpCode::InvalidSession => GatewayEvent::InvalidateSession, + OpCode::InvalidSession => { + let resumable = map.remove("d") + .ok_or_else(|| { + DeError::custom("expected gateway invalid session d") + }) + .and_then(bool::deserialize)?; + + GatewayEvent::InvalidateSession(resumable) + }, OpCode::Hello => { let mut d = map.remove("d") .ok_or_else(|| DeError::custom("expected gateway hello d")) diff --git a/src/model/gateway.rs b/src/model/gateway.rs index 4edc20e..ca846e8 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -1,6 +1,7 @@ +use parking_lot::RwLock; use serde::de::Error as DeError; use serde_json; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use super::utils::*; use super::*; diff --git a/src/model/guild/audit_log.rs b/src/model/guild/audit_log.rs index d019b61..b3caa73 100644 --- a/src/model/guild/audit_log.rs +++ b/src/model/guild/audit_log.rs @@ -1,4 +1,4 @@ -use super::super::{AuditLogEntryId, UserId}; +use super::super::{AuditLogEntryId, User, UserId, ChannelId, Webhook}; use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use std::fmt; use std::collections::HashMap; @@ -6,7 +6,7 @@ use std::mem::transmute; /// Determines to what entity an action was used on. #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum Target { Guild = 10, Channel = 20, @@ -28,10 +28,11 @@ pub enum Action { Invite(ActionInvite), Webhook(ActionWebhook), Emoji(ActionEmoji), + MessageDelete, } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionChannel { Create = 10, Update = 11, @@ -39,7 +40,7 @@ pub enum ActionChannel { } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionChannelOverwrite { Create = 13, Update = 14, @@ -47,7 +48,7 @@ pub enum ActionChannelOverwrite { } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionMember { Kick = 20, Prune = 21, @@ -58,7 +59,7 @@ pub enum ActionMember { } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionRole { Create = 30, Update = 31, @@ -66,7 +67,7 @@ pub enum ActionRole { } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionInvite { Create = 40, Update = 41, @@ -74,7 +75,7 @@ pub enum ActionInvite { } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionWebhook { Create = 50, Update = 51, @@ -82,7 +83,7 @@ pub enum ActionWebhook { } #[derive(Debug)] -#[repr(i32)] +#[repr(u8)] pub enum ActionEmoji { Create = 60, Delete = 61, @@ -92,6 +93,7 @@ pub enum ActionEmoji { #[derive(Debug, Deserialize)] pub struct Change { #[serde(rename = "key")] pub name: String, + // TODO: Change these to an actual type. #[serde(rename = "old_value")] pub old: String, #[serde(rename = "new_value")] pub new: String, } @@ -99,6 +101,8 @@ pub struct Change { #[derive(Debug)] pub struct AuditLogs { pub entries: HashMap<AuditLogEntryId, AuditLogEntry>, + pub webhooks: Vec<Webhook>, + pub users: Vec<User>, } #[derive(Debug, Deserialize)] @@ -106,8 +110,7 @@ pub struct AuditLogEntry { /// Determines to what entity an [`action`] was used on. /// /// [`action`]: #structfield.action - #[serde(deserialize_with = "deserialize_target", rename = "target_type")] - pub target: Target, + pub target_id: u64, /// Determines what action was done on a [`target`] /// /// [`target`]: #structfield.target @@ -118,30 +121,30 @@ pub struct AuditLogEntry { /// The user that did this action on a target. pub user_id: UserId, /// What changes were made. - pub changes: Vec<Change>, + pub changes: Option<Vec<Change>>, /// The id of this entry. pub id: AuditLogEntryId, + /// Some optional data assosiated with this entry. + pub options: Option<Options>, } -fn deserialize_target<'de, D: Deserializer<'de>>(de: D) -> Result<Target, D::Error> { - struct TargetVisitor; - - impl<'de> Visitor<'de> for TargetVisitor { - type Value = Target; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an integer between 0 to 70") - } - - fn visit_i32<E: de::Error>(self, value: i32) -> Result<Target, E> { - Ok(match value { - 10...70 => unsafe { transmute(value) }, - _ => return Err(E::custom(format!("unexpected target number: {}", value))), - }) - } - } +#[derive(Debug, Deserialize)] +pub struct Options { + /// Number of days after which inactive members were kicked. + pub delete_member_days: String, + /// Number of members removed by the prune + pub members_removed: String, + /// Channel in which the messages were deleted + pub channel_id: ChannelId, + /// Number of deleted messages. + pub count: u32, + /// Id of the overwritten entity + pub id: u64, + /// Type of overwritten entity ("member" or "role"). + #[serde(rename = "type")] pub kind: String, + /// Name of the role if type is "role" + pub role_name: String, - de.deserialize_i32(TargetVisitor) } fn deserialize_action<'de, D: Deserializer<'de>>(de: D) -> Result<Action, D::Error> { @@ -151,10 +154,10 @@ fn deserialize_action<'de, D: Deserializer<'de>>(de: D) -> Result<Action, D::Err type Value = Action; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("an integer between 1 to 62") + formatter.write_str("an integer between 1 to 72") } - fn visit_i32<E: de::Error>(self, value: i32) -> Result<Action, E> { + fn visit_u8<E: de::Error>(self, value: u8) -> Result<Action, E> { Ok(match value { 1 => Action::GuildUpdate, 10...12 => Action::Channel(unsafe { transmute(value) }), @@ -164,12 +167,13 @@ fn deserialize_action<'de, D: Deserializer<'de>>(de: D) -> Result<Action, D::Err 40...42 => Action::Invite(unsafe { transmute(value) }), 50...52 => Action::Webhook(unsafe { transmute(value) }), 60...62 => Action::Emoji(unsafe { transmute(value) }), + 72 => Action::MessageDelete, _ => return Err(E::custom(format!("Unexpected action number: {}", value))), }) } } - de.deserialize_i32(ActionVisitor) + de.deserialize_u8(ActionVisitor) } impl<'de> Deserialize<'de> for AuditLogs { @@ -178,6 +182,8 @@ impl<'de> Deserialize<'de> for AuditLogs { #[serde(field_identifier)] enum Field { #[serde(rename = "audit_log_entries")] Entries, + #[serde(rename = "webhooks")] Webhooks, + #[serde(rename = "users")] Users, } struct EntriesVisitor; @@ -190,22 +196,49 @@ impl<'de> Deserialize<'de> for AuditLogs { } fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<AuditLogs, V::Error> { - let audit_log_entries = loop { - if let Some(Field::Entries) = map.next_key()? { - break map.next_value::<Vec<AuditLogEntry>>()?; + let mut audit_log_entries = None; + let mut users = None; + let mut webhooks = None; + + while let Some(field) = map.next_key()? { + match field { + Field::Entries => { + if audit_log_entries.is_some() { + return Err(de::Error::duplicate_field("entries")); + } + + audit_log_entries = Some(map.next_value::<Vec<AuditLogEntry>>()?); + }, + Field::Webhooks => { + if webhooks.is_some() { + return Err(de::Error::duplicate_field("webhooks")); + } + + webhooks = Some(map.next_value::<Vec<Webhook>>()?); + }, + Field::Users => { + if users.is_some() { + return Err(de::Error::duplicate_field("users")); + } + + users = Some(map.next_value::<Vec<User>>()?); + }, } - }; + } Ok(AuditLogs { entries: audit_log_entries + .unwrap() .into_iter() .map(|entry| (entry.id, entry)) .collect(), + webhooks: webhooks.unwrap(), + users: users.unwrap(), }) } } - const FIELD: &'static [&'static str] = &["audit_log_entries"]; + const FIELD: &[&str] = &["audit_log_entries"]; de.deserialize_struct("AuditLogs", FIELD, EntriesVisitor) } } diff --git a/src/model/guild/emoji.rs b/src/model/guild/emoji.rs index 4a2190f..7f332ae 100644 --- a/src/model/guild/emoji.rs +++ b/src/model/guild/emoji.rs @@ -151,8 +151,8 @@ impl Emoji { /// ``` #[cfg(feature = "cache")] pub fn find_guild_id(&self) -> Option<GuildId> { - for guild in CACHE.read().unwrap().guilds.values() { - let guild = guild.read().unwrap(); + for guild in CACHE.read().guilds.values() { + let guild = guild.read(); if guild.emojis.contains_key(&self.id) { return Some(guild.id); diff --git a/src/model/guild/feature.rs b/src/model/guild/feature.rs deleted file mode 100644 index 40fd3fd..0000000 --- a/src/model/guild/feature.rs +++ /dev/null @@ -1,25 +0,0 @@ -/// A special feature, such as for VIP guilds, that a [`Guild`] has had granted -/// to them. -/// -/// [`Guild`]: struct.Guild.html -#[derive(Copy, Clone, Debug, Deserialize, Hash, Eq, PartialEq)] -pub enum Feature { - /// The [`Guild`] can set a custom [`splash`][`Guild::splash`] image on - /// invite URLs. - /// - /// [`Guild`]: struct.Guild.html - /// [`Guild::splash`]: struct.Guild.html#structfield.splash - #[serde(rename = "INVITE_SPLASH")] - InviteSplash, - /// The [`Guild`] can set a Vanity URL, which is a custom-named permanent - /// invite code. - /// - /// [`Guild`]: struct.Guild.html - #[serde(rename = "VANITY_URL")] - VanityUrl, - /// The [`Guild`] has access to VIP voice channel regions. - /// - /// [`Guild`]: struct.Guild.html - #[serde(rename = "VIP_REGIONS")] - VipRegions, -} diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index 622d059..37b3886 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -5,11 +5,11 @@ use CACHE; #[cfg(feature = "model")] use builder::{EditGuild, EditMember, EditRole}; #[cfg(feature = "model")] -use http; -#[cfg(feature = "model")] use internal::prelude::*; #[cfg(feature = "model")] use model::guild::BanOptions; +#[cfg(feature = "model")] +use {http, utils}; #[cfg(feature = "model")] impl GuildId { @@ -73,7 +73,12 @@ impl GuildId { /// Gets a list of the guild's audit log entries #[inline] - pub fn audit_logs(&self) -> Result<AuditLogs> { http::get_audit_logs(self.0) } + pub fn audit_logs(&self, action_type: Option<u8>, + user_id: Option<UserId>, + before: Option<AuditLogEntryId>, + limit: Option<u8>) -> Result<AuditLogs> { + http::get_audit_logs(self.0, action_type, user_id.map(|u| u.0), before.map(|a| a.0), limit) + } /// Gets all of the guild's channels over the REST API. /// @@ -168,7 +173,9 @@ impl GuildId { /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[inline] pub fn create_role<F: FnOnce(EditRole) -> EditRole>(&self, f: F) -> Result<Role> { - http::create_role(self.0, &f(EditRole::default()).0) + let map = utils::hashmap_to_json_map(f(EditRole::default()).0); + + http::create_role(self.0, &map) } /// Deletes the current guild if the current account is the owner of the @@ -229,7 +236,9 @@ impl GuildId { /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] pub fn edit<F: FnOnce(EditGuild) -> EditGuild>(&mut self, f: F) -> Result<PartialGuild> { - http::edit_guild(self.0, &f(EditGuild::default()).0) + let map = utils::hashmap_to_json_map(f(EditGuild::default()).0); + + http::edit_guild(self.0, &map) } /// Edits an [`Emoji`]'s name in the guild. @@ -266,7 +275,9 @@ impl GuildId { #[inline] pub fn edit_member<F, U>(&self, user_id: U, f: F) -> Result<()> where F: FnOnce(EditMember) -> EditMember, U: Into<UserId> { - http::edit_member(self.0, user_id.into().0, &f(EditMember::default()).0) + let map = utils::hashmap_to_json_map(f(EditMember::default()).0); + + http::edit_member(self.0, user_id.into().0, &map) } /// Edits the current user's nickname for the guild. @@ -300,7 +311,9 @@ impl GuildId { #[inline] pub fn edit_role<F, R>(&self, role_id: R, f: F) -> Result<Role> where F: FnOnce(EditRole) -> EditRole, R: Into<RoleId> { - http::edit_role(self.0, role_id.into().0, &f(EditRole::default()).0) + let map = utils::hashmap_to_json_map(f(EditRole::default()).0); + + http::edit_role(self.0, role_id.into().0, &map) } /// Edits the order of [`Role`]s @@ -326,7 +339,7 @@ impl GuildId { /// Search the cache for the guild. #[cfg(feature = "cache")] - pub fn find(&self) -> Option<Arc<RwLock<Guild>>> { CACHE.read().unwrap().guild(*self) } + pub fn find(&self) -> Option<Arc<RwLock<Guild>>> { CACHE.read().guild(*self) } /// Requests the guild over REST. /// @@ -429,7 +442,7 @@ impl GuildId { /// [`utils::shard_id`]: ../utils/fn.shard_id.html #[cfg(all(feature = "cache", feature = "utils"))] #[inline] - pub fn shard_id(&self) -> u64 { ::utils::shard_id(self.0, CACHE.read().unwrap().shard_count) } + pub fn shard_id(&self) -> u64 { ::utils::shard_id(self.0, CACHE.read().shard_count) } /// Returns the Id of the shard associated with the guild. /// diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index c81e2a3..e5feee7 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -6,15 +6,13 @@ use model::*; #[cfg(feature = "model")] use std::borrow::Cow; #[cfg(all(feature = "cache", feature = "model"))] -use CACHE; -#[cfg(all(feature = "cache", feature = "model"))] use internal::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use http; #[cfg(all(feature = "builder", feature = "cache", feature = "model"))] use builder::EditMember; #[cfg(all(feature = "cache", feature = "model", feature = "utils"))] use utils::Colour; +#[cfg(all(feature = "cache", feature = "model"))] +use {CACHE, http, utils}; /// A trait for allowing both u8 or &str or (u8, &str) to be passed into the `ban` methods in `Guild` and `Member`. pub trait BanOptions { @@ -89,7 +87,7 @@ impl Member { return Ok(()); } - match http::add_member_role(self.guild_id.0, self.user.read().unwrap().id.0, role_id.0) { + match http::add_member_role(self.guild_id.0, self.user.read().id.0, role_id.0) { Ok(()) => { self.roles.push(role_id); @@ -110,9 +108,10 @@ impl Member { pub fn add_roles(&mut self, role_ids: &[RoleId]) -> Result<()> { self.roles.extend_from_slice(role_ids); - let map = EditMember::default().roles(&self.roles).0; + let builder = EditMember::default().roles(&self.roles); + let map = utils::hashmap_to_json_map(builder.0); - match http::edit_member(self.guild_id.0, self.user.read().unwrap().id.0, &map) { + match http::edit_member(self.guild_id.0, self.user.read().id.0, &map) { Ok(()) => Ok(()), Err(why) => { self.roles.retain(|r| !role_ids.contains(r)); @@ -150,7 +149,7 @@ impl Member { http::ban_user( self.guild_id.0, - self.user.read().unwrap().id.0, + self.user.read().id.0, dmd, &*reason, ) @@ -159,8 +158,8 @@ impl Member { /// Determines the member's colour. #[cfg(all(feature = "cache", feature = "utils"))] pub fn colour(&self) -> Option<Colour> { - let cache = CACHE.read().unwrap(); - let guild = try_opt!(cache.guilds.get(&self.guild_id)).read().unwrap(); + let cache = CACHE.read(); + let guild = try_opt!(cache.guilds.get(&self.guild_id)).read(); let mut roles = self.roles .iter() @@ -176,6 +175,27 @@ impl Member { .map(|r| r.colour) } + /// Returns the "default channel" of the guild for the member. + /// (This returns the first channel that can be read by the member, if there isn't + /// one returns `None`) + #[cfg(feature = "cache")] + pub fn default_channel(&self) -> Option<Arc<RwLock<GuildChannel>>> { + let guild = match self.guild_id.find() { + Some(guild) => guild, + None => return None, + }; + + let reader = guild.read(); + + for (cid, channel) in &reader.channels { + if reader.permissions_for(*cid, self.user.read().id).read_messages() { + return Some(channel.clone()); + } + } + + None + } + /// Calculates the member's display name. /// /// The nickname takes priority over the member's username if it exists. @@ -184,7 +204,7 @@ impl Member { self.nick .as_ref() .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(self.user.read().unwrap().name.clone())) + .unwrap_or_else(|| Cow::Owned(self.user.read().name.clone())) } /// Returns the DiscordTag of a Member, taking possible nickname into account. @@ -193,7 +213,7 @@ impl Member { format!( "{}#{}", self.display_name(), - self.user.read().unwrap().discriminator + self.user.read().discriminator ) } @@ -207,9 +227,9 @@ impl Member { /// [`EditMember`]: ../builder/struct.EditMember.html #[cfg(feature = "cache")] pub fn edit<F: FnOnce(EditMember) -> EditMember>(&self, f: F) -> Result<()> { - let map = f(EditMember::default()).0; + let map = utils::hashmap_to_json_map(f(EditMember::default()).0); - http::edit_member(self.guild_id.0, self.user.read().unwrap().id.0, &map) + http::edit_member(self.guild_id.0, self.user.read().id.0, &map) } /// Kick the member from the guild. @@ -252,17 +272,16 @@ impl Member { let has_perms = CACHE .read() - .unwrap() .guilds .get(&self.guild_id) - .map(|guild| guild.read().unwrap().has_perms(req)); + .map(|guild| guild.read().has_perms(req)); if let Some(false) = has_perms { return Err(Error::Model(ModelError::InvalidPermissions(req))); } } - self.guild_id.kick(self.user.read().unwrap().id) + self.guild_id.kick(self.user.read().id) } /// Returns the guild-level permissions for the member. @@ -312,7 +331,7 @@ impl Member { return Ok(()); } - match http::remove_member_role(self.guild_id.0, self.user.read().unwrap().id.0, role_id.0) { + match http::remove_member_role(self.guild_id.0, self.user.read().id.0, role_id.0) { Ok(()) => { self.roles.retain(|r| r.0 != role_id.0); @@ -332,9 +351,10 @@ impl Member { pub fn remove_roles(&mut self, role_ids: &[RoleId]) -> Result<()> { self.roles.retain(|r| !role_ids.contains(r)); - let map = EditMember::default().roles(&self.roles).0; + let builder = EditMember::default().roles(&self.roles); + let map = utils::hashmap_to_json_map(builder.0); - match http::edit_member(self.guild_id.0, self.user.read().unwrap().id.0, &map) { + match http::edit_member(self.guild_id.0, self.user.read().id.0, &map) { Ok(()) => Ok(()), Err(why) => { self.roles.extend_from_slice(role_ids); @@ -356,7 +376,6 @@ impl Member { .find() .map(|g| g .read() - .unwrap() .roles .values() .filter(|role| self.roles.contains(&role.id)) @@ -378,7 +397,7 @@ impl Member { /// [Ban Members]: permissions/constant.BAN_MEMBERS.html #[cfg(feature = "cache")] pub fn unban(&self) -> Result<()> { - http::remove_ban(self.guild_id.0, self.user.read().unwrap().id.0) + http::remove_ban(self.guild_id.0, self.user.read().id.0) } } @@ -394,6 +413,6 @@ impl Display for Member { /// // This is in the format of `<@USER_ID>`. fn fmt(&self, f: &mut Formatter) -> FmtResult { - Display::fmt(&self.user.read().unwrap().mention(), f) + Display::fmt(&self.user.read().mention(), f) } } diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index fe9bc6a..ab74109 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -1,5 +1,4 @@ mod emoji; -mod feature; mod guild_id; mod integration; mod member; @@ -8,7 +7,6 @@ mod role; mod audit_log; pub use self::emoji::*; -pub use self::feature::*; pub use self::guild_id::*; pub use self::integration::*; pub use self::member::*; @@ -21,7 +19,6 @@ use serde::de::Error as DeError; use serde_json; use super::utils::*; use model::*; -use std; #[cfg(all(feature = "cache", feature = "model"))] use CACHE; @@ -31,6 +28,8 @@ use http; use builder::{EditGuild, EditMember, EditRole}; #[cfg(feature = "model")] use constants::LARGE_THRESHOLD; +#[cfg(feature = "model")] +use std; /// A representation of a banning of a user. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Hash)] @@ -62,8 +61,15 @@ pub struct Guild { /// VIP features enabled for the guild. Can be obtained through the /// [Discord Partnership] website. /// + /// The following is a list of known features: + /// + /// - `INVITE_SPLASH` + /// - `VANITY_URL` + /// - `VERIFIED` + /// - `VIP_REGIONS` + /// /// [Discord Partnership]: https://discordapp.com/partners - pub features: Vec<Feature>, + pub features: Vec<String>, /// The hash of the icon used by the guild. /// /// In the client, this appears on the guild list on the left-hand side. @@ -124,13 +130,10 @@ pub struct Guild { #[cfg(feature = "model")] impl Guild { - #[cfg(feature = "cache")] - /// Returns the "default" channel of the guild. - /// (This returns the first channel that can be read by the bot, if there isn't one, + /// Returns the "default" channel of the guild for the passed user id. + /// (This returns the first channel that can be read by the user, if there isn't one, /// returns `None`) - pub fn default_channel(&self) -> Option<GuildChannel> { - let uid = CACHE.read().unwrap().user.id; - + pub fn default_channel(&self, uid: UserId) -> Option<Arc<RwLock<GuildChannel>>> { for (cid, channel) in &self.channels { if self.permissions_in(*cid, uid).read_messages() { return Some(channel.read().unwrap().clone()); @@ -145,7 +148,7 @@ impl Guild { /// returns `None`) /// Note however that this is very costy if used in a server with lots of channels, /// members, or both. - pub fn default_channel_guaranteed(&self) -> Option<GuildChannel> { + pub fn default_channel_guaranteed(&self) -> Option<Arc<RwLock<GuildChannel>>> { for (cid, channel) in &self.channels { for memid in self.members.keys() { if self.permissions_in(*cid, *memid).read_messages() { @@ -239,7 +242,12 @@ impl Guild { /// /// [`AuditLogs`]: audit_log/struct.AuditLogs.html #[inline] - pub fn audit_logs(&self) -> Result<AuditLogs> { self.id.audit_logs() } + pub fn audit_logs(&self, action_type: Option<u8>, + user_id: Option<UserId>, + before: Option<AuditLogEntryId>, + limit: Option<u8>) -> Result<AuditLogs> { + self.id.audit_logs(action_type, user_id, before, limit) + } /// Gets all of the guild's channels over the REST API. /// @@ -402,7 +410,7 @@ impl Guild { pub fn delete(&self) -> Result<PartialGuild> { #[cfg(feature = "cache")] { - if self.owner_id != CACHE.read().unwrap().user.id { + if self.owner_id != CACHE.read().user.id { let req = Permissions::MANAGE_GUILD; return Err(Error::Model(ModelError::InvalidPermissions(req))); @@ -739,9 +747,9 @@ impl Guild { self.members .values() .find(|member| { - let name_matches = member.user.read().unwrap().name == name; + let name_matches = member.user.read().name == name; let discrim_matches = match discrim { - Some(discrim) => member.user.read().unwrap().discriminator == discrim, + Some(discrim) => member.user.read().discriminator == discrim, None => true, }; @@ -756,11 +764,6 @@ impl Guild { /// Retrieves all [`Member`] that start with a given `String`. /// - /// If the prefix is "zey", following results are possible: - /// - "zey", "zeyla", "zey mei" - /// If 'case_sensitive' is false, the following are not found: - /// - "Zey", "ZEYla", "zeY mei" - /// /// `sorted` decides whether the best early match of the `prefix` /// should be the criteria to sort the result. /// For the `prefix` "zey" and the unsorted result: @@ -775,9 +778,9 @@ impl Guild { .filter(|member| if case_sensitive { - member.user.read().unwrap().name.starts_with(prefix) + member.user.read().name.starts_with(prefix) } else { - starts_with_case_insensitive(&member.user.read().unwrap().name, prefix) + starts_with_case_insensitive(&member.user.read().name, prefix) } || member.nick.as_ref() @@ -794,24 +797,24 @@ impl Guild { .sort_by(|a, b| { let name_a = match a.nick { Some(ref nick) => { - if contains_case_insensitive(&a.user.read().unwrap().name[..], prefix) { - a.user.read().unwrap().name.clone() + if contains_case_insensitive(&a.user.read().name[..], prefix) { + a.user.read().name.clone() } else { nick.clone() } }, - None => a.user.read().unwrap().name.clone(), + None => a.user.read().name.clone(), }; let name_b = match b.nick { Some(ref nick) => { - if contains_case_insensitive(&b.user.read().unwrap().name[..], prefix) { - b.user.read().unwrap().name.clone() + if contains_case_insensitive(&b.user.read().name[..], prefix) { + b.user.read().name.clone() } else { nick.clone() } }, - None => b.user.read().unwrap().name.clone(), + None => b.user.read().name.clone(), }; closest_to_origin(prefix, &name_a[..], &name_b[..]) @@ -850,9 +853,9 @@ impl Guild { .filter(|member| if case_sensitive { - member.user.read().unwrap().name.contains(substring) + member.user.read().name.contains(substring) } else { - contains_case_insensitive(&member.user.read().unwrap().name, substring) + contains_case_insensitive(&member.user.read().name, substring) } || member.nick.as_ref() @@ -870,24 +873,24 @@ impl Guild { .sort_by(|a, b| { let name_a = match a.nick { Some(ref nick) => { - if contains_case_insensitive(&a.user.read().unwrap().name[..], substring) { - a.user.read().unwrap().name.clone() + if contains_case_insensitive(&a.user.read().name[..], substring) { + a.user.read().name.clone() } else { nick.clone() } }, - None => a.user.read().unwrap().name.clone(), + None => a.user.read().name.clone(), }; let name_b = match b.nick { Some(ref nick) => { - if contains_case_insensitive(&b.user.read().unwrap().name[..], substring) { - b.user.read().unwrap().name.clone() + if contains_case_insensitive(&b.user.read().name[..], substring) { + b.user.read().name.clone() } else { nick.clone() } }, - None => b.user.read().unwrap().name.clone(), + None => b.user.read().name.clone(), }; closest_to_origin(substring, &name_a[..], &name_b[..]) @@ -919,17 +922,17 @@ impl Guild { .values() .filter(|member| { if case_sensitive { - member.user.read().unwrap().name.contains(substring) + member.user.read().name.contains(substring) } else { - contains_case_insensitive(&member.user.read().unwrap().name, substring) + contains_case_insensitive(&member.user.read().name, substring) } }).collect(); if sorted { members .sort_by(|a, b| { - let name_a = &a.user.read().unwrap().name; - let name_b = &b.user.read().unwrap().name; + let name_a = &a.user.read().name; + let name_b = &b.user.read().name; closest_to_origin(substring, &name_a[..], &name_b[..]) }); members @@ -978,14 +981,14 @@ impl Guild { Some(ref nick) => { nick.clone() }, - None => a.user.read().unwrap().name.clone(), + None => a.user.read().name.clone(), }; let name_b = match b.nick { Some(ref nick) => { nick.clone() }, - None => b.user.read().unwrap().name.clone(), + None => b.user.read().name.clone(), }; closest_to_origin(substring, &name_a[..], &name_b[..]) @@ -1111,7 +1114,7 @@ impl Guild { } else { warn!( "(╯°□°)╯︵ ┻━┻ {} on {} has non-existent role {:?}", - member.user.read().unwrap().id, + member.user.read().id, self.id, role ); @@ -1124,7 +1127,7 @@ impl Guild { } if let Some(channel) = self.channels.get(&channel_id) { - let channel = channel.read().unwrap(); + let channel = channel.read(); // If this is a text channel, then throw out voice permissions. if channel.kind == ChannelType::Text { @@ -1362,16 +1365,16 @@ impl Guild { /// use serenity::CACHE; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { + /// fn message(&self, _: Context, msg: Message) { /// if let Some(arc) = msg.guild_id().unwrap().find() { - /// if let Some(role) = arc.read().unwrap().role_by_name("role_name") { + /// if let Some(role) = arc.read().role_by_name("role_name") { /// println!("{:?}", role); /// } /// } /// } /// } /// - /// let mut client = Client::new("token", Handler); + /// let mut client = Client::new("token", Handler).unwrap(); /// /// client.start().unwrap(); /// ``` @@ -1433,7 +1436,7 @@ impl<'de> Deserialize<'de> for Guild { .map_err(DeError::custom)?; let features = map.remove("features") .ok_or_else(|| DeError::custom("expected guild features")) - .and_then(serde_json::from_value::<Vec<Feature>>) + .and_then(serde_json::from_value::<Vec<String>>) .map_err(DeError::custom)?; let icon = match map.remove("icon") { Some(v) => Option::<String>::deserialize(v).map_err(DeError::custom)?, @@ -1523,11 +1526,13 @@ impl<'de> Deserialize<'de> for Guild { } /// Checks if a `&str` contains another `&str`. +#[cfg(feature = "model")] fn contains_case_insensitive(to_look_at: &str, to_find: &str) -> bool { to_look_at.to_lowercase().contains(to_find) } /// Checks if a `&str` starts with another `&str`. +#[cfg(feature = "model")] fn starts_with_case_insensitive(to_look_at: &str, to_find: &str) -> bool { to_look_at.to_lowercase().starts_with(to_find) } @@ -1539,6 +1544,7 @@ fn starts_with_case_insensitive(to_look_at: &str, to_find: &str) -> bool { /// expected to contain `origin` as substring. /// If not, using `closest_to_origin` would sort these /// the end. +#[cfg(feature = "model")] fn closest_to_origin(origin: &str, word_a: &str, word_b: &str) -> std::cmp::Ordering { let value_a = match word_a.find(origin) { Some(value) => value + word_a.len(), diff --git a/src/model/guild/partial_guild.rs b/src/model/guild/partial_guild.rs index cb2f8ef..c4181fd 100644 --- a/src/model/guild/partial_guild.rs +++ b/src/model/guild/partial_guild.rs @@ -17,7 +17,12 @@ pub struct PartialGuild { pub embed_channel_id: Option<ChannelId>, pub embed_enabled: bool, #[serde(deserialize_with = "deserialize_emojis")] pub emojis: HashMap<EmojiId, Emoji>, - pub features: Vec<Feature>, + /// Features enabled for the guild. + /// + /// Refer to [`Guild::features`] for more information. + /// + /// [`Guild::features`]: struct.Guild.html#structfield.features + pub features: Vec<String>, pub icon: Option<String>, pub mfa_level: u64, pub name: String, @@ -451,7 +456,7 @@ impl PartialGuild { /// use serenity::CACHE; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { + /// fn message(&self, _: Context, msg: Message) { /// if let Some(role) = /// msg.guild_id().unwrap().get().unwrap().role_by_name("role_name") { /// println!("Obtained role's reference: {:?}", role); @@ -459,7 +464,7 @@ impl PartialGuild { /// } /// } /// - /// let mut client = Client::new("token", Handler); + /// let mut client = Client::new("token", Handler).unwrap(); /// /// client.start().unwrap(); /// ``` diff --git a/src/model/guild/role.rs b/src/model/guild/role.rs index 7ac7019..3ed718e 100644 --- a/src/model/guild/role.rs +++ b/src/model/guild/role.rs @@ -106,8 +106,8 @@ impl Role { /// [`ModelError::GuildNotFound`]: enum.ModelError.html#variant.GuildNotFound #[cfg(feature = "cache")] pub fn find_guild(&self) -> Result<GuildId> { - for guild in CACHE.read().unwrap().guilds.values() { - let guild = guild.read().unwrap(); + for guild in CACHE.read().guilds.values() { + let guild = guild.read(); if guild.roles.contains_key(&RoleId(self.id.0)) { return Ok(guild.id); @@ -168,10 +168,10 @@ impl RoleId { /// Search the cache for the role. #[cfg(feature = "cache")] pub fn find(&self) -> Option<Role> { - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); for guild in cache.guilds.values() { - let guild = guild.read().unwrap(); + let guild = guild.read(); if !guild.roles.contains_key(self) { continue; diff --git a/src/model/invite.rs b/src/model/invite.rs index b4f326c..8ef848c 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -6,9 +6,9 @@ use super::{Permissions, utils as model_utils}; #[cfg(feature = "model")] use builder::CreateInvite; #[cfg(feature = "model")] -use http; -#[cfg(feature = "model")] use internal::prelude::*; +#[cfg(feature = "model")] +use {http, utils}; /// Information about an invite code. /// @@ -73,7 +73,9 @@ impl Invite { } } - http::create_invite(channel_id.0, &f(CreateInvite::default()).0) + let map = utils::hashmap_to_json_map(f(CreateInvite::default()).0); + + http::create_invite(channel_id.0, &map) } /// Deletes the invite. diff --git a/src/model/misc.rs b/src/model/misc.rs index 625e202..51c304d 100644 --- a/src/model/misc.rs +++ b/src/model/misc.rs @@ -174,7 +174,7 @@ impl FromStr for Role { #[cfg(all(feature = "model", feature = "utils"))] #[derive(Debug)] pub enum RoleIdParseError { - NotPresentInCache, + InvalidFormat, } #[cfg(all(feature = "model", feature = "utils"))] @@ -188,7 +188,7 @@ impl StdError for RoleIdParseError { use self::RoleIdParseError::*; match *self { - NotPresentInCache => "not present in cache", + InvalidFormat => "invalid role id format", } } } @@ -198,9 +198,10 @@ impl FromStr for RoleId { type Err = RoleIdParseError; fn from_str(s: &str) -> StdResult<Self, Self::Err> { - utils::parse_role(s) - .ok_or_else(|| RoleIdParseError::NotPresentInCache) - .map(RoleId) + Ok(match utils::parse_role(s) { + Some(id) => RoleId(id), + None => s.parse::<u64>().map(RoleId).map_err(|_| RoleIdParseError::InvalidFormat)?, + }) } } diff --git a/src/model/mod.rs b/src/model/mod.rs index 0817094..64987e1 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -32,11 +32,12 @@ pub use self::voice::*; pub use self::webhook::*; use chrono::NaiveDateTime; +use parking_lot::RwLock; use self::utils::*; use serde::de::Visitor; use std::collections::HashMap; use std::fmt::{Display, Formatter, Result as FmtResult}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use internal::prelude::*; #[cfg(feature = "utils")] @@ -67,7 +68,7 @@ macro_rules! id_u64 { self } } - + impl From<u64> for $name { fn from(id_as_u64: u64) -> $name { $name(id_as_u64) diff --git a/src/model/user.rs b/src/model/user.rs index bf725cc..9f36d0f 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -7,18 +7,22 @@ use model::misc::Mentionable; #[cfg(feature = "model")] use chrono::NaiveDateTime; +#[cfg(all(feature = "cache", feature = "model"))] +use parking_lot::RwLock; #[cfg(feature = "model")] use std::fmt::Write; #[cfg(feature = "model")] use std::mem; #[cfg(all(feature = "cache", feature = "model"))] -use std::sync::{Arc, RwLock}; +use std::sync::Arc; #[cfg(feature = "model")] use builder::{CreateMessage, EditProfile}; #[cfg(all(feature = "cache", feature = "model"))] use CACHE; #[cfg(feature = "model")] use http::{self, GuildPagination}; +#[cfg(feature = "model")] +use utils; /// Information about the current user. #[derive(Clone, Default, Debug, Deserialize)] @@ -46,7 +50,7 @@ impl CurrentUser { /// ```rust,no_run /// # use serenity::CACHE; /// # - /// # let cache = CACHE.read().unwrap(); + /// # let cache = CACHE.read(); /// # /// // assuming the cache has been unlocked /// let user = &cache.user; @@ -80,18 +84,20 @@ impl CurrentUser { /// /// let avatar = serenity::utils::read_image("./avatar.png").unwrap(); /// - /// CACHE.write().unwrap().user.edit(|p| p.avatar(Some(&avatar))); + /// CACHE.write().user.edit(|p| p.avatar(Some(&avatar))); /// ``` pub fn edit<F>(&mut self, f: F) -> Result<()> where F: FnOnce(EditProfile) -> EditProfile { - let mut map = Map::new(); - map.insert("username".to_string(), Value::String(self.name.clone())); + let mut map = HashMap::new(); + map.insert("username", Value::String(self.name.clone())); if let Some(email) = self.email.as_ref() { - map.insert("email".to_string(), Value::String(email.clone())); + map.insert("email", Value::String(email.clone())); } - match http::edit_profile(&f(EditProfile(map)).0) { + let map = utils::hashmap_to_json_map(f(EditProfile(map)).0); + + match http::edit_profile(&map) { Ok(new) => { let _ = mem::replace(self, new); @@ -123,7 +129,7 @@ impl CurrentUser { /// ```rust,no_run /// # use serenity::CACHE; /// # - /// # let cache = CACHE.read().unwrap(); + /// # let cache = CACHE.read(); /// # /// // assuming the cache has been unlocked /// let user = &cache.user; @@ -151,7 +157,7 @@ impl CurrentUser { /// ```rust,no_run /// # use serenity::CACHE; /// # - /// # let mut cache = CACHE.write().unwrap(); + /// # let mut cache = CACHE.write(); /// /// use serenity::model::permissions::Permissions; /// @@ -174,7 +180,7 @@ impl CurrentUser { /// ```rust,no_run /// # use serenity::CACHE; /// # - /// # let mut cache = CACHE.write().unwrap(); + /// # let mut cache = CACHE.write(); /// /// use serenity::model::Permissions; /// @@ -230,7 +236,7 @@ impl CurrentUser { /// ```rust,no_run /// # use serenity::CACHE; /// # - /// # let cache = CACHE.read().unwrap(); + /// # let cache = CACHE.read(); /// # /// // assuming the cache has been unlocked /// let user = &cache.user; @@ -254,7 +260,7 @@ impl CurrentUser { /// ```rust,no_run /// # use serenity::CACHE; /// # - /// # let cache = CACHE.read().unwrap(); + /// # let cache = CACHE.read(); /// # /// // assuming the cache has been unlocked /// println!("The current user's distinct identifier is {}", cache.user.tag()); @@ -424,27 +430,23 @@ impl User { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { + /// fn message(&self, _: Context, msg: Message) { /// if msg.content == "~help" { - /// let url = match CACHE.read() { - /// Ok(v) => { - /// match v.user.invite_url(Permissions::empty()) { - /// Ok(v) => v, - /// Err(why) => { - /// println!("Error creating invite url: {:?}", why); - /// - /// return; - /// }, - /// } - /// }, + /// let cache = CACHE.read(); + /// + /// let url = match cache.user.invite_url(Permissions::empty()) { + /// Ok(v) => v, /// Err(why) => { - /// println!("Error reading from CACHE: {:?}", why); + /// println!("Error creating invite url: {:?}", why); /// /// return; - /// } + /// }, /// }; - /// let help = format!("Helpful info here. Invite me with this link: <{}>", - /// url); + /// + /// let help = format!( + /// "Helpful info here. Invite me with this link: <{}>", + /// url, + /// ); /// /// match msg.author.direct_message(|m| m.content(&help)) { /// Ok(_) => { @@ -496,12 +498,12 @@ impl User { let private_channel_id = feature_cache! { { let finding = { - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); let finding = cache.private_channels .values() - .map(|ch| ch.read().unwrap()) - .find(|ch| ch.recipient.read().unwrap().id == self.id) + .map(|ch| ch.read()) + .find(|ch| ch.recipient.read().id == self.id) .map(|ch| ch.id); finding @@ -596,11 +598,10 @@ impl User { GuildContainer::Id(_guild_id) => { feature_cache! {{ CACHE.read() - .unwrap() .guilds .get(&_guild_id) .map(|g| { - g.read().unwrap().members.get(&self.id) + g.read().members.get(&self.id) .map(|m| m.roles.contains(&role_id)) .unwrap_or(false) }) @@ -629,11 +630,12 @@ impl User { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, _: Message) { + /// fn message(&self, _: Context, _: Message) { /// // normal message handling here /// } /// } - /// let mut client = Client::new("token", Handler); + /// + /// let mut client = Client::new("token", Handler).unwrap(); /// # /// use serenity::model::UserId; /// use serenity::CACHE; @@ -651,11 +653,11 @@ impl User { /// loop { /// thread::sleep(duration); /// - /// let cache = CACHE.read().unwrap(); + /// let cache = CACHE.read(); /// /// for id in &special_users { /// if let Some(user) = cache.user(*id) { - /// if let Err(why) = user.write().unwrap().refresh() { + /// if let Err(why) = user.write().refresh() { /// println!("Error refreshing {}: {:?}", id, why); /// } /// } @@ -700,7 +702,7 @@ impl User { /// struct Handler; /// /// impl EventHandler for Handler { - /// fn on_message(&self, _: Context, msg: Message) { + /// fn message(&self, _: Context, msg: Message) { /// if msg.content == "!mytag" { /// let content = MessageBuilder::new() /// .push("Your tag is ") @@ -711,7 +713,9 @@ impl User { /// } /// } /// } - /// let mut client = Client::new("token", Handler); client.start().unwrap(); + /// let mut client = Client::new("token", Handler).unwrap(); + /// + /// client.start().unwrap(); /// ``` #[inline] pub fn tag(&self) -> String { tag(&self.name, self.discriminator) } @@ -741,7 +745,7 @@ impl UserId { /// Search the cache for the user with the Id. #[cfg(feature = "cache")] - pub fn find(&self) -> Option<Arc<RwLock<User>>> { CACHE.read().unwrap().user(*self) } + pub fn find(&self) -> Option<Arc<RwLock<User>>> { CACHE.read().user(*self) } /// Gets a user by its Id over the REST API. /// @@ -750,8 +754,8 @@ impl UserId { pub fn get(&self) -> Result<User> { #[cfg(feature = "cache")] { - if let Some(user) = CACHE.read().unwrap().user(*self) { - return Ok(user.read().unwrap().clone()); + if let Some(user) = CACHE.read().user(*self) { + return Ok(user.read().clone()); } } @@ -771,12 +775,12 @@ impl<'a> From<&'a CurrentUser> for UserId { impl From<Member> for UserId { /// Gets the Id of a `Member`. - fn from(member: Member) -> UserId { member.user.read().unwrap().id } + fn from(member: Member) -> UserId { member.user.read().id } } impl<'a> From<&'a Member> for UserId { /// Gets the Id of a `Member`. - fn from(member: &Member) -> UserId { member.user.read().unwrap().id } + fn from(member: &Member) -> UserId { member.user.read().id } } impl From<User> for UserId { diff --git a/src/model/utils.rs b/src/model/utils.rs index a64156b..d366e5f 100644 --- a/src/model/utils.rs +++ b/src/model/utils.rs @@ -1,6 +1,7 @@ +use parking_lot::RwLock; use serde::de::Error as DeError; use std::collections::HashMap; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use super::*; #[cfg(feature = "cache")] @@ -44,7 +45,7 @@ pub fn deserialize_members<'de, D: Deserializer<'de>>( let mut members = HashMap::new(); for member in vec { - let user_id = member.user.read().unwrap().id; + let user_id = member.user.read().id; members.insert(user_id, member); } @@ -73,8 +74,8 @@ pub fn deserialize_private_channels<'de, D: Deserializer<'de>>( for private_channel in vec { let id = match private_channel { - Channel::Group(ref group) => group.read().unwrap().channel_id, - Channel::Private(ref channel) => channel.read().unwrap().id, + Channel::Group(ref group) => group.read().channel_id, + Channel::Private(ref channel) => channel.read().id, Channel::Guild(_) => unreachable!("Guild private channel decode"), Channel::Category(_) => unreachable!("Channel category private channel decode"), }; @@ -147,7 +148,7 @@ pub fn deserialize_voice_states<'de, D: Deserializer<'de>>( #[cfg(all(feature = "cache", feature = "model"))] pub fn user_has_perms(channel_id: ChannelId, mut permissions: Permissions) -> Result<bool> { - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); let current_user = &cache.user; let channel = match cache.channel(channel_id) { @@ -156,7 +157,7 @@ pub fn user_has_perms(channel_id: ChannelId, mut permissions: Permissions) -> Re }; let guild_id = match channel { - Channel::Guild(channel) => channel.read().unwrap().guild_id, + Channel::Guild(channel) => channel.read().guild_id, Channel::Group(_) | Channel::Private(_) | Channel::Category(_) => { // Both users in DMs, and all users in groups and maybe all channels in categories will // have the same diff --git a/src/model/webhook.rs b/src/model/webhook.rs index c8ae1b0..e00d018 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -7,7 +7,7 @@ use builder::ExecuteWebhook; #[cfg(feature = "model")] use internal::prelude::*; #[cfg(feature = "model")] -use http; +use {http, utils}; /// A representation of a webhook, which is a low-effort way to post messages to /// channels. They do not necessarily require a bot user or authentication to @@ -183,12 +183,9 @@ impl Webhook { wait: bool, f: F) -> Result<Option<Message>> { - http::execute_webhook( - self.id.0, - &self.token, - wait, - &f(ExecuteWebhook::default()).0, - ) + let map = utils::hashmap_to_json_map(f(ExecuteWebhook::default()).0); + + http::execute_webhook(self.id.0, &self.token, wait, &map) } /// Retrieves the latest information about the webhook, editing the diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 4b36b73..5783c41 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -13,18 +13,32 @@ pub use self::message_builder::{Content, ContentModifier, MessageBuilder}; pub use super::builder; use base64; +use internal::prelude::*; +use model::{EmojiId, EmojiIdentifier}; +use std::collections::HashMap; use std::ffi::OsStr; use std::fs::File; +use std::hash::Hash; use std::io::Read; use std::path::Path; -use internal::prelude::*; -use model::{EmojiId, EmojiIdentifier}; #[cfg(feature = "cache")] use cache::Cache; #[cfg(feature = "cache")] use CACHE; +/// Converts a HashMap into a final `serde_json::Map` representation. +pub fn hashmap_to_json_map<T>(map: HashMap<T, Value>) -> Map<String, Value> + where T: Eq + Hash + ToString { + let mut json_map = Map::new(); + + for (key, value) in map.into_iter() { + json_map.insert(key.to_string(), value); + } + + json_map +} + /// Determines if a name is NSFW. /// /// This checks that the name is either `"nsfw"` or, for names longer than that, @@ -455,7 +469,7 @@ pub fn shard_id(guild_id: u64, shard_count: u64) -> u64 { (guild_id >> 22) % sha #[cfg(feature = "cache")] pub fn with_cache<T, F>(f: F) -> T where F: Fn(&Cache) -> T { - let cache = CACHE.read().unwrap(); + let cache = CACHE.read(); f(&cache) } @@ -476,6 +490,6 @@ pub fn with_cache<T, F>(f: F) -> T #[cfg(feature = "cache")] pub fn with_cache_mut<T, F>(mut f: F) -> T where F: FnMut(&mut Cache) -> T { - let mut cache = CACHE.write().unwrap(); + let mut cache = CACHE.write(); f(&mut cache) } diff --git a/src/voice/connection.rs b/src/voice/connection.rs index 07033cc..e507f41 100644 --- a/src/voice/connection.rs +++ b/src/voice/connection.rs @@ -1,4 +1,5 @@ use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; +use parking_lot::Mutex; use opus::{ packet as opus_packet, Application as CodingMode, @@ -11,7 +12,7 @@ use std::collections::HashMap; use std::io::Write; use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; use std::sync::mpsc::{self, Receiver as MpscReceiver, Sender as MpscSender}; -use std::sync::{Arc, Mutex}; +use std::sync::Arc; use std::thread::{self, Builder as ThreadBuilder, JoinHandle}; use std::time::Duration; use super::audio::{AudioReceiver, AudioSource, AudioType, HEADER_LEN, SAMPLE_RATE}; @@ -223,10 +224,7 @@ impl Connection { // Send the voice websocket keepalive if it's time if self.keepalive_timer.check() { - self.client - .lock() - .unwrap() - .send_json(&payload::build_keepalive())?; + self.client.lock().send_json(&payload::build_keepalive())?; } // Send UDP keepalive if it's time @@ -357,10 +355,7 @@ impl Connection { self.speaking = speaking; - self.client - .lock() - .unwrap() - .send_json(&payload::build_speaking(speaking)) + self.client.lock().send_json(&payload::build_speaking(speaking)) } } @@ -452,7 +447,7 @@ fn start_threads(client: Arc<Mutex<Client>>, udp: &UdpSocket) -> Result<ThreadIt let ws_thread = ThreadBuilder::new() .name(format!("{} WS", thread_name)) .spawn(move || loop { - while let Ok(Some(msg)) = client.lock().unwrap().recv_json(VoiceEvent::decode) { + while let Ok(Some(msg)) = client.lock().recv_json(VoiceEvent::decode) { if tx_clone.send(ReceiverStatus::Websocket(msg)).is_ok() { return; } diff --git a/tests/resources/guild_create_features.json b/tests/resources/guild_create_features.json new file mode 100644 index 0000000..c522d7f --- /dev/null +++ b/tests/resources/guild_create_features.json @@ -0,0 +1,32 @@ +{ + "voice_states": [], + "verification_level": 2, + "unavailable": false, + "system_channel_id": "9876543210", + "splash": "abc", + "roles": [], + "region": "us-east", + "presences": [], + "owner_id": "157203992353570816", + "name": "abcdefg, hijklmnop, qrs, tuv, wx, y and z, now i know my abc's, next time won't you sing with me", + "mfa_level": 0, + "members": [], + "member_count": 585, + "large": true, + "joined_at": "2017-01-15T04:31:06.080000+00:00", + "id": "0123456789", + "icon": "def", + "features": [ + "VIP_REGIONS", + "VERIFIED", + "VANITY_URL", + "INVITE_SPLASH" + ], + "explicit_content_filter": 0, + "emojis": [], + "default_message_notifications": 1, + "channels": [], + "application_id": null, + "afk_timeout": 300, + "afk_channel_id": null +} diff --git a/tests/test_channels.rs b/tests/test_channels.rs index d871d5d..86523dc 100644 --- a/tests/test_channels.rs +++ b/tests/test_channels.rs @@ -1,12 +1,14 @@ #![cfg(feature = "model")] +extern crate parking_lot; extern crate serenity; #[cfg(feature = "utils")] mod utils { + use parking_lot::RwLock; use serenity::model::*; use std::collections::HashMap; - use std::sync::{Arc, RwLock}; + use std::sync::Arc; fn group() -> Group { Group { diff --git a/tests/test_create_embed.rs b/tests/test_create_embed.rs index 5c51dc6..dd84154 100644 --- a/tests/test_create_embed.rs +++ b/tests/test_create_embed.rs @@ -8,7 +8,7 @@ extern crate serenity; use serde_json::Value; use serenity::model::{Embed, EmbedField, EmbedImage}; use serenity::utils::builder::CreateEmbed; -use serenity::utils::Colour; +use serenity::utils::{self, Colour}; #[test] fn test_from_embed() { @@ -51,7 +51,7 @@ fn test_from_embed() { .title("still a hakase") .url("https://i.imgur.com/XfWpfCV.gif"); - let built = Value::Object(builder.0); + let built = Value::Object(utils::hashmap_to_json_map(builder.0)); let obj = json!({ "color": 0xFF0011, diff --git a/tests/test_deser.rs b/tests/test_deser.rs index 1024492..5288da2 100644 --- a/tests/test_deser.rs +++ b/tests/test_deser.rs @@ -165,3 +165,11 @@ fn webhooks_update() { fn message_type_7() { p!(MessageCreateEvent, "message_type_7"); } + +// Ensure string features are properly deserialized. +// +// Change made due to a new feature being added and not being in the enum. +#[test] +fn guild_features_deser() { + p!(GuildCreateEvent, "guild_create_features"); +} diff --git a/tests/test_formatters.rs b/tests/test_formatters.rs index b48c648..fbc60ca 100644 --- a/tests/test_formatters.rs +++ b/tests/test_formatters.rs @@ -1,3 +1,4 @@ +extern crate parking_lot; extern crate serenity; use serenity::model::*; @@ -14,8 +15,9 @@ fn test_formatters() { #[cfg(feature = "utils")] #[test] fn test_mention() { + use parking_lot::RwLock; use serenity::utils::Colour; - use std::sync::{Arc, RwLock}; + use std::sync::Arc; let channel = Channel::Guild(Arc::new(RwLock::new(GuildChannel { bitrate: None, |