diff options
| author | Zeyla Hellyer <[email protected]> | 2018-02-04 07:50:53 -0800 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2018-02-04 07:54:31 -0800 |
| commit | a9966371def331cd848f642e222627ee9decf354 (patch) | |
| tree | 316c75854ddea79230f98b66708c3f815b836227 /src | |
| parent | Partially revert the video url change (diff) | |
| download | serenity-a9966371def331cd848f642e222627ee9decf354.tar.xz serenity-a9966371def331cd848f642e222627ee9decf354.zip | |
Rewrite the library to use Futures
Rewrites the library to use Futures. This rewrites all of the gateway,
client, cache, most model methods, HTTP, and in a later commit the
framework and voice.
HTTP has been mostly rewritten to be ergonomic so that it can be used by
the user, and has been upgraded to hyper v0.11.
The gateway now uses `tokio-tungstenite` v0.4.
The client isn't yet in a working state.
The models now have a `client` optionally attached to them to make use
of `http` and `cache`-utilizing methods. The client isn't needed, in the
case that someone is using the library just to deserialize models.
The cache is now built around `Rc`s and `RefCell`s, instead of `Arc`s
and `RwLock`s.
Refer to example 01 for a working example. Much of the library still
doesn't work.
Diffstat (limited to 'src')
58 files changed, 6147 insertions, 9007 deletions
diff --git a/src/cache/cache_update.rs b/src/cache/cache_update.rs index a05cc3d..bdc0eea 100644 --- a/src/cache/cache_update.rs +++ b/src/cache/cache_update.rs @@ -1,3 +1,8 @@ +use model::prelude::*; +use std::cell::RefCell; +use std::collections::hash_map::{Entry, HashMap}; +use std::rc::Rc; +use std::mem; use super::Cache; pub(crate) trait CacheUpdate { @@ -5,3 +10,640 @@ pub(crate) trait CacheUpdate { fn update(&mut self, &mut Cache) -> Option<Self::Output>; } + +impl CacheUpdate for ChannelCreateEvent { + type Output = Channel; + + fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { + match self.channel { + Channel::Group(ref group) => { + let group = Rc::clone(group); + + let channel_id = { + let mut writer = group.borrow_mut(); + + for (recipient_id, recipient) in &mut writer.recipients { + cache.update_user_entry(&recipient.borrow()); + + *recipient = Rc::clone(&cache.users[recipient_id]); + } + + writer.channel_id + }; + + let old = cache.groups.insert(channel_id, group); + + old.map(Channel::Group) + }, + Channel::Guild(ref channel) => { + let (guild_id, channel_id) = { + let channel = channel.borrow(); + + (channel.guild_id, channel.id) + }; + + cache.channels.insert(channel_id, Rc::clone(channel)); + + cache + .guilds + .get_mut(&guild_id) + .and_then(|guild| { + let mut guild = guild.borrow_mut(); + + guild.channels.insert(channel_id, Rc::clone(channel)) + }) + .map(Channel::Guild) + }, + Channel::Private(ref channel) => { + let channel_id = channel.borrow().id; + + if let Some(channel) = cache.private_channels.get(&channel_id) { + return Some(Channel::Private(Rc::clone(channel))); + } + + let channel = Rc::clone(channel); + + let id = { + let mut writer = channel.borrow_mut(); + + let user_id = { + let user = writer.recipient.borrow(); + + cache.update_user_entry(&*user); + + user.id + }; + + writer.recipient = Rc::clone(&cache.users[&user_id]); + + writer.id + }; + + let old = cache.private_channels.insert(id, Rc::clone(&channel)); + old.map(Channel::Private) + }, + Channel::Category(ref category) => cache + .categories + .insert(category.borrow().id, Rc::clone(category)) + .map(Channel::Category), + } + } +} + +impl CacheUpdate for ChannelDeleteEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + match self.channel { + Channel::Guild(ref channel) => { + let (guild_id, channel_id) = { + let channel = channel.borrow(); + + (channel.guild_id, channel.id) + }; + + cache.channels.remove(&channel_id); + + if let Some(guild) = cache.guilds.get(&guild_id) { + let mut guild = guild.borrow_mut(); + + guild.channels.remove(&channel_id); + } + }, + Channel::Category(ref category) => { + let channel_id = category.borrow().id; + + cache.categories.remove(&channel_id); + }, + // We ignore these two due to the fact that the delete event for dms/groups + // will _not_ fire anymore. + Channel::Private(_) | Channel::Group(_) => unreachable!(), + }; + + None + } +} + +impl CacheUpdate for ChannelPinsUpdateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + if let Some(channel) = cache.channels.get(&self.channel_id) { + channel.borrow_mut().last_pin_timestamp = self.last_pin_timestamp; + + return None; + } + + if let Some(channel) = cache.private_channels.get_mut(&self.channel_id) { + channel.borrow_mut().last_pin_timestamp = self.last_pin_timestamp; + + return None; + } + + if let Some(group) = cache.groups.get_mut(&self.channel_id) { + group.borrow_mut().last_pin_timestamp = self.last_pin_timestamp; + + return None; + } + + None + } +} + +impl CacheUpdate for ChannelRecipientAddEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + cache.update_user_entry(&self.user); + let user = Rc::clone(&cache.users[&self.user.id]); + + cache.groups.get_mut(&self.channel_id).map(|group| { + group.borrow_mut().recipients.insert(self.user.id, user); + }); + + None + } +} + +impl CacheUpdate for ChannelRecipientRemoveEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + if let Some(group) = cache.groups.get_mut(&self.channel_id) { + group.borrow_mut().recipients.remove(&self.user.id); + } + + None + } +} + +impl CacheUpdate for ChannelUpdateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + match self.channel { + Channel::Group(ref group) => { + let (ch_id, no_recipients) = { + let group = group.borrow(); + + (group.channel_id, group.recipients.is_empty()) + }; + + match cache.groups.entry(ch_id) { + Entry::Vacant(e) => { + e.insert(Rc::clone(group)); + }, + Entry::Occupied(mut e) => { + let mut dest = e.get_mut().borrow_mut(); + + if no_recipients { + let recipients = mem::replace(&mut dest.recipients, HashMap::new()); + + dest.clone_from(&group.borrow()); + + dest.recipients = recipients; + } else { + dest.clone_from(&group.borrow()); + } + }, + } + }, + Channel::Guild(ref channel) => { + let (guild_id, channel_id) = { + let channel = channel.borrow(); + + (channel.guild_id, channel.id) + }; + + cache.channels.insert(channel_id, Rc::clone(channel)); + cache.guilds.get_mut(&guild_id).map(|guild| { + let mut guild = guild.borrow_mut(); + + guild.channels.insert(channel_id, Rc::clone(channel)) + }); + }, + Channel::Private(ref channel) => { + cache + .private_channels + .get_mut(&channel.borrow().id) + .map(|private| private.clone_from(channel)); + }, + Channel::Category(ref category) => { + cache + .categories + .get_mut(&category.borrow().id) + .map(|c| c.clone_from(category)); + }, + } + + None + } +} + +impl CacheUpdate for GuildCreateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + cache.unavailable_guilds.remove(&self.guild.id); + + let mut guild = self.guild.clone(); + + for (user_id, member) in &mut guild.members { + let mut member = member.borrow_mut(); + + cache.update_user_entry(&member.user.borrow_mut()); + + let user = Rc::clone(&cache.users[user_id]); + member.user = Rc::clone(&user); + } + + cache.channels.extend(guild.channels.clone()); + cache + .guilds + .insert(self.guild.id, Rc::new(RefCell::new(guild))); + + None + } +} + +impl CacheUpdate for GuildDeleteEvent { + type Output = Rc<RefCell<Guild>>; + + 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.borrow().channels.keys() { + cache.channels.remove(channel_id); + } + + guild + }) + } +} + +impl CacheUpdate for GuildEmojisUpdateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + if let Some(guild) = cache.guilds.get_mut(&self.guild_id) { + let mut guild = guild.borrow_mut(); + + guild.emojis.clone_from(&self.emojis); + } + + None + } +} + +impl CacheUpdate for GuildMemberAddEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + let user_id = self.member.user.borrow().id; + cache.update_user_entry(&self.member.user.borrow()); + + // Always safe due to being inserted above. + self.member.user = Rc::clone(&cache.users[&user_id]); + + if let Some(guild) = cache.guilds.get_mut(&self.guild_id) { + let mut guild = guild.borrow_mut(); + + guild.member_count += 1; + guild.members.insert(user_id, Rc::new(RefCell::new(self.member.clone()))); + } + + None + } +} + +impl CacheUpdate for GuildMemberRemoveEvent { + type Output = Rc<RefCell<Member>>; + + fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { + cache.guilds.get_mut(&self.guild_id).and_then(|guild| { + guild.try_borrow_mut().ok().and_then(|mut guild| { + guild.member_count -= 1; + guild.members.remove(&self.user.id) + }) + }) + } +} + +impl CacheUpdate for GuildMemberUpdateEvent { + type Output = Member; + + fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { + cache.update_user_entry(&self.user); + + if let Some(guild) = cache.guilds.get_mut(&self.guild_id) { + let mut guild = guild.borrow_mut(); + + let mut found = false; + + let item = { + let member = guild.members.get(&self.user.id).and_then(|member| { + member.try_borrow_mut().ok() + }); + if let Some(mut member) = member { + let item = Some(member.clone()); + + member.nick.clone_from(&self.nick); + member.roles.clone_from(&self.roles); + member.user.borrow_mut().clone_from(&self.user); + + found = true; + + item + } else { + None + } + }; + + if !found { + guild.members.insert( + self.user.id, + Rc::new(RefCell::new(Member { + client: None, + deaf: false, + guild_id: self.guild_id, + joined_at: None, + mute: false, + nick: self.nick.clone(), + roles: self.roles.clone(), + user: Rc::new(RefCell::new(self.user.clone())), + })), + ); + } + + item + } else { + None + } + } +} + +impl CacheUpdate for GuildMembersChunkEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + for member in self.members.values() { + cache.update_user_entry(&member.user.borrow()); + } + + if let Some(guild) = cache.guilds.get(&self.guild_id) { + let mut guild = guild.borrow_mut(); + + guild.members.extend({ + let mut m: HashMap<UserId, Rc<RefCell<Member>>> = HashMap::new(); + + for (id, member) in &self.members { + m.insert(*id, Rc::new(RefCell::new(member.clone()))); + } + + m + }); + } + + None + } +} + +impl CacheUpdate for GuildRoleCreateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + let guild = cache.guilds.get_mut(&self.guild_id).and_then(|guild| { + guild.try_borrow_mut().ok() + }); + + if let Some(mut guild) = guild { + guild + .roles + .insert(self.role.id, Rc::new(RefCell::new(self.role.clone()))); + } + + None + } +} + +impl CacheUpdate for GuildRoleDeleteEvent { + type Output = Rc<RefCell<Role>>; + + fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { + cache + .guilds + .get_mut(&self.guild_id) + .and_then(|guild| { + let mut guild = guild.borrow_mut(); + + guild.roles.remove(&self.role_id) + }) + } +} + +impl CacheUpdate for GuildRoleUpdateEvent { + type Output = Role; + + fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { + cache.guilds.get_mut(&self.guild_id).and_then(|guild| { + let mut guild = guild.borrow_mut(); + + guild.roles.get_mut(&self.role.id).map(|role| { + mem::replace(&mut *role.borrow_mut(), self.role.clone()) + }) + }) + } +} + +impl CacheUpdate for GuildUnavailableEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + cache.unavailable_guilds.insert(self.guild_id); + cache.guilds.remove(&self.guild_id); + + None + } +} + +impl CacheUpdate for GuildUpdateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + if let Some(guild) = cache.guilds.get(&self.guild.id) { + let mut guild = guild.try_borrow_mut().ok()?; + + guild.afk_timeout = self.guild.afk_timeout; + guild.afk_channel_id.clone_from(&self.guild.afk_channel_id); + guild.icon.clone_from(&self.guild.icon); + guild.name.clone_from(&self.guild.name); + guild.owner_id.clone_from(&self.guild.owner_id); + guild.region.clone_from(&self.guild.region); + guild.roles.clone_from(&self.guild.roles); + guild.verification_level = self.guild.verification_level; + } + + None + } +} + +impl CacheUpdate for PresenceUpdateEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + let user_id = self.presence.user_id; + + if let Some(user) = self.presence.user.as_mut() { + cache.update_user_entry(&user.borrow()); + *user = Rc::clone(&cache.users[&user_id]); + } + + if let Some(guild_id) = self.guild_id { + let guild = cache.guilds.get(&guild_id).and_then(|guild| { + guild.try_borrow_mut().ok() + }); + + if let Some(mut guild) = guild { + // If the member went offline, remove them from the presence list. + if self.presence.status == OnlineStatus::Offline { + guild.presences.remove(&self.presence.user_id); + } else { + guild + .presences + .insert(self.presence.user_id, Rc::new(RefCell::new(self.presence.clone()))); + } + + // Create a partial member instance out of the presence update + // data. This includes everything but `deaf`, `mute`, and + // `joined_at`. + if !guild.members.contains_key(&self.presence.user_id) { + if let Some(user) = self.presence.user.as_ref() { + let roles = self.roles.clone().unwrap_or_default(); + + guild.members.insert(self.presence.user_id, Rc::new(RefCell::new(Member { + client: None, + deaf: false, + guild_id: guild_id, + joined_at: None, + mute: false, + nick: self.presence.nick.clone(), + user: Rc::clone(&user), + roles, + }))); + } + } + } + } else if self.presence.status == OnlineStatus::Offline { + cache.presences.remove(&self.presence.user_id); + } else { + cache + .presences + .insert(self.presence.user_id, Rc::new(RefCell::new(self.presence.clone()))); + } + + None + } +} + +impl CacheUpdate for PresencesReplaceEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + cache.presences.extend({ + let mut p: HashMap<UserId, Rc<RefCell<Presence>>> = HashMap::default(); + + for presence in &self.presences { + p.insert(presence.user_id, Rc::new(RefCell::new(presence.clone()))); + } + + p + }); + + None + } +} + +impl CacheUpdate for ReadyEvent { + type Output = (); + + fn update(&mut self, cache: &mut Cache) -> Option<()> { + let mut ready = self.ready.clone(); + + for guild in ready.guilds { + match guild { + GuildStatus::Offline(unavailable) => { + cache.guilds.remove(&unavailable.id); + cache.unavailable_guilds.insert(unavailable.id); + }, + GuildStatus::OnlineGuild(guild) => { + cache.unavailable_guilds.remove(&guild.id); + cache.guilds.insert(guild.id, Rc::new(RefCell::new(guild))); + }, + GuildStatus::OnlinePartialGuild(_) => {}, + } + } + + // `ready.private_channels` will always be empty, and possibly be removed in the future. + // So don't handle it at all. + + for (user_id, presence) in &mut ready.presences { + let mut presence = presence.borrow_mut(); + + if let Some(ref user) = presence.user { + cache.update_user_entry(&user.borrow()); + } + + presence.user = cache.users.get(user_id).cloned(); + } + + cache.presences.extend(ready.presences); + cache.shard_count = ready.shard.map_or(1, |s| s[1]); + cache.user = ready.user; + + None + } +} + +impl CacheUpdate for UserUpdateEvent { + type Output = CurrentUser; + + fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { + Some(mem::replace(&mut cache.user, self.current_user.clone())) + } +} + +impl CacheUpdate for VoiceStateUpdateEvent { + type Output = (); + + 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.borrow_mut(); + + if self.voice_state.channel_id.is_some() { + // Update or add to the voice state list + { + let finding = guild.voice_states.get_mut(&self.voice_state.user_id); + + if let Some(srv_state) = finding { + srv_state.clone_from(&self.voice_state); + + return None; + } + } + + guild + .voice_states + .insert(self.voice_state.user_id, self.voice_state.clone()); + } else { + // Remove the user from the voice state list + guild.voice_states.remove(&self.voice_state.user_id); + } + } + + return None; + } + + None + } +} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 8cd402e..462cc38 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -43,11 +43,11 @@ //! [`http`]: ../http/index.html use model::prelude::*; -use parking_lot::RwLock; +use std::cell::RefCell; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::default::Default; -use std::sync::Arc; +use std::rc::Rc; mod cache_update; @@ -77,21 +77,21 @@ pub struct Cache { /// [`Event::GuildDelete`]: ../model/event/struct.GuildDeleteEvent.html /// [`Event::GuildUnavailable`]: ../model/event/struct.GuildUnavailableEvent.html /// [`Guild`]: ../model/guild/struct.Guild.html - pub channels: HashMap<ChannelId, Arc<RwLock<GuildChannel>>>, + pub channels: HashMap<ChannelId, Rc<RefCell<GuildChannel>>>, /// A map of channel categories. - pub categories: HashMap<ChannelId, Arc<RwLock<ChannelCategory>>>, + pub categories: HashMap<ChannelId, Rc<RefCell<ChannelCategory>>>, /// A map of the groups that the current user is in. /// /// For bot users this will always be empty, except for in [special cases]. /// /// [special cases]: index.html#special-cases-in-the-cache - pub groups: HashMap<ChannelId, Arc<RwLock<Group>>>, + pub groups: HashMap<ChannelId, Rc<RefCell<Group>>>, /// A map of guilds with full data available. This includes data like /// [`Role`]s and [`Emoji`]s that are not available through the REST API. /// /// [`Emoji`]: ../model/guild/struct.Emoji.html /// [`Role`]: ../model/guild/struct.Role.html - pub guilds: HashMap<GuildId, Arc<RwLock<Guild>>>, + pub guilds: HashMap<GuildId, Rc<RefCell<Guild>>>, /// A map of notes that a user has made for individual users. /// /// An empty note is equivalent to having no note, and creating an empty @@ -102,10 +102,10 @@ pub struct Cache { /// A map of users' presences. This is updated in real-time. Note that /// status updates are often "eaten" by the gateway, and this should not /// be treated as being entirely 100% accurate. - pub presences: HashMap<UserId, Presence>, + pub presences: HashMap<UserId, Rc<RefCell<Presence>>>, /// A map of direct message channels that the current user has open with /// other users. - pub private_channels: HashMap<ChannelId, Arc<RwLock<PrivateChannel>>>, + pub private_channels: HashMap<ChannelId, Rc<RefCell<PrivateChannel>>>, /// The total number of shards being used by the bot. pub shard_count: u64, /// A list of guilds which are "unavailable". Refer to the documentation for @@ -154,7 +154,7 @@ pub struct Cache { /// [`GuildSyncEvent`]: ../model/event/struct.GuildSyncEvent.html /// [`PresenceUpdateEvent`]: ../model/event/struct.PresenceUpdateEvent.html /// [`ReadyEvent`]: ../model/event/struct.ReadyEvent.html - pub users: HashMap<UserId, Arc<RwLock<User>>>, + pub users: HashMap<UserId, Rc<RefCell<User>>>, } impl Cache { @@ -212,7 +212,7 @@ impl Cache { let mut total = 0; for guild in self.guilds.values() { - let guild = guild.read(); + let guild = guild.borrow(); let members = guild.members.len() as u64; @@ -319,15 +319,15 @@ impl Cache { let id = id.into(); if let Some(channel) = self.channels.get(&id) { - return Some(Channel::Guild(Arc::clone(channel))); + return Some(Channel::Guild(Rc::clone(channel))); } if let Some(private_channel) = self.private_channels.get(&id) { - return Some(Channel::Private(Arc::clone(private_channel))); + return Some(Channel::Private(Rc::clone(private_channel))); } if let Some(group) = self.groups.get(&id) { - return Some(Channel::Group(Arc::clone(group))); + return Some(Channel::Group(Rc::clone(group))); } None @@ -361,7 +361,7 @@ impl Cache { /// # } /// ``` #[inline] - pub fn guild<G: Into<GuildId>>(&self, id: G) -> Option<Arc<RwLock<Guild>>> { + pub fn guild<G: Into<GuildId>>(&self, id: G) -> Option<Rc<RefCell<Guild>>> { self.guilds.get(&id.into()).cloned() } @@ -418,7 +418,7 @@ impl Cache { /// [`Guild`]: ../model/guild/struct.Guild.html /// [`channel`]: #method.channel #[inline] - pub fn guild_channel<C: Into<ChannelId>>(&self, id: C) -> Option<Arc<RwLock<GuildChannel>>> { + pub fn guild_channel<C: Into<ChannelId>>(&self, id: C) -> Option<Rc<RefCell<GuildChannel>>> { self.channels.get(&id.into()).cloned() } @@ -452,7 +452,7 @@ impl Cache { /// # } /// ``` #[inline] - pub fn group<C: Into<ChannelId>>(&self, id: C) -> Option<Arc<RwLock<Group>>> { + pub fn group<C: Into<ChannelId>>(&self, id: C) -> Option<Rc<RefCell<Group>>> { self.groups.get(&id.into()).cloned() } @@ -501,10 +501,13 @@ impl Cache { /// [`Client::on_message`]: ../client/struct.Client.html#method.on_message /// [`Guild`]: ../model/guild/struct.Guild.html /// [`members`]: ../model/guild/struct.Guild.html#structfield.members - pub fn member<G, U>(&self, guild_id: G, user_id: U) -> Option<Member> - where G: Into<GuildId>, U: Into<UserId> { + pub fn member<G: Into<GuildId>, U: Into<UserId>>( + &self, + guild_id: G, + user_id: U, + ) -> Option<Rc<RefCell<Member>>> { self.guilds.get(&guild_id.into()).and_then(|guild| { - guild.read().members.get(&user_id.into()).cloned() + guild.borrow().members.get(&user_id.into()).cloned() }) } @@ -543,7 +546,7 @@ impl Cache { #[inline] pub fn private_channel<C: Into<ChannelId>>(&self, channel_id: C) - -> Option<Arc<RwLock<PrivateChannel>>> { + -> Option<Rc<RefCell<PrivateChannel>>> { self.private_channels.get(&channel_id.into()).cloned() } @@ -575,11 +578,11 @@ impl Cache { /// # try_main().unwrap(); /// # } /// ``` - pub fn role<G, R>(&self, guild_id: G, role_id: R) -> Option<Role> - where G: Into<GuildId>, R: Into<RoleId> { + pub fn role<G: Into<GuildId>, R: Into<RoleId>>(&self, guild_id: G, role_id: R) + -> Option<Rc<RefCell<Role>>> { self.guilds .get(&guild_id.into()) - .and_then(|g| g.read().roles.get(&role_id.into()).cloned()) + .and_then(|g| g.borrow().roles.get(&role_id.into()).cloned()) } /// Retrieves a `User` from the cache's [`users`] map, if it exists. @@ -611,14 +614,13 @@ impl Cache { /// # } /// ``` #[inline] - pub fn user<U: Into<UserId>>(&self, user_id: U) -> Option<Arc<RwLock<User>>> { + pub fn user<U: Into<UserId>>(&self, user_id: U) -> Option<Rc<RefCell<User>>> { self.users.get(&user_id.into()).cloned() } #[inline] - pub fn categories<C: Into<ChannelId>>(&self, - channel_id: C) - -> Option<Arc<RwLock<ChannelCategory>>> { + pub fn category<C: Into<ChannelId>>(&self, channel_id: C) + -> Option<Rc<RefCell<ChannelCategory>>> { self.categories.get(&channel_id.into()).cloned() } @@ -630,10 +632,10 @@ impl Cache { pub(crate) fn update_user_entry(&mut self, user: &User) { match self.users.entry(user.id) { Entry::Vacant(e) => { - e.insert(Arc::new(RwLock::new(user.clone()))); + e.insert(Rc::new(RefCell::new(user.clone()))); }, - Entry::Occupied(mut e) => { - e.get_mut().write().clone_from(user); + Entry::Occupied(e) => { + e.get().borrow_mut().clone_from(user); }, } } diff --git a/src/client/bridge/gateway/event.rs b/src/client/bridge/gateway/event.rs deleted file mode 100644 index a967ab5..0000000 --- a/src/client/bridge/gateway/event.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! A collection of events created by the client, not a part of the Discord API -//! itself. - -use super::ShardId; -use ::gateway::ConnectionStage; - -#[derive(Clone, Debug)] -pub(crate) enum ClientEvent { - ShardStageUpdate(ShardStageUpdateEvent), -} - -/// An event denoting that a shard's connection stage was changed. -/// -/// # Examples -/// -/// This might happen when a shard changes from [`ConnectionStage::Identifying`] -/// to [`ConnectionStage::Connected`]. -/// -/// [`ConnectionStage::Connected`]: ../../../../gateway/enum.ConnectionStage.html#variant.Connected -/// [`ConnectionStage::Identifying`]: ../../../../gateway/enum.ConnectionStage.html#variant.Identifying -#[derive(Clone, Debug)] -pub struct ShardStageUpdateEvent { - /// The new connection stage. - pub new: ConnectionStage, - /// The old connection stage. - pub old: ConnectionStage, - /// The ID of the shard that had its connection stage change. - pub shard_id: ShardId, -} diff --git a/src/client/bridge/gateway/mod.rs b/src/client/bridge/gateway/mod.rs deleted file mode 100644 index 3cc6dca..0000000 --- a/src/client/bridge/gateway/mod.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! 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 - -pub mod event; - -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, ShardManagerOptions}; -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, ShardRunnerOptions}; -pub use self::shard_runner_message::ShardRunnerMessage; - -use std::fmt::{Display, Formatter, Result as FmtResult}; -use std::sync::mpsc::Sender; -use std::time::Duration as StdDuration; -use ::gateway::{ConnectionStage, InterMessage}; - -/// A message either for a [`ShardManager`] or a [`ShardRunner`]. -/// -/// [`ShardManager`]: struct.ShardManager.html -/// [`ShardRunner`]: struct.ShardRunner.html -#[derive(Clone, Debug)] -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), - /// An update from a shard runner, - ShardUpdate { - id: ShardId, - latency: Option<StdDuration>, - stage: ConnectionStage, - }, - /// Indicator that a [`ShardManagerMonitor`] should fully shutdown a shard - /// without bringing it back up. - /// - /// [`ShardManagerMonitor`]: struct.ShardManagerMonitor.html - Shutdown(ShardId), - /// 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, - /// Indicator that a [`ShardManager`] has initiated a shutdown, and for the - /// component that receives this to also shutdown with no further action - /// taken. - ShutdownInitiated, -} - -/// 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. - Start(ShardId, ShardId), - /// Message to shutdown the shard queuer. - Shutdown, -} - -/// 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); - -impl Display for ShardId { - fn fmt(&self, f: &mut Formatter) -> FmtResult { - write!(f, "{}", self.0) - } -} - -/// 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 { - /// The latency between when a heartbeat was sent and when the - /// acknowledgement was received. - pub latency: Option<StdDuration>, - /// The channel used to communicate with the shard runner, telling it - /// what to do with regards to its status. - pub runner_tx: Sender<InterMessage>, - /// The current connection stage of the shard. - pub stage: ConnectionStage, -} diff --git a/src/client/bridge/gateway/shard_manager.rs b/src/client/bridge/gateway/shard_manager.rs deleted file mode 100644 index b62ff00..0000000 --- a/src/client/bridge/gateway/shard_manager.rs +++ /dev/null @@ -1,360 +0,0 @@ -use gateway::InterMessage; -use internal::prelude::*; -use parking_lot::Mutex; -use std::collections::{HashMap, VecDeque}; -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, -}; -use threadpool::ThreadPool; -use typemap::ShareMap; - -#[cfg(feature = "framework")] -use framework::Framework; -#[cfg(feature = "voice")] -use client::bridge::voice::ClientVoiceManager; - -/// 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 = "voice")] -/// # use serenity::client::bridge::voice::ClientVoiceManager; -/// # #[cfg(feature = "voice")] -/// # use serenity::model::id::UserId; -/// # -/// # #[cfg(feature = "framework")] -/// # fn try_main() -> Result<(), Box<Error>> { -/// # -/// use parking_lot::Mutex; -/// use serenity::client::bridge::gateway::{ShardManager, ShardManagerOptions}; -/// 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(ShardManagerOptions { -/// data: &data, -/// event_handler: &event_handler, -/// framework: &framework, -/// // the shard index to start initiating from -/// shard_index: 0, -/// // the number of shards to initiate (this initiates 0, 1, and 2) -/// shard_init: 3, -/// // the total number of shards in use -/// shard_total: 5, -/// token: &token, -/// threadpool, -/// # #[cfg(feature = "voice")] -/// # voice_manager: &Arc::new(Mutex::new(ClientVoiceManager::new(0, UserId(0)))), -/// ws_url: &gateway_url, -/// }); -/// # 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 { - monitor_tx: Sender<ShardManagerMessage>, - /// The shard runners currently managed. - /// - /// **Note**: It is highly unrecommended to mutate this yourself unless you - /// need to. Instead prefer to use methods on this struct that are provided - /// where possible. - pub 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. - shard_init: u64, - /// The total shards in use, 1-indexed. - shard_total: u64, - shard_queuer: Sender<ShardQueuerMessage>, -} - -impl ShardManager { - /// Creates a new shard manager, returning both the manager and a monitor - /// for usage in a separate thread. - pub fn new<'a, H>( - opt: ShardManagerOptions<'a, H>, - ) -> (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(Mutex::new(HashMap::new())); - - let mut shard_queuer = ShardQueuer { - data: Arc::clone(opt.data), - event_handler: Arc::clone(opt.event_handler), - #[cfg(feature = "framework")] - framework: Arc::clone(opt.framework), - last_start: None, - manager_tx: thread_tx.clone(), - queue: VecDeque::new(), - runners: Arc::clone(&runners), - rx: shard_queue_rx, - threadpool: opt.threadpool, - token: Arc::clone(opt.token), - #[cfg(feature = "voice")] - voice_manager: Arc::clone(opt.voice_manager), - ws_url: Arc::clone(opt.ws_url), - }; - - thread::spawn(move || { - shard_queuer.run(); - }); - - let manager = Arc::new(Mutex::new(Self { - monitor_tx: thread_tx, - shard_index: opt.shard_index, - shard_init: opt.shard_init, - shard_queuer: shard_queue_tx, - shard_total: opt.shard_total, - runners, - })); - - (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; - - for shard_id in self.shard_index..shard_to { - let shard_total = self.shard_total; - - self.boot([ShardId(shard_id), ShardId(shard_total)]); - } - - Ok(()) - } - - /// 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; - } - - /// 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 client_msg = ShardClientMessage::Manager(shutdown); - let msg = InterMessage::Client(client_msg); - - 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) { - let keys = { - 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); - } - - let _ = self.shard_queuer.send(ShardQueuerMessage::Shutdown); - let _ = self.monitor_tx.send(ShardManagerMessage::ShutdownInitiated); - } - - fn boot(&mut self, shard_info: [ShardId; 2]) { - info!("Telling shard queuer to start shard {}", shard_info[0]); - - let msg = ShardQueuerMessage::Start(shard_info[0], shard_info[1]); - let _ = self.shard_queuer.send(msg); - } -} - -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); - } - } -} - -pub struct ShardManagerOptions<'a, H: EventHandler + Send + Sync + 'static> { - pub data: &'a Arc<Mutex<ShareMap>>, - pub event_handler: &'a Arc<H>, - #[cfg(feature = "framework")] - pub framework: &'a Arc<Mutex<Option<Box<Framework + Send>>>>, - pub shard_index: u64, - pub shard_init: u64, - pub shard_total: u64, - pub threadpool: ThreadPool, - pub token: &'a Arc<Mutex<String>>, - #[cfg(feature = "voice")] - pub voice_manager: &'a Arc<Mutex<ClientVoiceManager>>, - pub ws_url: &'a Arc<Mutex<String>>, -} diff --git a/src/client/bridge/gateway/shard_manager_monitor.rs b/src/client/bridge/gateway/shard_manager_monitor.rs deleted file mode 100644 index 9f9c04d..0000000 --- a/src/client/bridge/gateway/shard_manager_monitor.rs +++ /dev/null @@ -1,64 +0,0 @@ -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::ShardUpdate { id, latency, stage } => { - let manager = self.manager.lock(); - let mut runners = manager.runners.lock(); - - runners.get_mut(&id).map(|runner| { - runner.latency = latency; - runner.stage = stage; - }); - } - ShardManagerMessage::Shutdown(shard_id) => { - self.manager.lock().shutdown(shard_id); - }, - ShardManagerMessage::ShutdownAll => { - self.manager.lock().shutdown_all(); - - break; - }, - ShardManagerMessage::ShutdownInitiated => break, - } - } - } -} diff --git a/src/client/bridge/gateway/shard_messenger.rs b/src/client/bridge/gateway/shard_messenger.rs deleted file mode 100644 index 2331d4a..0000000 --- a/src/client/bridge/gateway/shard_messenger.rs +++ /dev/null @@ -1,277 +0,0 @@ -use gateway::InterMessage; -use model::prelude::*; -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<InterMessage>, -} - -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<InterMessage>) -> 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::id::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::id::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/guild/struct.Guild.html - /// [`Member`]: ../../model/guild/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::gateway::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::user::OnlineStatus; - /// - /// shard.set_status(OnlineStatus::DoNotDisturb); - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// [`DoNotDisturb`]: ../../model/user/enum.OnlineStatus.html#variant.DoNotDisturb - /// [`Invisible`]: ../../model/user/enum.OnlineStatus.html#variant.Invisible - /// [`Offline`]: ../../model/user/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<InterMessage>> { - self.tx.send(InterMessage::Client(ShardClientMessage::Runner(msg))) - } -} diff --git a/src/client/bridge/gateway/shard_queuer.rs b/src/client/bridge/gateway/shard_queuer.rs deleted file mode 100644 index afcd4c7..0000000 --- a/src/client/bridge/gateway/shard_queuer.rs +++ /dev/null @@ -1,200 +0,0 @@ -use gateway::Shard; -use internal::prelude::*; -use parking_lot::Mutex; -use std::collections::{HashMap, VecDeque}; -use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender}; -use std::sync::Arc; -use std::thread; -use std::time::{Duration, Instant}; -use super::super::super::EventHandler; -use super::{ - ShardId, - ShardManagerMessage, - ShardQueuerMessage, - ShardRunner, - ShardRunnerInfo, - ShardRunnerOptions, -}; -use threadpool::ThreadPool; -use typemap::ShareMap; -use ::gateway::ConnectionStage; - -#[cfg(feature = "voice")] -use client::bridge::voice::ClientVoiceManager; -#[cfg(feature = "framework")] -use framework::Framework; - -const WAIT_BETWEEN_BOOTS_IN_SECONDS: u64 = 5; - -/// The shard queuer is a simple loop that runs indefinitely to manage the -/// startup of shards. -/// -/// A shard queuer instance _should_ be run in its own thread, due to the -/// 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> { - /// 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>, - /// The shards that are queued for booting. - /// - /// This will typically be filled with previously failed boots. - pub queue: VecDeque<(u64, u64)>, - /// 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 client's voice manager. - #[cfg(feature = "voice")] - pub voice_manager: Arc<Mutex<ClientVoiceManager>>, - /// 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) { - // The duration to timeout from reads over the Rx channel. This can be - // done in a loop, and if the read times out then a shard can be - // started if one is presently waiting in the queue. - let wait_duration = Duration::from_secs(WAIT_BETWEEN_BOOTS_IN_SECONDS); - - loop { - match self.rx.recv_timeout(wait_duration) { - Ok(ShardQueuerMessage::Shutdown) => break, - Ok(ShardQueuerMessage::Start(id, total)) => { - self.checked_start(id.0, total.0); - }, - Err(RecvTimeoutError::Disconnected) => { - // If the sender half has disconnected then the queuer's - // lifespan has passed and can shutdown. - break; - }, - Err(RecvTimeoutError::Timeout) => { - if let Some((id, total)) = self.queue.pop_front() { - self.checked_start(id, total); - } - } - } - } - } - - fn check_last_start(&mut self) { - let instant = match self.last_start { - Some(instant) => instant, - None => return, - }; - - // We must wait 5 seconds between IDENTIFYs to avoid session - // invalidations. - let duration = Duration::from_secs(WAIT_BETWEEN_BOOTS_IN_SECONDS); - let elapsed = instant.elapsed(); - - if elapsed >= duration { - return; - } - - let to_sleep = duration - elapsed; - - thread::sleep(to_sleep); - } - - fn checked_start(&mut self, id: u64, total: u64) { - self.check_last_start(); - - if let Err(why) = self.start(id, total) { - warn!("Err starting shard {}: {:?}", id, why); - info!("Re-queueing start of shard {}", id); - - self.queue.push_back((id, total)); - } - - self.last_start = Some(Instant::now()); - } - - fn start(&mut self, shard_id: u64, shard_total: u64) -> Result<()> { - let shard_info = [shard_id, shard_total]; - - let shard = Shard::new( - Arc::clone(&self.ws_url), - Arc::clone(&self.token), - shard_info, - )?; - - let mut runner = ShardRunner::new(ShardRunnerOptions { - data: Arc::clone(&self.data), - event_handler: Arc::clone(&self.event_handler), - #[cfg(feature = "framework")] - framework: Arc::clone(&self.framework), - manager_tx: self.manager_tx.clone(), - threadpool: self.threadpool.clone(), - #[cfg(feature = "voice")] - voice_manager: Arc::clone(&self.voice_manager), - shard, - }); - - let runner_info = ShardRunnerInfo { - latency: None, - runner_tx: runner.runner_tx(), - stage: ConnectionStage::Disconnected, - }; - - thread::spawn(move || { - let _ = runner.run(); - }); - - self.runners.lock().insert(ShardId(shard_id), runner_info); - - Ok(()) - } -} diff --git a/src/client/bridge/gateway/shard_runner.rs b/src/client/bridge/gateway/shard_runner.rs deleted file mode 100644 index bfe4214..0000000 --- a/src/client/bridge/gateway/shard_runner.rs +++ /dev/null @@ -1,492 +0,0 @@ -use gateway::{InterMessage, ReconnectType, Shard, ShardAction}; -use internal::prelude::*; -use internal::ws_impl::{ReceiverExt, SenderExt}; -use model::event::{Event, GatewayEvent}; -use parking_lot::Mutex; -use serde::Deserialize; -use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; -use std::sync::Arc; -use super::super::super::dispatch::{DispatchEvent, dispatch}; -use super::super::super::EventHandler; -use super::event::{ClientEvent, ShardStageUpdateEvent}; -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 = "voice")] -use super::super::voice::ClientVoiceManager; - -/// 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<Mutex<ShareMap>>, - event_handler: Arc<H>, - #[cfg(feature = "framework")] - framework: Arc<Mutex<Option<Box<Framework + Send>>>>, - manager_tx: Sender<ShardManagerMessage>, - // channel to receive messages from the shard manager and dispatches - runner_rx: Receiver<InterMessage>, - // channel to send messages to the shard runner from the shard manager - runner_tx: Sender<InterMessage>, - shard: Shard, - threadpool: ThreadPool, - #[cfg(feature = "voice")] - voice_manager: Arc<Mutex<ClientVoiceManager>>, -} - -impl<H: EventHandler + Send + Sync + 'static> ShardRunner<H> { - /// Creates a new runner for a Shard. - pub fn new(opt: ShardRunnerOptions<H>) -> Self { - let (tx, rx) = mpsc::channel(); - - Self { - runner_rx: rx, - runner_tx: tx, - data: opt.data, - event_handler: opt.event_handler, - #[cfg(feature = "framework")] - framework: opt.framework, - manager_tx: opt.manager_tx, - shard: opt.shard, - threadpool: opt.threadpool, - #[cfg(feature = "voice")] - voice_manager: opt.voice_manager, - } - } - - /// 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.shard_info()); - - loop { - if !self.recv()? { - return Ok(()); - } - - // 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(); - } - - let pre = self.shard.stage(); - let (event, action, successful) = self.recv_event(); - let post = self.shard.stage(); - - if post != pre { - self.update_manager(); - - let e = ClientEvent::ShardStageUpdate(ShardStageUpdateEvent { - new: post, - old: pre, - shard_id: ShardId(self.shard.shard_info()[0]), - }); - self.dispatch(DispatchEvent::Client(e)); - } - - match action { - Some(ShardAction::Reconnect(ReconnectType::Reidentify)) => { - return self.request_restart() - }, - Some(other) => { - let _ = self.action(&other); - }, - None => {}, - } - - if let Some(event) = event { - self.dispatch(DispatchEvent::Model(event)); - } - - if !successful && !self.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<InterMessage> { - self.runner_tx.clone() - } - - /// 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::Reconnect(ReconnectType::Reidentify) => { - self.request_restart() - }, - ShardAction::Reconnect(ReconnectType::Resume) => { - self.shard.resume() - }, - ShardAction::Heartbeat => self.shard.heartbeat(), - ShardAction::Identify => self.shard.identify(), - } - } - - // 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 - } - - #[inline] - fn dispatch(&self, event: DispatchEvent) { - dispatch( - event, - #[cfg(feature = "framework")] - &self.framework, - &self.data, - &self.event_handler, - &self.runner_tx, - &self.threadpool, - self.shard.shard_info()[0], - ); - } - - // 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: InterMessage) -> bool { - match value { - InterMessage::Client(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 - }, - ShardManagerMessage::ShardUpdate { .. } - | ShardManagerMessage::ShutdownInitiated => { - // nb: not sent here - - true - }, - }, - InterMessage::Client(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() - }, - }, - InterMessage::Json(value) => { - // Value must be forwarded over the websocket - self.shard.client.send_json(&value).is_ok() - }, - } - } - - #[cfg(feature = "voice")] - fn handle_voice_event(&self, event: &Event) { - match *event { - Event::Ready(_) => { - self.voice_manager.lock().set( - self.shard.shard_info()[0], - self.runner_tx.clone(), - ); - }, - Event::VoiceServerUpdate(ref event) => { - if let Some(guild_id) = event.guild_id { - let mut manager = self.voice_manager.lock(); - let mut search = manager.get_mut(guild_id); - - if let Some(handler) = search { - handler.update_server(&event.endpoint, &event.token); - } - } - }, - Event::VoiceStateUpdate(ref event) => { - if let Some(guild_id) = event.guild_id { - let mut manager = self.voice_manager.lock(); - let mut search = manager.get_mut(guild_id); - - if let Some(handler) = search { - handler.update_state(&event.voice_state); - } - } - }, - _ => {}, - } - } - - // 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>, Option<ShardAction>, bool) { - let gw_event = match self.shard.client.recv_json() { - Ok(Some(value)) => { - GatewayEvent::deserialize(value).map(Some).map_err(From::from) - }, - Ok(None) => Ok(None), - Err(Error::WebSocket(WebSocketError::IoError(_))) => { - // Check that an amount of time at least double the - // heartbeat_interval has passed. - // - // If not, continue on trying to receive messages. - // - // If it has, attempt to auto-reconnect. - { - 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); - } - } - - debug!("Attempting to auto-reconnect"); - - match self.shard.reconnection_type() { - ReconnectType::Reidentify => return (None, None, false), - ReconnectType::Resume => { - if let Err(why) = self.shard.resume() { - warn!("Failed to resume: {:?}", why); - - return (None, None, false); - } - }, - } - - 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, None, false); - }, - Err(why) => Err(why), - }; - - let event = match gw_event { - Ok(Some(event)) => Ok(event), - Ok(None) => return (None, None, true), - Err(why) => Err(why), - }; - - 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, None, true); - }, - }; - - if let Ok(GatewayEvent::HeartbeatAck) = event { - self.update_manager(); - } - - #[cfg(feature = "voice")] - { - if let Ok(GatewayEvent::Dispatch(_, ref event)) = event { - self.handle_voice_event(&event); - } - } - - let event = match event { - Ok(GatewayEvent::Dispatch(_, event)) => Some(event), - _ => None, - }; - - (event, action, true) - } - - fn request_restart(&self) -> Result<()> { - self.update_manager(); - - 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); - - #[cfg(feature = "voice")] - { - self.voice_manager.lock().manager_remove(&shard_id.0); - } - - Ok(()) - } - - fn update_manager(&self) { - let _ = self.manager_tx.send(ShardManagerMessage::ShardUpdate { - id: ShardId(self.shard.shard_info()[0]), - latency: self.shard.latency(), - stage: self.shard.stage(), - }); - } -} - -/// Options to be passed to [`ShardRunner::new`]. -/// -/// [`ShardRunner::new`]: struct.ShardRunner.html#method.new -pub struct ShardRunnerOptions<H: EventHandler + Send + Sync + 'static> { - pub data: Arc<Mutex<ShareMap>>, - pub event_handler: Arc<H>, - #[cfg(feature = "framework")] - pub framework: Arc<Mutex<Option<Box<Framework + Send>>>>, - pub manager_tx: Sender<ShardManagerMessage>, - pub shard: Shard, - pub threadpool: ThreadPool, - #[cfg(feature = "voice")] - pub voice_manager: Arc<Mutex<ClientVoiceManager>>, -} diff --git a/src/client/bridge/gateway/shard_runner_message.rs b/src/client/bridge/gateway/shard_runner_message.rs deleted file mode 100644 index d4018b4..0000000 --- a/src/client/bridge/gateway/shard_runner_message.rs +++ /dev/null @@ -1,46 +0,0 @@ -use model::gateway::Game; -use model::user::OnlineStatus; -use model::id::GuildId; -use websocket::message::OwnedMessage; - -/// A message to send from a shard over a WebSocket. -#[derive(Clone, Debug)] -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/guild/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/guild/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 deleted file mode 100644 index 41fcdec..0000000 --- a/src/client/bridge/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! 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; - -#[cfg(feature = "voice")] -pub mod voice; diff --git a/src/client/bridge/voice/mod.rs b/src/client/bridge/voice/mod.rs deleted file mode 100644 index e67366b..0000000 --- a/src/client/bridge/voice/mod.rs +++ /dev/null @@ -1,117 +0,0 @@ -use gateway::InterMessage; -use std::collections::HashMap; -use std::sync::mpsc::Sender as MpscSender; -use ::model::id::{ChannelId, GuildId, UserId}; -use ::voice::{Handler, Manager}; -use ::utils; - -pub struct ClientVoiceManager { - managers: HashMap<u64, Manager>, - shard_count: u64, - user_id: UserId, -} - -impl ClientVoiceManager { - pub fn new(shard_count: u64, user_id: UserId) -> Self { - Self { - managers: HashMap::default(), - shard_count, - user_id, - } - } - - pub fn get<G: Into<GuildId>>(&self, guild_id: G) -> Option<&Handler> { - let (gid, sid) = self.manager_info(guild_id); - - self.managers.get(&sid)?.get(gid) - } - - pub fn get_mut<G: Into<GuildId>>(&mut self, guild_id: G) - -> Option<&mut Handler> { - let (gid, sid) = self.manager_info(guild_id); - - self.managers.get_mut(&sid)?.get_mut(gid) - } - - /// Refer to [`Manager::join`]. - /// - /// This is a shortcut to retrieving the inner [`Manager`] and then calling - /// its `join` method. - /// - /// [`Manager`]: ../../../voice/struct.Manager.html - /// [`Manager::join`]: ../../../voice/struct.Manager.html#method.join - pub fn join<C, G>(&mut self, guild_id: G, channel_id: C) - -> Option<&mut Handler> where C: Into<ChannelId>, G: Into<GuildId> { - let (gid, sid) = self.manager_info(guild_id); - - self.managers.get_mut(&sid).map(|manager| manager.join(gid, channel_id)) - } - - /// Refer to [`Manager::leave`]. - /// - /// This is a shortcut to retrieving the inner [`Manager`] and then calling - /// its `leave` method. - /// - /// [`Manager`]: ../../../voice/struct.Manager.html - /// [`Manager::leave`]: ../../../voice/struct.Manager.html#method.leave - pub fn leave<G: Into<GuildId>>(&mut self, guild_id: G) -> Option<()> { - let (gid, sid) = self.manager_info(guild_id); - - self.managers.get_mut(&sid).map(|manager| manager.leave(gid)) - } - - /// Refer to [`Manager::remove`]. - /// - /// This is a shortcut to retrieving the inner [`Manager`] and then calling - /// its `remove` method. - /// - /// [`Manager`]: ../../../voice/struct.Manager.html - /// [`Manager::remove`]: ../../../voice/struct.Manager.html#method.remove - pub fn remove<G: Into<GuildId>>(&mut self, guild_id: G) -> Option<()> { - let (gid, sid) = self.manager_info(guild_id); - - self.managers.get_mut(&sid).map(|manager| manager.leave(gid)) - } - - pub fn set(&mut self, shard_id: u64, sender: MpscSender<InterMessage>) { - self.managers.insert(shard_id, Manager::new(sender, self.user_id)); - } - - /// Sets the number of shards for the voice manager to use when calculating - /// guilds' shard numbers. - /// - /// You probably should not call this. - #[doc(hidden)] - pub fn set_shard_count(&mut self, shard_count: u64) { - self.shard_count = shard_count; - } - - /// Sets the ID of the user for the voice manager. - /// - /// You probably _really_ should not call this. - /// - /// But it's there if you need it. For some reason. - #[doc(hidden)] - pub fn set_user_id(&mut self, user_id: UserId) { - self.user_id = user_id; - } - - pub fn manager_get(&self, shard_id: &u64) -> Option<&Manager> { - self.managers.get(shard_id) - } - - pub fn manager_get_mut(&mut self, shard_id: &u64) -> Option<&mut Manager> { - self.managers.get_mut(shard_id) - } - - pub fn manager_remove(&mut self, shard_id: &u64) -> Option<Manager> { - self.managers.remove(shard_id) - } - - fn manager_info<G: Into<GuildId>>(&self, guild_id: G) -> (GuildId, u64) { - let guild_id = guild_id.into(); - let shard_id = utils::shard_id(guild_id.0, self.shard_count); - - (guild_id, shard_id) - } -} diff --git a/src/client/mod.rs b/src/client/mod.rs index 9dfaa10..7e55837 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -17,920 +17,160 @@ //! [`Client`]: struct.Client.html#examples //! [`Context`]: struct.Context.html //! [Client examples]: struct.Client.html#examples -#![allow(zero_ptr)] -pub mod bridge; +pub mod shard_manager; -mod context; -mod dispatch; mod error; -mod event_handler; -pub use self::context::Context; pub use self::error::Error as ClientError; -pub use self::event_handler::EventHandler; -// Note: the following re-exports are here for backwards compatibility -pub use gateway; -pub use http as rest; +use futures::Future; +use hyper::client::{Client as HyperClient, HttpConnector}; +use hyper::Body; +use hyper_tls::HttpsConnector; +use self::shard_manager::{ShardManager, ShardManagerOptions, ShardingStrategy}; +use std::cell::RefCell; +use std::rc::Rc; +use super::http::Client as HttpClient; +use super::FutureResult; +use tokio_core::reactor::Handle; #[cfg(feature = "cache")] -pub use CACHE; - -use http; -use internal::prelude::*; -use parking_lot::Mutex; -use self::bridge::gateway::{ShardManager, ShardManagerMonitor, ShardManagerOptions}; -use std::sync::Arc; -use threadpool::ThreadPool; -use typemap::ShareMap; - -#[cfg(feature = "framework")] -use framework::Framework; -#[cfg(feature = "voice")] -use model::id::UserId; -#[cfg(feature = "voice")] -use self::bridge::voice::ClientVoiceManager; +use cache::Cache; + +#[derive(Debug)] +pub struct ClientOptions { + pub handle: Handle, + pub http_client: Rc<HyperClient<HttpsConnector<HttpConnector>, Body>>, + pub sharding: ShardingStrategy, + pub token: String, +} -/// The Client is the way to be able to start sending authenticated requests -/// over the REST API, as well as initializing a WebSocket connection through -/// [`Shard`]s. Refer to the [documentation on using sharding][sharding docs] -/// for more information. -/// -/// # Event Handlers -/// -/// Event handlers can be configured. For example, the event handler -/// [`EventHandler::on_message`] will be dispatched to whenever a [`Event::MessageCreate`] is -/// received over the connection. -/// -/// Note that you do not need to manually handle events, as they are handled -/// internally and then dispatched to your event handlers. -/// -/// # Examples -/// -/// Creating a Client instance and adding a handler on every message -/// receive, acting as a "ping-pong" bot is simple: -/// -/// ```rust,ignore -/// use serenity::prelude::*; -/// use serenity::model::*; -/// -/// struct Handler; -/// -/// impl EventHandler for Handler { -/// fn on_message(&self, _: Context, msg: Message) { -/// if msg.content == "!ping" { -/// let _ = msg.channel_id.say("Pong!"); -/// } -/// } -/// } -/// -/// let mut client = Client::new("my token here", Handler); -/// -/// client.start(); -/// ``` -/// -/// [`Shard`]: gateway/struct.Shard.html -/// [`on_message`]: #method.on_message -/// [`Event::MessageCreate`]: ../model/event/enum.Event.html#variant.MessageCreate -/// [sharding docs]: gateway/index.html#sharding +#[derive(Debug)] pub struct Client { - /// A ShareMap which requires types to be Send + Sync. This is a map that - /// can be safely shared across contexts. - /// - /// The purpose of the data field is to be accessible and persistent across - /// contexts; that is, data can be modified by one context, and will persist - /// through the future and be accessible through other contexts. This is - /// useful for anything that should "live" through the program: counters, - /// database connections, custom user caches, etc. - /// - /// In the meaning of a context, this data can be accessed through - /// [`Context::data`]. - /// - /// # Examples - /// - /// Create a `MessageEventCounter` to track the following events: - /// - /// - [`Event::MessageCreate`] - /// - [`Event::MessageDelete`] - /// - [`Event::MessageDeleteBulk`] - /// - [`Event::MessageUpdate`] - /// - /// ```rust,ignore - /// extern crate serenity; - /// extern crate typemap; - /// - /// use serenity::prelude::*; - /// use serenity::model::*; - /// use std::collections::HashMap; - /// use std::env; - /// use typemap::Key; - /// - /// struct MessageEventCounter; - /// - /// impl Key for MessageEventCounter { - /// type Value = HashMap<String, u64>; - /// } - /// - /// macro_rules! reg { - /// ($ctx:ident $name:expr) => { - /// { - /// let mut data = $ctx.data.lock(); - /// let counter = data.get_mut::<MessageEventCounter>().unwrap(); - /// let entry = counter.entry($name).or_insert(0); - /// *entry += 1; - /// } - /// }; - /// } - /// - /// struct Handler; - /// - /// impl EventHandler for Handler { - /// fn on_message(&self, ctx: Context, _: Message) { reg!(ctx "MessageCreate") } - /// fn on_message_delete(&self, ctx: Context, _: ChannelId, _: MessageId) { - /// reg!(ctx "MessageDelete") } - /// fn on_message_delete_bulk(&self, ctx: Context, _: ChannelId, _: Vec<MessageId>) { - /// reg!(ctx "MessageDeleteBulk") } - /// fn on_message_update(&self, ctx: Context, _: ChannelId, _: MessageId) { - /// reg!(ctx "MessageUpdate") } - /// } - /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN").unwrap(), Handler); - /// - /// { - /// let mut data = client.data.lock(); - /// data.insert::<MessageEventCounter>(HashMap::default()); - /// } - /// - /// client.start().unwrap(); - /// ``` - /// - /// Refer to [example 05] for an example on using the `data` field. - /// - /// [`Context::data`]: struct.Context.html#method.data - /// [`Event::MessageCreate`]: ../model/event/enum.Event.html#variant.MessageCreate - /// [`Event::MessageDelete`]: ../model/event/enum.Event.html#variant.MessageDelete - /// [`Event::MessageDeleteBulk`]: ../model/event/enum.Event.html#variant.MessageDeleteBulk - /// [`Event::MessageUpdate`]: ../model/event/enum.Event.html#variant.MessageUpdate - /// [example 05]: - /// https://github.com/zeyla/serenity/tree/master/examples/05_command_framework - pub data: Arc<Mutex<ShareMap>>, - /// A vector of all active shards that have received their [`Event::Ready`] - /// payload, and have dispatched to [`on_ready`] if an event handler was - /// configured. - /// - /// [`Event::Ready`]: ../model/event/enum.Event.html#variant.Ready - /// [`on_ready`]: #method.on_ready - #[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. - /// - /// # Examples - /// - /// If you call [`client.start_shard(3, 5)`][`Client::start_shard`], this - /// HashMap will only ever contain a single key of `3`, as that's the only - /// Shard the client is responsible for. - /// - /// If you call [`client.start_shards(10)`][`Client::start_shards`], this - /// HashMap will contain keys 0 through 9, one for each shard handled by the - /// client. - /// - /// Printing the number of shards currently instantiated by the client every - /// 5 seconds: - /// - /// ```rust,no_run - /// # use serenity::client::{Client, EventHandler}; - /// # use std::error::Error; - /// # use std::time::Duration; - /// # use std::{env, thread}; - /// - /// # fn try_main() -> Result<(), Box<Error>> { - /// - /// struct Handler; - /// - /// impl EventHandler for Handler { } - /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler)?; - /// - /// let shard_manager = client.shard_manager.clone(); - /// - /// thread::spawn(move || { - /// loop { - /// println!("Shard count instantiated: {}", - /// shard_manager.lock().shards_instantiated().len()); - /// - /// thread::sleep(Duration::from_millis(5000)); - /// } - /// }); - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// Shutting down all connections after one minute of operation: - /// - /// ```rust,no_run - /// # use std::error::Error; - /// # - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::{Client, EventHandler}; - /// use std::time::Duration; - /// use std::{env, thread}; - /// - /// struct Handler; - /// - /// impl EventHandler for Handler { } - /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler)?; - /// - /// // Create a clone of the `Arc` containing the shard manager. - /// let shard_manager = client.shard_manager.clone(); - /// - /// // Create a thread which will sleep for 60 seconds and then have the - /// // shard manager shutdown. - /// thread::spawn(move || { - /// thread::sleep(Duration::from_secs(60)); - /// - /// shard_manager.lock().shutdown_all(); - /// - /// println!("Shutdown shard manager!"); - /// }); - /// - /// println!("Client shutdown: {:?}", client.start()); - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// [`Client::start_shard`]: #method.start_shard - /// [`Client::start_shards`]: #method.start_shards - 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, - /// The token in use by the client. - pub token: Arc<Mutex<String>>, - /// The voice manager for the client. - /// - /// This is an ergonomic structure for interfacing over shards' voice - /// connections. - #[cfg(feature = "voice")] - pub voice_manager: Arc<Mutex<ClientVoiceManager>>, - /// 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>>, + #[cfg(feature = "cache")] + pub cache: Rc<RefCell<Cache>>, + pub handle: Handle, + pub http: Rc<HttpClient>, + pub shard_manager: ShardManager, + token: Rc<String>, + ws_uri: Rc<String>, } impl Client { - /// Creates a Client for a bot user. - /// - /// Discord has a requirement of prefixing bot tokens with `"Bot "`, which - /// this function will automatically do for you if not already included. - /// - /// # Examples - /// - /// Create a Client, using a token from an environment variable: - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # use std::error::Error; - /// # - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::Client; - /// use std::env; - /// - /// let token = env::var("DISCORD_TOKEN")?; - /// let client = Client::new(&token, Handler)?; - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - pub fn new<H>(token: &str, handler: H) -> Result<Self> - where H: EventHandler + Send + Sync + 'static { - let token = token.trim(); - - let token = if token.starts_with("Bot ") { - token.to_string() - } else { - format!("Bot {}", token) - }; - - http::set_token(&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); - - #[cfg(feature = "framework")] - let framework = Arc::new(Mutex::new(None)); - #[cfg(feature = "voice")] - let voice_manager = Arc::new(Mutex::new(ClientVoiceManager::new( - 0, - UserId(0), - ))); - - let (shard_manager, shard_manager_worker) = { - ShardManager::new(ShardManagerOptions { - data: &data, - event_handler: &event_handler, - #[cfg(feature = "framework")] - framework: &framework, - shard_index: 0, - shard_init: 0, - shard_total: 0, - threadpool: threadpool.clone(), - token: &locked, - #[cfg(feature = "voice")] - voice_manager: &voice_manager, - ws_url: &url, + pub fn new(options: ClientOptions) -> FutureResult<Self> { + let token = { + let trimmed = options.token.trim(); + + Rc::new(if trimmed.starts_with("Bot ") { + trimmed.to_string() + } else { + format!("Bot {}", trimmed) }) }; - Ok(Client { - token: locked, - ws_uri: url, - #[cfg(feature = "framework")] - framework, - data, - shard_manager, - shard_manager_worker, - threadpool, - #[cfg(feature = "voice")] - voice_manager, - }) - } - - /// Sets a framework to be used with the client. All message events will be - /// passed through the framework _after_ being passed to the [`on_message`] - /// event handler. - /// - /// See the [framework module-level documentation][framework docs] for more - /// information on usage. - /// - /// # Examples - /// - /// Create a simple framework that responds to a `~ping` command: - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// use serenity::framework::StandardFramework; - /// - /// struct Handler; - /// - /// 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)?; - /// client.with_framework(StandardFramework::new() - /// .configure(|c| c.prefix("~")) - /// .on("ping", |_, msg, _| { - /// msg.channel_id.say("Pong!")?; - /// - /// Ok(()) - /// })); - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// Using your own framework: - /// - /// ```rust,ignore - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// use serenity::Framework; - /// use serenity::client::Context; - /// use serenity::model::*; - /// use tokio_core::reactor::Handle; - /// use std::collections::HashMap; - /// - /// - /// struct MyFramework { - /// commands: HashMap<String, Box<Fn(Message, Vec<String>)>>, - /// } - /// - /// impl Framework for MyFramework { - /// fn dispatch(&mut self, _: Context, msg: Message, tokio_handle: &Handle) { - /// let args = msg.content.split_whitespace(); - /// let command = match args.next() { - /// Some(command) => { - /// if !command.starts_with('*') { return; } - /// command - /// }, - /// None => return, - /// }; - /// - /// let command = match self.commands.get(&command) { - /// Some(command) => command, None => return, - /// }; - /// - /// tokio_handle.spawn_fn(move || { (command)(msg, args); Ok() }); - /// } - /// } - /// - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::Client; - /// use std::env; - /// - /// 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!"))); - /// map - /// }}); - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// Refer to the documentation for the `framework` module for more in-depth - /// information. - /// - /// [`on_message`]: #method.on_message - /// [framework docs]: ../framework/index.html - #[cfg(feature = "framework")] - pub fn with_framework<F: Framework + Send + 'static>(&mut self, f: F) { - *self.framework.lock() = Some(Box::new(f)); - } - - /// Establish the connection and start listening for events. - /// - /// This will start receiving events in a loop and start dispatching the - /// events to your registered handlers. - /// - /// Note that this should be used only for users and for bots which are in - /// less than 2500 guilds. If you have a reason for sharding and/or are in - /// more than 2500 guilds, use one of these depending on your use case: - /// - /// Refer to the [Gateway documentation][gateway docs] for more information - /// on effectively using sharding. - /// - /// # Examples - /// - /// Starting a Client with only 1 shard, out of 1 total: - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::Client; - /// use std::env; - /// - /// 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); - /// } - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// [gateway docs]: gateway/index.html#sharding - pub fn start(&mut self) -> Result<()> { - self.start_connection([0, 0, 1]) - } - - /// Establish the connection(s) and start listening for events. - /// - /// This will start receiving events in a loop and start dispatching the - /// events to your registered handlers. - /// - /// This will retrieve an automatically determined number of shards to use - /// from the API - determined by Discord - and then open a number of shards - /// equivalent to that amount. - /// - /// Refer to the [Gateway documentation][gateway docs] for more information - /// on effectively using sharding. - /// - /// # Examples - /// - /// Start as many shards as needed using autosharding: - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::Client; - /// use std::env; - /// - /// 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); - /// } - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to - /// an error. - /// - /// [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown - /// [gateway docs]: gateway/index.html#sharding - pub fn start_autosharded(&mut self) -> Result<()> { - let (x, y) = { - let res = http::get_bot_gateway()?; - - (res.shards as u64 - 1, res.shards as u64) - }; - - self.start_connection([0, x, y]) - } - - /// Establish a sharded connection and start listening for events. - /// - /// This will start receiving events and dispatch them to your registered - /// handlers. - /// - /// This will create a single shard by ID. If using one shard per process, - /// you will need to start other processes with the other shard IDs in some - /// way. - /// - /// Refer to the [Gateway documentation][gateway docs] for more information - /// on effectively using sharding. - /// - /// # Examples - /// - /// Start shard 3 of 5: - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::Client; - /// use std::env; - /// - /// 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); - /// } - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// Start shard 0 of 1 (you may also be interested in [`start`] or - /// [`start_autosharded`]): - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::Client; - /// use std::env; - /// - /// let mut client = Client::new(&env::var("DISCORD_TOKEN")?, Handler)?; - /// - /// if let Err(why) = client.start_shard(0, 1) { - /// println!("Err with client: {:?}", why); - /// } - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to - /// an error. - /// - /// [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown - /// [`start`]: #method.start - /// [`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]) - } - - /// Establish sharded connections and start listening for events. - /// - /// This will start receiving events and dispatch them to your registered - /// handlers. - /// - /// This will create and handle all shards within this single process. If - /// you only need to start a single shard within the process, or a range of - /// shards, use [`start_shard`] or [`start_shard_range`], respectively. - /// - /// Refer to the [Gateway documentation][gateway docs] for more information - /// on effectively using sharding. - /// - /// # Examples - /// - /// Start all of 8 shards: - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::Client; - /// use std::env; - /// - /// 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); - /// } - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to - /// an error. - /// - /// [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown - /// [`start_shard`]: #method.start_shard - /// [`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]) - } - - /// Establish a range of sharded connections and start listening for events. - /// - /// This will start receiving events and dispatch them to your registered - /// handlers. - /// - /// This will create and handle all shards within a given range within this - /// single process. If you only need to start a single shard within the - /// process, or all shards within the process, use [`start_shard`] or - /// [`start_shards`], respectively. - /// - /// Refer to the [Gateway documentation][gateway docs] for more - /// information on effectively using sharding. - /// - /// # Examples - /// - /// For a bot using a total of 10 shards, initialize shards 4 through 7: - /// - /// ```rust,ignore - /// # use serenity::prelude::EventHandler; - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// use serenity::Client; - /// use std::env; - /// - /// let token = env::var("DISCORD_TOKEN").unwrap(); - /// let mut client = Client::new(&token, Handler); - /// - /// let _ = client.start_shard_range([4, 7], 10); - /// ``` - /// - /// ```rust,no_run - /// # use serenity::prelude::EventHandler; - /// # use std::error::Error; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler {} - /// # fn try_main() -> Result<(), Box<Error>> { - /// use serenity::client::Client; - /// use std::env; - /// - /// 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); - /// } - /// # Ok(()) - /// # } - /// # - /// # fn main() { - /// # try_main().unwrap(); - /// # } - /// ``` - /// - /// # Errors - /// - /// Returns a [`ClientError::Shutdown`] when all shards have shutdown due to - /// an error. - /// - /// - /// [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown - /// [`start_shard`]: #method.start_shard - /// [`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]) - } - - // Shard data layout is: - // 0: first shard number to initialize - // 1: shard number to initialize up to and including - // 2: total number of shards the bot is sharding for - // - // Not all shards need to be initialized in this process. - // - // # Errors - // - // Returns a [`ClientError::Shutdown`] when all shards have shutdown due to - // an error. - // - // [`ClientError::Shutdown`]: enum.ClientError.html#variant.Shutdown - fn start_connection(&mut self, shard_data: [u64; 3]) -> Result<()> { - #[cfg(feature = "voice")] - self.voice_manager.lock().set_shard_count(shard_data[2]); - - // This is kind of gross, but oh well. - // - // Both the framework and voice bridge need the user's ID, so we'll only - // retrieve it over REST if at least one of those are enabled. - #[cfg(any(all(feature = "standard_framework", feature = "framework"), - feature = "voice"))] - { - let user = http::get_current_user()?; - - // Update the framework's current user if the feature is enabled. - // - // This also acts as a form of check to ensure the token is correct. - #[cfg(all(feature = "standard_framework", feature = "framework"))] - { - if let Some(ref mut framework) = *self.framework.lock() { - framework.update_current_user(user.id); - } - } - - #[cfg(feature = "voice")] - { - self.voice_manager.lock().set_user_id(user.id); - } - } - - { - let mut manager = self.shard_manager.lock(); - - let init = shard_data[1] - shard_data[0] + 1; - - manager.set_shards(shard_data[0], init, shard_data[2]); - - 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"); - - manager.shutdown_all(); - - return Err(Error::Client(ClientError::ShardBootFailure)); + let h2 = options.handle.clone(); + let strategy = options.sharding; + let client = Rc::new(HttpClient::new( + options.http_client, + options.handle.clone(), + Rc::clone(&token), + )); + + let done = client.get_bot_gateway().map(move |gateway| { + let uri = Rc::new(gateway.url); + + Client { + #[cfg(feature = "cache")] + cache: Rc::new(RefCell::new(Cache::default())), handle: h2, + http: client, + shard_manager: ShardManager::new(ShardManagerOptions { + strategy: strategy, + token: Rc::clone(&token), + ws_uri: Rc::clone(&uri), + }), + token: token, + ws_uri: Rc::clone(&uri), } - } + }).from_err(); - self.shard_manager_worker.run(); - - Ok(()) - } -} - -/// Validates that a token is likely in a valid format. -/// -/// This performs the following checks on a given token: -/// -/// - At least one character long; -/// - Contains 3 parts (split by the period char `'.'`); -/// - The second part of the token is at least 6 characters long; -/// - The token does not contain any whitespace prior to or after the token. -/// -/// # Examples -/// -/// Validate that a token is valid and that a number of invalid tokens are -/// actually invalid: -/// -/// ```rust,no_run -/// use serenity::client::validate_token; -/// -/// // ensure a valid token is in fact valid: -/// assert!(validate_token("Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg").is_ok()); -/// -/// // "cat" isn't a valid token: -/// assert!(validate_token("cat").is_err()); -/// -/// // tokens must have three parts, separated by periods (this is still -/// // actually an invalid token): -/// assert!(validate_token("aaa.abcdefgh.bbb").is_ok()); -/// -/// // the second part must be _at least_ 6 characters long: -/// assert!(validate_token("a.abcdef.b").is_ok()); -/// assert!(validate_token("a.abcde.b").is_err()); -/// ``` -/// -/// # Errors -/// -/// Returns a [`ClientError::InvalidToken`] when one of the above checks fail. -/// The type of failure is not specified. -/// -/// [`ClientError::InvalidToken`]: enum.ClientError.html#variant.InvalidToken -pub fn validate_token(token: &str) -> Result<()> { - if token.is_empty() { - return Err(Error::Client(ClientError::InvalidToken)); - } - - let parts: Vec<&str> = token.split('.').collect(); - - // Check that the token has a total of 3 parts. - if parts.len() != 3 { - return Err(Error::Client(ClientError::InvalidToken)); - } - - // Check that the second part is at least 6 characters long. - if parts[1].len() < 6 { - return Err(Error::Client(ClientError::InvalidToken)); + Box::new(done) } - // Check that there is no whitespace before/after the token. - if token.trim() != token { - return Err(Error::Client(ClientError::InvalidToken)); - } + // pub fn connect(&self) -> ::futures::Stream<Item = Dispatch, Error = ::Error> { + // self.shard_manager.start().map(|(shard_id, msg)| { + // Dispatch { + // msg, + // shard_id, + // } + // }) + // } +} - Ok(()) +pub struct Dispatch { + pub msg: ::model::event::GatewayEvent, + pub shard_id: u64, } + +// Validates that a token is likely in a valid format. +// +// This performs the following checks on a given token: +// +// - At least one character long; +// - Contains 3 parts (split by the period char `'.'`); +// - The second part of the token is at least 6 characters long; +// - The token does not contain any whitespace prior to or after the token. +// +// # Examples +// +// Validate that a token is valid and that a number of invalid tokens are +// actually invalid: +// +// ```rust,no_run +// use serenity::client::validate_token; +// +// // ensure a valid token is in fact valid: +// assert!(validate_token"Mjg4NzYwMjQxMzYzODc3ODg4.C_ikow.j3VupLBuE1QWZng3TMGH0z_UAwg").is_ok()); +// +// // "cat" isn't a valid token: +// assert!(validate_token("cat").is_err()); +// +// // tokens must have three parts, separated by periods (this is still +// // actually an invalid token): +// assert!(validate_token("aaa.abcdefgh.bbb").is_ok()); +// +// // the second part must be _at least_ 6 characters long: +// assert!(validate_token("a.abcdef.b").is_ok()); +// assert!(validate_token("a.abcde.b").is_err()); +// ``` +// +// # Errors +// +// Returns a [`ClientError::InvalidToken`] when one of the above checks fail. +// The type of failure is not specified. +// +// [`ClientError::InvalidToken`]: enum.ClientError.html#variant.InvalidToken +// pub fn validate_token(token: &str) -> Result<()> { +// if token.is_empty() { +// return Err(Error::Client(ClientError::InvalidToken)); +// } + +// let parts: Vec<&str> = token.split('.').collect(); + +// // Check that the token has a total of 3 parts. +// if parts.len() != 3 { +// return Err(Error::Client(ClientError::InvalidToken)); +// } + +// // Check that the second part is at least 6 characters long. +// if parts[1].len() < 6 { +// return Err(Error::Client(ClientError::InvalidToken)); +// } + +// // Check that there is no whitespace before/after the token. +// if token.trim() != token { +// return Err(Error::Client(ClientError::InvalidToken)); +// } + +// Ok(()) +// } diff --git a/src/client/shard_manager.rs b/src/client/shard_manager.rs new file mode 100644 index 0000000..0de3deb --- /dev/null +++ b/src/client/shard_manager.rs @@ -0,0 +1,62 @@ +use std::collections::VecDeque; +use std::rc::Rc; + +#[derive(Clone, Copy, Debug)] +pub enum ShardingStrategy { + Autoshard, + Range([u64; 3]), +} + +impl ShardingStrategy { + pub fn auto() -> Self { + ShardingStrategy::Autoshard + } + + pub fn multi(count: u64) -> Self { + ShardingStrategy::Range([0, count, count]) + } + + pub fn simple() -> Self { + ShardingStrategy::Range([0, 1, 1]) + } + + pub fn range(index: u64, count: u64, total: u64) -> Self { + ShardingStrategy::Range([index, count, total]) + } +} + +impl Default for ShardingStrategy { + fn default() -> Self { + ShardingStrategy::Autoshard + } +} + +#[derive(Clone, Debug, Default)] +pub struct ShardManagerOptions { + pub strategy: ShardingStrategy, + pub token: Rc<String>, + pub ws_uri: Rc<String>, +} + +#[derive(Debug)] +pub struct ShardManager { + pub queue: VecDeque<u64>, + pub shards: (), + pub strategy: ShardingStrategy, + pub token: Rc<String>, + pub ws_uri: Rc<String>, + non_exhaustive: (), +} + +impl ShardManager { + pub fn new(options: ShardManagerOptions) -> Self { + Self { + queue: VecDeque::new(), + shards: (), + strategy: options.strategy, + token: options.token, + ws_uri: options.ws_uri, + non_exhaustive: (), + } + } +} diff --git a/src/error.rs b/src/error.rs index 489d7f5..64b64af 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,19 +1,24 @@ +use futures::{Canceled, Future}; +use hyper::error::UriError; use internal::prelude::*; use model::ModelError; use serde_json::Error as JsonError; +use std::cell::{BorrowError, BorrowMutError}; use std::error::Error as StdError; use std::fmt::{self, Display, Error as FormatError}; use std::io::Error as IoError; use std::num::ParseIntError; +#[cfg(feature = "tungstenite")] +use futures::sync::mpsc::SendError; #[cfg(feature = "hyper")] use hyper::Error as HyperError; #[cfg(feature = "native-tls")] use native_tls::Error as TlsError; #[cfg(feature = "voice")] use opus::Error as OpusError; -#[cfg(feature = "websocket")] -use websocket::result::WebSocketError; +#[cfg(feature = "tungstenite")] +use tungstenite::{Error as TungsteniteError, Message as TungsteniteMessage}; #[cfg(feature = "client")] use client::ClientError; #[cfg(feature = "gateway")] @@ -33,6 +38,8 @@ use voice::VoiceError; /// [`Error`]: enum.Error.html pub type Result<T> = StdResult<T, Error>; +pub type FutureResult<T> = Box<Future<Item = T, Error = Error>>; + /// A common error enum returned by most of the library's functionality within a /// custom [`Result`]. /// @@ -47,6 +54,12 @@ pub type Result<T> = StdResult<T, Error>; /// [`Result`]: type.Result.html #[derive(Debug)] pub enum Error { + /// A cell could not be immutably borrowed. + Borrow(BorrowError), + /// An error occurred while mutably borrowing from an `std::cell::RefCell`. + BorrowMut(BorrowMutError), + /// A future was canceled, most likely reading from an mpsc channel. + Canceled(Canceled), /// An error while decoding a payload. Decode(&'static str, Value), /// There was an error with a format. @@ -73,6 +86,8 @@ pub enum Error { /// /// [`Error::Decode`]: #variant.Decode Other(&'static str), + /// A `hyper` error while parsing a Uri. + Uri(UriError), /// An error from the `url` crate. Url(String), /// A [client] error. @@ -94,9 +109,12 @@ pub enum Error { /// An error from the `native-tls` crate. #[cfg(feature = "native-tls")] Tls(TlsError), - /// An error from the `rust-websocket` crate. - #[cfg(feature = "gateway")] - WebSocket(WebSocketError), + /// An error while sending a message over a WebSocket. + #[cfg(feature = "tungstenite")] + WebSocketSend(SendError<TungsteniteMessage>), + /// An error from the `tungstenite` crate. + #[cfg(feature = "tungstenite")] + Tungstenite(TungsteniteError), /// An error from the `opus` crate. #[cfg(feature = "voice")] Opus(OpusError), @@ -107,6 +125,18 @@ pub enum Error { Voice(VoiceError), } +impl From<BorrowError> for Error { + fn from(e: BorrowError) -> Error { Error::Borrow(e) } +} + +impl From<BorrowMutError> for Error { + fn from(e: BorrowMutError) -> Error { Error::BorrowMut(e) } +} + +impl From<Canceled> for Error { + fn from(e: Canceled) -> Error { Error::Canceled(e) } +} + impl From<FormatError> for Error { fn from(e: FormatError) -> Error { Error::Format(e) } } @@ -142,14 +172,27 @@ impl From<OpusError> for Error { fn from(e: OpusError) -> Error { Error::Opus(e) } } +#[cfg(feature = "tungstenite")] +impl From<SendError<TungsteniteMessage>> for Error { + fn from(err: SendError<TungsteniteMessage>) -> Self { + Error::WebSocketSend(err) + } +} + #[cfg(feature = "native-tls")] impl From<TlsError> for Error { fn from(e: TlsError) -> Error { Error::Tls(e) } } -#[cfg(feature = "gateway")] -impl From<WebSocketError> for Error { - fn from(e: WebSocketError) -> Error { Error::WebSocket(e) } +#[cfg(feature = "tungstenite")] +impl From<TungsteniteError> for Error { + fn from(err: TungsteniteError) -> Self { + Error::Tungstenite(err) + } +} + +impl From<UriError> for Error { + fn from(e: UriError) -> Error { Error::Uri(e) } } impl Display for Error { @@ -161,6 +204,9 @@ impl Display for Error { impl StdError for Error { fn description(&self) -> &str { match *self { + Error::Borrow(ref inner) => inner.description(), + Error::BorrowMut(ref inner) => inner.description(), + Error::Canceled(ref inner) => inner.description(), Error::Decode(msg, _) | Error::Other(msg) => msg, Error::ExceededLimit(..) => "Input exceeded a limit", Error::Format(ref inner) => inner.description(), @@ -169,6 +215,7 @@ impl StdError for Error { Error::Model(ref inner) => inner.description(), Error::Num(ref inner) => inner.description(), Error::Url(ref inner) => inner, + Error::Uri(ref inner) => inner.description(), #[cfg(feature = "client")] Error::Client(ref inner) => inner.description(), #[cfg(feature = "gateway")] @@ -181,10 +228,12 @@ impl StdError for Error { Error::Opus(ref inner) => inner.description(), #[cfg(feature = "native-tls")] Error::Tls(ref inner) => inner.description(), + #[cfg(feature = "tungstenite")] + Error::Tungstenite(ref inner) => inner.description(), #[cfg(feature = "voice")] Error::Voice(_) => "Voice error", - #[cfg(feature = "gateway")] - Error::WebSocket(ref inner) => inner.description(), + #[cfg(feature = "tungstenite")] + Error::WebSocketSend(ref inner) => inner.description(), } } @@ -194,8 +243,6 @@ impl StdError for Error { Error::Hyper(ref inner) => Some(inner), Error::Json(ref inner) => Some(inner), Error::Io(ref inner) => Some(inner), - #[cfg(feature = "gateway")] - Error::WebSocket(ref inner) => Some(inner), _ => None, } } diff --git a/src/gateway/error.rs b/src/gateway/error.rs index b75c5d4..c767823 100644 --- a/src/gateway/error.rs +++ b/src/gateway/error.rs @@ -1,6 +1,5 @@ use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; -use websocket::message::CloseData; /// An error that occurred while attempting to deal with the gateway. /// @@ -10,8 +9,6 @@ use websocket::message::CloseData; pub enum Error { /// There was an error building a URL. BuildingUrl, - /// The connection closed, potentially uncleanly. - Closed(Option<CloseData>), /// Expected a Hello during a handshake ExpectedHello, /// When there was an error sending a heartbeat. @@ -56,7 +53,6 @@ impl StdError for Error { match *self { BuildingUrl => "Error building url", - Closed(_) => "Connection closed", ExpectedHello => "Expected a Hello", HeartbeatFailed => "Failed sending a heartbeat", InvalidAuthentication => "Sent invalid authentication", diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index c4748a0..fd2b083 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -51,24 +51,22 @@ 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 futures::stream::SplitStream; use model::gateway::Game; use model::user::OnlineStatus; -use serde_json::Value; use std::fmt::{Display, Formatter, Result as FmtResult}; -use websocket::sync::client::Client; -use websocket::sync::stream::{TcpStream, TlsStream}; - -#[cfg(feature = "client")] -use client::bridge::gateway::ShardClientMessage; +use tokio_core::net::TcpStream; +use tokio_tls::TlsStream; +use tokio_tungstenite::stream::Stream; +use tokio_tungstenite::WebSocketStream; pub type CurrentPresence = (Option<Game>, OnlineStatus); -pub type WsClient = Client<TlsStream<TcpStream>>; +pub type ShardStream = + SplitStream<WebSocketStream<Stream<TcpStream, TlsStream<TcpStream>>>>; /// Indicates the current connection stage of a [`Shard`]. /// @@ -167,18 +165,6 @@ impl Display for ConnectionStage { } } -/// A message to be passed around within the library. -/// -/// As a user you usually don't need to worry about this, but when working with -/// the lower-level internals of the `client`, `gateway, and `voice` modules it -/// may be necessary. -#[derive(Clone, Debug)] -pub enum InterMessage { - #[cfg(feature = "client")] - Client(ShardClientMessage), - Json(Value), -} - pub enum ShardAction { Heartbeat, Identify, diff --git a/src/gateway/shard.rs b/src/gateway/shard.rs index da87c61..5fdadd7 100644 --- a/src/gateway/shard.rs +++ b/src/gateway/shard.rs @@ -1,362 +1,125 @@ -use constants::{self, close_codes}; -use internal::prelude::*; +use chrono::Utc; +use constants::{GATEWAY_VERSION, LARGE_THRESHOLD, OpCode}; +use tokio_core::reactor::Handle; +use futures::future::Future; +use futures::stream::Stream as FuturesStream; +use futures::sync::mpsc::{self, UnboundedSender}; +use futures::Sink; use model::event::{Event, GatewayEvent}; use model::gateway::Game; use model::id::GuildId; use model::user::OnlineStatus; -use parking_lot::Mutex; -use std::sync::Arc; -use std::time::{Duration as StdDuration, Instant}; -use super::{ - ConnectionStage, - CurrentPresence, - ShardAction, - GatewayError, - ReconnectType, - WsClient, - WebSocketGatewayClientExt, -}; -use websocket::client::Url; -use websocket::stream::sync::AsTcpStream; -use websocket::sync::client::ClientBuilder; -use websocket::WebSocketError; - -/// 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 -/// websocket, such as setting the active game, reconnecting, syncing guilds, -/// and more. -/// -/// Refer to the [module-level documentation][module docs] for information on -/// effectively using multiple shards, if you need to. -/// -/// Note that there are additional methods available if you are manually -/// managing a shard yourself, although they are hidden from the documentation -/// since there are few use cases for doing such. -/// -/// # Stand-alone shards -/// -/// You may instantiate a shard yourself - decoupled from the [`Client`] - if -/// you need to. For most use cases, you will not need to do this, and you can -/// leave the client to do it. -/// -/// This can be done by passing in the required parameters to [`new`]. You can -/// then manually handle the shard yourself and receive events via -/// [`receive`]. -/// -/// **Note**: You _really_ do not need to do this. Just call one of the -/// appropriate methods on the [`Client`]. -/// -/// # Examples -/// -/// See the documentation for [`new`] on how to use this. -/// -/// [`Client`]: ../struct.Client.html -/// [`new`]: #method.new -/// [`receive`]: #method.receive -/// [docs]: https://discordapp.com/developers/docs/topics/gateway#sharding -/// [module docs]: index.html#sharding +use serde_json::{self, Error as JsonError, Value}; +use std::cell::RefCell; +use std::env::consts; +use std::io::{Error as IoError, ErrorKind as IoErrorKind}; +use std::rc::Rc; +use std::time::{Duration, Instant}; +use super::{ConnectionStage, CurrentPresence, ShardStream}; +use tungstenite::{Error as TungsteniteError, Message as TungsteniteMessage}; +use tokio_timer::Timer; +use tokio_tungstenite::connect_async; +use url::Url; +use std::str::FromStr; +use ::Error; + +const CONNECTION: &'static str = "wss://gateway.discord.gg/?v=6&encoding=json"; + +#[derive(Copy, Clone, Debug)] +struct HeartbeatInfo { + pub heartbeat_instants: (Option<Instant>, Option<Instant>), + pub heartbeater: bool, + pub last_heartbeat_acknowledged: bool, + pub seq: u64, + pub shard_info: [u64; 2], +} + pub struct Shard { - pub client: WsClient, current_presence: CurrentPresence, - /// A tuple of: - /// - /// - the last instant that a heartbeat was sent - /// - the last instant that an acknowledgement was received - /// - /// This can be used to calculate [`latency`]. - /// - /// [`latency`]: fn.latency.html - heartbeat_instants: (Option<Instant>, Option<Instant>), - heartbeat_interval: Option<u64>, - /// This is used by the heartbeater to determine whether the last - /// heartbeat was sent without an acknowledgement, and whether to reconnect. - // This _must_ be set to `true` in `Shard::handle_event`'s - // `Ok(GatewayEvent::HeartbeatAck)` arm. - last_heartbeat_acknowledged: bool, - seq: u64, + handle: Handle, + heartbeat_info: Rc<RefCell<HeartbeatInfo>>, + interval: Option<u64>, session_id: Option<String>, shard_info: [u64; 2], - /// Whether the shard has permanently shutdown. - shutdown: bool, stage: ConnectionStage, - pub token: Arc<Mutex<String>>, - ws_url: Arc<Mutex<String>>, + stream: Option<ShardStream>, + token: String, + tx: UnboundedSender<TungsteniteMessage>, } impl Shard { - /// Instantiates a new instance of a Shard, bypassing the client. - /// - /// **Note**: You should likely never need to do this yourself. - /// - /// # Examples - /// - /// Instantiating a new Shard manually for a bot with no shards, and - /// then listening for events: - /// - /// ```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 = Arc::new(Mutex::new(env::var("DISCORD_BOT_TOKEN")?)); - /// // retrieve the gateway response, which contains the URL to connect to - /// 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 mut client = connect(&*ws_url.lock())?; - - 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; - let seq = 0; - let stage = ConnectionStage::Handshake; - let session_id = None; - - Ok(Shard { - shutdown: false, - client, - current_presence, - heartbeat_instants, - heartbeat_interval, - last_heartbeat_acknowledged, - seq, - stage, - token, - session_id, - shard_info, - ws_url, - }) - } - - /// 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 - /// [`shutdown_clean`]. - /// - /// [`shutdown`]: #method.shutdown - /// [`shutdown_clean`]: #method.shutdown_clean - #[inline] - pub fn is_shutdown(&self) -> bool { - self.shutdown - } - - /// Retrieves the heartbeat instants of the shard. - /// - /// 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 - } - - /// 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() - } - - /// 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. - /// - /// This sets the last heartbeat time to now, and - /// `last_heartbeat_acknowledged` to `false`. - /// - /// # Errors - /// - /// 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); - }, + pub fn new(token: String, shard_info: [u64; 2], handle: Handle) + -> Box<Future<Item = Shard, Error = Error>> { + let done = connect_async(Url::from_str(CONNECTION).unwrap(), handle.remote().clone()) + .map(move |(duplex, _)| { + let (sink, stream) = duplex.split(); + let (tx, rx) = mpsc::unbounded(); + + let done = rx + .map_err(|why| { + error!("Err select sink rx: {:?}", why); + + TungsteniteError::Io(IoError::new( + IoErrorKind::Other, + "Err selecting sink rx", + )) + }) + .forward(sink) + .map(|_| ()) + .map_err(|_| ()); + + handle.spawn(done); + + Self { + current_presence: (None, OnlineStatus::Online), + handle: handle.clone(), + heartbeat_info: Rc::new(RefCell::new(HeartbeatInfo { + heartbeat_instants: (None, None), + heartbeater: false, + last_heartbeat_acknowledged: true, + seq: 0, + shard_info, + })), + interval: None, + session_id: None, + stage: ConnectionStage::Handshake, + stream: Some(stream), + shard_info, + token, + tx, } + }) + .map_err(From::from); - 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() - } - - /// ```rust,no_run - /// # #[cfg(feature = "model")] - /// # fn main() { - /// # use serenity::client::gateway::Shard; - /// # use std::sync::Arc; - /// # use serenity::prelude::Mutex; - /// # - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let mut shard = Shard::new(mutex.clone(), mutex, [0, 1]).unwrap(); - /// # - /// use serenity::model::gateway::Game; - /// - /// shard.set_game(Some(Game::playing("Heroes of the Storm"))); - /// # } - /// # - /// # #[cfg(not(feature = "model"))] - /// # fn main() { } - /// ``` - #[inline] - pub fn set_game(&mut self, game: Option<Game>) { - self.current_presence.0 = game; - } - - #[inline] - pub fn set_presence(&mut self, status: OnlineStatus, game: Option<Game>) { - self.set_game(game); - self.set_status(status); + Box::new(done) } - #[inline] - pub fn set_status(&mut self, mut status: OnlineStatus) { - if status == OnlineStatus::Offline { - status = OnlineStatus::Invisible; + pub fn parse(&self, msg: TungsteniteMessage) -> Result<GatewayEvent, JsonError> { + match msg { + TungsteniteMessage::Binary(v) => serde_json::from_slice(&v), + TungsteniteMessage::Text(v) => serde_json::from_str(&v), + _ => unreachable!("parse other"), } - - self.current_presence.1 = status; } - /// 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. - /// - /// 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 - /// - /// Retrieving the shard info for the second shard, out of two shards total: - /// - /// ```rust,no_run - /// # extern crate serenity; - /// # #[cfg(feature = "model")] - /// # fn main() { - /// # use serenity::client::gateway::Shard; - /// # use serenity::prelude::Mutex; - /// # use std::sync::Arc; - /// # - /// # let mutex = Arc::new(Mutex::new("".to_string())); - /// # - /// # let shard = Shard::new(mutex.clone(), mutex, [1, 2]).unwrap(); - /// # - /// assert_eq!(shard.shard_info(), [1, 2]); - /// # } - /// # - /// # #[cfg(not(feature = "model"))] - /// # fn main() {} - /// ``` - pub fn shard_info(&self) -> [u64; 2] { self.shard_info } - - /// Returns the current connection stage of the shard. - pub fn stage(&self) -> ConnectionStage { - self.stage - } - /// Handles an event from the gateway over the receiver, requiring the - /// receiver to be passed if a reconnect needs to occur. + /// Processes the given event to determine if something needs to be done. /// - /// The best case scenario is that one of two values is returned: - /// - /// - `Ok(None)`: a heartbeat, late hello, or session invalidation was - /// received; - /// - `Ok(Some((event, None)))`: an op0 dispatch was received, and the - /// shard's voice state will be updated, _if_ the `voice` feature is - /// enabled. - /// - /// # Errors - /// - /// Returns a `GatewayError::InvalidAuthentication` if invalid - /// authentication was sent in the IDENTIFY. - /// - /// Returns a `GatewayError::InvalidShardData` if invalid shard data was - /// sent in the IDENTIFY. - /// - /// Returns a `GatewayError::NoAuthentication` if no authentication was sent - /// in the IDENTIFY. - /// - /// 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<ShardAction>> { + /// For example, an event may cause the shard to need to reconnect due to a + /// session invalidating. + pub fn process(&mut self, event: &GatewayEvent) { 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); + GatewayEvent::Dispatch(seq, ref event) => { + let mut info = self.heartbeat_info.borrow_mut(); + let self_seq = info.seq; + + if seq > self_seq + 1 { + warn!( + "[Shard {:?}] Heartbeat off; them: {}, us: {}", + self.shard_info, + seq, + self_seq, + ); } match *event { @@ -370,240 +133,156 @@ impl Shard { info!("[Shard {:?}] Resumed", self.shard_info); self.stage = ConnectionStage::Connected; - self.last_heartbeat_acknowledged = true; - self.heartbeat_instants = (Some(Instant::now()), None); + info.last_heartbeat_acknowledged = true; + info.heartbeat_instants = (Some(Instant::now()), None); }, _ => {}, } - self.seq = seq; - - Ok(None) + info.seq = seq; }, - Ok(GatewayEvent::Heartbeat(s)) => { + GatewayEvent::Heartbeat(s) => { info!("[Shard {:?}] Received shard heartbeat", self.shard_info); - // Received seq is off -- attempt to resume. - if s > self.seq + 1 { + if s > self.heartbeat_info.borrow().seq + 1 { info!( "[Shard {:?}] Received off sequence (them: {}; us: {}); resuming", self.shard_info, s, - self.seq + self.heartbeat_info.borrow().seq ); - - if self.stage == ConnectionStage::Handshake { - self.stage = ConnectionStage::Identifying; - - return Ok(Some(ShardAction::Identify)); - } else { - warn!( - "[Shard {:?}] Heartbeat during non-Handshake; auto-reconnecting", - self.shard_info - ); - - return Ok(Some(ShardAction::Reconnect(self.reconnection_type()))); - } } - Ok(Some(ShardAction::Heartbeat)) + self.heartbeat().unwrap(); }, - Ok(GatewayEvent::HeartbeatAck) => { - self.heartbeat_instants.1 = Some(Instant::now()); - self.last_heartbeat_acknowledged = true; - + GatewayEvent::HeartbeatAck => { trace!("[Shard {:?}] Received heartbeat ack", self.shard_info); - Ok(None) + let mut info = self.heartbeat_info.borrow_mut(); + info.heartbeat_instants.1 = Some(Instant::now()); + info.last_heartbeat_acknowledged = true; }, - Ok(GatewayEvent::Hello(interval)) => { - debug!("[Shard {:?}] Received a Hello; interval: {}", - self.shard_info, - interval); + GatewayEvent::Hello(interval) => { + debug!( + "[Shard {:?}] Received a Hello; interval: {}", + self.shard_info, + interval, + ); if self.stage == ConnectionStage::Resuming { - return Ok(None); + return; } if interval > 0 { - self.heartbeat_interval = Some(interval); + self.interval = Some(interval); } - Ok(Some(if self.stage == ConnectionStage::Handshake { - ShardAction::Identify - } else { - debug!("[Shard {:?}] Received late Hello; autoreconnecting", - self.shard_info); + if self.stage == ConnectionStage::Handshake { + let heartbeat_info = Rc::clone(&self.heartbeat_info); + let mut tx = self.tx.clone(); - ShardAction::Reconnect(self.reconnection_type()) - })) - }, - Ok(GatewayEvent::InvalidateSession(resumable)) => { - info!( - "[Shard {:?}] Received session invalidation", - self.shard_info, - ); + let done = Timer::default() + .interval(Duration::from_millis(interval)) + .for_each(move |_| { + let info = heartbeat_info.borrow(); - Ok(Some(if resumable { - ShardAction::Reconnect(ReconnectType::Resume) - } else { - ShardAction::Reconnect(ReconnectType::Reidentify) - })) - }, - Ok(GatewayEvent::Reconnect) => { - Ok(Some(ShardAction::Reconnect(ReconnectType::Reidentify))) - }, - Err(Error::Gateway(GatewayError::Closed(ref data))) => { - let num = data.as_ref().map(|d| d.status_code); - let clean = num == Some(1000); - - match num { - Some(close_codes::UNKNOWN_OPCODE) => { - warn!("[Shard {:?}] Sent invalid opcode", - self.shard_info); - }, - Some(close_codes::DECODE_ERROR) => { - warn!("[Shard {:?}] Sent invalid message", - self.shard_info); - }, - Some(close_codes::NOT_AUTHENTICATED) => { - warn!("[Shard {:?}] Sent no authentication", - self.shard_info); - - return Err(Error::Gateway(GatewayError::NoAuthentication)); - }, - Some(close_codes::AUTHENTICATION_FAILED) => { - warn!("Sent invalid authentication"); + heartbeat( + &mut tx, + info.seq, + info.shard_info, + ).unwrap(); - return Err(Error::Gateway(GatewayError::InvalidAuthentication)); - }, - Some(close_codes::ALREADY_AUTHENTICATED) => { - warn!("[Shard {:?}] Already authenticated", - self.shard_info); - }, - Some(close_codes::INVALID_SEQUENCE) => { - warn!("[Shard {:?}] Sent invalid seq: {}", - self.shard_info, - self.seq); + Ok(()) + }).map_err(|why| { + warn!("Err in shard heartbeat timer: {:?}", why); - self.seq = 0; - }, - Some(close_codes::RATE_LIMITED) => { - warn!("[Shard {:?}] Gateway ratelimited", - self.shard_info); - }, - Some(close_codes::INVALID_SHARD) => { - warn!("[Shard {:?}] Sent invalid shard data", - self.shard_info); + () + }); - return Err(Error::Gateway(GatewayError::InvalidShardData)); - }, - Some(close_codes::SHARDING_REQUIRED) => { - error!("[Shard {:?}] Shard has too many guilds", - self.shard_info); + self.handle.spawn(done); - return Err(Error::Gateway(GatewayError::OverloadedShard)); - }, - Some(4006) | Some(close_codes::SESSION_TIMEOUT) => { - info!("[Shard {:?}] Invalid session", self.shard_info); + self.identify().unwrap(); - self.session_id = None; - }, - Some(other) if !clean => { - warn!( - "[Shard {:?}] Unknown unclean close {}: {:?}", - self.shard_info, - other, - data.as_ref().map(|d| &d.reason), - ); - }, - _ => {}, + return; } - let resume = num.map(|x| { - x != close_codes::AUTHENTICATION_FAILED && - self.session_id.is_some() - }).unwrap_or(true); + self.autoreconnect().unwrap(); + }, + GatewayEvent::InvalidateSession(resumable) => { + info!( + "[Shard {:?}] Received session invalidation", + self.shard_info, + ); - Ok(Some(if resume { - ShardAction::Reconnect(ReconnectType::Resume) + if resumable { + self.resume().unwrap(); } else { - ShardAction::Reconnect(ReconnectType::Reidentify) - })) - }, - Err(Error::WebSocket(ref why)) => { - if let WebSocketError::NoDataAvailable = *why { - if self.heartbeat_instants.1.is_none() { - return Ok(None); - } + self.identify().unwrap(); } - - warn!("[Shard {:?}] Websocket error: {:?}", - self.shard_info, - why); - info!("[Shard {:?}] Will attempt to auto-reconnect", - self.shard_info); - - Ok(Some(ShardAction::Reconnect(self.reconnection_type()))) }, - _ => Ok(None), + GatewayEvent::Reconnect => { + debug!("7"); + self.reconnect().unwrap(); + }, } } - 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) - }; + /// Returns a stream of WebSocket messages. + /// + /// These can be parsed via the [`parse`] method. This should be fed to the + /// shard via [`process`], so that it can process actionable messages, such + /// as heartbeats and session invalidations. + /// + /// This will _take_ the stream from the Shard, leaving the shard without a + /// stream. Attempting to retrieve a stream of messages a second time will + /// result in a panic. + /// + /// # Panics + /// + /// Panics if a stream of messages was already taken from the Shard. You can + /// check this beforehand via [`messages_present`] if you need to. + /// + /// [`messages_present`]: #method.messages_present + /// [`parse`]: #method.parse + /// [`process`]: #method.process + pub fn messages(&mut self) -> ShardStream { + self.stream.take().unwrap() + } - // 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(()); - } - } + pub fn current_presence(&self) -> &CurrentPresence { + &self.current_presence + } - // 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, - ); + pub fn heartbeat_instants(&self) -> (Option<Instant>, Option<Instant>) { + self.heartbeat_info.borrow().heartbeat_instants + } - why - }); - } + pub fn heartbeat_interval(&self) -> Option<&u64> { + self.interval.as_ref() + } - // Otherwise, we're good to heartbeat. - if let Err(why) = self.heartbeat() { - warn!("[Shard {:?}] Err heartbeating: {:?}", self.shard_info, why); + pub fn last_heartbeat_ack(&self) -> Option<Instant> { + self.heartbeat_info.borrow().heartbeat_instants.1 + } - self.reconnect() - } else { - trace!("[Shard {:?}] Heartbeated", self.shard_info); + pub fn last_heartbeat_acknowledged(&self) -> bool { + self.heartbeat_info.borrow().last_heartbeat_acknowledged + } - Ok(()) - } + pub fn last_heartbeat_sent(&self) -> Option<Instant> { + self.heartbeat_info.borrow().heartbeat_instants.0 } /// Calculates the heartbeat latency between the shard and the gateway. - // Shamelessly stolen from brayzure's commit in eris: - // <https://github.com/abalabahaha/eris/commit/0ce296ae9a542bcec0edf1c999ee2d9986bed5a6> - pub fn latency(&self) -> Option<StdDuration> { - if let (Some(sent), Some(received)) = self.heartbeat_instants { + /// + /// This will return `None` if: + /// + /// - a heartbeat acknowledgement has not been received yet (the shard just + /// started); or + /// - a heartbeat was sent and the following acknowledgement has not been + /// received, which would result in a negative latency. + pub fn latency(&self) -> Option<Duration> { + if let (Some(sent), Some(received)) = self.heartbeat_info.borrow().heartbeat_instants { if received > sent { return Some(received - sent); } @@ -612,231 +291,215 @@ impl Shard { None } - /// Performs a deterministic reconnect. + /// Whether a stream of messages is present. /// - /// The type of reconnect is deterministic on whether a [`session_id`]. + /// If a stream of messages is present, it can be taken via the [`messages`] + /// method. /// - /// 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 should_reconnect(&mut self) -> Option<ReconnectType> { - if self.stage == ConnectionStage::Connecting { - return None; - } + /// [`messages`]: #method.messages + pub fn messages_present(&self) -> bool { + self.stream.is_some() + } - Some(self.reconnection_type()) + pub fn seq(&self) -> u64 { + self.heartbeat_info.borrow().seq } - pub fn reconnection_type(&self) -> ReconnectType { - if self.session_id().is_some() { - ReconnectType::Resume - } else { - ReconnectType::Reidentify - } + pub fn session_id(&self) -> Option<&str> { + self.session_id.as_ref().map(AsRef::as_ref) } - /// 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::id::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::id::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/guild/struct.Guild.html - /// [`Member`]: ../../model/guild/struct.Member.html - pub fn chunk_guilds<It>( + pub fn shard_info(&self) -> [u64; 2] { + self.shard_info + } + + pub fn stage(&self) -> ConnectionStage { + self.stage + } + + pub fn chunk_guilds<It: IntoIterator<Item = GuildId>>( &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", self.shard_info); - - self.client.send_chunk_guilds( - guild_ids, - &self.shard_info, - limit, - query, - ) + ) -> Result<(), Error> { + debug!("[Shard {:?}] Requesting member chunks", shard_info); + + self.send_value(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(""), + }, + })) } - // Sets the shard as going into identifying stage, which sets: - // - // - 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())?; + pub fn set_game(&mut self, game: Option<Game>) -> Result<(), Error> { + self._set_game(game); - self.heartbeat_instants.0 = Some(Instant::now()); - self.stage = ConnectionStage::Identifying; + self.presence_update() + } - Ok(()) + pub fn set_presence(&mut self, status: OnlineStatus, game: Option<Game>) + -> Result<(), Error> { + self._set_game(game); + self._set_status(status); + + self.presence_update() } - /// Initializes a new WebSocket client. - /// - /// This will set the stage of the shard before and after instantiation of - /// the client. - pub fn initialize(&mut self) -> Result<WsClient> { - debug!("[Shard {:?}] Initializing", self.shard_info); - - // We need to do two, sort of three things here: - // - // - set the stage of the shard as opening the websocket connection - // - open the websocket connection - // - if successful, set the current stage as Handshaking - // - // This is used to accurately assess whether the state of the shard is - // accurate when a Hello is received. - self.stage = ConnectionStage::Connecting; - let mut client = connect(&self.ws_url.lock())?; - self.stage = ConnectionStage::Handshake; + pub fn set_status(&mut self, status: OnlineStatus) -> Result<(), Error> { + self._set_status(status); - let _ = set_client_timeout(&mut client); + self.presence_update() + } - Ok(client) + pub fn send(&mut self, msg: TungsteniteMessage) -> Result<(), Error> { + send(&mut self.tx, msg) } - 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 autoreconnect(&mut self) -> Result<(), Error> { + info!("[Shard {:?}] Autoreconnecting", self.shard_info); + + if self.session_id.is_some() { + self.resume() + } else { + debug!("6"); + self.reconnect() + } } - pub fn resume(&mut self) -> Result<()> { - debug!("Shard {:?}] Attempting to resume", self.shard_info); + fn heartbeat(&mut self) -> Result<(), Error> { + trace!( + "[Shard {:?}] Sending heartbeat d: {:?}", + self.shard_info, + self.heartbeat_info.borrow().seq, + ); + + heartbeat( + &mut self.tx, + self.heartbeat_info.borrow().seq, + self.shard_info, + ) + } - self.client = self.initialize()?; - self.stage = ConnectionStage::Resuming; + fn identify(&mut self) -> Result<(), Error> { + self.stage = ConnectionStage::Identifying; - match self.session_id.as_ref() { - Some(session_id) => { - self.client.send_resume( - &self.shard_info, - session_id, - &self.seq, - &self.token.lock(), - ) + debug!("[Shard {:?}] Identifying", self.shard_info); + + let v = json!({ + "op": OpCode::Identify.num(), + "d": { + "compression": false, + "large_threshold": LARGE_THRESHOLD, + "shard": self.shard_info, + "token": self.token, + "v": GATEWAY_VERSION, + "properties": { + "$browser": "test", + "$device": "test", + "$os": consts::OS, + }, }, - None => Err(Error::Gateway(GatewayError::NoSessionId)), - } + }); + + self.send_value(v) } - pub fn reconnect(&mut self) -> Result<()> { - info!("[Shard {:?}] Attempting to reconnect", self.shard_info()); + fn presence_update(&mut self) -> Result<(), Error> { + debug!("[Shard {:?}] Sending presence update", self.shard_info); + + let now = Utc::now().timestamp() as u64; + + let v = { + let &(ref game, ref status) = &self.current_presence; + + 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, + })), + }, + }) + }; - self.reset(); - self.client = self.initialize()?; + self.send_value(v) + } + + fn reconnect(&mut self) -> Result<(), Error> { + self.stage = ConnectionStage::Connecting; + info!("[Shard {:?}] Attempting to reconnect", self.shard_info); - Ok(()) + unreachable!("reconnect"); } - pub fn update_presence(&mut self) -> Result<()> { - self.client.send_presence_update( - &self.shard_info, - &self.current_presence, - ) + fn resume(&mut self) -> Result<(), Error> { + self.stage = ConnectionStage::Resuming; + + debug!( + "[Shard {:?}] Sending resume; seq: {}", + self.shard_info, + self.heartbeat_info.borrow().seq, + ); + + let v = json!({ + "op": OpCode::Resume.num(), + "d": { + "session_id": self.session_id, + "seq": self.heartbeat_info.borrow().seq, + "token": self.token, + }, + }); + + self.send_value(v) } -} -fn connect(base_url: &str) -> Result<WsClient> { - let url = build_gateway_url(base_url)?; - let client = ClientBuilder::from_url(&url).connect_secure(None)?; + fn send_value(&mut self, value: Value) -> Result<(), Error> { + let json = serde_json::to_string(&value)?; - Ok(client) + send(&mut self.tx, TungsteniteMessage::Text(json)) + } + + fn _set_game(&mut self, game: Option<Game>) { + self.current_presence.0 = game; + } + + fn _set_status(&mut self, mut status: OnlineStatus) { + if status == OnlineStatus::Invisible { + status = OnlineStatus::Offline; + } + + self.current_presence.1 = status; + } } -fn set_client_timeout(client: &mut WsClient) -> Result<()> { - let stream = client.stream_ref().as_tcp(); - stream.set_read_timeout(Some(StdDuration::from_millis(100)))?; - stream.set_write_timeout(Some(StdDuration::from_secs(5)))?; +fn heartbeat( + tx: &mut UnboundedSender<TungsteniteMessage>, + seq: u64, + shard_info: [u64; 2], +) -> Result<(), Error> { + trace!("[Shard {:?}] Sending heartbeat", shard_info); + + let v = serde_json::to_string(&json!({ + "op": OpCode::Heartbeat.num(), + "d": seq, + }))?; - Ok(()) + send(tx, TungsteniteMessage::Text(v)) } -fn build_gateway_url(base: &str) -> Result<Url> { - Url::parse(&format!("{}?v={}", base, constants::GATEWAY_VERSION)) - .map_err(|why| { - warn!("Error building gateway URL with base `{}`: {:?}", base, why); +fn send(tx: &mut UnboundedSender<TungsteniteMessage>, msg: TungsteniteMessage) + -> Result<(), Error> { + trace!("Sending message over gateway: {:?}", msg); - Error::Gateway(GatewayError::BuildingUrl) - }) + tx.start_send(msg).map(|_| ()).map_err(From::from) } diff --git a/src/gateway/ws_client_ext.rs b/src/gateway/ws_client_ext.rs deleted file mode 100644 index 934383c..0000000 --- a/src/gateway/ws_client_ext.rs +++ /dev/null @@ -1,133 +0,0 @@ -use chrono::Utc; -use constants::{self, OpCode}; -use gateway::{CurrentPresence, WsClient}; -use internal::prelude::*; -use internal::ws_impl::SenderExt; -use model::id::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/constants.rs b/src/http/constants.rs new file mode 100644 index 0000000..ad08754 --- /dev/null +++ b/src/http/constants.rs @@ -0,0 +1,13 @@ +//! A set of constants denoting the URIs that the lib uses and a constant +//! representing the version in use. + +/// The base URI to the REST API. +pub const API_URI_BASE: &str = "https://discordapp.com/api"; +/// The versioned URI to the REST API. +pub const API_URI_VERSIONED: &str = "https://discordapp.com/api/v6"; +/// The status page base URI. +pub const STATUS_URI_BASE: &str = "https://status.discordapp.com/api"; +/// The versioned URI to the status page. +pub const STATUS_URI_VERSIONED: &str = "https://status.discordapp.com/api/v2"; +/// The API version that the library supports and uses. +pub const VERSION: u8 = 6; diff --git a/src/http/error.rs b/src/http/error.rs index 12de978..9cb27f0 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -1,17 +1,44 @@ -use hyper::client::Response; +use futures::Canceled; +use hyper::Response; +use hyper::error::{Error as HyperError, UriError}; +use serde_json::Error as JsonError; +use std::cell::BorrowMutError; use std::error::Error as StdError; -use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::fmt::{Display, Error as FmtError, Formatter, Result as FmtResult}; +use std::io::Error as IoError; +use std::result::Result as StdResult; +use super::ratelimiting::RateLimitError; +use tokio_timer::TimerError; + +pub type Result<T> = StdResult<T, Error>; #[derive(Debug)] pub enum Error { - /// When a non-successful status code was received for a request. - UnsuccessfulRequest(Response), - /// When the decoding of a ratelimit header could not be properly decoded - /// into an `i64`. - RateLimitI64, - /// When the decoding of a ratelimit header could not be properly decoded - /// from UTF-8. - RateLimitUtf8, + /// There was an error mutably borrowing an `std::cell::RefCell`. + BorrowMut(BorrowMutError), + /// A future was canceled. + /// + /// This most likely occurred during a pre-emptive ratelimit. + Canceled(Canceled), + /// An error from the `std::fmt` module. + Format(FmtError), + /// An error from the `hyper` crate. + Hyper(HyperError), + /// When a status code was unexpectedly received for a request's status. + InvalidRequest(Response), + /// An error from the `std::io` module. + Io(IoError), + /// An error from the `serde_json` crate. + Json(JsonError), + /// An error from the `ratelimiting` module. + RateLimit(RateLimitError), + /// An error occurred while creating a timer. + Timer(TimerError), + /// When a status is received, but the verification to ensure the response + /// is valid does not recognize the status. + UnknownStatus(u16), + /// A `hyper` error while parsing a Uri. + Uri(UriError), } impl Display for Error { @@ -21,11 +48,65 @@ impl Display for Error { impl StdError for Error { fn description(&self) -> &str { match *self { - Error::UnsuccessfulRequest(_) => { - "A non-successful response status code was received" - }, - Error::RateLimitI64 => "Error decoding a header into an i64", - Error::RateLimitUtf8 => "Error decoding a header from UTF-8", + Error::BorrowMut(ref inner) => inner.description(), + Error::Canceled(ref inner) => inner.description(), + Error::Format(ref inner) => inner.description(), + Error::Hyper(ref inner) => inner.description(), + Error::InvalidRequest(_) => "Received an unexpected status code", + Error::Io(ref inner) => inner.description(), + Error::Json(ref inner) => inner.description(), + Error::RateLimit(ref inner) => inner.description(), + Error::Timer(ref inner) => inner.description(), + Error::UnknownStatus(_) => "Verification does not understand status", + Error::Uri(ref inner) => inner.description(), } } } + +impl From<BorrowMutError> for Error { + fn from(err: BorrowMutError) -> Self { + Error::BorrowMut(err) + } +} + +impl From<Canceled> for Error { + fn from(err: Canceled) -> Self { + Error::Canceled(err) + } +} + +impl From<FmtError> for Error { + fn from(err: FmtError) -> Self { + Error::Format(err) + } +} + +impl From<HyperError> for Error { + fn from(err: HyperError) -> Self { + Error::Hyper(err) + } +} + +impl From<IoError> for Error { + fn from(err: IoError) -> Self { + Error::Io(err) + } +} + +impl From<JsonError> for Error { + fn from(err: JsonError) -> Self { + Error::Json(err) + } +} + +impl From<RateLimitError> for Error { + fn from(err: RateLimitError) -> Self { + Error::RateLimit(err) + } +} + +impl From<TimerError> for Error { + fn from(err: TimerError) -> Self { + Error::Timer(err) + } +} diff --git a/src/http/macros.rs b/src/http/macros.rs new file mode 100644 index 0000000..87596f6 --- /dev/null +++ b/src/http/macros.rs @@ -0,0 +1,23 @@ +macro_rules! try_uri { + ($url:expr) => {{ + match ::hyper::Uri::from_str($url) { + Ok(v) => v, + Err(why) => return Box::new(::futures::future::err(::Error::Uri(why))), + } + }}; +} + +macro_rules! api { + ($e:expr) => { + concat!("https://discordapp.com/api/v6", $e) + }; + ($e:expr, $($rest:tt)*) => { + format!(api!($e), $($rest)*) + }; +} + +macro_rules! status { + ($e:expr) => { + concat!("https://status.discordapp.com/api/v2", $e) + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs index 6b56abd..a4c4cbc 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -23,42 +23,53 @@ //! [`Client`]: ../struct.Client.html //! [model]: ../model/index.html +#[macro_use] mod macros; + pub mod ratelimiting; mod error; - -pub use self::error::Error as HttpError; -pub use hyper::status::{StatusClass, StatusCode}; - -use constants; -use hyper::client::{Client as HyperClient, Request, RequestBuilder, Response as HyperResponse}; -use hyper::header::ContentType; -use hyper::method::Method; -use hyper::mime::{Mime, SubLevel, TopLevel}; -use hyper::net::HttpsConnector; -use hyper::{header, Error as HyperError, Result as HyperResult, Url}; -use hyper_native_tls::NativeTlsClient; -use internal::prelude::*; +mod routing; +mod utils; + +pub use hyper::StatusCode; +pub use self::error::{Error as HttpError, Result}; +pub use self::routing::{Path, Route}; + +use futures::{Future, Stream, future}; +use hyper::client::{Client as HyperClient, HttpConnector}; +use hyper::header::{Authorization, ContentType}; +use hyper::{Body, Method, Request, Response}; +use hyper_tls::HttpsConnector; use model::prelude::*; -use multipart::client::Multipart; -use parking_lot::Mutex; -use self::ratelimiting::Route; -use serde_json; -use std::collections::BTreeMap; -use std::default::Default; -use std::fmt::Write as FmtWrite; +use self::ratelimiting::RateLimiter; +use serde::de::DeserializeOwned; +use serde_json::{self, Number, Value}; +use std::cell::RefCell; +use std::fmt::Write; use std::fs::File; -use std::io::ErrorKind as IoErrorKind; -use std::path::{Path, PathBuf}; -use std::sync::Arc; +use std::path::{Path as StdPath, PathBuf}; +use std::rc::Rc; +use std::str::FromStr; +use tokio_core::reactor::Handle; +use ::builder::*; +use ::{Error, utils as serenity_utils}; + +macro_rules! ftry { + ($code:expr) => { + match $code { + Ok(v) => v, + Err(why) => return Box::new(future::err(From::from(why))), + } + } +} + +pub type FutureResult<T> = Box<Future<Item = T, Error = Error>>; /// An method used for ratelimiting special routes. /// /// This is needed because `hyper`'s `Method` enum does not derive Copy. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum LightMethod { - /// Indicates that a route is for "any" method. - Any, /// Indicates that a route is for the `DELETE` method only. Delete, /// Indicates that a route is for the `GET` method only. @@ -71,1807 +82,1496 @@ pub enum LightMethod { Put, } -lazy_static! { - static ref TOKEN: Arc<Mutex<String>> = Arc::new(Mutex::new(String::default())); +impl LightMethod { + pub fn hyper_method(&self) -> Method { + match *self { + LightMethod::Delete => Method::Delete, + LightMethod::Get => Method::Get, + LightMethod::Patch => Method::Patch, + LightMethod::Post => Method::Post, + LightMethod::Put => Method::Put, + } + } } -/// Sets the token to be used across all requests which require authentication. -/// -/// If you are using the client module, you don't need to use this. If you're -/// using serenity solely for HTTP, you need to use this. -/// -/// # Examples -/// -/// Setting the token from an environment variable: -/// -/// ```rust,no_run -/// # use std::error::Error; -/// # -/// # fn try_main() -> Result<(), Box<Error>> { -/// # -/// use serenity::http; -/// use std::env; -/// -/// http::set_token(&env::var("DISCORD_TOKEN")?); -/// # Ok(()) -/// # } -/// # -/// # fn main() { -/// # try_main().unwrap(); -/// # } -pub fn set_token(token: &str) { TOKEN.lock().clone_from(&token.to_string()); } - -/// Adds a [`User`] as a recipient to a [`Group`]. -/// -/// **Note**: Groups have a limit of 10 recipients, including the current user. -/// -/// [`Group`]: ../model/channel/struct.Group.html -/// [`Group::add_recipient`]: ../model/channel/struct.Group.html#method.add_recipient -/// [`User`]: ../model/user/struct.User.html -pub fn add_group_recipient(group_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::None, - put, - "/channels/{}/recipients/{}", - group_id, - user_id - ), - ) -} +#[derive(Clone, Debug)] +pub struct Client { + pub client: Rc<HyperClient<HttpsConnector<HttpConnector>, Body>>, + pub handle: Handle, + pub ratelimiter: Rc<RefCell<RateLimiter>>, + pub token: Rc<String>, +} + +impl Client { + pub fn new( + client: Rc<HyperClient<HttpsConnector<HttpConnector>, Body>>, + handle: Handle, + token: Rc<String>, + ) -> Self { + Self { + ratelimiter: Rc::new(RefCell::new(RateLimiter::new(handle.clone()))), + token, + client, + handle, + } + } -/// Adds a single [`Role`] to a [`Member`] in a [`Guild`]. -/// -/// **Note**: Requires the [Manage Roles] permission and respect of role -/// hierarchy. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`Member`]: ../model/guild/struct.Member.html -/// [`Role`]: ../model/guild/struct.Role.html -/// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html -pub fn add_member_role(guild_id: u64, user_id: u64, role_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdMembersIdRolesId(guild_id), - put, - "/guilds/{}/members/{}/roles/{}", - guild_id, - user_id, - role_id - ), - ) -} + pub fn set_token(&mut self, token: Rc<String>) { + self.token = token; + } -/// Bans a [`User`] from a [`Guild`], removing their messages sent in the last -/// X number of days. -/// -/// Passing a `delete_message_days` of `0` is equivalent to not removing any -/// messages. Up to `7` days' worth of messages may be deleted. -/// -/// **Note**: Requires that you have the [Ban Members] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`User`]: ../model/user/struct.User.html -/// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html -pub fn ban_user(guild_id: u64, user_id: u64, delete_message_days: u8, reason: &str) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdBansUserId(guild_id), - put, - "/guilds/{}/bans/{}?delete_message_days={}&reason={}", + /// Adds a [`User`] as a recipient to a [`Group`]. + /// + /// **Note**: Groups have a limit of 10 recipients, including the current + /// user. + /// + /// [`Group`]: ../model/channel/struct.Group.html + /// [`Group::add_recipient`]: ../model/channel/struct.Group.html#method.add_recipient + /// [`User`]: ../model/user/struct.User.html + pub fn add_group_recipient(&self, group_id: u64, user_id: u64) + -> FutureResult<()> { + self.verify(Route::AddGroupRecipient { group_id, user_id }, None) + } + + /// Adds a single [`Role`] to a [`Member`] in a [`Guild`]. + /// + /// **Note**: Requires the [Manage Roles] permission and respect of role + /// hierarchy. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`Member`]: ../model/guild/struct.Member.html + /// [`Role`]: ../model/guild/struct.Role.html + /// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html + pub fn add_member_role( + &self, + guild_id: u64, + user_id: u64, + role_id: u64 + ) -> FutureResult<()> { + self.verify(Route::AddMemberRole { guild_id, user_id, role_id }, None) + } + + /// Bans a [`User`] from a [`Guild`], removing their messages sent in the + /// last X number of days. + /// + /// Passing a `delete_message_days` of `0` is equivalent to not removing any + /// messages. Up to `7` days' worth of messages may be deleted. + /// + /// **Note**: Requires that you have the [Ban Members] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`User`]: ../model/user/struct.User.html + /// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html + pub fn ban_user( + &self, + guild_id: u64, + user_id: u64, + delete_message_days: u8, + reason: &str, + ) -> FutureResult<()> { + self.verify(Route::GuildBanUser { + delete_message_days: Some(delete_message_days), + reason: Some(reason), guild_id, user_id, - delete_message_days, - reason - ), - ) -} + }, None) + } -/// Broadcasts that the current user is typing in the given [`Channel`]. -/// -/// This lasts for about 10 seconds, and will then need to be renewed to -/// indicate that the current user is still typing. -/// -/// This should rarely be used for bots, although it is a good indicator that a -/// long-running command is still being processed. -/// -/// [`Channel`]: ../model/channel/enum.Channel.html -pub fn broadcast_typing(channel_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdTyping(channel_id), - post, - "/channels/{}/typing", - channel_id - ), - ) -} + /// Broadcasts that the current user is typing in the given [`Channel`]. + /// + /// This lasts for about 10 seconds, and will then need to be renewed to + /// indicate that the current user is still typing. + /// + /// This should rarely be used for bots, although it is a good indicator + /// that a long-running command is still being processed. + /// + /// [`Channel`]: ../model/channel/enum.Channel.html + pub fn broadcast_typing(&self, channel_id: u64) + -> FutureResult<()> { + self.verify(Route::BroadcastTyping { channel_id }, None) + } -/// Creates a [`GuildChannel`] in the [`Guild`] given its Id. -/// -/// Refer to the Discord's [docs] for information on what fields this requires. -/// -/// **Note**: Requires the [Manage Channels] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -/// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-channel -/// [Manage Channels]: ../model/permissions/constant.MANAGE_CHANNELS.html -pub fn create_channel(guild_id: u64, map: &Value) -> Result<GuildChannel> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdChannels(guild_id), - post(body), - "/guilds/{}/channels", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildChannel>(response) - .map_err(From::from) -} + /// Creates a [`GuildChannel`] in the [`Guild`] given its Id. + /// + /// Refer to the Discord's [docs] for information on what fields this + /// requires. + /// + /// **Note**: Requires the [Manage Channels] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + /// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-channel + /// [Manage Channels]: ../model/permissions/constant.MANAGE_CHANNELS.html + pub fn create_channel( + &self, + guild_id: u64, + name: &str, + kind: ChannelType, + category_id: Option<u64>, + ) -> FutureResult<GuildChannel> { + self.post(Route::CreateChannel { guild_id }, Some(&json!({ + "name": name, + "parent_id": category_id, + "type": kind as u8, + }))) + } -/// Creates an emoji in the given [`Guild`] with the given data. -/// -/// View the source code for [`Context::create_emoji`] to see what fields this -/// requires. -/// -/// **Note**: Requires the [Manage Emojis] permission. -/// -/// [`Context::create_emoji`]: ../struct.Context.html#method.create_emoji -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [Manage Emojis]: ../model/permissions/constant.MANAGE_EMOJIS.html -pub fn create_emoji(guild_id: u64, map: &Value) -> Result<Emoji> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdEmojis(guild_id), - post(body), - "/guilds/{}/emojis", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Emoji>(response) - .map_err(From::from) -} + /// Creates an emoji in the given [`Guild`] with the given data. + /// + /// View the source code for [`Context::create_emoji`] to see what fields + /// this requires. + /// + /// **Note**: Requires the [Manage Emojis] permission. + /// + /// [`Context::create_emoji`]: ../struct.Context.html#method.create_emoji + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [Manage Emojis]: ../model/permissions/constant.MANAGE_EMOJIS.html + pub fn create_emoji(&self, guild_id: u64, name: &str, image: &str) + -> FutureResult<Emoji> { + self.post(Route::CreateEmoji { guild_id }, Some(&json!({ + "image": image, + "name": name, + }))) + } -/// Creates a guild with the data provided. -/// -/// Only a [`PartialGuild`] will be immediately returned, and a full [`Guild`] -/// will be received over a [`Shard`], if at least one is running. -/// -/// **Note**: This endpoint is currently limited to 10 active guilds. The -/// limits are raised for whitelisted [GameBridge] applications. See the -/// [documentation on this endpoint] for more info. -/// -/// # Examples -/// -/// Create a guild called `"test"` in the [US West region]: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serde_json::Value; -/// use serenity::http; -/// -/// let map = ObjectBuilder::new() -/// .insert("name", "test") -/// .insert("region", "us-west") -/// .build(); -/// -/// let _result = http::create_guild(map); -/// ``` -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`PartialGuild`]: ../model/guild/struct.PartialGuild.html -/// [`Shard`]: ../gateway/struct.Shard.html -/// [GameBridge]: https://discordapp.com/developers/docs/topics/gamebridge -/// [US West Region]: ../model/guild/enum.Region.html#variant.UsWest -/// [documentation on this endpoint]: -/// https://discordapp.com/developers/docs/resources/guild#create-guild -/// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild -pub fn create_guild(map: &Value) -> Result<PartialGuild> { - let body = map.to_string(); - let response = request!(Route::Guilds, post(body), "/guilds"); - - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Creates a guild with the data provided. + /// + /// Only a [`PartialGuild`] will be immediately returned, and a full + /// [`Guild`] will be received over a [`Shard`], if at least one is running. + /// + /// **Note**: This endpoint is currently limited to 10 active guilds. The + /// limits are raised for whitelisted [GameBridge] applications. See the + /// [documentation on this endpoint] for more info. + /// + /// # Examples + /// + /// Create a guild called `"test"` in the [US West region]: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serde_json::Value; + /// use serenity::http; + /// + /// let map = ObjectBuilder::new() + /// .insert("name", "test") + /// .insert("region", "us-west") + /// .build(); + /// + /// let _result = http::create_guild(map); + /// ``` + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`PartialGuild`]: ../model/guild/struct.PartialGuild.html + /// [`Shard`]: ../gateway/struct.Shard.html + /// [GameBridge]: https://discordapp.com/developers/docs/topics/gamebridge + /// [US West Region]: ../model/enum.Region.html#variant.UsWest + /// [documentation on this endpoint]: + /// https://discordapp.com/developers/docs/resources/guild#create-guild + /// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild + pub fn create_guild(&self, map: &Value) + -> FutureResult<PartialGuild> { + self.post(Route::CreateGuild, Some(map)) + } -/// Creates an [`Integration`] for a [`Guild`]. -/// -/// Refer to Discord's [docs] for field information. -/// -/// **Note**: Requires the [Manage Guild] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`Integration`]: ../model/guild/struct.Integration.html -/// [Manage Guild]: ../model/permissions/constant.MANAGE_GUILD.html -/// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-integration -pub fn create_guild_integration(guild_id: u64, integration_id: u64, map: &Value) -> Result<()> { - let body = map.to_string(); - - verify( - 204, - request!( - Route::GuildsIdIntegrations(guild_id), - post(body), - "/guilds/{}/integrations/{}", + /// Creates an [`Integration`] for a [`Guild`]. + /// + /// Refer to Discord's [docs] for field information. + /// + /// **Note**: Requires the [Manage Guild] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`Integration`]: ../model/guild/struct.Integration.html + /// [Manage Guild]: ../model/permissions/constant.MANAGE_GUILD.html + /// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-integration + pub fn create_guild_integration( + &self, + guild_id: u64, + integration_id: u64, + kind: &str, + ) -> FutureResult<()> { + let json = json!({ + "id": integration_id, + "type": kind, + }); + self.verify(Route::CreateGuildIntegration { guild_id, - integration_id - ), - ) -} - -/// Creates a [`RichInvite`] for the given [channel][`GuildChannel`]. -/// -/// Refer to Discord's [docs] for field information. -/// -/// All fields are optional. -/// -/// **Note**: Requires the [Create Invite] permission. -/// -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -/// [`RichInvite`]: ../model/guild/struct.RichInvite.html -/// [Create Invite]: ../model/permissions/constant.CREATE_INVITE.html -/// [docs]: https://discordapp.com/developers/docs/resources/channel#create-channel-invite -pub fn create_invite(channel_id: u64, map: &JsonMap) -> Result<RichInvite> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::ChannelsIdInvites(channel_id), - post(body), - "/channels/{}/invites", - channel_id - ); - - serde_json::from_reader::<HyperResponse, RichInvite>(response) - .map_err(From::from) -} + integration_id, + }, Some(&json)) + } -/// Creates a permission override for a member or a role in a channel. -pub fn create_permission(channel_id: u64, target_id: u64, map: &Value) -> Result<()> { - let body = map.to_string(); + /// Creates a [`RichInvite`] for the given [channel][`GuildChannel`]. + /// + /// Refer to Discord's [docs] for field information. + /// + /// All fields are optional. + /// + /// **Note**: Requires the [Create Invite] permission. + /// + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + /// [`RichInvite`]: ../model/guild/struct.RichInvite.html + /// [Create Invite]: ../model/permissions/constant.CREATE_INVITE.html + /// [docs]: https://discordapp.com/developers/docs/resources/channel#create-channel-invite + pub fn create_invite<F>(&self, channel_id: u64, f: F) + -> FutureResult<RichInvite> + where F: FnOnce(CreateInvite) -> CreateInvite { + let map = serenity_utils::vecmap_to_json_map(f(CreateInvite::default()).0); + + self.post(Route::CreateInvite { channel_id }, Some(&Value::Object(map))) + } - verify( - 204, - request!( - Route::ChannelsIdPermissionsOverwriteId(channel_id), - put(body), - "/channels/{}/permissions/{}", + /// Creates a permission override for a member or a role in a channel. + pub fn create_permission( + &self, + channel_id: u64, + target: &PermissionOverwrite, + ) -> FutureResult<()> { + let (id, kind) = match target.kind { + PermissionOverwriteType::Member(id) => (id.0, "member"), + PermissionOverwriteType::Role(id) => (id.0, "role"), + }; + let map = json!({ + "allow": target.allow.bits(), + "deny": target.deny.bits(), + "id": id, + "type": kind, + }); + + self.verify(Route::CreatePermission { + target_id: id, channel_id, - target_id - ), - ) -} + }, Some(&map)) + } -/// Creates a private channel with a user. -pub fn create_private_channel(map: &Value) -> Result<PrivateChannel> { - let body = map.to_string(); - let response = request!(Route::UsersMeChannels, post(body), "/users/@me/channels"); + /// Creates a private channel with a user. + pub fn create_private_channel(&self, user_id: u64) + -> FutureResult<PrivateChannel> { + let map = json!({ + "recipient_id": user_id, + }); - serde_json::from_reader::<HyperResponse, PrivateChannel>(response) - .map_err(From::from) -} + self.post(Route::CreatePrivateChannel, Some(&map)) + } -/// Reacts to a message. -pub fn create_reaction(channel_id: u64, - message_id: u64, - reaction_type: &ReactionType) - -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - put, - "/channels/{}/messages/{}/reactions/{}/@me", + /// Reacts to a message. + pub fn create_reaction( + &self, + channel_id: u64, + message_id: u64, + reaction_type: &ReactionType + ) -> FutureResult<()> { + self.verify(Route::CreateReaction { + reaction: &utils::reaction_type_data(reaction_type), channel_id, message_id, - reaction_type.as_data() - ), - ) -} + }, None) + } -/// Creates a role. -pub fn create_role(guild_id: u64, map: &JsonMap) -> Result<Role> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::GuildsIdRoles(guild_id), - post(body), - "/guilds/{}/roles", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Role>(response) - .map_err(From::from) -} + /// Creates a role. + pub fn create_role<F>(&self, guild_id: u64, f: F) -> FutureResult<Role> + where F: FnOnce(EditRole) -> EditRole { + let map = serenity_utils::vecmap_to_json_map(f(EditRole::default()).0); -/// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing in -/// the given data. -/// -/// This method requires authentication. -/// -/// The Value is a map with the values of: -/// -/// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar -/// (_optional_); -/// - **name**: the name of the webhook, limited to between 2 and 100 characters -/// long. -/// -/// # Examples -/// -/// Creating a webhook named `test`: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let channel_id = 81384788765712384; -/// let map = ObjectBuilder::new().insert("name", "test").build(); -/// -/// let webhook = http::create_webhook(channel_id, map).expect("Error creating"); -/// ``` -/// -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -pub fn create_webhook(channel_id: u64, map: &Value) -> Result<Webhook> { - let body = map.to_string(); - let response = request!( - Route::ChannelsIdWebhooks(channel_id), - post(body), - "/channels/{}/webhooks", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + self.post(Route::CreateRole { guild_id }, Some(&Value::Object(map))) + } -/// Deletes a private channel or a channel in a guild. -pub fn delete_channel(channel_id: u64) -> Result<Channel> { - let response = request!( - Route::ChannelsId(channel_id), - delete, - "/channels/{}", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Channel>(response) - .map_err(From::from) -} + /// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing + /// in the given data. + /// + /// This method requires authentication. + /// + /// The Value is a map with the values of: + /// + /// - **avatar**: base64-encoded 128x128 image for the webhook's default + /// avatar (_optional_); + /// - **name**: the name of the webhook, limited to between 2 and 100 + /// characters long. + /// + /// # Examples + /// + /// Creating a webhook named `test`: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let channel_id = 81384788765712384; + /// let map = ObjectBuilder::new().insert("name", "test").build(); + /// + /// let webhook = http::create_webhook(channel_id, map) + /// .expect("Error creating"); + /// ``` + /// + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + pub fn create_webhook(&self, channel_id: u64, map: &Value) + -> FutureResult<Webhook> { + self.post(Route::CreateWebhook { channel_id }, Some(map)) + } -/// Deletes an emoji from a server. -pub fn delete_emoji(guild_id: u64, emoji_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdEmojisId(guild_id), - delete, - "/guilds/{}/emojis/{}", - guild_id, - emoji_id - ), - ) -} + /// Deletes a private channel or a channel in a guild. + pub fn delete_channel(&self, channel_id: u64) -> FutureResult<Channel> { + self.delete(Route::DeleteChannel { channel_id }, None) + } -/// Deletes a guild, only if connected account owns it. -pub fn delete_guild(guild_id: u64) -> Result<PartialGuild> { - let response = request!(Route::GuildsId(guild_id), delete, "/guilds/{}", guild_id); + /// Deletes an emoji from a server. + pub fn delete_emoji(&self, guild_id: u64, emoji_id: u64) + -> FutureResult<()> { + self.delete(Route::DeleteEmoji { guild_id, emoji_id }, None) + } - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Deletes a guild, only if connected account owns it. + pub fn delete_guild(&self, guild_id: u64) + -> FutureResult<PartialGuild> { + self.delete(Route::DeleteGuild { guild_id }, None) + } -/// Remvoes an integration from a guild. -pub fn delete_guild_integration(guild_id: u64, integration_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdIntegrationsId(guild_id), - delete, - "/guilds/{}/integrations/{}", + /// Remvoes an integration from a guild. + pub fn delete_guild_integration( + &self, + guild_id: u64, + integration_id: u64, + ) -> FutureResult<()> { + self.verify(Route::DeleteGuildIntegration { guild_id, - integration_id - ), - ) -} - -/// Deletes an invite by code. -pub fn delete_invite(code: &str) -> Result<Invite> { - let response = request!(Route::InvitesCode, delete, "/invites/{}", code); + integration_id, + }, None) + } - serde_json::from_reader::<HyperResponse, Invite>(response) - .map_err(From::from) -} + /// Deletes an invite by code. + pub fn delete_invite(&self, code: &str) -> FutureResult<Invite> { + self.delete(Route::DeleteInvite { code }, None) + } -/// Deletes a message if created by us or we have -/// specific permissions. -pub fn delete_message(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdMessagesId(LightMethod::Delete, channel_id), - delete, - "/channels/{}/messages/{}", - channel_id, - message_id - ), - ) -} + /// Deletes a message if created by us or we have specific permissions. + pub fn delete_message(&self, channel_id: u64, message_id: u64) + -> FutureResult<()> { + self.verify(Route::DeleteMessage { channel_id, message_id }, None) + } -/// Deletes a bunch of messages, only works for bots. -pub fn delete_messages(channel_id: u64, map: &Value) -> Result<()> { - let body = map.to_string(); - - verify( - 204, - request!( - Route::ChannelsIdMessagesBulkDelete(channel_id), - post(body), - "/channels/{}/messages/bulk-delete", - channel_id - ), - ) -} + /// Deletes a bunch of messages, only works for bots. + pub fn delete_messages<T, It>(&self, channel_id: u64, message_ids: It) + -> FutureResult<()> + where T: AsRef<MessageId>, It: IntoIterator<Item=T> { + let ids = message_ids + .into_iter() + .map(|id| id.as_ref().0) + .collect::<Vec<u64>>(); -/// Deletes all of the [`Reaction`]s associated with a [`Message`]. -/// -/// # Examples -/// -/// ```rust,no_run -/// use serenity::http; -/// use serenity::model::id::{ChannelId, MessageId}; -/// -/// let channel_id = ChannelId(7); -/// let message_id = MessageId(8); -/// -/// let _ = http::delete_message_reactions(channel_id.0, message_id.0) -/// .expect("Error deleting reactions"); -/// ``` -/// -/// [`Message`]: ../model/channel/struct.Message.html -/// [`Reaction`]: ../model/channel/struct.Reaction.html -pub fn delete_message_reactions(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdMessagesIdReactions(channel_id), - delete, - "/channels/{}/messages/{}/reactions", - channel_id, - message_id - ), - ) -} + let map = json!({ + "messages": ids, + }); -/// Deletes a permission override from a role or a member in a channel. -pub fn delete_permission(channel_id: u64, target_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdPermissionsOverwriteId(channel_id), - delete, - "/channels/{}/permissions/{}", - channel_id, - target_id - ), - ) -} + self.verify(Route::DeleteMessages { channel_id }, Some(&map)) + } -/// Deletes a reaction from a message if owned by us or -/// we have specific permissions. -pub fn delete_reaction(channel_id: u64, - message_id: u64, - user_id: Option<u64>, - reaction_type: &ReactionType) - -> Result<()> { - let user = user_id - .map(|uid| uid.to_string()) - .unwrap_or_else(|| "@me".to_string()); - - verify( - 204, - request!( - Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - delete, - "/channels/{}/messages/{}/reactions/{}/{}", + /// Deletes all of the [`Reaction`]s associated with a [`Message`]. + /// + /// # Examples + /// + /// ```rust,no_run + /// use serenity::http; + /// use serenity::model::{ChannelId, MessageId}; + /// + /// let channel_id = ChannelId(7); + /// let message_id = MessageId(8); + /// + /// let _ = http::delete_message_reactions(channel_id.0, message_id.0) + /// .expect("Error deleting reactions"); + /// ``` + /// + /// [`Message`]: ../model/channel/struct.Message.html + /// [`Reaction`]: ../model/channel/struct.Reaction.html + pub fn delete_message_reactions(&self, channel_id: u64, message_id: u64) + -> FutureResult<()> { + self.verify(Route::DeleteMessageReactions { channel_id, message_id, - reaction_type.as_data(), - user - ), - ) -} - -/// Deletes a role from a server. Can't remove the default everyone role. -pub fn delete_role(guild_id: u64, role_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdRolesId(guild_id), - delete, - "/guilds/{}/roles/{}", - guild_id, - role_id - ), - ) -} - -/// Deletes a [`Webhook`] given its Id. -/// -/// This method requires authentication, whereas [`delete_webhook_with_token`] -/// does not. -/// -/// # Examples -/// -/// Deletes a webhook given its Id: -/// -/// ```rust,no_run -/// use serenity::{Client, http}; -/// use std::env; -/// -/// // Due to the `delete_webhook` function requiring you to authenticate, you -/// // must have set the token first. -/// http::set_token(&env::var("DISCORD_TOKEN").unwrap()); -/// -/// http::delete_webhook(245037420704169985).expect("Error deleting webhook"); -/// ``` -/// -/// [`Webhook`]: ../model/webhook/struct.Webhook.html -/// [`delete_webhook_with_token`]: fn.delete_webhook_with_token.html -pub fn delete_webhook(webhook_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::WebhooksId(webhook_id), - delete, - "/webhooks/{}", - webhook_id, - ), - ) -} + }, None) + } -/// Deletes a [`Webhook`] given its Id and unique token. -/// -/// This method does _not_ require authentication. -/// -/// # Examples -/// -/// Deletes a webhook given its Id and unique token: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// -/// http::delete_webhook_with_token(id, token).expect("Error deleting webhook"); -/// ``` -/// -/// [`Webhook`]: ../model/webhook/struct.Webhook.html -pub fn delete_webhook_with_token(webhook_id: u64, token: &str) -> Result<()> { - let client = request_client!(); - - verify( - 204, - retry(|| { - client - .delete(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - }).map_err(Error::Hyper)?, - ) -} + /// Deletes a permission override from a role or a member in a channel. + pub fn delete_permission(&self, channel_id: u64, target_id: u64) + -> FutureResult<()> { + self.verify(Route::DeletePermission { channel_id, target_id }, None) + } -/// Changes channel information. -pub fn edit_channel(channel_id: u64, map: &JsonMap) -> Result<GuildChannel> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::ChannelsId(channel_id), - patch(body), - "/channels/{}", - channel_id - ); - - serde_json::from_reader::<HyperResponse, GuildChannel>(response) - .map_err(From::from) -} + /// Deletes a reaction from a message if owned by us or + /// we have specific permissions. + pub fn delete_reaction( + &self, + channel_id: u64, + message_id: u64, + user_id: Option<u64>, + reaction_type: &ReactionType, + ) -> FutureResult<()> { + let reaction_type = utils::reaction_type_data(reaction_type); + let user = user_id + .map(|uid| uid.to_string()) + .unwrap_or_else(|| "@me".to_string()); + + self.verify(Route::DeleteReaction { + reaction: &reaction_type, + user: &user, + channel_id, + message_id, + }, None) + } -/// Changes emoji information. -pub fn edit_emoji(guild_id: u64, emoji_id: u64, map: &Value) -> Result<Emoji> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdEmojisId(guild_id), - patch(body), - "/guilds/{}/emojis/{}", - guild_id, - emoji_id - ); - - serde_json::from_reader::<HyperResponse, Emoji>(response) - .map_err(From::from) -} + /// Deletes a role from a server. Can't remove the default everyone role. + pub fn delete_role(&self, guild_id: u64, role_id: u64) + -> FutureResult<()> { + self.verify(Route::DeleteRole { guild_id, role_id }, None) + } -/// Changes guild information. -pub fn edit_guild(guild_id: u64, map: &JsonMap) -> Result<PartialGuild> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::GuildsId(guild_id), - patch(body), - "/guilds/{}", - guild_id - ); - - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Deletes a [`Webhook`] given its Id. + /// + /// This method requires authentication, whereas + /// [`delete_webhook_with_token`] does not. + /// + /// # Examples + /// + /// Deletes a webhook given its Id: + /// + /// ```rust,no_run + /// use serenity::{Client, http}; + /// use std::env; + /// + /// // Due to the `delete_webhook` function requiring you to authenticate, + /// // you must have set the token first. + /// http::set_token(&env::var("DISCORD_TOKEN").unwrap()); + /// + /// http::delete_webhook(245037420704169985) + /// .expect("Error deleting webhook"); + /// ``` + /// + /// [`Webhook`]: ../model/webhook/struct.Webhook.html + /// [`delete_webhook_with_token`]: fn.delete_webhook_with_token.html + pub fn delete_webhook(&self, webhook_id: u64) -> FutureResult<()> { + self.verify(Route::DeleteWebhook { webhook_id }, None) + } -/// Edits the positions of a guild's channels. -pub fn edit_guild_channel_positions(guild_id: u64, value: &Value) - -> Result<()> { - let body = serde_json::to_string(value)?; - - verify( - 204, - request!( - Route::GuildsIdChannels(guild_id), - patch(body), - "/guilds/{}/channels", - guild_id, - ), - ) -} + /// Deletes a [`Webhook`] given its Id and unique token. + /// + /// This method does _not_ require authentication. + /// + /// # Examples + /// + /// Deletes a webhook given its Id and unique token: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// + /// http::delete_webhook_with_token(id, token) + /// .expect("Error deleting webhook"); + /// ``` + /// + /// [`Webhook`]: ../model/webhook/struct.Webhook.html + pub fn delete_webhook_with_token(&self, webhook_id: u64, token: &str) + -> FutureResult<()> { + self.verify(Route::DeleteWebhookWithToken { webhook_id, token }, None) + } -/// Edits a [`Guild`]'s embed setting. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn edit_guild_embed(guild_id: u64, map: &Value) -> Result<GuildEmbed> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdEmbed(guild_id), - patch(body), - "/guilds/{}/embed", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildEmbed>(response) - .map_err(From::from) -} + /// Changes channel information. + pub fn edit_channel<F>(&self, channel_id: u64, f: F) + -> FutureResult<GuildChannel> + where F: FnOnce(EditChannel) -> EditChannel { + let channel = f(EditChannel::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(channel)); -/// Does specific actions to a member. -pub fn edit_member(guild_id: u64, user_id: u64, map: &JsonMap) -> Result<()> { - let body = serde_json::to_string(map)?; + self.patch(Route::EditChannel { channel_id }, Some(&map)) + } - verify( - 204, - request!( - Route::GuildsIdMembersId(guild_id), - patch(body), - "/guilds/{}/members/{}", - guild_id, - user_id - ), - ) -} + /// Changes emoji information. + pub fn edit_emoji(&self, guild_id: u64, emoji_id: u64, name: &str) + -> FutureResult<Emoji> { + let map = json!({ + "name": name, + }); -/// Edits a message by Id. -/// -/// **Note**: Only the author of a message can modify it. -pub fn edit_message(channel_id: u64, message_id: u64, map: &Value) -> Result<Message> { - let body = map.to_string(); - let response = request!( - Route::ChannelsIdMessagesId(LightMethod::Any, channel_id), - patch(body), - "/channels/{}/messages/{}", - channel_id, - message_id - ); - - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + self.patch(Route::EditEmoji { guild_id, emoji_id }, Some(&map)) + } -/// Edits the current user's nickname for the provided [`Guild`] via its Id. -/// -/// Pass `None` to reset the nickname. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn edit_nickname(guild_id: u64, new_nickname: Option<&str>) -> Result<()> { - let map = json!({ "nick": new_nickname }); - let body = map.to_string(); - let response = request!( - Route::GuildsIdMembersMeNick(guild_id), - patch(body), - "/guilds/{}/members/@me/nick", - guild_id - ); - - verify(200, response) -} + /// Changes guild information. + pub fn edit_guild<F>(&self, guild_id: u64, f: F) + -> FutureResult<PartialGuild> + where F: FnOnce(EditGuild) -> EditGuild { + let guild = f(EditGuild::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(guild)); -/// Edits the current user's profile settings. -/// -/// For bot users, the password is optional. -/// -/// # User Accounts -/// -/// If a new token is received due to a password change, then the stored token -/// internally will be updated. -/// -/// **Note**: this token change may cause requests made between the actual token -/// change and when the token is internally changed to be invalid requests, as -/// the token may be outdated. -pub fn edit_profile(map: &JsonMap) -> Result<CurrentUser> { - let body = serde_json::to_string(map)?; - let response = request!(Route::UsersMe, patch(body), "/users/@me"); - - let mut value = serde_json::from_reader::<HyperResponse, Value>(response)?; - - if let Some(map) = value.as_object_mut() { - if !TOKEN.lock().starts_with("Bot ") { - if let Some(Value::String(token)) = map.remove("token") { - set_token(&token); - } - } + self.patch(Route::EditGuild { guild_id }, Some(&map)) } - serde_json::from_value::<CurrentUser>(value) - .map_err(From::from) -} + /// Edits the positions of a guild's channels. + pub fn edit_guild_channel_positions<It>(&self, guild_id: u64, channels: It) + -> FutureResult<()> where It: IntoIterator<Item = (ChannelId, u64)> { + let items = channels.into_iter().map(|(id, pos)| json!({ + "id": id, + "position": pos, + })).collect(); -/// Changes a role in a guild. -pub fn edit_role(guild_id: u64, role_id: u64, map: &JsonMap) -> Result<Role> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::GuildsIdRolesId(guild_id), - patch(body), - "/guilds/{}/roles/{}", - guild_id, - role_id - ); - - serde_json::from_reader::<HyperResponse, Role>(response) - .map_err(From::from) -} + let map = Value::Array(items); -/// Changes the position of a role in a guild. -pub fn edit_role_position(guild_id: u64, role_id: u64, position: u64) -> Result<Vec<Role>> { - let body = serde_json::to_string(&json!({ - "id": role_id, - "position": position, - }))?; - let response = request!( - Route::GuildsIdRolesId(guild_id), - patch(body), - "/guilds/{}/roles/{}", - guild_id, - role_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Role>>(response) - .map_err(From::from) -} + self.patch(Route::EditGuildChannels { guild_id }, Some(&map)) + } -/// Edits a the webhook with the given data. -/// -/// The Value is a map with optional values of: -/// -/// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar -/// (_optional_); -/// - **name**: the name of the webhook, limited to between 2 and 100 characters -/// long. -/// -/// Note that, unlike with [`create_webhook`], _all_ values are optional. -/// -/// This method requires authentication, whereas [`edit_webhook_with_token`] -/// does not. -/// -/// # Examples -/// -/// Edit the image of a webhook given its Id and unique token: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let image = serenity::utils::read_image("./webhook_img.png") -/// .expect("Error reading image"); -/// let map = ObjectBuilder::new().insert("avatar", image).build(); -/// -/// let edited = http::edit_webhook_with_token(id, token, map) -/// .expect("Error editing webhook"); -/// ``` -/// -/// [`create_webhook`]: fn.create_webhook.html -/// [`edit_webhook_with_token`]: fn.edit_webhook_with_token.html -// The tests are ignored, rather than no_run'd, due to rustdoc tests with -// 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(webhook_id), - patch(body), - "/webhooks/{}", - webhook_id, - ); - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + /// Edits a [`Guild`]'s embed setting. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + // todo + pub fn edit_guild_embed(&self, guild_id: u64, map: &Value) + -> FutureResult<GuildEmbed> { + self.patch(Route::EditGuildEmbed { guild_id }, Some(map)) + } -/// Edits the webhook with the given data. -/// -/// Refer to the documentation for [`edit_webhook`] for more information. -/// -/// This method does _not_ require authentication. -/// -/// # Examples -/// -/// Edit the name of a webhook given its Id and unique token: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let map = ObjectBuilder::new().insert("name", "new name").build(); -/// -/// let edited = http::edit_webhook_with_token(id, token, map) -/// .expect("Error editing webhook"); -/// ``` -/// -/// [`edit_webhook`]: fn.edit_webhook.html -pub fn edit_webhook_with_token(webhook_id: u64, token: &str, map: &JsonMap) -> Result<Webhook> { - let body = serde_json::to_string(map)?; - let client = request_client!(); - - let response = retry(|| { - client - .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - .body(&body) - }).map_err(Error::Hyper)?; - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + /// Does specific actions to a member. + pub fn edit_member<F>(&self, guild_id: u64, user_id: u64, f: F) + -> FutureResult<()> where F: FnOnce(EditMember) -> EditMember { + let member = f(EditMember::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(member)); -/// Executes a webhook, posting a [`Message`] in the webhook's associated -/// [`Channel`]. -/// -/// This method does _not_ require authentication. -/// -/// Pass `true` to `wait` to wait for server confirmation of the message sending -/// before receiving a response. From the [Discord docs]: -/// -/// > waits for server confirmation of message send before response, and returns -/// > the created message body (defaults to false; when false a message that is -/// > not saved does not return an error) -/// -/// The map can _optionally_ contain the following data: -/// -/// - `avatar_url`: Override the default avatar of the webhook with a URL. -/// - `tts`: Whether this is a text-to-speech message (defaults to `false`). -/// - `username`: Override the default username of the webhook. -/// -/// Additionally, _at least one_ of the following must be given: -/// -/// - `content`: The content of the message. -/// - `embeds`: An array of rich embeds. -/// -/// **Note**: For embed objects, all fields are registered by Discord except for -/// `height`, `provider`, `proxy_url`, `type` (it will always be `rich`), -/// `video`, and `width`. The rest will be determined by Discord. -/// -/// # Examples -/// -/// Sending a webhook with message content of `test`: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let map = ObjectBuilder::new().insert("content", "test").build(); -/// -/// let message = match http::execute_webhook(id, token, true, map) { -/// Ok(Some(message)) => message, -/// Ok(None) => { -/// println!("Expected a webhook message"); -/// -/// return; -/// }, -/// Err(why) => { -/// println!("Error executing webhook: {:?}", why); -/// -/// return; -/// }, -/// }; -/// ``` -/// -/// [`Channel`]: ../model/channel/enum.Channel.html -/// [`Message`]: ../model/channel/struct.Message.html -/// [Discord docs]: https://discordapp.com/developers/docs/resources/webhook#querystring-params -pub fn execute_webhook(webhook_id: u64, - token: &str, - wait: bool, - map: &JsonMap) - -> Result<Option<Message>> { - let body = serde_json::to_string(map)?; - - let client = request_client!(); - - let response = retry(|| { - client - .post(&format!( - api!("/webhooks/{}/{}?wait={}"), - webhook_id, - token, - wait - )) - .body(&body) - .header(ContentType( - Mime(TopLevel::Application, SubLevel::Json, vec![]), - )) - }).map_err(Error::Hyper)?; - - if response.status == StatusCode::NoContent { - return Ok(None); - } - - serde_json::from_reader::<HyperResponse, Message>(response) - .map(Some) - .map_err(From::from) -} - -/// Gets the active maintenances from Discord's Status API. -/// -/// Does not require authentication. -pub fn get_active_maintenances() -> Result<Vec<Maintenance>> { - let client = request_client!(); + self.verify(Route::EditMember { guild_id, user_id }, Some(&map)) + } - let response = retry(|| { - client.get(status!("/scheduled-maintenances/active.json")) - })?; + /// Edits a message by Id. + /// + /// **Note**: Only the author of a message can modify it. + pub fn edit_message<F: FnOnce(EditMessage) -> EditMessage>( + &self, + channel_id: u64, + message_id: u64, + f: F, + ) -> FutureResult<Message> { + let msg = f(EditMessage::default()); + let map = Value::Object(serenity_utils::vecmap_to_json_map(msg.0)); + + self.patch(Route::EditMessage { channel_id, message_id }, Some(&map)) + } - let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + /// Edits the current user's nickname for the provided [`Guild`] via its Id. + /// + /// Pass `None` to reset the nickname. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + pub fn edit_nickname(&self, guild_id: u64, new_nickname: Option<&str>) + -> FutureResult<()> { + self.patch(Route::EditNickname { guild_id }, Some(&json!({ + "nick": new_nickname, + }))) + } - match map.remove("scheduled_maintenances") { - Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) - .map_err(From::from), - None => Ok(vec![]), + /// Edits the current user's profile settings. + pub fn edit_profile(&self, map: &Value) -> FutureResult<CurrentUser> { + self.patch(Route::EditProfile, Some(map)) } -} -/// Gets all the users that are banned in specific guild. -pub fn get_bans(guild_id: u64) -> Result<Vec<Ban>> { - let response = request!( - Route::GuildsIdBans(guild_id), - get, - "/guilds/{}/bans", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Ban>>(response) - .map_err(From::from) -} + /// Changes a role in a guild. + pub fn edit_role<F>(&self, guild_id: u64, role_id: u64, f: F) + -> FutureResult<Role> where F: FnOnce(EditRole) -> EditRole { + let role = f(EditRole::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(role)); -/// Gets all audit logs in a specific guild. -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?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) - .map_err(From::from) -} + self.patch(Route::EditRole { guild_id, role_id }, Some(&map)) + } -/// Gets current bot gateway. -pub fn get_bot_gateway() -> Result<BotGateway> { - let response = request!(Route::GatewayBot, get, "/gateway/bot"); + /// Changes the position of a role in a guild. + pub fn edit_role_position( + &self, + guild_id: u64, + role_id: u64, + position: u64, + ) -> FutureResult<Vec<Role>> { + self.patch(Route::EditRole { guild_id, role_id }, Some(&json!({ + "id": role_id, + "position": position, + }))) + } - serde_json::from_reader::<HyperResponse, BotGateway>(response) - .map_err(From::from) -} + /// Edits a the webhook with the given data. + /// + /// The Value is a map with optional values of: + /// + /// - **avatar**: base64-encoded 128x128 image for the webhook's default + /// avatar (_optional_); + /// - **name**: the name of the webhook, limited to between 2 and 100 + /// characters long. + /// + /// Note that, unlike with [`create_webhook`], _all_ values are optional. + /// + /// This method requires authentication, whereas [`edit_webhook_with_token`] + /// does not. + /// + /// # Examples + /// + /// Edit the image of a webhook given its Id and unique token: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let image = serenity::utils::read_image("./webhook_img.png") + /// .expect("Error reading image"); + /// let map = ObjectBuilder::new().insert("avatar", image).build(); + /// + /// let edited = http::edit_webhook_with_token(id, token, map) + /// .expect("Error editing webhook"); + /// ``` + /// + /// [`create_webhook`]: fn.create_webhook.html + /// [`edit_webhook_with_token`]: fn.edit_webhook_with_token.html + // The tests are ignored, rather than no_run'd, due to rustdoc tests with + // external crates being incredibly messy and misleading in the end user's + // view. + pub fn edit_webhook( + &self, + webhook_id: u64, + name: Option<&str>, + avatar: Option<&str>, + ) -> FutureResult<Webhook> { + let map = json!({ + "avatar": avatar, + "name": name, + }); + + self.patch(Route::EditWebhook { webhook_id }, Some(&map)) + } -/// Gets all invites for a channel. -pub fn get_channel_invites(channel_id: u64) -> Result<Vec<RichInvite>> { - let response = request!( - Route::ChannelsIdInvites(channel_id), - get, - "/channels/{}/invites", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response) - .map_err(From::from) -} + /// Edits the webhook with the given data. + /// + /// Refer to the documentation for [`edit_webhook`] for more information. + /// + /// This method does _not_ require authentication. + /// + /// # Examples + /// + /// Edit the name of a webhook given its Id and unique token: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let map = ObjectBuilder::new().insert("name", "new name").build(); + /// + /// let edited = http::edit_webhook_with_token(id, token, map) + /// .expect("Error editing webhook"); + /// ``` + /// + /// [`edit_webhook`]: fn.edit_webhook.html + pub fn edit_webhook_with_token( + &self, + webhook_id: u64, + token: &str, + name: Option<&str>, + avatar: Option<&str>, + ) -> FutureResult<Webhook> { + let map = json!({ + "avatar": avatar, + "name": name, + }); + let route = Route::EditWebhookWithToken { + token, + webhook_id, + }; -/// Retrieves the webhooks for the given [channel][`GuildChannel`]'s Id. -/// -/// This method requires authentication. -/// -/// # Examples -/// -/// Retrieve all of the webhooks owned by a channel: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let channel_id = 81384788765712384; -/// -/// let webhooks = http::get_channel_webhooks(channel_id) -/// .expect("Error getting channel webhooks"); -/// ``` -/// -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -pub fn get_channel_webhooks(channel_id: u64) -> Result<Vec<Webhook>> { - let response = request!( - Route::ChannelsIdWebhooks(channel_id), - get, - "/channels/{}/webhooks", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response) - .map_err(From::from) -} + self.patch(route, Some(&map)) + } -/// Gets channel information. -pub fn get_channel(channel_id: u64) -> Result<Channel> { - let response = request!( - Route::ChannelsId(channel_id), - get, - "/channels/{}", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Channel>(response) - .map_err(From::from) -} + /// Executes a webhook, posting a [`Message`] in the webhook's associated + /// [`Channel`]. + /// + /// This method does _not_ require authentication. + /// + /// Pass `true` to `wait` to wait for server confirmation of the message + /// sending before receiving a response. From the [Discord docs]: + /// + /// > waits for server confirmation of message send before response, and + /// > returns the created message body (defaults to false; when false a + /// > message that is not saved does not return an error) + /// + /// The map can _optionally_ contain the following data: + /// + /// - `avatar_url`: Override the default avatar of the webhook with a URL. + /// - `tts`: Whether this is a text-to-speech message (defaults to `false`). + /// - `username`: Override the default username of the webhook. + /// + /// Additionally, _at least one_ of the following must be given: + /// + /// - `content`: The content of the message. + /// - `embeds`: An array of rich embeds. + /// + /// **Note**: For embed objects, all fields are registered by Discord except + /// for `height`, `provider`, `proxy_url`, `type` (it will always be + /// `rich`), `video`, and `width`. The rest will be determined by Discord. + /// + /// # Examples + /// + /// Sending a webhook with message content of `test`: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let map = ObjectBuilder::new().insert("content", "test").build(); + /// + /// let message = match http::execute_webhook(id, token, true, map) { + /// Ok(Some(message)) => message, + /// Ok(None) => { + /// println!("Expected a webhook message"); + /// + /// return; + /// }, + /// Err(why) => { + /// println!("Error executing webhook: {:?}", why); + /// + /// return; + /// }, + /// }; + /// ``` + /// + /// [`Channel`]: ../model/channel/enum.Channel.html + /// [`Message`]: ../model/channel/struct.Message.html + /// [Discord docs]: https://discordapp.com/developers/docs/resources/webhook#querystring-params + pub fn execute_webhook<F: FnOnce(ExecuteWebhook) -> ExecuteWebhook>( + &self, + webhook_id: u64, + token: &str, + wait: bool, + f: F, + ) -> FutureResult<Option<Message>> { + let execution = f(ExecuteWebhook::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(execution)); + + let route = Route::ExecuteWebhook { + token, + wait, + webhook_id, + }; -/// Gets all channels in a guild. -pub fn get_channels(guild_id: u64) -> Result<Vec<GuildChannel>> { - let response = request!( - Route::ChannelsId(guild_id), - get, - "/guilds/{}/channels", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<GuildChannel>>(response) - .map_err(From::from) -} + if wait { + self.post(route, Some(&map)) + } else { + Box::new(self.verify(route, Some(&map)).map(|_| None)) + } + } -/// Gets information about the current application. -/// -/// **Note**: Only applications may use this endpoint. -pub fn get_current_application_info() -> Result<CurrentApplicationInfo> { - let response = request!(Route::None, get, "/oauth2/applications/@me"); + /// Gets the active maintenances from Discord's Status API. + /// + /// Does not require authentication. + // pub fn get_active_maintenances() -> FutureResult<Vec<Maintenance>> { + // let client = request_client!(); - serde_json::from_reader::<HyperResponse, CurrentApplicationInfo>(response) - .map_err(From::from) -} + // let response = retry(|| { + // client.get(status!("/scheduled-maintenances/active.json")) + // })?; -/// Gets information about the user we're connected with. -pub fn get_current_user() -> Result<CurrentUser> { - let response = request!(Route::UsersMe, get, "/users/@me"); + // let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; - serde_json::from_reader::<HyperResponse, CurrentUser>(response) - .map_err(From::from) -} + // match map.remove("scheduled_maintenances") { + // Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) + // .map_err(From::from), + // None => Ok(vec![]), + // } + // } -/// Gets current gateway. -pub fn get_gateway() -> Result<Gateway> { - let response = request!(Route::Gateway, get, "/gateway"); + /// Gets all the users that are banned in specific guild. + pub fn get_bans(&self, guild_id: u64) -> FutureResult<Vec<Ban>> { + self.get(Route::GetBans { guild_id }) + } - serde_json::from_reader::<HyperResponse, Gateway>(response) - .map_err(From::from) -} + /// Gets all audit logs in a specific guild. + pub fn get_audit_logs( + &self, + guild_id: u64, + action_type: Option<u8>, + user_id: Option<u64>, + before: Option<u64>, + limit: Option<u8>, + ) -> FutureResult<AuditLogs> { + self.get(Route::GetAuditLogs { + action_type, + before, + guild_id, + limit, + user_id, + }) + } -/// Gets guild information. -pub fn get_guild(guild_id: u64) -> Result<PartialGuild> { - let response = request!(Route::GuildsId(guild_id), get, "/guilds/{}", guild_id); + /// Gets current bot gateway. + pub fn get_bot_gateway(&self) -> FutureResult<BotGateway> { + self.get(Route::GetBotGateway) + } - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Gets all invites for a channel. + pub fn get_channel_invites(&self, channel_id: u64) + -> FutureResult<Vec<RichInvite>> { + self.get(Route::GetChannelInvites { channel_id }) + } -/// Gets a guild embed information. -pub fn get_guild_embed(guild_id: u64) -> Result<GuildEmbed> { - let response = request!( - Route::GuildsIdEmbed(guild_id), - get, - "/guilds/{}/embeds", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildEmbed>(response) - .map_err(From::from) -} + /// Retrieves the webhooks for the given [channel][`GuildChannel`]'s Id. + /// + /// This method requires authentication. + /// + /// # Examples + /// + /// Retrieve all of the webhooks owned by a channel: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let channel_id = 81384788765712384; + /// + /// let webhooks = http::get_channel_webhooks(channel_id) + /// .expect("Error getting channel webhooks"); + /// ``` + /// + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + pub fn get_channel_webhooks(&self, channel_id: u64) + -> FutureResult<Vec<Webhook>> { + self.get(Route::GetChannelWebhooks { channel_id }) + } -/// Gets integrations that a guild has. -pub fn get_guild_integrations(guild_id: u64) -> Result<Vec<Integration>> { - let response = request!( - Route::GuildsIdIntegrations(guild_id), - get, - "/guilds/{}/integrations", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Integration>>(response) - .map_err(From::from) -} + /// Gets channel information. + pub fn get_channel(&self, channel_id: u64) -> FutureResult<Channel> { + self.get(Route::GetChannel { channel_id }) + } -/// Gets all invites to a guild. -pub fn get_guild_invites(guild_id: u64) -> Result<Vec<RichInvite>> { - let response = request!( - Route::GuildsIdInvites(guild_id), - get, - "/guilds/{}/invites", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response) - .map_err(From::from) -} + /// Gets all channels in a guild. + pub fn get_channels(&self, guild_id: u64) + -> FutureResult<Vec<GuildChannel>> { + self.get(Route::GetChannels { guild_id }) + } -/// Gets the members of a guild. Optionally pass a `limit` and the Id of the -/// user to offset the result by. -pub fn get_guild_members(guild_id: u64, - limit: Option<u64>, - after: Option<u64>) - -> Result<Vec<Member>> { - let response = request!( - Route::GuildsIdMembers(guild_id), - get, - "/guilds/{}/members?limit={}&after={}", - guild_id, - limit.unwrap_or(500), - after.unwrap_or(0) - ); - - let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?; - - if let Some(values) = v.as_array_mut() { - let num = Value::Number(Number::from(guild_id)); - - for value in values { - if let Some(element) = value.as_object_mut() { - element.insert("guild_id".to_string(), num.clone()); - } - } + /// Gets information about the current application. + /// + /// **Note**: Only applications may use this endpoint. + pub fn get_current_application_info(&self) + -> FutureResult<CurrentApplicationInfo> { + self.get(Route::GetCurrentApplicationInfo) } - serde_json::from_value::<Vec<Member>>(v).map_err(From::from) -} + /// Gets information about the user we're connected with. + pub fn get_current_user(&self, ) -> FutureResult<CurrentUser> { + self.get(Route::GetCurrentUser) + } -/// Gets the amount of users that can be pruned. -pub fn get_guild_prune_count(guild_id: u64, map: &Value) -> Result<GuildPrune> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdPrune(guild_id), - get(body), - "/guilds/{}/prune", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildPrune>(response) - .map_err(From::from) -} + /// Gets current gateway. + pub fn get_gateway(&self) -> FutureResult<Gateway> { + self.get(Route::GetGateway) + } -/// Gets regions that a guild can use. If a guild has the `VIP_REGIONS` feature -/// enabled, then additional VIP-only regions are returned. -pub fn get_guild_regions(guild_id: u64) -> Result<Vec<VoiceRegion>> { - let response = request!( - Route::GuildsIdRegions(guild_id), - get, - "/guilds/{}/regions", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response) - .map_err(From::from) -} + /// Gets guild information. + pub fn get_guild(&self, guild_id: u64) -> FutureResult<PartialGuild> { + self.get(Route::GetGuild { guild_id }) + } -/// Retrieves a list of roles in a [`Guild`]. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn get_guild_roles(guild_id: u64) -> Result<Vec<Role>> { - let response = request!( - Route::GuildsIdRoles(guild_id), - get, - "/guilds/{}/roles", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Role>>(response) - .map_err(From::from) -} + /// Gets a guild embed information. + pub fn get_guild_embed(&self, guild_id: u64) + -> FutureResult<GuildEmbed> { + self.get(Route::GetGuildEmbed { guild_id }) + } -/// Retrieves the webhooks for the given [guild][`Guild`]'s Id. -/// -/// This method requires authentication. -/// -/// # Examples -/// -/// Retrieve all of the webhooks owned by a guild: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let guild_id = 81384788765712384; -/// -/// let webhooks = http::get_guild_webhooks(guild_id) -/// .expect("Error getting guild webhooks"); -/// ``` -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn get_guild_webhooks(guild_id: u64) -> Result<Vec<Webhook>> { - let response = request!( - Route::GuildsIdWebhooks(guild_id), - get, - "/guilds/{}/webhooks", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response) - .map_err(From::from) -} + /// Gets integrations that a guild has. + pub fn get_guild_integrations(&self, guild_id: u64) + -> FutureResult<Vec<Integration>> { + self.get(Route::GetGuildIntegrations { guild_id }) + } -/// Gets a paginated list of the current user's guilds. -/// -/// The `limit` has a maximum value of 100. -/// -/// [Discord's documentation][docs] -/// -/// # Examples -/// -/// Get the first 10 guilds after a certain guild's Id: -/// -/// ```rust,no_run -/// use serenity::http::{GuildPagination, get_guilds}; -/// use serenity::model::id::GuildId; -/// -/// let guild_id = GuildId(81384788765712384); -/// -/// let guilds = get_guilds(&GuildPagination::After(guild_id), 10).unwrap(); -/// ``` -/// -/// [docs]: https://discordapp.com/developers/docs/resources/user#get-current-user-guilds -pub fn get_guilds(target: &GuildPagination, limit: u64) -> Result<Vec<GuildInfo>> { - let mut uri = format!("/users/@me/guilds?limit={}", limit); + /// Gets all invites to a guild. + pub fn get_guild_invites(&self, guild_id: u64) + -> FutureResult<Vec<RichInvite>> { + self.get(Route::GetGuildInvites { guild_id }) + } - match *target { - GuildPagination::After(id) => { - write!(uri, "&after={}", id)?; - }, - GuildPagination::Before(id) => { - write!(uri, "&before={}", id)?; - }, + /// Gets the members of a guild. Optionally pass a `limit` and the Id of the + /// user to offset the result by. + pub fn get_guild_members( + &self, + guild_id: u64, + limit: Option<u64>, + after: Option<u64> + ) -> FutureResult<Vec<Member>> { + let done = self.get(Route::GetGuildMembers { after, guild_id, limit }) + .and_then(move |mut v: Value| { + if let Some(values) = v.as_array_mut() { + let num = Value::Number(Number::from(guild_id)); + + for value in values { + if let Some(element) = value.as_object_mut() { + element.insert("guild_id".to_string(), num.clone()); + } + } + } + + serde_json::from_value::<Vec<Member>>(v).map_err(From::from) + }); + + Box::new(done) } - let response = request!(Route::UsersMeGuilds, get, "{}", uri); + /// Gets the amount of users that can be pruned. + pub fn get_guild_prune_count(&self, guild_id: u64, days: u16) + -> FutureResult<GuildPrune> { + self.get(Route::GetGuildPruneCount { + days: days as u64, + guild_id, + }) + } - serde_json::from_reader::<HyperResponse, Vec<GuildInfo>>(response) - .map_err(From::from) -} + /// Gets regions that a guild can use. If a guild has + /// [`Feature::VipRegions`] enabled, then additional VIP-only regions are + /// returned. + /// + /// [`Feature::VipRegions`]: ../model/enum.Feature.html#variant.VipRegions + pub fn get_guild_regions(&self, guild_id: u64) + -> FutureResult<Vec<VoiceRegion>> { + self.get(Route::GetGuildRegions { guild_id }) + } -/// Gets information about a specific invite. -#[allow(unused_mut)] -pub fn get_invite(code: &str, stats: bool) -> Result<Invite> { - let mut invite = code; + /// Retrieves a list of roles in a [`Guild`]. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + pub fn get_guild_roles(&self, guild_id: u64) + -> FutureResult<Vec<Role>> { + self.get(Route::GetGuildRoles { guild_id }) + } - #[cfg(feature = "utils")] - { - invite = ::utils::parse_invite(invite); + /// Retrieves the webhooks for the given [guild][`Guild`]'s Id. + /// + /// This method requires authentication. + /// + /// # Examples + /// + /// Retrieve all of the webhooks owned by a guild: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let guild_id = 81384788765712384; + /// + /// let webhooks = http::get_guild_webhooks(guild_id) + /// .expect("Error getting guild webhooks"); + /// ``` + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + pub fn get_guild_webhooks(&self, guild_id: u64) + -> FutureResult<Vec<Webhook>> { + self.get(Route::GetGuildWebhooks { guild_id }) } - let mut uri = format!("/invites/{}", invite); + /// Gets a paginated list of the current user's guilds. + /// + /// The `limit` has a maximum value of 100. + /// + /// [Discord's documentation][docs] + /// + /// # Examples + /// + /// Get the first 10 guilds after a certain guild's Id: + /// + /// ```rust,no_run + /// use serenity::http::{GuildPagination, get_guilds}; + /// use serenity::model::GuildId; + /// + /// let guild_id = GuildId(81384788765712384); + /// + /// let guilds = get_guilds(&GuildPagination::After(guild_id), 10).unwrap(); + /// ``` + /// + /// [docs]: https://discordapp.com/developers/docs/resources/user#get-current-user-guilds + pub fn get_guilds(&self, target: &GuildPagination, limit: u64) + -> FutureResult<Vec<GuildInfo>> { + let (after, before) = match *target { + GuildPagination::After(v) => (Some(v.0), None), + GuildPagination::Before(v) => (None, Some(v.0)), + }; - if stats { - uri.push_str("?with_counts=true"); + self.get(Route::GetGuilds { after, before, limit }) } - let response = request!(Route::InvitesCode, get, "{}", uri); - - serde_json::from_reader::<HyperResponse, Invite>(response) - .map_err(From::from) -} + /// Gets information about a specific invite. + pub fn get_invite<'a>(&self, code: &'a str, stats: bool) + -> Box<Future<Item = Invite, Error = Error> + 'a> { + self.get(Route::GetInvite { code, stats }) + } -/// Gets member of a guild. -pub fn get_member(guild_id: u64, user_id: u64) -> Result<Member> { - let response = request!( - Route::GuildsIdMembersId(guild_id), - get, - "/guilds/{}/members/{}", - guild_id, - user_id - ); + /// Gets member of a guild. + pub fn get_member(&self, guild_id: u64, user_id: u64) + -> FutureResult<Member> { + let done = self.get::<Value>(Route::GetMember { guild_id, user_id }) + .and_then(move |mut v| { + if let Some(map) = v.as_object_mut() { + map.insert("guild_id".to_string(), Value::Number(Number::from(guild_id))); + } - let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?; + serde_json::from_value(v).map_err(From::from) + }); - if let Some(map) = v.as_object_mut() { - map.insert("guild_id".to_string(), Value::Number(Number::from(guild_id))); + Box::new(done) } - serde_json::from_value::<Member>(v).map_err(From::from) -} - -/// Gets a message by an Id, bots only. -pub fn get_message(channel_id: u64, message_id: u64) -> Result<Message> { - let response = request!( - Route::ChannelsIdMessagesId(LightMethod::Any, channel_id), - get, - "/channels/{}/messages/{}", - channel_id, - message_id - ); - - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + /// Gets a message by an Id, bots only. + pub fn get_message(&self, channel_id: u64, message_id: u64) + -> FutureResult<Message> { + self.get(Route::GetMessage { channel_id, message_id }) + } -/// Gets X messages from a channel. -pub fn get_messages(channel_id: u64, query: &str) -> Result<Vec<Message>> { - let url = format!(api!("/channels/{}/messages{}"), channel_id, query); - let client = request_client!(); + /// Gets X messages from a channel. + pub fn get_messages<'a, F: FnOnce(GetMessages) -> GetMessages>( + &'a self, + channel_id: u64, + f: F, + ) -> Box<Future<Item = Vec<Message>, Error = Error> + 'a> { + let mut map = f(GetMessages::default()).0; - let response = request(Route::ChannelsIdMessages(channel_id), || client.get(&url))?; + let limit = map.remove(&"limit").unwrap_or(50); + let mut query = format!("?limit={}", limit); - serde_json::from_reader::<HyperResponse, Vec<Message>>(response) - .map_err(From::from) -} + if let Some(after) = map.remove(&"after") { + ftry!(write!(query, "&after={}", after)); + } -/// Gets all pins of a channel. -pub fn get_pins(channel_id: u64) -> Result<Vec<Message>> { - let response = request!( - Route::ChannelsIdPins(channel_id), - get, - "/channels/{}/pins", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Message>>(response) - .map_err(From::from) -} + if let Some(around) = map.remove(&"around") { + ftry!(write!(query, "&around={}", around)); + } -/// Gets user Ids based on their reaction to a message. This endpoint is dumb. -pub fn get_reaction_users(channel_id: u64, - message_id: u64, - reaction_type: &ReactionType, - limit: u8, - after: Option<u64>) - -> Result<Vec<User>> { - let mut uri = format!( - "/channels/{}/messages/{}/reactions/{}?limit={}", - channel_id, - message_id, - reaction_type.as_data(), - limit - ); - - if let Some(user_id) = after { - write!(uri, "&after={}", user_id)?; - } - - let response = request!( - Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - get, - "{}", - uri - ); - - serde_json::from_reader::<HyperResponse, Vec<User>>(response) - .map_err(From::from) -} + if let Some(before) = map.remove(&"before") { + ftry!(write!(query, "&before={}", before)); + } -/// Gets the current unresolved incidents from Discord's Status API. -/// -/// Does not require authentication. -pub fn get_unresolved_incidents() -> Result<Vec<Incident>> { - let client = request_client!(); + self.get(Route::GetMessages { + channel_id, + query, + }) + } - let response = retry(|| client.get(status!("/incidents/unresolved.json")))?; + /// Gets all pins of a channel. + pub fn get_pins(&self, channel_id: u64) -> FutureResult<Vec<Message>> { + self.get(Route::GetPins { channel_id }) + } - let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + /// Gets user Ids based on their reaction to a message. + pub fn get_reaction_users( + &self, + channel_id: u64, + message_id: u64, + reaction_type: &ReactionType, + limit: Option<u8>, + after: Option<u64> + ) -> FutureResult<Vec<User>> { + let reaction = utils::reaction_type_data(reaction_type); + self.get(Route::GetReactionUsers { + limit: limit.unwrap_or(50), + after, + channel_id, + message_id, + reaction, + }) + } - match map.remove("incidents") { - Some(v) => serde_json::from_value::<Vec<Incident>>(v) - .map_err(From::from), - None => Ok(vec![]), + // /// Gets the current unresolved incidents from Discord's Status API. + // /// + // /// Does not require authentication. + // pub fn get_unresolved_incidents(&self) -> FutureResult<Vec<Incident>> { + // let client = request_client!(); + + // let response = retry(|| client.get(status!("/incidents/unresolved.json")))?; + + // let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + + // match map.remove("incidents") { + // Some(v) => serde_json::from_value::<Vec<Incident>>(v) + // .map_err(From::from), + // None => Ok(vec![]), + // } + // } + + // /// Gets the upcoming (planned) maintenances from Discord's Status API. + // /// + // /// Does not require authentication. + // pub fn get_upcoming_maintenances(&self) -> FutureResult<Vec<Maintenance>> { + // let client = request_client!(); + + // let response = retry(|| { + // client.get(status!("/scheduled-maintenances/upcoming.json")) + // })?; + + // let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + + // match map.remove("scheduled_maintenances") { + // Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) + // .map_err(From::from), + // None => Ok(vec![]), + // } + // } + + /// Gets a user by Id. + pub fn get_user(&self, user_id: u64) -> FutureResult<User> { + self.get(Route::GetUser { user_id }) } -} -/// Gets the upcoming (planned) maintenances from Discord's Status API. -/// -/// Does not require authentication. -pub fn get_upcoming_maintenances() -> Result<Vec<Maintenance>> { - let client = request_client!(); + /// Gets our DM channels. + pub fn get_user_dm_channels(&self) + -> FutureResult<Vec<PrivateChannel>> { + self.get(Route::GetUserDmChannels) + } - let response = retry(|| { - client.get(status!("/scheduled-maintenances/upcoming.json")) - })?; + /// Gets all voice regions. + pub fn get_voice_regions(&self) -> FutureResult<Vec<VoiceRegion>> { + self.get(Route::GetVoiceRegions) + } - let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + /// Retrieves a webhook given its Id. + /// + /// This method requires authentication, whereas [`get_webhook_with_token`] does + /// not. + /// + /// # Examples + /// + /// Retrieve a webhook by Id: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let webhook = http::get_webhook(id).expect("Error getting webhook"); + /// ``` + /// + /// [`get_webhook_with_token`]: fn.get_webhook_with_token.html + pub fn get_webhook(&self, webhook_id: u64) -> FutureResult<Webhook> { + self.get(Route::GetWebhook { webhook_id }) + } - match map.remove("scheduled_maintenances") { - Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) - .map_err(From::from), - None => Ok(vec![]), + /// Retrieves a webhook given its Id and unique token. + /// + /// This method does _not_ require authentication. + /// + /// # Examples + /// + /// Retrieve a webhook by Id and its unique token: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// + /// let webhook = http::get_webhook_with_token(id, token) + /// .expect("Error getting webhook"); + /// ``` + pub fn get_webhook_with_token<'a>( + &self, + webhook_id: u64, + token: &'a str, + ) -> Box<Future<Item = Webhook, Error = Error> + 'a> { + self.get(Route::GetWebhookWithToken { token, webhook_id }) } -} -/// Gets a user by Id. -pub fn get_user(user_id: u64) -> Result<User> { - let response = request!(Route::UsersId, get, "/users/{}", user_id); + /// Kicks a member from a guild. + pub fn kick_member(&self, guild_id: u64, user_id: u64) + -> FutureResult<()> { + self.verify(Route::KickMember { guild_id, user_id }, None) + } - serde_json::from_reader::<HyperResponse, User>(response) - .map_err(From::from) -} + /// Leaves a group DM. + pub fn leave_group(&self, group_id: u64) -> FutureResult<()> { + self.verify(Route::DeleteChannel { + channel_id: group_id, + }, None) + } -/// Gets our DM channels. -pub fn get_user_dm_channels() -> Result<Vec<PrivateChannel>> { - let response = request!(Route::UsersMeChannels, get, "/users/@me/channels"); + /// Leaves a guild. + pub fn leave_guild(&self, guild_id: u64) -> FutureResult<()> { + self.verify(Route::LeaveGuild { guild_id }, None) + } - serde_json::from_reader::<HyperResponse, Vec<PrivateChannel>>(response) - .map_err(From::from) -} + /// Deletes a user from group DM. + pub fn remove_group_recipient(&self, group_id: u64, user_id: u64) + -> FutureResult<()> { + self.verify(Route::RemoveGroupRecipient { group_id, user_id }, None) + } -/// Gets all voice regions. -pub fn get_voice_regions() -> Result<Vec<VoiceRegion>> { - let response = request!(Route::VoiceRegions, get, "/voice/regions"); + // /// Sends file(s) to a channel. + // /// + // /// # Errors + // /// + // /// Returns an + // /// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] + // /// if the file is too large to send. + // /// + // /// [`HttpError::InvalidRequest`]: enum.HttpError.html#variant.InvalidRequest + // pub fn send_files<'a, F, T, It>( + // &self, + // channel_id: u64, + // files: It, + // f: F, + // ) -> FutureResult<Message> + // where F: FnOnce(CreateMessage) -> CreateMessage, + // T: Into<AttachmentType<'a>>, + // It: IntoIterator<Item = T> { + // let msg = f(CreateMessage::default()); + // let map = Value::Object(serenity_utils::vecmap_to_json_map(msg.0)); + + // let uri = format!(api!("/channels/{}/messages"), channel_id); + // let url = match Url::parse(&uri) { + // Ok(url) => url, + // Err(_) => return Err(Error::Url(uri)), + // }; + + // let mut request = Multipart::from_request(request)?; + // let mut file_num = "0".to_string(); + + // for file in files { + // match file.into() { + // AttachmentType::Bytes((mut bytes, filename)) => { + // request + // .write_stream(&file_num, &mut bytes, Some(filename), None)?; + // }, + // AttachmentType::File((mut f, filename)) => { + // request + // .write_stream(&file_num, &mut f, Some(filename), None)?; + // }, + // AttachmentType::Path(p) => { + // request.write_file(&file_num, &p)?; + // }, + // } + + // unsafe { + // let vec = file_num.as_mut_vec(); + // vec[0] += 1; + // } + // } + + // for (k, v) in map { + // match v { + // Value::Bool(false) => request.write_text(&k, "false")?, + // Value::Bool(true) => request.write_text(&k, "true")?, + // Value::Number(inner) => request.write_text(&k, inner.to_string())?, + // Value::String(inner) => request.write_text(&k, inner)?, + // _ => continue, + // }; + // } + + // let response = request.send()?; + + // if response.status.class() != StatusClass::Success { + // return Err(Error::InvalidRequest(response.status)); + // } + + // serde_json::from_reader::<HyperResponse, Message>(response) + // .map_err(From::from) + // } + + /// Sends a message to a channel. + pub fn send_message<F>(&self, channel_id: u64, f: F) -> FutureResult<Message> + where F: FnOnce(CreateMessage) -> CreateMessage { + let msg = f(CreateMessage::default()); + let map = Value::Object(serenity_utils::vecmap_to_json_map(msg.0)); + + self.post(Route::CreateMessage { channel_id }, Some(&map)) + } - serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response) - .map_err(From::from) -} + /// Pins a message in a channel. + pub fn pin_message(&self, channel_id: u64, message_id: u64) + -> FutureResult<()> { + self.verify(Route::PinMessage { channel_id, message_id }, None) + } -/// Retrieves a webhook given its Id. -/// -/// This method requires authentication, whereas [`get_webhook_with_token`] does -/// not. -/// -/// # Examples -/// -/// Retrieve a webhook by Id: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let webhook = http::get_webhook(id).expect("Error getting webhook"); -/// ``` -/// -/// [`get_webhook_with_token`]: fn.get_webhook_with_token.html -pub fn get_webhook(webhook_id: u64) -> Result<Webhook> { - let response = request!( - Route::WebhooksId(webhook_id), - get, - "/webhooks/{}", - webhook_id, - ); - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + /// Unbans a user from a guild. + pub fn remove_ban(&self, guild_id: u64, user_id: u64) + -> FutureResult<()> { + self.verify(Route::RemoveBan { guild_id, user_id }, None) + } -/// Retrieves a webhook given its Id and unique token. -/// -/// This method does _not_ require authentication. -/// -/// # Examples -/// -/// Retrieve a webhook by Id and its unique token: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// -/// let webhook = http::get_webhook_with_token(id, token) -/// .expect("Error getting webhook"); -/// ``` -pub fn get_webhook_with_token(webhook_id: u64, token: &str) -> Result<Webhook> { - let client = request_client!(); - - let response = retry(|| { - client - .get(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - }).map_err(Error::Hyper)?; - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + /// Deletes a single [`Role`] from a [`Member`] in a [`Guild`]. + /// + /// **Note**: Requires the [Manage Roles] permission and respect of role + /// hierarchy. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`Member`]: ../model/guild/struct.Member.html + /// [`Role`]: ../model/guild/struct.Role.html + /// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html + pub fn remove_member_role( + &self, + guild_id: u64, + user_id: u64, + role_id: u64, + ) -> FutureResult<()> { + self.verify( + Route::RemoveMemberRole { guild_id, role_id, user_id }, + None, + ) + } -/// Kicks a member from a guild. -pub fn kick_member(guild_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdMembersId(guild_id), - delete, - "/guilds/{}/members/{}", + /// Starts removing some members from a guild based on the last time they've been online. + pub fn start_guild_prune(&self, guild_id: u64, days: u16) + -> FutureResult<GuildPrune> { + self.post(Route::StartGuildPrune { + days: days as u64, guild_id, - user_id - ), - ) -} - -/// Leaves a group DM. -pub fn leave_group(guild_id: u64) -> Result<Group> { - let response = request!(Route::None, delete, "/channels/{}", guild_id); + }, None) + } - serde_json::from_reader::<HyperResponse, Group>(response) - .map_err(From::from) -} + /// Starts syncing an integration with a guild. + pub fn start_integration_sync(&self, guild_id: u64, integration_id: u64) + -> FutureResult<()> { + self.verify( + Route::StartIntegrationSync { guild_id, integration_id }, + None, + ) + } -/// Leaves a guild. -pub fn leave_guild(guild_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::UsersMeGuildsId, - delete, - "/users/@me/guilds/{}", - guild_id - ), - ) -} + /// Unpins a message from a channel. + pub fn unpin_message(&self, channel_id: u64, message_id: u64) + -> FutureResult<()> { + self.verify(Route::UnpinMessage { channel_id, message_id }, None) + } -/// Deletes a user from group DM. -pub fn remove_group_recipient(group_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::None, - delete, - "/channels/{}/recipients/{}", - group_id, - user_id - ), - ) -} + fn delete<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + self.request(route, map) + } -/// Sends file(s) to a channel. -/// -/// # Errors -/// -/// Returns an -/// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] -/// if the file is too large to send. -/// -/// [`HttpError::InvalidRequest`]: enum.HttpError.html#variant.InvalidRequest -pub fn send_files<'a, T, It: IntoIterator<Item=T>>(channel_id: u64, files: It, map: JsonMap) -> Result<Message> - where T: Into<AttachmentType<'a>> { - let uri = format!(api!("/channels/{}/messages"), channel_id); - let url = match Url::parse(&uri) { - Ok(url) => url, - Err(_) => return Err(Error::Url(uri)), - }; - - let tc = NativeTlsClient::new()?; - let connector = HttpsConnector::new(tc); - let mut request = Request::with_connector(Method::Post, url, &connector)?; - request - .headers_mut() - .set(header::Authorization(TOKEN.lock().clone())); - request - .headers_mut() - .set(header::UserAgent(constants::USER_AGENT.to_string())); - - let mut request = Multipart::from_request(request)?; - let mut file_num = "0".to_string(); - - for file in files { - match file.into() { - AttachmentType::Bytes((mut bytes, filename)) => { - request - .write_stream(&file_num, &mut bytes, Some(filename), None)?; - }, - AttachmentType::File((mut f, filename)) => { - request - .write_stream(&file_num, &mut f, Some(filename), None)?; - }, - AttachmentType::Path(p) => { - request.write_file(&file_num, &p)?; - }, - } + fn get<'a, T: DeserializeOwned + 'static>(&self, route: Route<'a>) + -> Box<Future<Item = T, Error = Error> + 'a> { + self.request(route, None) + } - unsafe { - let vec = file_num.as_mut_vec(); - vec[0] += 1; - } + fn patch<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + self.request(route, map) } - for (k, v) in map { - match v { - Value::Bool(false) => request.write_text(&k, "false")?, - Value::Bool(true) => request.write_text(&k, "true")?, - Value::Number(inner) => request.write_text(&k, inner.to_string())?, - Value::String(inner) => request.write_text(&k, inner)?, - _ => continue, - }; + fn post<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + self.request(route, map) } - let response = request.send()?; + fn request<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + let (method, path, url) = route.deconstruct(); - if response.status.class() != StatusClass::Success { - return Err(Error::Http(HttpError::UnsuccessfulRequest(response))); - } + let built_uri = try_uri!(url.as_ref()); + let mut request = Request::new(method.hyper_method(), built_uri); - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + if let Some(value) = map { + request.set_body(ftry!(serde_json::to_string(value))); + } -/// Sends a message to a channel. -pub fn send_message(channel_id: u64, map: &Value) -> Result<Message> { - let body = map.to_string(); - let response = request!( - Route::ChannelsIdMessages(channel_id), - post(body), - "/channels/{}/messages", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + { + let headers = request.headers_mut(); + headers.set(Authorization(self.token())); + headers.set(ContentType::json()); + } -/// Pins a message in a channel. -pub fn pin_message(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdPinsMessageId(channel_id), - put, - "/channels/{}/pins/{}", - channel_id, - message_id - ), - ) -} + let client = Rc::clone(&self.client); -/// Unbans a user from a guild. -pub fn remove_ban(guild_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdBansUserId(guild_id), - delete, - "/guilds/{}/bans/{}", - guild_id, - user_id - ), - ) -} + Box::new(ftry!(self.ratelimiter.try_borrow_mut()).take(&path) + .and_then(move |_| client.request(request).map_err(From::from)) + .from_err() + .and_then(verify_status) + .and_then(|res| res.body().concat2().map_err(From::from)) + .and_then(|body| serde_json::from_slice(&body).map_err(From::from))) + } -/// Deletes a single [`Role`] from a [`Member`] in a [`Guild`]. -/// -/// **Note**: Requires the [Manage Roles] permission and respect of role -/// hierarchy. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`Member`]: ../model/guild/struct.Member.html -/// [`Role`]: ../model/guild/struct.Role.html -/// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html -pub fn remove_member_role(guild_id: u64, user_id: u64, role_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdMembersIdRolesId(guild_id), - delete, - "/guilds/{}/members/{}/roles/{}", - guild_id, - user_id, - role_id - ), - ) -} + fn verify<'a>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = (), Error = Error>> { + let (method, path, url) = route.deconstruct(); -/// Starts removing some members from a guild based on the last time they've been online. -pub fn start_guild_prune(guild_id: u64, map: &Value) -> Result<GuildPrune> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdPrune(guild_id), - post(body), - "/guilds/{}/prune", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildPrune>(response) - .map_err(From::from) -} + let mut request = Request::new( + method.hyper_method(), + try_uri!(url.as_ref()), + ); -/// Starts syncing an integration with a guild. -pub fn start_integration_sync(guild_id: u64, integration_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdIntegrationsIdSync(guild_id), - post, - "/guilds/{}/integrations/{}/sync", - guild_id, - integration_id - ), - ) -} + if let Some(value) = map { + request.set_body(ftry!(serde_json::to_string(value))); + } -/// Unpins a message from a channel. -pub fn unpin_message(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdPinsMessageId(channel_id), - delete, - "/channels/{}/pins/{}", - channel_id, - message_id - ), - ) -} + { + let headers = request.headers_mut(); + headers.set(Authorization(self.token())); + headers.set(ContentType::json()); + } -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().clone())) - .header(header::ContentType::json()) - })?; + let client = Rc::clone(&self.client); - if response.status.class() == StatusClass::Success { - Ok(response) - } else { - Err(Error::Http(HttpError::UnsuccessfulRequest(response))) + Box::new(ftry!(self.ratelimiter.try_borrow_mut()).take(&path) + .and_then(move |_| client.request(request).map_err(From::from)) + .map_err(From::from) + .and_then(verify_status) + .map(|_| ())) } -} -pub(crate) fn retry<'a, F>(f: F) -> HyperResult<HyperResponse> - where F: Fn() -> RequestBuilder<'a> { - let req = || { - f().header(header::UserAgent(constants::USER_AGENT.to_string())) - .send() - }; + fn token(&self) -> String { + let pointer = Rc::into_raw(Rc::clone(&self.token)); + let token = unsafe { + (*pointer).clone() + }; - match req() { - Err(HyperError::Io(ref io)) if io.kind() == IoErrorKind::ConnectionAborted => req(), - other => other, - } -} + unsafe { + drop(Rc::from_raw(pointer)); + } -fn verify(expected: u16, response: HyperResponse) -> Result<()> { - if response.status.to_u16() == expected { - return Ok(()); + token } +} - debug!("Expected {}, got {}", expected, response.status); - trace!("Unsuccessful response: {:?}", response); - Err(Error::Http(HttpError::UnsuccessfulRequest(response))) +/// Verifies the status of the response according to the method used to create +/// the accompanying request. +/// +/// If the status code is correct (a 200 is expected and the response status is +/// also 200), then the future resolves. Otherwise, a leaf future is returned +/// with an error as the `Error` type. +/// +/// # Errors +/// +/// Returns [`Error::InvalidRequest`] if the response status code is unexpected. +/// +/// [`Error::InvalidRequest`]: enum.Error.html#variant.InvalidRequest +fn verify_status(response: Response) -> + Box<Future<Item = Response, Error = Error>> { + if response.status().is_success() { + Box::new(future::ok(response)) + } else { + Box::new(future::err(Error::Http(HttpError::InvalidRequest(response)))) + } } /// Enum that allows a user to pass a `Path` or a `File` type to `send_files` @@ -1881,7 +1581,7 @@ pub enum AttachmentType<'a> { /// Indicates that the `AttachmentType` is a `File` File((&'a File, &'a str)), /// Indicates that the `AttachmentType` is a `Path` - Path(&'a Path), + Path(&'a StdPath), } impl<'a> From<(&'a [u8], &'a str)> for AttachmentType<'a> { @@ -1889,11 +1589,11 @@ impl<'a> From<(&'a [u8], &'a str)> for AttachmentType<'a> { } impl<'a> From<&'a str> for AttachmentType<'a> { - fn from(s: &'a str) -> AttachmentType { AttachmentType::Path(Path::new(s)) } + fn from(s: &'a str) -> AttachmentType { AttachmentType::Path(StdPath::new(s)) } } -impl<'a> From<&'a Path> for AttachmentType<'a> { - fn from(path: &'a Path) -> AttachmentType { +impl<'a> From<&'a StdPath> for AttachmentType<'a> { + fn from(path: &'a StdPath) -> AttachmentType { AttachmentType::Path(path) } } diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index ca269e3..bf9ad8d 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -38,21 +38,156 @@ //! differentiating between different ratelimits. //! //! [Taken from]: https://discordapp.com/developers/docs/topics/rate-limits#rate-limits -#![allow(zero_ptr)] +#![cfg_attr(feature = "cargo-clippy", allow(zero_ptr))] use chrono::Utc; -use hyper::client::{RequestBuilder, Response}; +use futures::sync::oneshot::{self, Receiver, Sender}; +use futures::{Future, future}; +use hyper::client::{Response}; use hyper::header::Headers; -use hyper::status::StatusCode; -use internal::prelude::*; -use parking_lot::Mutex; -use std::collections::HashMap; -use std::sync::Arc; +use hyper::StatusCode; +use std::cell::RefCell; +use std::collections::{HashMap, VecDeque}; +use std::error::Error as StdError; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::rc::Rc; use std::time::Duration; -use std::{str, thread, i64}; -use super::{HttpError, LightMethod}; +use std::{i64, str, u8}; +use super::{Error, Path, Result}; +use tokio_core::reactor::Handle; +use tokio_timer::Timer; -lazy_static! { +#[derive(Debug)] +pub enum RateLimitError { + /// When the decoding of a header could not be properly decoded as UTF-8. + DecodingUtf8, + /// When the decoding of a header could not be properly decoded as an `i64`. + DecodingInteger, +} + +impl Display for RateLimitError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_str(self.description()) + } +} + +impl StdError for RateLimitError { + fn description(&self) -> &str { + use self::RateLimitError::*; + + match *self { + DecodingInteger => "Error decoding a header into an i64", + DecodingUtf8 => "Error decoding a header from UTF-8", + } + } +} + +#[derive(Clone, Debug)] +pub enum RateLimit { + Global(i64), + NotReached(RateLimitHeaders), + Reached(i64), +} + +impl RateLimit { + fn from_headers(headers: &Headers) -> Result<Self> { + if headers.get_raw("x-ratelimit-global").is_some() { + if let Some(retry_after) = parse_header(headers, "retry-after")? { + debug!("Global ratelimited for {}ms", retry_after); + + return Ok(RateLimit::Global(retry_after)); + } + + warn!("Globally ratelimited with no retry-after? Skipping..."); + } + + if let Some(retry_after) = parse_header(headers, "retry-after")? { + return Ok(RateLimit::Reached(retry_after)); + } + + let limit = parse_header(headers, "x-ratelimit-limit")?.map(|x| x as u8); + let remaining = parse_header(headers, "x-ratelimit-remaining")?.map(|x| x as u8); + let reset = parse_header(headers, "x-ratelimit-remaining")?; + + Ok(RateLimit::NotReached(RateLimitHeaders { + limit, + remaining, + reset, + })) + } +} + +#[derive(Clone, Debug)] +pub struct RateLimitHeaders { + pub limit: Option<u8>, + pub remaining: Option<u8>, + pub reset: Option<i64>, +} + +/// A set of data containing information about the ratelimits for a particular +/// [`Route`], which is stored in the [`ROUTES`] mutex. +/// +/// See the [Discord docs] on ratelimits for more information. +/// +/// **Note**: You should _not_ mutate any of the fields, as this can cause 429s. +/// +/// [`ROUTES`]: struct.ROUTES.html +/// [`Route`]: enum.Route.html +/// [Discord docs]: https://discordapp.com/developers/docs/topics/rate-limits +// todo: impl Debug +#[derive(Debug)] +pub struct Bucket { + /// The total number of requests that can be made in a period of time. + pub limit: i64, + /// A queue of requests that were held back due to a pre-emptive ratelimit. + pub queue: VecDeque<Sender<()>>, + /// The number of requests remaining in the period of time. + pub remaining: i64, + /// When the interval resets and the [`limit`] resets to the value of + /// [`remaining`]. + /// + /// [`limit`]: #structfield.limit + /// [`remaining`]: #structfield.remaining + pub reset: i64, + /// Whether the bucket has a timeout in the background to release (part of) + /// the queue. + pub timeout: bool, +} + +impl Default for Bucket { + fn default() -> Self { + Self { + limit: i64::MAX, + queue: VecDeque::new(), + remaining: i64::MAX, + reset: i64::MAX, + timeout: false, + } + } +} + +impl Bucket { + fn take(&mut self) -> Option<Receiver<()>> { + if self.reset == 0 { + let (tx, rx) = oneshot::channel(); + + self.queue.push_back(tx); + + Some(rx) + } else { + None + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct Global { + pub blocked: bool, + pub queue: Rc<RefCell<VecDeque<Receiver<()>>>>, +} + +#[derive(Clone, Debug)] +pub struct RateLimiter { /// The global mutex is a mutex unlocked and then immediately re-locked /// prior to every request, to abide by Discord's global ratelimit. /// @@ -67,7 +202,9 @@ lazy_static! { /// The only reason that you would need to use the global mutex is to /// block requests yourself. This has the side-effect of potentially /// blocking many of your event handlers or framework commands. - pub static ref GLOBAL: Arc<Mutex<()>> = Arc::new(Mutex::new(())); + pub global: Rc<RefCell<Global>>, + /// A handle to the core that this is running on. + pub handle: Handle, /// The routes mutex is a HashMap of each [`Route`] and their respective /// ratelimit information. /// @@ -88,264 +225,121 @@ lazy_static! { /// /// [`RateLimit`]: struct.RateLimit.html /// [`Route`]: enum.Route.html - pub static ref ROUTES: Arc<Mutex<HashMap<Route, Arc<Mutex<RateLimit>>>>> = { - Arc::new(Mutex::new(HashMap::default())) - }; + pub routes: Rc<RefCell<HashMap<Path, Bucket>>>, } -/// A representation of all routes registered within the library. These are safe -/// and memory-efficient representations of each path that functions exist for -/// in the [`http`] module. -/// -/// [`http`]: ../index.html -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Route { - /// Route for the `/channels/:channel_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsId(u64), - /// Route for the `/channels/:channel_id/invites` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdInvites(u64), - /// Route for the `/channels/:channel_id/messages` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessages(u64), - /// Route for the `/channels/:channel_id/messages/bulk-delete` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesBulkDelete(u64), - /// Route for the `/channels/:channel_id/messages/:message_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - // This route is a unique case. The ratelimit for message _deletions_ is - // different than the overall route ratelimit. - // - // Refer to the docs on [Rate Limits] in the yellow warning section. - // - // Additionally, this needs to be a `LightMethod` from the parent module - // and _not_ a `hyper` `Method` due to `hyper`'s not deriving `Copy`. - // - // [Rate Limits]: https://discordapp.com/developers/docs/topics/rate-limits - ChannelsIdMessagesId(LightMethod, u64), - /// Route for the `/channels/:channel_id/messages/:message_id/ack` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesIdAck(u64), - /// Route for the `/channels/:channel_id/messages/:message_id/reactions` - /// path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesIdReactions(u64), - /// Route for the - /// `/channels/:channel_id/messages/:message_id/reactions/:reaction/@me` - /// path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesIdReactionsUserIdType(u64), - /// Route for the `/channels/:channel_id/permissions/:target_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdPermissionsOverwriteId(u64), - /// Route for the `/channels/:channel_id/pins` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdPins(u64), - /// Route for the `/channels/:channel_id/pins/:message_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdPinsMessageId(u64), - /// Route for the `/channels/:channel_id/typing` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdTyping(u64), - /// Route for the `/channels/:channel_id/webhooks` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdWebhooks(u64), - /// Route for the `/gateway` path. - Gateway, - /// Route for the `/gateway/bot` path. - GatewayBot, - /// Route for the `/guilds` path. - Guilds, - /// Route for the `/guilds/:guild_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsId(u64), - /// Route for the `/guilds/:guild_id/bans` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdBans(u64), - /// Route for the `/guilds/:guild_id/audit-logs` path. - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdAuditLogs(u64), - /// Route for the `/guilds/:guild_id/bans/:user_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdBansUserId(u64), - /// Route for the `/guilds/:guild_id/channels/:channel_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdChannels(u64), - /// Route for the `/guilds/:guild_id/embed` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdEmbed(u64), - /// Route for the `/guilds/:guild_id/emojis` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdEmojis(u64), - /// Route for the `/guilds/:guild_id/emojis/:emoji_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdEmojisId(u64), - /// Route for the `/guilds/:guild_id/integrations` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdIntegrations(u64), - /// Route for the `/guilds/:guild_id/integrations/:integration_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdIntegrationsId(u64), - /// Route for the `/guilds/:guild_id/integrations/:integration_id/sync` - /// path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdIntegrationsIdSync(u64), - /// Route for the `/guilds/:guild_id/invites` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdInvites(u64), - /// Route for the `/guilds/:guild_id/members` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembers(u64), - /// Route for the `/guilds/:guild_id/members/:user_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembersId(u64), - /// Route for the `/guilds/:guild_id/members/:user_id/roles/:role_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembersIdRolesId(u64), - /// Route for the `/guilds/:guild_id/members/@me/nick` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembersMeNick(u64), - /// Route for the `/guilds/:guild_id/prune` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdPrune(u64), - /// Route for the `/guilds/:guild_id/regions` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdRegions(u64), - /// Route for the `/guilds/:guild_id/roles` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdRoles(u64), - /// Route for the `/guilds/:guild_id/roles/:role_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdRolesId(u64), - /// Route for the `/guilds/:guild_id/webhooks` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdWebhooks(u64), - /// Route for the `/invites/:code` path. - InvitesCode, - /// Route for the `/users/:user_id` path. - UsersId, - /// Route for the `/users/@me` path. - UsersMe, - /// Route for the `/users/@me/channels` path. - UsersMeChannels, - /// Route for the `/users/@me/guilds` path. - UsersMeGuilds, - /// Route for the `/users/@me/guilds/:guild_id` path. - UsersMeGuildsId, - /// Route for the `/voice/regions` path. - VoiceRegions, - /// Route for the `/webhooks/:webhook_id` path. - 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. - None, +impl RateLimiter { + pub fn new(handle: Handle) -> Self { + Self { + global: Rc::new(RefCell::new(Global::default())), + routes: Rc::new(RefCell::new(HashMap::new())), + handle, + } + } + + pub fn take(&mut self, route: &Path) + -> Box<Future<Item = (), Error = Error>> { + // TODO: handle global + let mut routes = self.routes.borrow_mut(); + let bucket = routes.entry(*route).or_insert_with(Default::default); + let take = bucket.take(); + + match take { + Some(rx) => { + if !bucket.timeout { + let reset_ms = (bucket.reset * 1000) as u64; + let now = Utc::now(); + let now_millis = now.timestamp_subsec_millis() as i64; + let now_ms = (now.timestamp() * 1000) + now_millis; + let wait_ms = reset_ms.saturating_sub(now_ms as u64); + let duration = Duration::from_millis(wait_ms as u64); + + let done = Timer::default() + .sleep(duration) + .map(|_| { + () + }).map_err(|why| { + warn!("Err with pre-ratelimit sleep: {:?}", why); + + () + }); + + self.handle.spawn(done); + } + + Box::new(rx.from_err()) + }, + None => Box::new(future::ok(())), + } + } + + pub fn handle<'a>(&'a mut self, route: &'a Path, response: &'a Response) + -> Result<Option<Box<Future<Item = (), Error = ()>>>> { + let mut routes = self.routes.borrow_mut(); + let bucket = routes.entry(*route).or_insert_with(Default::default); + + if response.status() != StatusCode::TooManyRequests { + return Ok(None); + } + + Ok(match RateLimit::from_headers(&response.headers())? { + RateLimit::Global(millis) => { + debug!("Globally ratelimited for {:?}ms", millis); + + self.global.borrow_mut().blocked = true; + let global = Rc::clone(&self.global); + + let done = Timer::default() + .sleep(Duration::from_millis(millis as u64)) + .map(move |_| { + let mut global = global.borrow_mut(); + global.blocked = false; + }) + .map_err(|why| { + warn!("Err with global ratelimit timer: {:?}", why); + + () + }); + + Some(Box::new(done)) + }, + RateLimit::NotReached(headers) => { + let RateLimitHeaders { limit, remaining, reset } = headers; + + if let Some(reset) = reset { + if reset != bucket.reset { + bucket.reset = reset; + + if let Some(limit) = limit { + bucket.limit = limit as i64; + } + + if let Some(remaining) = remaining { + bucket.remaining = remaining as i64; + } + } + } + + None + }, + RateLimit::Reached(millis) => { + debug!("Ratelimited on route {:?} for {:?}ms", route, millis); + + let done = Timer::default() + .sleep(Duration::from_millis(millis as u64)) + .map_err(|why| { + warn!("Err with ratelimited timer: {:?}", why); + + () + }); + + Some(Box::new(done)) + }, + }) + } } +/* pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> where F: Fn() -> RequestBuilder<'a> { loop { @@ -500,15 +494,25 @@ impl RateLimit { }) } } +*/ -fn parse_header(headers: &Headers, header: &str) -> Result<Option<i64>> { - headers.get_raw(header).map_or(Ok(None), |header| { +fn parse_header(headers: &Headers, header_raw: &str) -> Result<Option<i64>> { + headers.get_raw(header_raw).map_or(Ok(None), |header| { str::from_utf8(&header[0]) - .map_err(|_| Error::Http(HttpError::RateLimitUtf8)) + .map_err(|why| { + warn!("Error parsing {} as utf8: {:?}", header_raw, why); + + RateLimitError::DecodingUtf8 + }) .and_then(|v| { v.parse::<i64>() .map(Some) - .map_err(|_| Error::Http(HttpError::RateLimitI64)) + .map_err(|why| { + warn!("Error parsing {}: {:?} to i64", header_raw, why); + + RateLimitError::DecodingInteger + }) }) + .map_err(From::from) }) } diff --git a/src/http/routing.rs b/src/http/routing.rs new file mode 100644 index 0000000..780d21c --- /dev/null +++ b/src/http/routing.rs @@ -0,0 +1,1411 @@ +use std::borrow::Cow; +use std::fmt::{Display, Formatter, Result as FmtResult, Write}; +use super::LightMethod; + +/// A representation of all path registered within the library. These are safe +/// and memory-efficient representations of each path that request functions +/// exist for. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Path { + /// Route for the `/channels/:channel_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsId(u64), + /// Route for the `/channels/:channel_id/invites` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdInvites(u64), + /// Route for the `/channels/:channel_id/messages` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessages(u64), + /// Route for the `/channels/:channel_id/messages/bulk-delete` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesBulkDelete(u64), + /// Route for the `/channels/:channel_id/messages/:message_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + // This route is a unique case. The ratelimit for message _deletions_ is + // different than the overall route ratelimit. + // + // Refer to the docs on [Rate Limits] in the yellow warning section. + // + // Additionally, this needs to be a `LightMethod` from the parent module + // and _not_ a `hyper` `Method` due to `hyper`'s not deriving `Copy`. + // + // [Rate Limits]: https://discordapp.com/developers/docs/topics/rate-limits + ChannelsIdMessagesId(LightMethod, u64), + /// Route for the `/channels/:channel_id/messages/:message_id/ack` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesIdAck(u64), + /// Route for the `/channels/:channel_id/messages/:message_id/reactions` + /// path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesIdReactions(u64), + /// Route for the + /// `/channels/:channel_id/messages/:message_id/reactions/:reaction/@me` + /// path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesIdReactionsUserIdType(u64), + /// Route for the `/channels/:channel_id/permissions/:target_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdPermissionsOverwriteId(u64), + /// Route for the `/channels/:channel_id/pins` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdPins(u64), + /// Route for the `/channels/:channel_id/pins/:message_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdPinsMessageId(u64), + /// Route for the `/channels/:channel_id/recipients/:user_id` path. + /// + /// The data is the relevant `ChannelId`. + ChannelsIdRecipientsId(u64), + /// Route for the `/channels/:channel_id/typing` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdTyping(u64), + /// Route for the `/channels/:channel_id/webhooks` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdWebhooks(u64), + /// Route for the `/gateway` path. + Gateway, + /// Route for the `/gateway/bot` path. + GatewayBot, + /// Route for the `/guilds` path. + Guilds, + /// Route for the `/guilds/:guild_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsId(u64), + /// Route for the `/guilds/:guild_id/bans` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdBans(u64), + /// Route for the `/guilds/:guild_id/audit-logs` path. + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdAuditLogs(u64), + /// Route for the `/guilds/:guild_id/bans/:user_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdBansUserId(u64), + /// Route for the `/guilds/:guild_id/channels/:channel_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdChannels(u64), + /// Route for the `/guilds/:guild_id/embed` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdEmbed(u64), + /// Route for the `/guilds/:guild_id/emojis` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdEmojis(u64), + /// Route for the `/guilds/:guild_id/emojis/:emoji_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdEmojisId(u64), + /// Route for the `/guilds/:guild_id/integrations` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdIntegrations(u64), + /// Route for the `/guilds/:guild_id/integrations/:integration_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdIntegrationsId(u64), + /// Route for the `/guilds/:guild_id/integrations/:integration_id/sync` + /// path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdIntegrationsIdSync(u64), + /// Route for the `/guilds/:guild_id/invites` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdInvites(u64), + /// Route for the `/guilds/:guild_id/members` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembers(u64), + /// Route for the `/guilds/:guild_id/members/:user_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembersId(u64), + /// Route for the `/guilds/:guild_id/members/:user_id/roles/:role_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembersIdRolesId(u64), + /// Route for the `/guilds/:guild_id/members/@me/nick` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembersMeNick(u64), + /// Route for the `/guilds/:guild_id/prune` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdPrune(u64), + /// Route for the `/guilds/:guild_id/regions` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdRegions(u64), + /// Route for the `/guilds/:guild_id/roles` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdRoles(u64), + /// Route for the `/guilds/:guild_id/roles/:role_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdRolesId(u64), + /// Route for the `/guilds/:guild_id/webhooks` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdWebhooks(u64), + /// Route for the `/invites/:code` path. + InvitesCode, + StatusMaintenancesActive, + /// Route for the `/users/:user_id` path. + UsersId, + /// Route for the `/users/@me` path. + UsersMe, + /// Route for the `/users/@me/channels` path. + UsersMeChannels, + /// Route for the `/users/@me/guilds` path. + UsersMeGuilds, + /// Route for the `/users/@me/guilds/:guild_id` path. + UsersMeGuildsId, + /// Route for the `/voice/regions` path. + VoiceRegions, + /// Route for the `/webhooks/:webhook_id` path. + WebhooksId(u64), +} + +impl Path { + pub fn channel(channel_id: u64) -> String { + format!(api!("/channels/{}"), channel_id) + } + + pub fn channel_invites(channel_id: u64) -> String { + format!(api!("/channels/{}/invites"), channel_id) + } + + pub fn channel_message(channel_id: u64, message_id: u64) -> String { + format!(api!("/channels/{}/messages/{}"), channel_id, message_id) + } + + pub fn channel_message_reaction<D, T>( + channel_id: u64, + message_id: u64, + user_id: D, + reaction_type: T + ) -> String where D: Display, T: Display { + format!( + api!("/channels/{}/messages/{}/reactions/{}/{}"), + channel_id, + message_id, + reaction_type, + user_id, + ) + } + + pub fn channel_message_reactions( + channel_id: u64, + message_id: u64, + reaction: &str, + limit: u8, + after: Option<u64>, + ) -> String { + let mut uri = format!( + api!("/channels/{}/messages/{}/reactions/{}?limit={}"), + channel_id, + message_id, + reaction, + limit, + ); + + if let Some(after) = after { + let _ = write!(uri, "&after={}", after); + } + + uri + } + + pub fn channel_messages(channel_id: u64) -> String { + format!(api!("/channels/{}/messages"), channel_id) + } + + pub fn channel_messages_bulk_delete(channel_id: u64) -> String { + format!(api!("/channels/{}/messages/bulk-delete"), channel_id) + } + + pub fn channel_permission(channel_id: u64, target_id: u64) -> String { + format!(api!("/channels/{}/permissions/{}"), channel_id, target_id) + } + + pub fn channel_pin(channel_id: u64, message_id: u64) -> String { + format!(api!("/channels/{}/pins/{}"), channel_id, message_id) + } + + pub fn channel_pins(channel_id: u64) -> String { + format!(api!("/channels/{}/pins"), channel_id) + } + + pub fn channel_typing(channel_id: u64) -> String { + format!(api!("/channels/{}/typing"), channel_id) + } + + pub fn channel_webhooks(channel_id: u64) -> String { + format!(api!("/channels/{}/webhooks"), channel_id) + } + + pub fn gateway() -> &'static str { + api!("/gateway") + } + + pub fn gateway_bot() -> &'static str { + api!("/gateway/bot") + } + + pub fn group_recipient(group_id: u64, user_id: u64) -> String { + format!(api!("/channels/{}/recipients/{}"), group_id, user_id) + } + + pub fn guild(guild_id: u64) -> String { + format!(api!("/guilds/{}"), guild_id) + } + + pub fn guild_audit_logs( + guild_id: u64, + action_type: Option<u8>, + user_id: Option<u64>, + before: Option<u64>, + limit: Option<u8>, + ) -> String { + let mut s = format!( + api!("/guilds/{}/audit-logs?"), + guild_id, + ); + + if let Some(action_type) = action_type { + let _ = write!(s, "&action_type={}", action_type); + } + + if let Some(before) = before { + let _ = write!(s, "&before={}", before); + } + + if let Some(limit) = limit { + let _ = write!(s, "&limit={}", limit); + } + + if let Some(user_id) = user_id { + let _ = write!(s, "&user_id={}", user_id); + } + + s + } + + pub fn guild_ban(guild_id: u64, user_id: u64) -> String { + format!(api!("/guilds/{}/bans/{}"), guild_id, user_id) + } + + pub fn guild_ban_optioned( + guild_id: u64, + user_id: u64, + delete_message_days: u8, + reason: &str, + ) -> String { + format!( + api!("/guilds/{}/bans/{}?delete_message_days={}&reason={}"), + guild_id, + user_id, + delete_message_days, + reason, + ) + } + + pub fn guild_bans(guild_id: u64) -> String { + format!(api!("/guilds/{}/bans"), guild_id) + } + + pub fn guild_channels(guild_id: u64) -> String { + format!(api!("/guilds/{}/channels"), guild_id) + } + + pub fn guild_embed(guild_id: u64) -> String { + format!(api!("/guilds/{}/embed"), guild_id) + } + + pub fn guild_emojis(guild_id: u64) -> String { + format!(api!("/guilds/{}/emojis"), guild_id) + } + + pub fn guild_emoji(guild_id: u64, emoji_id: u64) -> String { + format!(api!("/guilds/{}/emojis/{}"), guild_id, emoji_id) + } + + pub fn guild_integration( + guild_id: u64, + integration_id: u64, + ) -> String { + format!(api!("/guilds/{}/integrations/{}"), guild_id, integration_id) + } + + pub fn guild_integration_sync( + guild_id: u64, + integration_id: u64, + ) -> String { + format!( + api!("/guilds/{}/integrations/{}/sync"), + guild_id, + integration_id, + ) + } + + pub fn guild_integrations(guild_id: u64) -> String { + format!(api!("/guilds/{}/integrations"), guild_id) + } + + pub fn guild_invites(guild_id: u64) -> String { + format!(api!("/guilds/{}/invites"), guild_id) + } + + pub fn guild_member(guild_id: u64, user_id: u64) -> String { + format!(api!("/guilds/{}/members/{}"), guild_id, user_id) + } + + pub fn guild_member_role( + guild_id: u64, + user_id: u64, + role_id: u64, + ) -> String { + format!( + api!("/guilds/{}/members/{}/roles/{}"), + guild_id, + user_id, + role_id, + ) + } + + pub fn guild_members(guild_id: u64) -> String { + format!(api!("/guilds/{}/members"), guild_id) + } + + pub fn guild_members_optioned( + guild_id: u64, + after: Option<u64>, + limit: Option<u64>, + ) -> String { + let mut s = format!(api!("/guilds/{}/members?"), guild_id); + + if let Some(after) = after { + let _ = write!(s, "&after={}", after); + } + + if let Some(limit) = limit { + let _ = write!(s, "&limit={}", limit); + } + + s + } + + pub fn guild_nickname(guild_id: u64) -> String { + format!(api!("/guilds/{}/members/@me/nick"), guild_id) + } + + pub fn guild_prune(guild_id: u64, days: u64) -> String { + format!(api!("/guilds/{}/prune?days={}"), guild_id, days) + } + + pub fn guild_regions(guild_id: u64) -> String { + format!(api!("/guilds/{}/regions"), guild_id) + } + + pub fn guild_role(guild_id: u64, role_id: u64) -> String { + format!(api!("/guilds/{}/roles/{}"), guild_id, role_id) + } + + pub fn guild_roles(guild_id: u64) -> String { + format!(api!("/guilds/{}/roles"), guild_id) + } + + pub fn guild_webhooks(guild_id: u64) -> String { + format!(api!("/guilds/{}/webhooks"), guild_id) + } + + pub fn guilds() -> &'static str { + api!("/guilds") + } + + pub fn invite(code: &str) -> String { + format!(api!("/invites/{}"), code) + } + + pub fn invite_optioned(code: &str, stats: bool) -> String { + format!(api!("/invites/{}?with_counts={}"), code, stats) + } + + pub fn oauth2_application_current() -> &'static str { + api!("/oauth2/applications/@me") + } + + pub fn private_channel() -> &'static str { + api!("/users/@me/channels") + } + + pub fn status_incidents_unresolved() -> &'static str { + status!("/incidents/unresolved.json") + } + + pub fn status_maintenances_active() -> &'static str { + status!("/scheduled-maintenances/active.json") + } + + pub fn status_maintenances_upcoming() -> &'static str { + status!("/scheduled-maintenances/upcoming.json") + } + + pub fn user<D: Display>(target: D) -> String { + format!(api!("/users/{}"), target) + } + + pub fn user_dm_channels<D: Display>(target: D) -> String { + format!(api!("/users/{}/channels"), target) + } + + pub fn user_guild<D: Display>(target: D, guild_id: u64) -> String { + format!(api!("/users/{}/guilds/{}"), target, guild_id) + } + + pub fn user_guilds<D: Display>(target: D) -> String { + format!(api!("/users/{}/guilds"), target) + } + + pub fn user_guilds_optioned<D: Display>( + target: D, + after: Option<u64>, + before: Option<u64>, + limit: u64, + ) -> String { + let mut s = format!(api!("/users/{}/guilds?limit={}&"), target, limit); + + if let Some(after) = after { + let _ = write!(s, "&after={}", after); + } + + if let Some(before) = before { + let _ = write!(s, "&before={}", before); + } + + s + } + + pub fn voice_regions() -> &'static str { + api!("/voice/regions") + } + + pub fn webhook(webhook_id: u64) -> String { + format!(api!("/webhooks/{}"), webhook_id) + } + + pub fn webhook_with_token<D>(webhook_id: u64, token: D) -> String + where D: Display { + format!(api!("/webhooks/{}/{}"), webhook_id, token) + } + + pub fn webhook_with_token_optioned<D>(webhook_id: u64, token: D, wait: bool) + -> String where D: Display { + format!(api!("/webhooks/{}/{}?wait={}"), webhook_id, token, wait) + } + + /// Returns the path's base string without replacements. + pub fn base(&self) -> &str { + use self::Path::*; + + match *self { + ChannelsId(_) => "/channels/{}", + ChannelsIdInvites(_) => "/channels/{}/invites", + ChannelsIdMessages(_) => "/channels/{}/messages", + ChannelsIdMessagesBulkDelete(_) => "/channels/{}/messages/bulk-delete", + ChannelsIdMessagesId(_, _) => "/channels/{}/messages/{}", + ChannelsIdMessagesIdAck(_) => "/channels/{}/messages/ack", + ChannelsIdMessagesIdReactions(_) => "/channels/{}/messages/{}/reactions", + ChannelsIdMessagesIdReactionsUserIdType(_) => "/channels/{}/messages/{}/reactions/{}/@me", + ChannelsIdPermissionsOverwriteId(_) => "/channels/{}/permissions/{}", + ChannelsIdPins(_) => "/channels/{}/pins", + ChannelsIdPinsMessageId(_) => "/channels/{}/pins/{}", + ChannelsIdRecipientsId(_) => "/channels/{}/recipients/{}", + ChannelsIdTyping(_) => "/channels/{}/typing", + ChannelsIdWebhooks(_) => "/channels/{}/webhooks", + Gateway => "/gateway", + GatewayBot => "/gateway/bot", + Guilds => "/guilds", + GuildsId(_) => "/guilds/{}", + GuildsIdBans(_) => "/guilds/{}/bans", + GuildsIdAuditLogs(_) => "/guilds/{}/audit-logs", + GuildsIdBansUserId(_) => "/guilds/{}/bans/{}", + GuildsIdChannels(_) => "/guilds/{}/channels", + GuildsIdEmbed(_) => "/guilds/{}/embed", + GuildsIdEmojis(_) => "/guilds/{}/emojis", + GuildsIdEmojisId(_) => "/guilds/{}/emojis/{}", + GuildsIdIntegrations(_) => "/guilds/{}/integrations", + GuildsIdIntegrationsId(_) => "/guilds/{}/integrations/{}", + GuildsIdIntegrationsIdSync(_) => "/guilds/{}/integrations/{}/sync", + GuildsIdInvites(_) => "/guilds/{}/invites", + GuildsIdMembers(_) => "/guilds/{}/membes", + GuildsIdMembersId(_) => "/guilds/{}/members/{}", + GuildsIdMembersIdRolesId(_) => "/guilds/{}/members/{}/roles/{}", + GuildsIdMembersMeNick(_) => "/guilds/{}/members/@me/nick", + GuildsIdPrune(_) => "/guilds/{}/prune", + GuildsIdRegions(_) => "/guilds/{}/regions", + GuildsIdRoles(_) => "/guilds/{}/roles", + GuildsIdRolesId(_) => "/guilds/{}/roles/{}", + GuildsIdWebhooks(_) => "/guilds/{}/webhooks", + InvitesCode => "/invites/{}", + StatusMaintenancesActive => "/scheduled-maintenances/active.json", + UsersId => "/users/{}", + UsersMe => "/users/@me", + UsersMeChannels => "/users/@me/channels", + UsersMeGuilds => "/users/@me/guilds", + UsersMeGuildsId => "/users/@me/guilds/{}", + VoiceRegions => "/voice/regions", + WebhooksId(_) => "/webhooks/{}", + } + } +} + +impl Display for Path { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_str(self.base()) + } +} + +#[derive(Clone)] +pub enum Route<'a> { + AddGroupRecipient { + group_id: u64, + user_id: u64, + }, + AddMemberRole { + guild_id: u64, + role_id: u64, + user_id: u64, + }, + GuildBanUser { + guild_id: u64, + user_id: u64, + delete_message_days: Option<u8>, + reason: Option<&'a str>, + }, + BroadcastTyping { + channel_id: u64, + }, + CreateChannel { + guild_id: u64, + }, + CreateEmoji { + guild_id: u64, + }, + CreateGuild, + CreateGuildIntegration { + guild_id: u64, + integration_id: u64, + }, + CreateInvite { + channel_id: u64, + }, + CreateMessage { + channel_id: u64, + }, + CreatePermission { + channel_id: u64, + target_id: u64, + }, + CreatePrivateChannel, + CreateReaction { + channel_id: u64, + message_id: u64, + reaction: &'a str, + }, + CreateRole { + guild_id: u64, + }, + CreateWebhook { + channel_id: u64, + }, + DeleteChannel { + channel_id: u64, + }, + DeleteEmoji { + guild_id: u64, + emoji_id: u64, + }, + DeleteGuild { + guild_id: u64, + }, + DeleteGuildIntegration { + guild_id: u64, + integration_id: u64, + }, + DeleteInvite { + code: &'a str, + }, + DeleteMessage { + channel_id: u64, + message_id: u64, + }, + DeleteMessages { + channel_id: u64, + }, + DeleteMessageReactions { + channel_id: u64, + message_id: u64, + }, + DeletePermission { + channel_id: u64, + target_id: u64, + }, + DeleteReaction { + channel_id: u64, + message_id: u64, + user: &'a str, + reaction: &'a str, + }, + DeleteRole { + guild_id: u64, + role_id: u64, + }, + DeleteWebhook { + webhook_id: u64, + }, + DeleteWebhookWithToken { + token: &'a str, + webhook_id: u64, + }, + EditChannel { + channel_id: u64, + }, + EditEmoji { + guild_id: u64, + emoji_id: u64, + }, + EditGuild { + guild_id: u64, + }, + EditGuildChannels { + guild_id: u64, + }, + EditGuildEmbed { + guild_id: u64, + }, + EditMember { + guild_id: u64, + user_id: u64, + }, + EditMessage { + channel_id: u64, + message_id: u64, + }, + EditNickname { + guild_id: u64, + }, + EditProfile, + EditRole { + guild_id: u64, + role_id: u64, + }, + EditWebhook { + webhook_id: u64, + }, + EditWebhookWithToken { + token: &'a str, + webhook_id: u64, + }, + ExecuteWebhook { + token: &'a str, + wait: bool, + webhook_id: u64, + }, + GetActiveMaintenance, + GetAuditLogs { + action_type: Option<u8>, + before: Option<u64>, + guild_id: u64, + limit: Option<u8>, + user_id: Option<u64>, + }, + GetBans { + guild_id: u64, + }, + GetBotGateway, + GetChannel { + channel_id: u64, + }, + GetChannelInvites { + channel_id: u64, + }, + GetChannelWebhooks { + channel_id: u64, + }, + GetChannels { + guild_id: u64, + }, + GetCurrentApplicationInfo, + GetCurrentUser, + GetGateway, + GetGuild { + guild_id: u64, + }, + GetGuildEmbed { + guild_id: u64, + }, + GetGuildIntegrations { + guild_id: u64, + }, + GetGuildInvites { + guild_id: u64, + }, + GetGuildMembers { + after: Option<u64>, + limit: Option<u64>, + guild_id: u64, + }, + GetGuildPruneCount { + days: u64, + guild_id: u64, + }, + GetGuildRegions { + guild_id: u64, + }, + GetGuildRoles { + guild_id: u64, + }, + GetGuildWebhooks { + guild_id: u64, + }, + GetGuilds { + after: Option<u64>, + before: Option<u64>, + limit: u64, + }, + GetInvite { + code: &'a str, + stats: bool, + }, + GetMember { + guild_id: u64, + user_id: u64, + }, + GetMessage { + channel_id: u64, + message_id: u64, + }, + GetMessages { + channel_id: u64, + query: String, + }, + GetPins { + channel_id: u64, + }, + GetReactionUsers { + after: Option<u64>, + channel_id: u64, + limit: u8, + message_id: u64, + reaction: String, + }, + GetUnresolvedIncidents, + GetUpcomingMaintenances, + GetUser { + user_id: u64, + }, + GetUserDmChannels, + GetVoiceRegions, + GetWebhook { + webhook_id: u64, + }, + GetWebhookWithToken { + token: &'a str, + webhook_id: u64, + }, + KickMember { + guild_id: u64, + user_id: u64, + }, + LeaveGroup { + group_id: u64, + }, + LeaveGuild { + guild_id: u64, + }, + RemoveGroupRecipient { + group_id: u64, + user_id: u64, + }, + PinMessage { + channel_id: u64, + message_id: u64, + }, + RemoveBan { + guild_id: u64, + user_id: u64, + }, + RemoveMemberRole { + guild_id: u64, + role_id: u64, + user_id: u64, + }, + StartGuildPrune { + days: u64, + guild_id: u64, + }, + StartIntegrationSync { + guild_id: u64, + integration_id: u64, + }, + UnpinMessage { + channel_id: u64, + message_id: u64, + }, +} + +impl<'a> Route<'a> { + pub fn deconstruct(&self) -> (LightMethod, Path, Cow<str>) { + match *self { + Route::AddGroupRecipient { group_id, user_id } => ( + LightMethod::Post, + Path::ChannelsIdRecipientsId(group_id), + Cow::from(Path::group_recipient(group_id, user_id)), + ), + Route::AddMemberRole { guild_id, role_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdMembersIdRolesId(guild_id), + Cow::from(Path::guild_member_role(guild_id, user_id, role_id)), + ), + Route::GuildBanUser { + guild_id, + delete_message_days, + reason, + user_id, + } => ( + // TODO + LightMethod::Delete, + Path::GuildsIdBansUserId(guild_id), + Cow::from(Path::guild_ban_optioned( + guild_id, + user_id, + delete_message_days.unwrap_or(0), + reason.unwrap_or(""), + )), + ), + Route::BroadcastTyping { channel_id } => ( + LightMethod::Post, + Path::ChannelsIdTyping(channel_id), + Cow::from(Path::channel_typing(channel_id)), + ), + Route::CreateChannel { guild_id } => ( + LightMethod::Post, + Path::GuildsIdChannels(guild_id), + Cow::from(Path::guild_channels(guild_id)), + ), + Route::CreateEmoji { guild_id } => ( + LightMethod::Post, + Path::GuildsIdEmojis(guild_id), + Cow::from(Path::guild_emojis(guild_id)), + ), + Route::CreateGuild => ( + LightMethod::Post, + Path::Guilds, + Cow::from(Path::guilds()), + ), + Route::CreateGuildIntegration { guild_id, integration_id } => ( + LightMethod::Post, + Path::GuildsIdIntegrationsId(guild_id), + Cow::from(Path::guild_integration(guild_id, integration_id)), + ), + Route::CreateInvite { channel_id } => ( + LightMethod::Post, + Path::ChannelsIdInvites(channel_id), + Cow::from(Path::channel_invites(channel_id)), + ), + Route::CreateMessage { channel_id } => ( + LightMethod::Post, + Path::ChannelsIdMessages(channel_id), + Cow::from(Path::channel_messages(channel_id)), + ), + Route::CreatePermission { channel_id, target_id } => ( + LightMethod::Post, + Path::ChannelsIdPermissionsOverwriteId(channel_id), + Cow::from(Path::channel_permission(channel_id, target_id)), + ), + Route::CreatePrivateChannel => ( + LightMethod::Post, + Path::UsersMeChannels, + Cow::from(Path::user_dm_channels("@me")), + ), + Route::CreateReaction { channel_id, message_id, reaction } => ( + LightMethod::Put, + Path::ChannelsIdMessagesIdReactionsUserIdType(channel_id), + Cow::from(Path::channel_message_reaction( + channel_id, + message_id, + "@me", + reaction, + )), + ), + Route::CreateRole { guild_id } => ( + LightMethod::Delete, + Path::GuildsIdRoles(guild_id), + Cow::from(Path::guild_roles(guild_id)), + ), + Route::CreateWebhook { channel_id } => ( + LightMethod::Delete, + Path::ChannelsIdWebhooks(channel_id), + Cow::from(Path::channel_webhooks(channel_id)), + ), + Route::DeleteChannel { channel_id } => ( + LightMethod::Delete, + Path::ChannelsId(channel_id), + Cow::from(Path::channel(channel_id)), + ), + Route::DeleteEmoji { emoji_id, guild_id } => ( + LightMethod::Delete, + Path::GuildsIdEmojisId(guild_id), + Cow::from(Path::guild_emoji(guild_id, emoji_id)), + ), + Route::DeleteGuild { guild_id } => ( + LightMethod::Delete, + Path::GuildsId(guild_id), + Cow::from(Path::guild(guild_id)), + ), + Route::DeleteGuildIntegration { guild_id, integration_id } => ( + LightMethod::Delete, + Path::GuildsIdIntegrationsId(guild_id), + Cow::from(Path::guild_integration(guild_id, integration_id)), + ), + Route::DeleteInvite { code } => ( + LightMethod::Delete, + Path::InvitesCode, + Cow::from(Path::invite(code)), + ), + Route::DeleteMessage { channel_id, message_id } => ( + LightMethod::Delete, + Path::ChannelsIdMessagesId(LightMethod::Delete, message_id), + Cow::from(Path::channel_message(channel_id, message_id)), + ), + Route::DeleteMessages { channel_id } => ( + LightMethod::Delete, + Path::ChannelsIdMessagesBulkDelete(channel_id), + Cow::from(Path::channel_messages_bulk_delete(channel_id)), + ), + Route::DeletePermission { channel_id, target_id } => ( + LightMethod::Delete, + Path::ChannelsIdPermissionsOverwriteId(channel_id), + Cow::from(Path::channel_permission(channel_id, target_id)), + ), + Route::DeleteReaction { + channel_id, + message_id, + reaction, + user, + } => ( + LightMethod::Delete, + Path::ChannelsIdMessagesIdReactionsUserIdType(channel_id), + Cow::from(Path::channel_message_reaction( + channel_id, + message_id, + user, + reaction, + )) + ), + Route::DeleteRole { guild_id, role_id } => ( + LightMethod::Delete, + Path::GuildsIdRolesId(guild_id), + Cow::from(Path::guild_role(guild_id, role_id)), + ), + Route::DeleteWebhook { webhook_id } => ( + LightMethod::Delete, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook(webhook_id)), + ), + Route::DeleteWebhookWithToken { token, webhook_id } => ( + LightMethod::Delete, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token(webhook_id, token)), + ), + Route::EditChannel { channel_id } => ( + LightMethod::Patch, + Path::ChannelsId(channel_id), + Cow::from(Path::channel(channel_id)), + ), + Route::EditEmoji { emoji_id, guild_id } => ( + LightMethod::Patch, + Path::GuildsIdEmojisId(guild_id), + Cow::from(Path::guild_emoji(guild_id, emoji_id)), + ), + Route::EditGuild { guild_id } => ( + LightMethod::Patch, + Path::GuildsId(guild_id), + Cow::from(Path::guild(guild_id)), + ), + Route::EditGuildChannels { guild_id } => ( + LightMethod::Patch, + Path::GuildsIdChannels(guild_id), + Cow::from(Path::guild_channels(guild_id)), + ), + Route::EditGuildEmbed { guild_id } => ( + LightMethod::Patch, + Path::GuildsIdEmbed(guild_id), + Cow::from(Path::guild_embed(guild_id)), + ), + Route::EditMember { guild_id, user_id } => ( + LightMethod::Patch, + Path::GuildsIdMembersId(guild_id), + Cow::from(Path::guild_member(guild_id, user_id)), + ), + Route::EditMessage { channel_id, message_id } => ( + LightMethod::Patch, + Path::ChannelsIdMessagesId(LightMethod::Patch, channel_id), + Cow::from(Path::channel_message(channel_id, message_id)), + ), + Route::EditNickname { guild_id } => ( + LightMethod::Patch, + Path::GuildsIdMembersMeNick(guild_id), + Cow::from(Path::guild_nickname(guild_id)), + ), + Route::EditProfile => ( + LightMethod::Patch, + Path::UsersMe, + Cow::from(Path::user("@me")), + ), + Route::EditRole { guild_id, role_id } => ( + LightMethod::Patch, + Path::GuildsIdRolesId(guild_id), + Cow::from(Path::guild_role(guild_id, role_id)), + ), + Route::EditWebhook { webhook_id } => ( + LightMethod::Patch, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook(webhook_id)), + ), + Route::EditWebhookWithToken { token, webhook_id } => ( + LightMethod::Patch, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token(webhook_id, token)), + ), + Route::ExecuteWebhook { token, wait, webhook_id } => ( + LightMethod::Post, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token_optioned( + webhook_id, + token, + wait, + )), + ), + Route::GetActiveMaintenance => ( + LightMethod::Get, + Path::StatusMaintenancesActive, + Cow::from(Path::status_maintenances_active()), + ), + Route::GetAuditLogs { + action_type, + before, + guild_id, + limit, + user_id, + } => ( + LightMethod::Get, + Path::GuildsIdAuditLogs(guild_id), + Cow::from(Path::guild_audit_logs( + guild_id, + action_type, + user_id, + before, + limit, + )), + ), + Route::GetBans { guild_id } => ( + LightMethod::Get, + Path::GuildsIdBans(guild_id), + Cow::from(Path::guild_bans(guild_id)), + ), + Route::GetBotGateway => ( + LightMethod::Get, + Path::GatewayBot, + Cow::from(Path::gateway_bot()), + ), + Route::GetChannel { channel_id } => ( + LightMethod::Get, + Path::ChannelsId(channel_id), + Cow::from(Path::channel(channel_id)), + ), + Route::GetChannelInvites { channel_id } => ( + LightMethod::Get, + Path::ChannelsIdInvites(channel_id), + Cow::from(Path::channel_invites(channel_id)), + ), + Route::GetChannelWebhooks { channel_id } => ( + LightMethod::Get, + Path::ChannelsIdWebhooks(channel_id), + Cow::from(Path::channel_webhooks(channel_id)), + ), + Route::GetChannels { guild_id } => ( + LightMethod::Get, + Path::GuildsIdChannels(guild_id), + Cow::from(Path::guild_channels(guild_id)), + ), + Route::GetCurrentUser => ( + LightMethod::Get, + Path::UsersMe, + Cow::from(Path::user("@me")), + ), + Route::GetGateway => ( + LightMethod::Get, + Path::Gateway, + Cow::from(Path::gateway()), + ), + Route::GetGuild { guild_id } => ( + LightMethod::Get, + Path::GuildsId(guild_id), + Cow::from(Path::guild(guild_id)), + ), + Route::GetGuildEmbed { guild_id } => ( + LightMethod::Get, + Path::GuildsIdEmbed(guild_id), + Cow::from(Path::guild_embed(guild_id)), + ), + Route::GetGuildIntegrations { guild_id } => ( + LightMethod::Get, + Path::GuildsIdIntegrations(guild_id), + Cow::from(Path::guild_integrations(guild_id)), + ), + Route::GetGuildInvites { guild_id } => ( + LightMethod::Get, + Path::GuildsIdInvites(guild_id), + Cow::from(Path::guild_invites(guild_id)), + ), + Route::GetGuildMembers { after, guild_id, limit } => ( + LightMethod::Get, + Path::GuildsIdMembers(guild_id), + Cow::from(Path::guild_members_optioned(guild_id, after, limit)), + ), + Route::GetGuildPruneCount { days, guild_id } => ( + LightMethod::Get, + Path::GuildsIdPrune(guild_id), + Cow::from(Path::guild_prune(guild_id, days)), + ), + Route::GetGuildRegions { guild_id } => ( + LightMethod::Get, + Path::GuildsIdRegions(guild_id), + Cow::from(Path::guild_regions(guild_id)), + ), + Route::GetGuildRoles { guild_id } => ( + LightMethod::Get, + Path::GuildsIdRoles(guild_id), + Cow::from(Path::guild_roles(guild_id)), + ), + Route::GetGuildWebhooks { guild_id } => ( + LightMethod::Get, + Path::GuildsIdWebhooks(guild_id), + Cow::from(Path::guild_webhooks(guild_id)), + ), + Route::GetGuilds { after, before, limit } => ( + LightMethod::Get, + Path::UsersMeGuilds, + Cow::from(Path::user_guilds_optioned( + "@me", + after, + before, + limit, + )), + ), + Route::GetInvite { code, stats } => ( + LightMethod::Get, + Path::InvitesCode, + Cow::from(Path::invite_optioned(code, stats)), + ), + Route::GetMember { guild_id, user_id } => ( + LightMethod::Get, + Path::GuildsIdMembersId(guild_id), + Cow::from(Path::guild_member(guild_id, user_id)), + ), + Route::GetMessage { channel_id, message_id } => ( + LightMethod::Get, + Path::ChannelsIdMessagesId(LightMethod::Get, channel_id), + Cow::from(Path::channel_message(channel_id, message_id)), + ), + Route::GetPins { channel_id } => ( + LightMethod::Get, + Path::ChannelsIdPins(channel_id), + Cow::from(Path::channel_pins(channel_id)), + ), + Route::GetReactionUsers { + after, + channel_id, + limit, + message_id, + ref reaction, + } => ( + LightMethod::Get, + Path::ChannelsIdMessagesIdReactions(channel_id), + Cow::from(Path::channel_message_reactions( + channel_id, + message_id, + reaction, + limit, + after,)), + ), + Route::GetUser { user_id } => ( + LightMethod::Get, + Path::UsersId, + Cow::from(Path::user(user_id)), + ), + Route::GetUserDmChannels => ( + LightMethod::Get, + Path::UsersMeChannels, + Cow::from(Path::user_dm_channels("@me")), + ), + Route::GetVoiceRegions => ( + LightMethod::Get, + Path::VoiceRegions, + Cow::from(Path::voice_regions()), + ), + Route::GetWebhook { webhook_id } => ( + LightMethod::Get, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook(webhook_id)), + ), + Route::GetWebhookWithToken { token, webhook_id } => ( + LightMethod::Get, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token(webhook_id, token)), + ), + Route::KickMember { guild_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdMembersId(guild_id), + Cow::from(Path::guild_member(guild_id, user_id)), + ), + Route::LeaveGroup { group_id } => ( + LightMethod::Delete, + Path::ChannelsId(group_id), + Cow::from(Path::channel(group_id)), + ), + Route::LeaveGuild { guild_id } => ( + LightMethod::Delete, + Path::UsersMeGuildsId, + Cow::from(Path::user_guild("@me", guild_id)), + ), + Route::RemoveGroupRecipient { group_id, user_id } => ( + LightMethod::Delete, + Path::ChannelsIdRecipientsId(group_id), + Cow::from(Path::group_recipient(group_id, user_id)), + ), + Route::PinMessage { channel_id, message_id } => ( + LightMethod::Put, + Path::ChannelsIdPins(channel_id), + Cow::from(Path::channel_pin(channel_id, message_id)), + ), + Route::RemoveBan { guild_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdBansUserId(guild_id), + Cow::from(Path::guild_ban(guild_id, user_id)), + ), + Route::RemoveMemberRole { guild_id, role_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdMembersIdRolesId(guild_id), + Cow::from(Path::guild_member_role(guild_id, user_id, role_id)), + ), + Route::StartGuildPrune { days, guild_id } => ( + LightMethod::Post, + Path::GuildsIdPrune(guild_id), + Cow::from(Path::guild_prune(guild_id, days)), + ), + Route::StartIntegrationSync { guild_id, integration_id } => ( + LightMethod::Post, + Path::GuildsIdIntegrationsId(guild_id), + Cow::from(Path::guild_integration_sync( + guild_id, + integration_id, + )), + ), + Route::UnpinMessage { channel_id, message_id } => ( + LightMethod::Delete, + Path::ChannelsIdPinsMessageId(channel_id), + Cow::from(Path::channel_pin(channel_id, message_id)), + ), + _ => unreachable!(), // TODO: finish 5 unconvered variants + } + } +} diff --git a/src/http/utils.rs b/src/http/utils.rs new file mode 100644 index 0000000..017fe0e --- /dev/null +++ b/src/http/utils.rs @@ -0,0 +1,12 @@ +use model::channel::ReactionType; + +pub fn reaction_type_data(reaction_type: &ReactionType) -> String { + match *reaction_type { + ReactionType::Custom { + id, + ref name, + .. + } => format!("{}:{}", name.as_ref().map_or("", |s| s.as_str()), id), + ReactionType::Unicode(ref unicode) => unicode.clone(), + } +} diff --git a/src/internal/macros.rs b/src/internal/macros.rs index 875326a..39808db 100644 --- a/src/internal/macros.rs +++ b/src/internal/macros.rs @@ -1,48 +1,5 @@ //! A set of macros for easily working with internals. -#[cfg(feature = "http")] -macro_rules! request { - ($route:expr, $method:ident($body:expr), $url:expr, $($rest:tt)*) => {{ - let client = request_client!(); - - request($route, || client - .$method(&format!(api!($url), $($rest)*)) - .body(&$body))? - }}; - ($route:expr, $method:ident($body:expr), $url:expr) => {{ - let client = request_client!(); - - request($route, || client - .$method(api!($url)) - .body(&$body))? - }}; - ($route:expr, $method:ident, $url:expr, $($rest:tt)*) => {{ - let client = request_client!(); - - request($route, || client - .$method(&format!(api!($url), $($rest)*)))? - }}; - ($route:expr, $method:ident, $url:expr) => {{ - let client = request_client!(); - - request($route, || client - .$method(api!($url)))? - }}; -} - -#[cfg(feature = "http")] -macro_rules! request_client { - () => {{ - use hyper::net::HttpsConnector; - use hyper_native_tls::NativeTlsClient; - - let tc = NativeTlsClient::new()?; - let connector = HttpsConnector::new(tc); - - HyperClient::with_connector(connector) - }} -} - #[cfg(any(feature = "model", feature = "utils"))] macro_rules! cdn { ($e:expr) => { @@ -53,23 +10,6 @@ macro_rules! cdn { }; } -#[cfg(feature = "http")] -macro_rules! api { - ($e:expr) => { - concat!("https://discordapp.com/api/v6", $e) - }; - ($e:expr, $($rest:tt)*) => { - format!(api!($e), $($rest)*) - }; -} - -#[cfg(feature = "http")] -macro_rules! status { - ($e:expr) => { - concat!("https://status.discordapp.com/api/v2", $e) - } -} - // Enable/disable check for cache #[cfg(any(all(feature = "cache", any(feature = "client", feature = "model"))))] macro_rules! feature_cache { @@ -89,11 +29,11 @@ macro_rules! feature_cache { } } -#[cfg(all(feature = "client", not(feature = "framework")))] -macro_rules! feature_framework { - ($enabled:block else $disabled:block) => { - { - $disabled +macro_rules! ftry { + ($code:expr) => { + match $code { + Ok(v) => v, + Err(why) => return Box::new(::futures::future::err(From::from(why))), } } } diff --git a/src/internal/mod.rs b/src/internal/mod.rs index a0c8142..8943cd2 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -3,13 +3,6 @@ pub mod macros; pub mod prelude; -mod rwlock_ext; - -pub use self::rwlock_ext::RwLockExt; - -#[cfg(feature = "gateway")] -pub mod ws_impl; - #[cfg(feature = "voice")] mod timer; diff --git a/src/internal/rwlock_ext.rs b/src/internal/rwlock_ext.rs deleted file mode 100644 index 6235370..0000000 --- a/src/internal/rwlock_ext.rs +++ /dev/null @@ -1,31 +0,0 @@ -use parking_lot::RwLock as ParkingLotRwLock; -use std::sync::{Arc, RwLock}; - -pub trait RwLockExt<T> { - fn with<Y, F: Fn(&T) -> Y>(&self, f: F) -> Y; - fn with_mut<Y, F: FnMut(&mut T) -> Y>(&self, f: F) -> Y; -} - -impl<T> RwLockExt<T> for Arc<RwLock<T>> { - fn with<Y, F: Fn(&T) -> Y>(&self, f: F) -> Y { - let r = self.read().unwrap(); - f(&r) - } - - fn with_mut<Y, F: FnMut(&mut T) -> Y>(&self, mut f: F) -> Y { - let mut w = self.write().unwrap(); - 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) - } -} diff --git a/src/internal/timer.rs b/src/internal/timer.rs deleted file mode 100644 index fa2fcc2..0000000 --- a/src/internal/timer.rs +++ /dev/null @@ -1,51 +0,0 @@ -use chrono::{DateTime, Duration, Utc}; -use std::time::Duration as StdDuration; -use std::thread; - -#[derive(Debug)] -pub struct Timer { - due: DateTime<Utc>, - duration: Duration, -} - -impl Timer { - pub fn new(duration_in_ms: u64) -> Timer { - let duration = Duration::milliseconds(duration_in_ms as i64); - - Timer { - due: Utc::now() + duration, - duration: duration, - } - } - - pub fn await(&mut self) { - let due_time = (self.due.timestamp() * 1000) + i64::from(self.due.timestamp_subsec_millis()); - let now_time = { - let now = Utc::now(); - - (now.timestamp() * 1000) + i64::from(now.timestamp_subsec_millis()) - }; - - if due_time > now_time { - let sleep_time = due_time - now_time; - - if sleep_time > 0 { - thread::sleep(StdDuration::from_millis(sleep_time as u64)); - } - } - - self.due = self.due + self.duration; - } - - pub fn check(&mut self) -> bool { - if Utc::now() >= self.due { - self.due = self.due + self.duration; - - true - } else { - false - } - } - - pub fn reset(&mut self) { self.due = Utc::now() + self.duration; } -} diff --git a/src/internal/ws_impl.rs b/src/internal/ws_impl.rs deleted file mode 100644 index 8988e06..0000000 --- a/src/internal/ws_impl.rs +++ /dev/null @@ -1,45 +0,0 @@ -use flate2::read::ZlibDecoder; -use gateway::GatewayError; -use internal::prelude::*; -use serde_json; -use websocket::message::OwnedMessage; -use websocket::sync::stream::{TcpStream, TlsStream}; -use websocket::sync::Client as WsClient; - -pub trait ReceiverExt { - fn recv_json(&mut self) -> Result<Option<Value>>; -} - -pub trait SenderExt { - fn send_json(&mut self, value: &Value) -> Result<()>; -} - -impl ReceiverExt for WsClient<TlsStream<TcpStream>> { - fn recv_json(&mut self) -> Result<Option<Value>> { - Ok(match self.recv_message()? { - OwnedMessage::Binary(bytes) => { - serde_json::from_reader(ZlibDecoder::new(&bytes[..])).map(Some)? - }, - OwnedMessage::Close(data) => return Err(Error::Gateway(GatewayError::Closed(data))), - OwnedMessage::Text(payload) => { - serde_json::from_str(&payload).map(Some)? - }, - OwnedMessage::Ping(x) => { - self.send_message(&OwnedMessage::Pong(x)) - .map_err(Error::from)?; - - None - }, - OwnedMessage::Pong(_) => None, - }) - } -} - -impl SenderExt for WsClient<TlsStream<TcpStream>> { - fn send_json(&mut self, value: &Value) -> Result<()> { - serde_json::to_string(value) - .map(OwnedMessage::Text) - .map_err(Error::from) - .and_then(|m| self.send_message(&m).map_err(Error::from)) - } -} @@ -107,13 +107,15 @@ extern crate serde_derive; #[macro_use] extern crate serde_json; -#[cfg(feature = "lazy_static")] -#[macro_use] -extern crate lazy_static; - extern crate chrono; -extern crate parking_lot; +extern crate futures; extern crate serde; +extern crate tokio_core; +extern crate tokio_timer; +extern crate tokio_tls; +extern crate tokio_tungstenite; +extern crate tungstenite; +extern crate url; #[cfg(feature = "base64")] extern crate base64; @@ -123,8 +125,8 @@ extern crate byteorder; extern crate flate2; #[cfg(feature = "hyper")] extern crate hyper; -#[cfg(feature = "hyper-native-tls")] -extern crate hyper_native_tls; +#[cfg(feature = "hyper-tls")] +extern crate hyper_tls; #[cfg(feature = "multipart")] extern crate multipart; #[cfg(feature = "native-tls")] @@ -166,44 +168,7 @@ pub mod voice; mod error; -pub use error::{Error, Result}; +pub use error::{Error, FutureResult, Result}; #[cfg(feature = "client")] pub use client::Client; - -#[cfg(feature = "cache")] -use cache::Cache; -#[cfg(feature = "cache")] -use parking_lot::RwLock; - -#[cfg(feature = "cache")] -lazy_static! { - /// A mutable and lazily-initialized static binding. It can be accessed - /// across any function and in any context. - /// - /// This [`Cache`] instance is updated for every event received, so you do - /// not need to maintain your own cache. - /// - /// See the [cache module documentation] for more details. - /// - /// The Cache itself is wrapped within an `RwLock`, which allows for - /// multiple readers or at most one writer at a time across threads. This - /// means that you may have multiple commands reading from the Cache - /// concurrently. - /// - /// # Examples - /// - /// Retrieve the [current user][`CurrentUser`]'s Id, by opening a Read - /// guard: - /// - /// ```rust,ignore - /// use serenity::CACHE; - /// - /// println!("{}", CACHE.read().user.id); - /// ``` - /// - /// [`CurrentUser`]: model/struct.CurrentUser.html - /// [`Cache`]: cache/struct.Cache.html - /// [cache module documentation]: cache/index.html - pub static ref CACHE: RwLock<Cache> = RwLock::new(Cache::default()); -} diff --git a/src/model/channel/attachment.rs b/src/model/channel/attachment.rs index 0530b6f..65df67e 100644 --- a/src/model/channel/attachment.rs +++ b/src/model/channel/attachment.rs @@ -1,10 +1,3 @@ -#[cfg(feature = "model")] -use hyper::Client as HyperClient; -#[cfg(feature = "model")] -use internal::prelude::*; -#[cfg(feature = "model")] -use std::io::Read; - /// A file uploaded with a message. Not to be confused with [`Embed`]s. /// /// [`Embed`]: struct.Embed.html @@ -35,84 +28,4 @@ impl Attachment { self.width .and_then(|width| self.height.map(|height| (width, height))) } - - /// Downloads the attachment, returning back a vector of bytes. - /// - /// # Examples - /// - /// Download all of the attachments associated with a [`Message`]: - /// - /// ```rust,no_run - /// use serenity::model::prelude::*; - /// use serenity::prelude::*; - /// use std::env; - /// use std::fs::File; - /// use std::io::Write; - /// use std::path::Path; - /// - /// struct Handler; - /// - /// impl EventHandler for Handler { - /// fn message(&self, _: Context, message: Message) { - /// for attachment in message.attachments { - /// let content = match attachment.download() { - /// Ok(content) => content, - /// Err(why) => { - /// println!("Error downloading attachment: {:?}", why); - /// let _ = message.channel_id.say("Error downloading attachment"); - /// - /// return; - /// }, - /// }; - /// - /// let mut file = match File::create(&attachment.filename) { - /// Ok(file) => file, - /// Err(why) => { - /// println!("Error creating file: {:?}", why); - /// let _ = message.channel_id.say("Error creating file"); - /// - /// return; - /// }, - /// }; - /// - /// if let Err(why) = file.write(&content) { - /// println!("Error writing to file: {:?}", why); - /// - /// return; - /// } - /// - /// let _ = message.channel_id.say(&format!("Saved {:?}", attachment.filename)); - /// } - /// } - /// - /// 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).unwrap(); - /// - /// client.start().unwrap(); - /// ``` - /// - /// # Errors - /// - /// Returns an [`Error::Io`] when there is a problem reading the contents - /// of the HTTP response. - /// - /// Returns an [`Error::Hyper`] when there is a problem retrieving the - /// attachment. - /// - /// [`Error::Hyper`]: ../enum.Error.html#variant.Hyper - /// [`Error::Io`]: ../enum.Error.html#variant.Io - /// [`Message`]: struct.Message.html - pub fn download(&self) -> Result<Vec<u8>> { - let hyper = request_client!(); - let mut response = hyper.get(&self.url).send()?; - - let mut bytes = vec![]; - response.read_to_end(&mut bytes)?; - - Ok(bytes) - } } diff --git a/src/model/channel/channel_category.rs b/src/model/channel/channel_category.rs index 7a49c7e..451d550 100644 --- a/src/model/channel/channel_category.rs +++ b/src/model/channel/channel_category.rs @@ -1,11 +1,13 @@ +use client::Client; +use futures::{Future, future}; use model::prelude::*; +use std::rc::Rc; +use ::FutureResult; #[cfg(all(feature = "builder", feature = "model"))] use builder::EditChannel; -#[cfg(all(feature = "builder", feature = "model"))] -use http; -#[cfg(all(feature = "model", feature = "utils"))] -use utils::{self as serenity_utils, VecMap}; +#[cfg(feature = "utils")] +use utils as serenity_utils; /// A category of [`GuildChannel`]s. /// @@ -35,14 +37,17 @@ pub struct ChannelCategory { /// /// [`GuildChannel`]: struct.GuildChannel.html pub permission_overwrites: Vec<PermissionOverwrite>, + #[serde(skip)] + pub(crate) client: Option<Rc<Client>>, } -#[cfg(feature = "model")] impl ChannelCategory { /// Adds a permission overwrite to the category's channels. #[inline] - pub fn create_permission(&self, target: &PermissionOverwrite) -> Result<()> { - self.id.create_permission(target) + pub fn create_permission(&self, target: &PermissionOverwrite) -> FutureResult<()> { + ftryopt!(self.client) + .http + .create_permission(self.id.0, target) } /// Deletes all permission overrides in the category from the channels. @@ -51,23 +56,39 @@ impl ChannelCategory { /// /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html #[inline] - pub fn delete_permission(&self, permission_type: PermissionOverwriteType) -> Result<()> { - self.id.delete_permission(permission_type) + pub fn delete_permission(&self, permission_type: PermissionOverwriteType) + -> FutureResult<()> { + let id = match permission_type { + PermissionOverwriteType::Member(id) => id.0, + PermissionOverwriteType::Role(id) => id.0, + }; + + let done = ftryopt!(self.client) + .http + .delete_permission(self.id.0, id); + + Box::new(done) } /// Deletes this category. #[inline] - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_CHANNELS; - if !utils::user_has_perms(self.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.delete().map(|_| ()) + let done = client.http.delete_channel(self.id.0).map(|_| ()); + + Box::new(done) } /// Modifies the category's settings, such as its position or name. @@ -82,47 +103,22 @@ impl ChannelCategory { /// category.edit(|c| c.name("test").bitrate(86400)); /// ``` #[cfg(all(feature = "builder", feature = "model", feature = "utils"))] - pub fn edit<F>(&mut self, f: F) -> Result<()> - where F: FnOnce(EditChannel) -> EditChannel { + pub fn edit<F: FnOnce(EditChannel) -> EditChannel>(&self, f: F) + -> FutureResult<GuildChannel> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_CHANNELS; - if !utils::user_has_perms(self.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - let mut map = VecMap::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 map = serenity_utils::vecmap_to_json_map(f(EditChannel(map)).0); - - http::edit_channel(self.id.0, &map).map(|channel| { - let GuildChannel { - id, - category_id, - permission_overwrites, - nsfw, - name, - position, - kind, - .. - } = channel; - - *self = ChannelCategory { - id, - category_id, - permission_overwrites, - nsfw, - name, - position, - kind, - }; - () - }) + ftryopt!(self.client).http.edit_channel(self.id.0, f) } #[cfg(feature = "utils")] diff --git a/src/model/channel/channel_id.rs b/src/model/channel/channel_id.rs index 00dc1f2..fc6d748 100644 --- a/src/model/channel/channel_id.rs +++ b/src/model/channel/channel_id.rs @@ -1,550 +1,13 @@ -use internal::RwLockExt; use model::prelude::*; -#[cfg(feature = "model")] -use std::borrow::Cow; -#[cfg(feature = "model")] -use std::fmt::Write as FmtWrite; -#[cfg(feature = "model")] -use builder::{CreateMessage, EditChannel, EditMessage, GetMessages}; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; -#[cfg(feature = "model")] -use http::{self, AttachmentType}; -#[cfg(feature = "model")] -use utils; - -#[cfg(feature = "model")] -impl ChannelId { - /// Broadcasts that the current user is typing to a channel for the next 5 - /// seconds. - /// - /// After 5 seconds, another request must be made to continue broadcasting - /// that the current user is typing. - /// - /// This should rarely be used for bots, and should likely only be used for - /// signifying that a long-running command is still being executed. - /// - /// **Note**: Requires the [Send Messages] permission. - /// - /// # Examples - /// - /// ```rust,ignore - /// use serenity::model::ChannelId; - /// - /// let _successful = ChannelId(7).broadcast_typing(); - /// ``` - /// - /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[inline] - pub fn broadcast_typing(&self) -> Result<()> { http::broadcast_typing(self.0) } - - /// Creates a [permission overwrite][`PermissionOverwrite`] for either a - /// single [`Member`] or [`Role`] within the channel. - /// - /// Refer to the documentation for [`GuildChannel::create_permission`] for - /// more information. - /// - /// Requires the [Manage Channels] permission. - /// - /// [`GuildChannel::create_permission`]: struct.GuildChannel.html#method.create_permission - /// [`Member`]: struct.Member.html - /// [`PermissionOverwrite`]: struct.PermissionOverwrite.html - /// [`Role`]: struct.Role.html - /// [Manage Channels]: permissions/constant.MANAGE_CHANNELS.html - pub fn create_permission(&self, target: &PermissionOverwrite) -> Result<()> { - let (id, kind) = match target.kind { - PermissionOverwriteType::Member(id) => (id.0, "member"), - PermissionOverwriteType::Role(id) => (id.0, "role"), - }; - - let map = json!({ - "allow": target.allow.bits(), - "deny": target.deny.bits(), - "id": id, - "type": kind, - }); - - http::create_permission(self.0, id, &map) - } - - /// React to a [`Message`] with a custom [`Emoji`] or unicode character. - /// - /// [`Message::react`] may be a more suited method of reacting in most - /// cases. - /// - /// Requires the [Add Reactions] permission, _if_ the current user is the - /// first user to perform a react with a certain emoji. - /// - /// [`Emoji`]: struct.Emoji.html - /// [`Message`]: struct.Message.html - /// [`Message::react`]: struct.Message.html#method.react - /// [Add Reactions]: permissions/constant.ADD_REACTIONS.html - #[inline] - pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - http::create_reaction(self.0, message_id.into().0, &reaction_type.into()) - } - - /// Deletes this channel, returning the channel on a successful deletion. - #[inline] - pub fn delete(&self) -> Result<Channel> { http::delete_channel(self.0) } - - /// Deletes a [`Message`] given its Id. - /// - /// Refer to [`Message::delete`] for more information. - /// - /// Requires the [Manage Messages] permission, if the current user is not - /// the author of the message. - /// - /// [`Message`]: struct.Message.html - /// [`Message::delete`]: struct.Message.html#method.delete - /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - #[inline] - pub fn delete_message<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - http::delete_message(self.0, message_id.into().0) - } - - /// Deletes all messages by Ids from the given vector in the given channel. - /// - /// Refer to the documentation for [`Channel::delete_messages`] for more - /// information. - /// - /// Requires the [Manage Messages] permission. - /// - /// **Note**: Messages that are older than 2 weeks can't be deleted using - /// this method. - /// - /// # Errors - /// - /// Returns [`ModelError::BulkDeleteAmount`] if an attempt was made to - /// delete either 0 or more than 100 messages. - /// - /// [`Channel::delete_messages`]: enum.Channel.html#method.delete_messages - /// [`ModelError::BulkDeleteAmount`]: ../enum.ModelError.html#variant.BulkDeleteAmount - /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - pub fn delete_messages<T: AsRef<MessageId>, It: IntoIterator<Item=T>>(&self, message_ids: It) -> Result<()> { - let ids = message_ids - .into_iter() - .map(|message_id| message_id.as_ref().0) - .collect::<Vec<u64>>(); - let len = ids.len(); - - if len == 0 || len > 100 { - return Err(Error::Model(ModelError::BulkDeleteAmount)); - } else if ids.len() == 1 { - self.delete_message(ids[0]) - } else { - let map = json!({ "messages": ids }); - - http::delete_messages(self.0, &map) - } - } - - /// Deletes all permission overrides in the channel from a member or role. - /// - /// **Note**: Requires the [Manage Channel] permission. - /// - /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html - pub fn delete_permission(&self, permission_type: PermissionOverwriteType) -> Result<()> { - http::delete_permission( - self.0, - match permission_type { - PermissionOverwriteType::Member(id) => id.0, - PermissionOverwriteType::Role(id) => id.0, - }, - ) - } - - /// Deletes the given [`Reaction`] from the channel. - /// - /// **Note**: Requires the [Manage Messages] permission, _if_ the current - /// user did not perform the reaction. - /// - /// [`Reaction`]: struct.Reaction.html - /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - pub fn delete_reaction<M, R>(&self, - message_id: M, - user_id: Option<UserId>, - reaction_type: R) - -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - http::delete_reaction( - self.0, - message_id.into().0, - user_id.map(|uid| uid.0), - &reaction_type.into(), - ) - } - - - /// Edits the settings of a [`Channel`], optionally setting new values. - /// - /// Refer to `EditChannel`'s documentation for its methods. - /// - /// Requires the [Manage Channel] permission. - /// - /// # Examples - /// - /// Change a voice channel's name and bitrate: - /// - /// ```rust,ignore - /// // assuming a `channel_id` has been bound - /// - /// channel_id.edit(|c| c.name("test").bitrate(64000)); - /// ``` - /// - /// [`Channel`]: enum.Channel.html - /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html - #[cfg(feature = "utils")] - #[inline] - pub fn edit<F: FnOnce(EditChannel) -> EditChannel>(&self, f: F) -> Result<GuildChannel> { - let map = utils::vecmap_to_json_map(f(EditChannel::default()).0); - - http::edit_channel(self.0, &map) - } - - /// Edits a [`Message`] in the channel given its Id. - /// - /// Message editing preserves all unchanged message data. - /// - /// Refer to the documentation for [`EditMessage`] for more information - /// regarding message restrictions and requirements. - /// - /// **Note**: Requires that the current user be the author of the message. - /// - /// # Errors - /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the [`the limit`], containing the number of unicode code points - /// over the limit. - /// - /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong - /// [`EditMessage`]: ../builder/struct.EditMessage.html - /// [`Message`]: struct.Message.html - /// [`the limit`]: ../builder/struct.EditMessage.html#method.content - #[cfg(feature = "utils")] - pub fn edit_message<F, M>(&self, message_id: M, f: F) -> Result<Message> - where F: FnOnce(EditMessage) -> EditMessage, M: Into<MessageId> { - let msg = f(EditMessage::default()); - - 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))); - } - } - } - - let map = utils::vecmap_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().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().channel(*self) { - return Ok(channel); - } - } - - http::get_channel(self.0) - } - - /// Gets all of the channel's invites. - /// - /// Requires the [Manage Channels] permission. - /// [Manage Channels]: permissions/constant.MANAGE_CHANNELS.html - #[inline] - pub fn invites(&self) -> Result<Vec<RichInvite>> { http::get_channel_invites(self.0) } - - /// Gets a message from the channel. - /// - /// Requires the [Read Message History] permission. - /// - /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html - #[inline] - pub fn message<M: Into<MessageId>>(&self, message_id: M) -> Result<Message> { - http::get_message(self.0, message_id.into().0) - .map(|mut msg| { - msg.transform_content(); - - msg - }) - } - - /// Gets messages from the channel. - /// - /// Refer to [`Channel::messages`] for more information. - /// - /// Requires the [Read Message History] permission. - /// - /// [`Channel::messages`]: enum.Channel.html#method.messages - /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html - pub fn messages<F>(&self, f: F) -> Result<Vec<Message>> - where F: FnOnce(GetMessages) -> GetMessages { - let mut map = f(GetMessages::default()).0; - let mut query = format!("?limit={}", map.remove(&"limit").unwrap_or(50)); - - if let Some(after) = map.remove(&"after") { - write!(query, "&after={}", after)?; - } else if let Some(around) = map.remove(&"around") { - write!(query, "&around={}", around)?; - } else if let Some(before) = map.remove(&"before") { - write!(query, "&before={}", before)?; - } - - http::get_messages(self.0, &query).map(|msgs| { - msgs.into_iter() - .map(|mut msg| { - msg.transform_content(); - - msg - }) - .collect::<Vec<Message>>() - }) - } - - /// Returns the name of whatever channel this id holds. - #[cfg(feature = "model")] - pub fn name(&self) -> Option<String> { - use self::Channel::*; - - let finding = feature_cache! {{ - Some(self.find()) - } else { - None - }}; - - let channel = if let Some(Some(c)) = finding { - c - } else { - return None; - }; - - Some(match channel { - 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().name().to_string(), - Private(channel) => channel.read().name(), - }) - } - - /// Pins a [`Message`] to the channel. - /// - /// [`Message`]: struct.Message.html - #[inline] - pub fn pin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - http::pin_message(self.0, message_id.into().0) - } - - /// Gets the list of [`Message`]s which are pinned to the channel. - /// - /// [`Message`]: struct.Message.html - #[inline] - pub fn pins(&self) -> Result<Vec<Message>> { http::get_pins(self.0) } - - /// Gets the list of [`User`]s who have reacted to a [`Message`] with a - /// certain [`Emoji`]. - /// - /// Refer to [`Channel::reaction_users`] for more information. - /// - /// **Note**: Requires the [Read Message History] permission. - /// - /// [`Channel::reaction_users`]: enum.Channel.html#method.reaction_users - /// [`Emoji`]: struct.Emoji.html - /// [`Message`]: struct.Message.html - /// [`User`]: struct.User.html - /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html - pub fn reaction_users<M, R, U>(&self, - message_id: M, - reaction_type: R, - limit: Option<u8>, - after: U, - ) -> Result<Vec<User>> where M: Into<MessageId>, - R: Into<ReactionType>, - U: Into<Option<UserId>> { - let limit = limit.map_or(50, |x| if x > 100 { 100 } else { x }); - - http::get_reaction_users( - self.0, - message_id.into().0, - &reaction_type.into(), - limit, - after.into().map(|x| x.0), - ) - } - - /// Sends a message with just the given message content in the channel. - /// - /// # Errors - /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. - /// - /// [`ChannelId`]: struct.ChannelId.html - /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong - #[inline] - pub fn say<D: ::std::fmt::Display>(&self, content: D) -> Result<Message> { - self.send_message(|m| m.content(content)) - } - - /// Sends a file along with optional message contents. The filename _must_ - /// be specified. - /// - /// Message contents may be passed by using the [`CreateMessage::content`] - /// method. - /// - /// An embed can _not_ be sent when sending a file. If you set one, it will - /// be automatically removed. - /// - /// The [Attach Files] and [Send Messages] permissions are required. - /// - /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # Examples - /// - /// Send files with the paths `/path/to/file.jpg` and `/path/to/file2.jpg`: - /// - /// ```rust,no_run - /// use serenity::model::id::ChannelId; - /// - /// let channel_id = ChannelId(7); - /// - /// let paths = vec!["/path/to/file.jpg", "path/to/file2.jpg"]; - /// - /// let _ = channel_id.send_files(paths, |m| m.content("a file")); - /// ``` - /// - /// Send files using `File`: - /// - /// ```rust,no_run - /// use serenity::model::id::ChannelId; - /// use std::fs::File; - /// - /// let channel_id = ChannelId(7); - /// - /// let f1 = File::open("my_file.jpg").unwrap(); - /// let f2 = File::open("my_file2.jpg").unwrap(); - /// - /// let files = vec![(&f1, "my_file.jpg"), (&f2, "my_file2.jpg")]; - /// - /// let _ = channel_id.send_files(files, |m| m.content("a file")); - /// ``` - /// - /// # Errors - /// - /// If the content of the message is over the above limit, then a - /// [`ClientError::MessageTooLong`] will be returned, containing the number - /// of unicode code points over the limit. - /// - /// Returns an - /// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] - /// if the file is too large to send. - /// - /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong - /// [`HttpError::InvalidRequest`]: ../http/enum.HttpError.html#variant.InvalidRequest - /// [`CreateMessage::content`]: ../utils/builder/struct.CreateMessage.html#method.content - /// [`GuildChannel`]: struct.GuildChannel.html - /// [Attach Files]: permissions/constant.ATTACH_FILES.html - /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[cfg(feature = "utils")] - 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 msg = f(CreateMessage::default()); - - 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))); - } - } - } - - let _ = msg.0.remove(&"embed"); - let map = utils::vecmap_to_json_map(msg.0); - - http::send_files(self.0, files, map) - } - - /// Sends a message to the channel. - /// - /// Refer to the documentation for [`CreateMessage`] for more information - /// regarding message restrictions and requirements. - /// - /// Requires the [Send Messages] permission. - /// - /// **Note**: Message contents must be under 2000 unicode code points. - /// - /// # Errors - /// - /// Returns a [`ModelError::MessageTooLong`] if the content of the message - /// is over the above limit, containing the number of unicode code points - /// over the limit. - /// - /// [`Channel`]: enum.Channel.html - /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong - /// [`CreateMessage`]: ../builder/struct.CreateMessage.html - /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[cfg(feature = "utils")] - pub fn send_message<F>(&self, f: F) -> Result<Message> - where F: FnOnce(CreateMessage) -> CreateMessage { - let msg = f(CreateMessage::default()); - let map = utils::vecmap_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) = msg.1 { - for reaction in reactions { - self.create_reaction(message.id, reaction)?; - } - } - - Ok(message) - } - - /// Unpins a [`Message`] in the channel given by its Id. - /// - /// Requires the [Manage Messages] permission. - /// - /// [`Message`]: struct.Message.html - /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - #[inline] - pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - http::unpin_message(self.0, message_id.into().0) - } - - /// Retrieves the channel's webhooks. - /// - /// **Note**: Requires the [Manage Webhooks] permission. - /// - /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html - #[inline] - pub fn webhooks(&self) -> Result<Vec<Webhook>> { http::get_channel_webhooks(self.0) } -} - impl From<Channel> for ChannelId { /// Gets the Id of a `Channel`. fn from(channel: Channel) -> ChannelId { match channel { - Channel::Group(group) => group.with(|g| g.channel_id), - Channel::Guild(ch) => ch.with(|c| c.id), - Channel::Private(ch) => ch.with(|c| c.id), - Channel::Category(ch) => ch.with(|c| c.id), + Channel::Group(group) => group.borrow().channel_id, + Channel::Guild(ch) => ch.borrow().id, + Channel::Private(ch) => ch.borrow().id, + Channel::Category(ch) => ch.borrow().id, } } } @@ -553,10 +16,10 @@ impl<'a> From<&'a Channel> for ChannelId { /// Gets the Id of a `Channel`. fn from(channel: &Channel) -> ChannelId { match *channel { - Channel::Group(ref group) => group.with(|g| g.channel_id), - Channel::Guild(ref ch) => ch.with(|c| c.id), - Channel::Private(ref ch) => ch.with(|c| c.id), - Channel::Category(ref ch) => ch.with(|c| c.id), + Channel::Group(ref group) => group.borrow().channel_id, + Channel::Guild(ref ch) => ch.borrow().id, + Channel::Private(ref ch) => ch.borrow().id, + Channel::Category(ref ch) => ch.borrow().id, } } } diff --git a/src/model/channel/group.rs b/src/model/channel/group.rs index 61fe67e..f55a9dd 100644 --- a/src/model/channel/group.rs +++ b/src/model/channel/group.rs @@ -1,13 +1,14 @@ use chrono::{DateTime, FixedOffset}; +use client::Client; +use futures::{Future, future}; use model::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; +use ::FutureResult; -#[cfg(feature = "model")] +// #[cfg(feature = "model")] use builder::{CreateMessage, EditMessage, GetMessages}; #[cfg(feature = "model")] -use http::{self, AttachmentType}; -#[cfg(feature = "model")] -use internal::RwLockExt; -#[cfg(feature = "model")] use std::borrow::Cow; #[cfg(feature = "model")] use std::fmt::Write as FmtWrite; @@ -35,10 +36,11 @@ pub struct Group { /// A map of the group's recipients. #[serde(deserialize_with = "deserialize_users", serialize_with = "serialize_users")] - pub recipients: HashMap<UserId, Arc<RwLock<User>>>, + pub recipients: HashMap<UserId, Rc<RefCell<User>>>, + #[serde(skip)] + pub(crate) client: Option<Rc<Client>>, } -#[cfg(feature = "model")] impl Group { /// Adds the given user to the group. If the user is already in the group, /// then nothing is done. @@ -49,20 +51,24 @@ impl Group { /// user. /// /// [`http::add_group_recipient`]: ../http/fn.add_group_recipient.html - pub fn add_recipient<U: Into<UserId>>(&self, user: U) -> Result<()> { + pub fn add_recipient<U: Into<UserId>>(&self, user: U) -> FutureResult<()> { let user = user.into(); // If the group already contains the recipient, do nothing. if self.recipients.contains_key(&user) { - return Ok(()); + return Box::new(future::ok(())); } - http::add_group_recipient(self.channel_id.0, user.0) + ftryopt!(self.client) + .http + .add_group_recipient(self.channel_id.0, user.0) } /// Broadcasts that the current user is typing in the group. #[inline] - pub fn broadcast_typing(&self) -> Result<()> { self.channel_id.broadcast_typing() } + pub fn broadcast_typing(&self) -> FutureResult<()> { + ftryopt!(self.client).http.broadcast_typing(self.channel_id.0) + } /// React to a [`Message`] with a custom [`Emoji`] or unicode character. /// @@ -77,9 +83,14 @@ impl Group { /// [`Message::react`]: struct.Message.html#method.react /// [Add Reactions]: permissions/constant.ADD_REACTIONS.html #[inline] - pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) -> Result<()> + pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) + -> FutureResult<()> where M: Into<MessageId>, R: Into<ReactionType> { - self.channel_id.create_reaction(message_id, reaction_type) + ftryopt!(self.client).http.create_reaction( + self.channel_id.0, + message_id.into().0, + &reaction_type.into(), + ) } /// Deletes all messages by Ids from the given vector in the channel. @@ -100,8 +111,14 @@ impl Group { /// [`ModelError::BulkDeleteAmount`]: ../enum.ModelError.html#variant.BulkDeleteAmount /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn delete_messages<T: AsRef<MessageId>, It: IntoIterator<Item=T>>(&self, message_ids: It) -> Result<()> { - self.channel_id.delete_messages(message_ids) + pub fn delete_messages<T: AsRef<MessageId>, It: IntoIterator<Item=T>>( + &self, + message_ids: It, + ) -> FutureResult<()> { + ftryopt!(self.client).http.delete_messages( + self.channel_id.0, + message_ids, + ) } /// Deletes all permission overrides in the channel from a member @@ -111,8 +128,17 @@ impl Group { /// /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html #[inline] - pub fn delete_permission(&self, permission_type: PermissionOverwriteType) -> Result<()> { - self.channel_id.delete_permission(permission_type) + pub fn delete_permission(&self, permission_type: PermissionOverwriteType) + -> FutureResult<()> { + let overwrite_id = match permission_type { + PermissionOverwriteType::Member(id) => id.0, + PermissionOverwriteType::Role(id) => id.0, + }; + + ftryopt!(self.client).http.delete_permission( + self.channel_id.0, + overwrite_id, + ) } /// Deletes the given [`Reaction`] from the channel. @@ -123,14 +149,18 @@ impl Group { /// [`Reaction`]: struct.Reaction.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn delete_reaction<M, R>(&self, - message_id: M, - user_id: Option<UserId>, - reaction_type: R) - -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - self.channel_id - .delete_reaction(message_id, user_id, reaction_type) + pub fn delete_reaction<M, R>( + &self, + message_id: M, + user_id: Option<UserId>, + reaction_type: R, + ) -> FutureResult<()> where M: Into<MessageId>, R: Into<ReactionType> { + ftryopt!(self.client).http.delete_reaction( + self.channel_id.0, + message_id.into().0, + user_id.map(|x| x.0), + &reaction_type.into(), + ) } /// Edits a [`Message`] in the channel given its Id. @@ -153,9 +183,14 @@ impl Group { /// [`Message`]: struct.Message.html /// [`the limit`]: ../builder/struct.EditMessage.html#method.content #[inline] - pub fn edit_message<F, M>(&self, message_id: M, f: F) -> Result<Message> + pub fn edit_message<F, M>(&self, message_id: M, f: F) + -> FutureResult<Message> where F: FnOnce(EditMessage) -> EditMessage, M: Into<MessageId> { - self.channel_id.edit_message(message_id, f) + ftryopt!(self.client).http.edit_message( + self.channel_id.0, + message_id.into().0, + f, + ) } /// Returns the formatted URI of the group's icon if one exists. @@ -178,7 +213,9 @@ impl Group { /// Leaves the group. #[inline] - pub fn leave(&self) -> Result<Group> { http::leave_group(self.channel_id.0) } + pub fn leave(&self) -> FutureResult<()> { + ftryopt!(self.client).http.leave_group(self.channel_id.0) + } /// Gets a message from the channel. /// @@ -186,8 +223,8 @@ impl Group { /// /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn message<M: Into<MessageId>>(&self, message_id: M) -> Result<Message> { - self.channel_id.message(message_id) + pub fn message<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<Message> { + ftryopt!(self.client).http.get_message(self.channel_id.0, message_id.into().0) } /// Gets messages from the channel. @@ -196,9 +233,9 @@ impl Group { /// /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn messages<F>(&self, f: F) -> Result<Vec<Message>> - where F: FnOnce(GetMessages) -> GetMessages { - self.channel_id.messages(f) + pub fn messages<'a, F: FnOnce(GetMessages) -> GetMessages>(&'a self, f: F) + -> Box<Future<Item = Vec<Message>, Error = Error> + 'a> { + ftryopt!(self.client).http.get_messages(self.channel_id.0, f) } /// Generates a name for the group. @@ -211,12 +248,14 @@ impl Group { Some(ref name) => Cow::Borrowed(name), None => { let mut name = match self.recipients.values().nth(0) { - Some(recipient) => recipient.with(|c| c.name.clone()), + Some(recipient) => recipient.borrow().name.clone(), None => return Cow::Borrowed("Empty Group"), }; for recipient in self.recipients.values().skip(1) { - let _ = write!(name, ", {}", recipient.with(|r| r.name.clone())); + let recipient = recipient.borrow(); + + let _ = write!(name, ", {}", recipient.name); } Cow::Owned(name) @@ -226,7 +265,9 @@ impl Group { /// Retrieves the list of messages that have been pinned in the group. #[inline] - pub fn pins(&self) -> Result<Vec<Message>> { self.channel_id.pins() } + pub fn pins(&self) -> FutureResult<Vec<Message>> { + ftryopt!(self.client).http.get_pins(self.channel_id.0) + } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a /// certain [`Emoji`]. @@ -247,25 +288,34 @@ impl Group { reaction_type: R, limit: Option<u8>, after: U, - ) -> Result<Vec<User>> where M: Into<MessageId>, - R: Into<ReactionType>, - U: Into<Option<UserId>> { - self.channel_id.reaction_users(message_id, reaction_type, limit, after) + ) -> FutureResult<Vec<User>> + where M: Into<MessageId>, R: Into<ReactionType>, U: Into<Option<UserId>> { + ftryopt!(self.client).http.get_reaction_users( + self.channel_id.0, + message_id.into().0, + &reaction_type.into(), + limit, + after.into().map(|x| x.0), + ) } /// Removes a recipient from the group. If the recipient is already not in /// the group, then nothing is done. /// /// **Note**: This is only available to the group owner. - pub fn remove_recipient<U: Into<UserId>>(&self, user: U) -> Result<()> { + pub fn remove_recipient<U>(&self, user: U) -> FutureResult<()> + where U: Into<UserId> { let user = user.into(); // If the group does not contain the recipient already, do nothing. if !self.recipients.contains_key(&user) { - return Ok(()); + return Box::new(future::ok(())); } - http::remove_group_recipient(self.channel_id.0, user.0) + ftryopt!(self.client).http.remove_group_recipient( + self.channel_id.0, + user.0, + ) } /// Sends a message with just the given message content in the channel. @@ -279,7 +329,10 @@ impl Group { /// [`ChannelId`]: ../model/id/struct.ChannelId.html /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong #[inline] - pub fn say(&self, content: &str) -> Result<Message> { self.channel_id.say(content) } + pub fn say(&self, content: &str) -> FutureResult<Message> { + ftryopt!(self.client).http.send_message(self.channel_id.0, |f| f + .content(content)) + } /// Sends (a) file(s) along with optional message contents. /// @@ -299,11 +352,15 @@ impl Group { /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[inline] - 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>> { - self.channel_id.send_files(files, f) - } + // todo + // #[inline] + // pub fn send_files<'a, F, T, It>(&self, files: It, f: F) + // -> FutureResult<Message> + // where F: FnOnce(CreateMessage) -> CreateMessage, + // T: Into<AttachmentType<'a>>, + // It: IntoIterator<Item=T> { + // ftryopt!(self.client).http.send_files(self.channel_id.0, files, f) + // } /// Sends a message to the group with the given content. /// @@ -315,8 +372,9 @@ impl Group { /// [`CreateMessage`]: ../builder/struct.CreateMessage.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html #[inline] - pub fn send_message<F: FnOnce(CreateMessage) -> CreateMessage>(&self, f: F) -> Result<Message> { - self.channel_id.send_message(f) + pub fn send_message<F>(&self, f: F) -> FutureResult<Message> + where F: FnOnce(CreateMessage) -> CreateMessage { + ftryopt!(self.client).http.send_message(self.channel_id.0, f) } /// Unpins a [`Message`] in the channel given by its Id. @@ -326,7 +384,9 @@ impl Group { /// [`Message`]: struct.Message.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - self.channel_id.unpin(message_id) + pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<()> { + ftryopt!(self.client) + .http + .unpin_message(self.channel_id.0, message_id.into().0) } } diff --git a/src/model/channel/guild_channel.rs b/src/model/channel/guild_channel.rs index 9b7d431..ea99377 100644 --- a/src/model/channel/guild_channel.rs +++ b/src/model/channel/guild_channel.rs @@ -1,20 +1,18 @@ use chrono::{DateTime, FixedOffset}; +use futures::{Future, future}; use model::prelude::*; +use std::cell::RefCell; +use super::super::WrappedClient; +use ::FutureResult; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; #[cfg(feature = "model")] use builder::{CreateInvite, CreateMessage, EditChannel, EditMessage, GetMessages}; -#[cfg(feature = "model")] -use http::{self, AttachmentType}; #[cfg(all(feature = "cache", feature = "model"))] use internal::prelude::*; #[cfg(feature = "model")] use std::fmt::{Display, Formatter, Result as FmtResult}; -#[cfg(feature = "model")] -use std::mem; #[cfg(all(feature = "model", feature = "utils"))] -use utils::{self as serenity_utils, VecMap}; +use utils as serenity_utils; /// Represents a guild's text or voice channel. Some methods are available only /// for voice channels and some are only available for text channels. @@ -77,6 +75,8 @@ pub struct GuildChannel { // default to `false`. #[serde(default)] pub nsfw: bool, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -94,7 +94,9 @@ impl GuildChannel { /// /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - pub fn broadcast_typing(&self) -> Result<()> { self.id.broadcast_typing() } + pub fn broadcast_typing(&self) -> FutureResult<()> { + ftryopt!(self.client).http.broadcast_typing(self.id.0) + } /// Creates an invite leading to the given channel. /// @@ -106,20 +108,22 @@ impl GuildChannel { /// let invite = channel.create_invite(|i| i.max_uses(5)); /// ``` #[cfg(feature = "utils")] - pub fn create_invite<F>(&self, f: F) -> Result<RichInvite> + pub fn create_invite<F>(&self, f: F) -> FutureResult<RichInvite> where F: FnOnce(CreateInvite) -> CreateInvite { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::CREATE_INVITE; - if !utils::user_has_perms(self.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - let map = serenity_utils::vecmap_to_json_map(f(CreateInvite::default()).0); - - http::create_invite(self.id.0, &map) + client.http.create_invite(self.id.0, f) } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -226,22 +230,27 @@ impl GuildChannel { /// [Send Messages]: permissions/constant.SEND_MESSAGES.html /// [Send TTS Messages]: permissions/constant.SEND_TTS_MESSAGES.html #[inline] - pub fn create_permission(&self, target: &PermissionOverwrite) -> Result<()> { - self.id.create_permission(target) + pub fn create_permission(&self, target: &PermissionOverwrite) + -> FutureResult<()> { + ftryopt!(self.client).http.create_permission(self.id.0, target) } /// Deletes this channel, returning the channel on a successful deletion. - pub fn delete(&self) -> Result<Channel> { + pub fn delete(&self) -> FutureResult<Channel> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_CHANNELS; - if !utils::user_has_perms(self.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.delete() + client.http.delete_channel(self.id.0) } /// Deletes all messages by Ids from the given vector in the channel. @@ -262,8 +271,9 @@ impl GuildChannel { /// [`ModelError::BulkDeleteAmount`]: ../enum.ModelError.html#variant.BulkDeleteAmount /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn delete_messages<T: AsRef<MessageId>, It: IntoIterator<Item=T>>(&self, message_ids: It) -> Result<()> { - self.id.delete_messages(message_ids) + pub fn delete_messages<T, It>(&self, message_ids: It) -> FutureResult<()> + where T: AsRef<MessageId>, It: IntoIterator<Item=T> { + ftryopt!(self.client).http.delete_messages(self.id.0, message_ids) } /// Deletes all permission overrides in the channel from a member @@ -273,8 +283,14 @@ impl GuildChannel { /// /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html #[inline] - pub fn delete_permission(&self, permission_type: PermissionOverwriteType) -> Result<()> { - self.id.delete_permission(permission_type) + pub fn delete_permission(&self, permission_type: PermissionOverwriteType) + -> FutureResult<()> { + let overwrite_id = match permission_type { + PermissionOverwriteType::Member(id) => id.0, + PermissionOverwriteType::Role(id) => id.0, + }; + + ftryopt!(self.client).http.delete_permission(self.id.0, overwrite_id) } /// Deletes the given [`Reaction`] from the channel. @@ -285,13 +301,18 @@ impl GuildChannel { /// [`Reaction`]: struct.Reaction.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn delete_reaction<M, R>(&self, - message_id: M, - user_id: Option<UserId>, - reaction_type: R) - -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - self.id.delete_reaction(message_id, user_id, reaction_type) + pub fn delete_reaction<M, R>( + &self, + message_id: M, + user_id: Option<UserId>, + reaction_type: R, + ) -> FutureResult<()> where M: Into<MessageId>, R: Into<ReactionType> { + ftryopt!(self.client).http.delete_reaction( + self.id.0, + message_id.into().0, + user_id.map(|x| x.0), + &reaction_type.into(), + ) } /// Modifies a channel's settings, such as its position or name. @@ -306,32 +327,22 @@ impl GuildChannel { /// channel.edit(|c| c.name("test").bitrate(86400)); /// ``` #[cfg(feature = "utils")] - pub fn edit<F>(&mut self, f: F) -> Result<()> - where F: FnOnce(EditChannel) -> EditChannel { + pub fn edit<F: FnOnce(EditChannel) -> EditChannel>(&self, f: F) + -> FutureResult<GuildChannel> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_CHANNELS; - if !utils::user_has_perms(self.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - let mut map = VecMap::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 = serenity_utils::vecmap_to_json_map(f(EditChannel(map)).0); - - match http::edit_channel(self.id.0, &edited) { - Ok(channel) => { - mem::replace(self, channel); - - Ok(()) - }, - Err(why) => Err(why), - } + client.http.edit_channel(self.id.0, f) } /// Edits a [`Message`] in the channel given its Id. @@ -354,9 +365,14 @@ impl GuildChannel { /// [`Message`]: struct.Message.html /// [`the limit`]: ../builder/struct.EditMessage.html#method.content #[inline] - pub fn edit_message<F, M>(&self, message_id: M, f: F) -> Result<Message> + pub fn edit_message<F, M>(&self, message_id: M, f: F) + -> FutureResult<Message> where F: FnOnce(EditMessage) -> EditMessage, M: Into<MessageId> { - self.id.edit_message(message_id, f) + ftryopt!(self.client).http.edit_message( + self.id.0, + message_id.into().0, + f, + ) } /// Attempts to find this channel's guild in the Cache. @@ -364,14 +380,20 @@ 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().guild(self.guild_id) } + pub fn guild(&self) -> Option<Rc<RefCell<Guild>>> { + self.client.as_ref()?.cache.try_borrow().ok().and_then(|cache| { + cache.guild(self.guild_id) + }) + } /// Gets all of the channel's invites. /// /// Requires the [Manage Channels] permission. /// [Manage Channels]: permissions/constant.MANAGE_CHANNELS.html #[inline] - pub fn invites(&self) -> Result<Vec<RichInvite>> { self.id.invites() } + pub fn invites(&self) -> FutureResult<Vec<RichInvite>> { + ftryopt!(self.client).http.get_channel_invites(self.id.0) + } /// Determines if the channel is NSFW. /// @@ -395,8 +417,9 @@ impl GuildChannel { /// /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn message<M: Into<MessageId>>(&self, message_id: M) -> Result<Message> { - self.id.message(message_id) + pub fn message<M: Into<MessageId>>(&self, message_id: M) + -> FutureResult<Message> { + ftryopt!(self.client).http.get_message(self.id.0, message_id.into().0) } /// Gets messages from the channel. @@ -408,9 +431,9 @@ impl GuildChannel { /// [`Channel::messages`]: enum.Channel.html#method.messages /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn messages<F>(&self, f: F) -> Result<Vec<Message>> - where F: FnOnce(GetMessages) -> GetMessages { - self.id.messages(f) + pub fn messages<'a, F: FnOnce(GetMessages) -> GetMessages>(&'a self, f: F) + -> Box<Future<Item = Vec<Message>, Error = Error> + 'a> { + ftryopt!(self.client).http.get_messages(self.id.0, f) } /// Returns the name of the guild channel. @@ -510,19 +533,24 @@ impl GuildChannel { /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html #[cfg(feature = "cache")] - pub fn permissions_for<U: Into<UserId>>(&self, user_id: U) -> Result<Permissions> { + 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().permissions_in(self.id, user_id)) + .map(|g| g.borrow().permissions_in(self.id, user_id)) } /// Pins a [`Message`] to the channel. #[inline] - pub fn pin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { self.id.pin(message_id) } + pub fn pin<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<()> { + ftryopt!(self.client).http.pin_message(self.id.0, message_id.into().0) + } /// Gets all channel's pins. #[inline] - pub fn pins(&self) -> Result<Vec<Message>> { self.id.pins() } + pub fn pins(&self) -> FutureResult<Vec<Message>> { + ftryopt!(self.client).http.get_pins(self.id.0) + } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a /// certain [`Emoji`]. @@ -542,10 +570,15 @@ impl GuildChannel { reaction_type: R, limit: Option<u8>, after: U, - ) -> Result<Vec<User>> where M: Into<MessageId>, - R: Into<ReactionType>, - U: Into<Option<UserId>> { - self.id.reaction_users(message_id, reaction_type, limit, after) + ) -> FutureResult<Vec<User>> + where M: Into<MessageId>, R: Into<ReactionType>, U: Into<Option<UserId>> { + ftryopt!(self.client).http.get_reaction_users( + self.id.0, + message_id.into().0, + &reaction_type.into(), + limit, + after.into().map(|x| x.0), + ) } /// Sends a message with just the given message content in the channel. @@ -559,7 +592,11 @@ impl GuildChannel { /// [`ChannelId`]: struct.ChannelId.html /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong #[inline] - pub fn say(&self, content: &str) -> Result<Message> { self.id.say(content) } + pub fn say(&self, content: &str) -> FutureResult<Message> { + ftryopt!(self.client) + .http + .send_message(self.id.0, |m| m.content(content)) + } /// Sends (a) file(s) along with optional message contents. /// @@ -579,11 +616,15 @@ impl GuildChannel { /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[inline] - 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>> { - self.id.send_files(files, f) - } + // todo + // #[inline] + // pub fn send_files<'a, F, T, It>(&self, files: It, f: F) + // -> FutureResult<Message> + // where F: FnOnce(CreateMessage) -> CreateMessage, + // T: Into<AttachmentType<'a>>, + // It: IntoIterator<Item=T> { + // ftryopt!(self.client).http.send_files(self.id.0, files, f) + // } /// Sends a message to the channel with the given content. /// @@ -604,17 +645,22 @@ impl GuildChannel { /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [`Message`]: struct.Message.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - pub fn send_message<F: FnOnce(CreateMessage) -> CreateMessage>(&self, f: F) -> Result<Message> { + pub fn send_message<F: FnOnce(CreateMessage) -> CreateMessage>(&self, f: F) + -> FutureResult<Message> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::SEND_MESSAGES; - if !utils::user_has_perms(self.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.send_message(f) + client.http.send_message(self.id.0, f) } /// Unpins a [`Message`] in the channel given by its Id. @@ -624,8 +670,8 @@ impl GuildChannel { /// [`Message`]: struct.Message.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - self.id.unpin(message_id) + pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<()> { + ftryopt!(self.client).http.unpin_message(self.id.0, message_id.into().0) } /// Retrieves the channel's webhooks. @@ -634,7 +680,9 @@ impl GuildChannel { /// /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[inline] - pub fn webhooks(&self) -> Result<Vec<Webhook>> { self.id.webhooks() } + pub fn webhooks(&self) -> FutureResult<Vec<Webhook>> { + ftryopt!(self.client).http.get_channel_webhooks(self.id.0) + } } #[cfg(feature = "model")] diff --git a/src/model/channel/message.rs b/src/model/channel/message.rs index feff845..97c0ab2 100644 --- a/src/model/channel/message.rs +++ b/src/model/channel/message.rs @@ -1,19 +1,20 @@ //! Models relating to Discord channels. use chrono::{DateTime, FixedOffset}; +use futures::future; use model::prelude::*; use serde_json::Value; +use std::cell::RefCell; +use std::rc::Rc; +use super::super::WrappedClient; +use ::FutureResult; #[cfg(feature = "model")] -use builder::{CreateEmbed, EditMessage}; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; +use builder::EditMessage; #[cfg(all(feature = "cache", feature = "model"))] use std::fmt::Write; #[cfg(feature = "model")] -use std::mem; -#[cfg(feature = "model")] -use {constants, http, utils as serenity_utils}; +use constants; /// A representation of a message over a guild's text channel, a group, or a /// private channel. @@ -65,6 +66,8 @@ pub struct Message { pub tts: bool, /// The Id of the webhook that sent this message, if one did. pub webhook_id: Option<WebhookId>, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -111,13 +114,20 @@ impl Message { /// # } /// ``` #[cfg(feature = "cache")] - #[inline] - pub fn channel(&self) -> Option<Channel> { CACHE.read().channel(self.channel_id) } + pub fn channel(&self) -> Option<Channel> { + self.client.as_ref()?.cache.try_borrow().ok()?.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().user.id } + pub fn is_own(&self) -> Result<bool> { + let client = self.client.as_ref().ok_or_else(|| { + Error::Model(ModelError::ClientNotPresent) + })?; + + Ok(self.author.id == client.cache.try_borrow()?.user.id) + } /// Deletes the message. /// @@ -133,19 +143,24 @@ impl Message { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [`ModelError::InvalidUser`]: enum.ModelError.html#variant.InvalidUser /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { + let cache = ftry!(client.cache.try_borrow()); let req = Permissions::MANAGE_MESSAGES; - let is_author = self.author.id == CACHE.read().user.id; - let has_perms = utils::user_has_perms(self.channel_id, req)?; + let is_author = self.author.id == cache.user.id; + let has_perms = ftry!(cache.user_has_perms(self.channel_id, req)); if !is_author && !has_perms { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.channel_id.delete_message(self.id) + client.http.delete_message(self.channel_id.0, self.id.0) } /// Deletes all of the [`Reaction`]s associated with the message. @@ -161,17 +176,24 @@ impl Message { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [`Reaction`]: struct.Reaction.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - pub fn delete_reactions(&self) -> Result<()> { + pub fn delete_reactions(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_MESSAGES; - if !utils::user_has_perms(self.channel_id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.channel_id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - http::delete_message_reactions(self.channel_id.0, self.id.0) + client.http.delete_message_reactions( + self.channel_id.0, + self.id.0, + ) } /// Edits this message, replacing the original content with new content. @@ -206,35 +228,18 @@ impl Message { /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [`EditMessage`]: ../builder/struct.EditMessage.html /// [`the limit`]: ../builder/struct.EditMessage.html#method.content - pub fn edit<F>(&mut self, f: F) -> Result<()> - where F: FnOnce(EditMessage) -> EditMessage { + pub fn edit<F: FnOnce(EditMessage) -> EditMessage>(&self, f: F) + -> FutureResult<Message> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { - if self.author.id != CACHE.read().user.id { - return Err(Error::Model(ModelError::InvalidUser)); + if self.author.id != ftry!(client.cache.try_borrow()).user.id { + return Box::new(future::err(Error::Model(ModelError::InvalidUser))); } } - let mut builder = EditMessage::default(); - - if !self.content.is_empty() { - builder = builder.content(&self.content); - } - - if let Some(embed) = self.embeds.get(0) { - builder = builder.embed(|_| CreateEmbed::from(embed.clone())); - } - - let map = serenity_utils::vecmap_to_json_map(f(builder).0); - - match http::edit_message(self.channel_id.0, self.id.0, &Value::Object(map)) { - Ok(edited) => { - mem::replace(self, edited); - - Ok(()) - }, - Err(why) => Err(why), - } + ftryopt!(self.client).http.edit_message(self.channel_id.0, self.id.0, f) } pub(crate) fn transform_content(&mut self) { @@ -275,14 +280,21 @@ impl Message { result = result.replace(&u.mention(), &at_distinct); } - // Then replace all role mentions. - for id in &self.mention_roles { - let mention = id.mention(); - - if let Some(role) = id.find() { - result = result.replace(&mention, &format!("@{}", role.name)); - } else { - result = result.replace(&mention, "@deleted-role"); + if let Some(guild) = self.guild() { + if let Some(guild) = guild.try_borrow().ok() { + // Then replace all role mentions. + for id in &self.mention_roles { + let mention = id.mention(); + + if let Some(role) = guild.roles.get(id).and_then(|r| r.try_borrow().ok()) { + result = result.replace( + &mention, + &format!("@{}", role.name), + ); + } else { + result = result.replace(&mention, "@deleted-role"); + } + } } } @@ -314,9 +326,15 @@ impl Message { reaction_type: R, limit: Option<u8>, after: U, - ) -> Result<Vec<User>> where R: Into<ReactionType>, - U: Into<Option<UserId>> { - self.channel_id.reaction_users(self.id, reaction_type, limit, after) + ) -> FutureResult<Vec<User>> + where R: Into<ReactionType>, U: Into<Option<UserId>> { + ftryopt!(self.client).http.get_reaction_users( + self.channel_id.0, + self.id.0, + &reaction_type.into(), + limit, + after.into().map(|x| x.0), + ) } /// Returns the associated `Guild` for the message if one is in the cache. @@ -328,9 +346,10 @@ impl Message { /// /// [`guild_id`]: #method.guild_id #[cfg(feature = "cache")] - pub fn guild(&self) -> Option<Arc<RwLock<Guild>>> { - self.guild_id() - .and_then(|guild_id| CACHE.read().guild(guild_id)) + pub fn guild(&self) -> Option<Rc<RefCell<Guild>>> { + self.guild_id().and_then(|guild_id| { + self.client.as_ref()?.cache.try_borrow().ok()?.guild(guild_id) + }) } /// Retrieves the Id of the guild that the message was sent in, if sent in @@ -340,19 +359,25 @@ impl Message { /// cache. #[cfg(feature = "cache")] pub fn guild_id(&self) -> Option<GuildId> { - match CACHE.read().channel(self.channel_id) { - Some(Channel::Guild(ch)) => Some(ch.read().guild_id), + match self.client.as_ref()?.cache.try_borrow().ok()?.channel(self.channel_id) { + Some(Channel::Guild(ch)) => Some(ch.borrow().guild_id), _ => None, } } /// True if message was sent using direct messages. + /// + /// Returns `false` if a client is not present on the model. #[cfg(feature = "cache")] pub fn is_private(&self) -> bool { - match CACHE.read().channel(self.channel_id) { - Some(Channel::Group(_)) | Some(Channel::Private(_)) => true, - _ => false, - } + self.client.as_ref().map(|client| { + let cache = client.cache.borrow(); + + match cache.channel(self.channel_id) { + Some(Channel::Group(_)) | Some(Channel::Private(_)) => true, + _ => false, + } + }).unwrap_or(false) } /// Retrieves a clone of the author's Member instance, if this message was @@ -364,8 +389,10 @@ impl Message { /// /// [`Guild::members`]: ../guild/struct.Guild.html#structfield.members #[cfg(feature = "cache")] - pub fn member(&self) -> Option<Member> { - self.guild().and_then(|g| g.read().members.get(&self.author.id).cloned()) + pub fn member(&self) -> Option<Rc<RefCell<Member>>> { + self.guild().and_then(|guild| { + guild.try_borrow().ok()?.members.get(&self.author.id).cloned() + }) } /// Checks the length of a string to ensure that it is within Discord's @@ -399,17 +426,21 @@ impl Message { /// /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - pub fn pin(&self) -> Result<()> { + pub fn pin(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_MESSAGES; - if !utils::user_has_perms(self.channel_id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.channel_id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.channel_id.pin(self.id.0) + client.http.pin_message(self.channel_id.0, self.id.0) } /// React to the message with a custom [`Emoji`] or unicode character. @@ -426,17 +457,26 @@ impl Message { /// [`Emoji`]: struct.Emoji.html /// [Add Reactions]: permissions/constant.ADD_REACTIONS.html /// [permissions]: permissions - pub fn react<R: Into<ReactionType>>(&self, reaction_type: R) -> Result<()> { + pub fn react<R: Into<ReactionType>>(&self, reaction_type: R) + -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::ADD_REACTIONS; - if !utils::user_has_perms(self.channel_id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.channel_id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - http::create_reaction(self.channel_id.0, self.id.0, &reaction_type.into()) + client.http.create_reaction( + self.channel_id.0, + self.id.0, + &reaction_type.into(), + ) } /// Replies to the user, mentioning them prior to the content in the form @@ -461,17 +501,23 @@ impl Message { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - pub fn reply(&self, content: &str) -> Result<Message> { + pub fn reply(&self, content: &str) -> FutureResult<Message> { if let Some(length_over) = Message::overflow_length(content) { - return Err(Error::Model(ModelError::MessageTooLong(length_over))); + return Box::new(future::err(Error::Model( + ModelError::MessageTooLong(length_over), + ))); } + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::SEND_MESSAGES; - if !utils::user_has_perms(self.channel_id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.channel_id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } @@ -479,12 +525,12 @@ impl Message { gen.push_str(": "); gen.push_str(content); - let map = json!({ - "content": gen, - "tts": false, - }); + let done = client.http.send_message( + self.channel_id.0, + |f| f.content(gen).tts(false), + ); - http::send_message(self.channel_id.0, &map) + Box::new(done) } /// Unpins the message from its channel. @@ -499,17 +545,21 @@ impl Message { /// /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html - pub fn unpin(&self) -> Result<()> { + pub fn unpin(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { let req = Permissions::MANAGE_MESSAGES; - if !utils::user_has_perms(self.channel_id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(client.cache.borrow().user_has_perms(self.channel_id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - http::unpin_message(self.channel_id.0, self.id.0) + client.http.unpin_message(self.channel_id.0, self.id.0) } pub(crate) fn check_content_length(map: &JsonMap) -> Result<()> { diff --git a/src/model/channel/mod.rs b/src/model/channel/mod.rs index eadc7bb..9177064 100644 --- a/src/model/channel/mod.rs +++ b/src/model/channel/mod.rs @@ -20,17 +20,18 @@ pub use self::private_channel::*; pub use self::reaction::*; pub use self::channel_category::*; -use internal::RwLockExt; +use futures::Future; use model::prelude::*; use serde::de::Error as DeError; use serde::ser::{SerializeStruct, Serialize, Serializer}; use serde_json; +use std::cell::RefCell; +use std::rc::Rc; use super::utils::deserialize_u64; +use ::FutureResult; #[cfg(feature = "model")] -use builder::{CreateMessage, EditMessage, GetMessages}; -#[cfg(feature = "model")] -use http::AttachmentType; +use builder::{CreateMessage, EditMessage}; #[cfg(feature = "model")] use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -38,30 +39,25 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; #[derive(Clone, Debug)] pub enum Channel { /// A group. A group comprises of only one channel. - Group(Arc<RwLock<Group>>), + Group(Rc<RefCell<Group>>), /// A [text] or [voice] channel within a [`Guild`]. /// /// [`Guild`]: struct.Guild.html /// [text]: enum.ChannelType.html#variant.Text /// [voice]: enum.ChannelType.html#variant.Voice - Guild(Arc<RwLock<GuildChannel>>), + Guild(Rc<RefCell<GuildChannel>>), /// A private channel to another [`User`]. No other users may access the /// channel. For multi-user "private channels", use a group. /// /// [`User`]: struct.User.html - Private(Arc<RwLock<PrivateChannel>>), + Private(Rc<RefCell<PrivateChannel>>), /// A category of [`GuildChannel`]s /// /// [`GuildChannel`]: struct.GuildChannel.html - Category(Arc<RwLock<ChannelCategory>>), + Category(Rc<RefCell<ChannelCategory>>), } impl Channel { - - ///////////////////////////////////////////////////////////////////////// - // Adapter for each variant - ///////////////////////////////////////////////////////////////////////// - /// Converts from `Channel` to `Option<Arc<RwLock<Group>>>`. /// /// Converts `self` into an `Option<Arc<RwLock<Group>>>`, consuming `self`, @@ -88,9 +84,7 @@ impl Channel { /// } /// # } /// ``` - - - pub fn group(self) -> Option<Arc<RwLock<Group>>> { + pub fn group(self) -> Option<Rc<RefCell<Group>>> { match self { Channel::Group(lock) => Some(lock), _ => None, @@ -119,8 +113,7 @@ impl Channel { /// } /// # } /// ``` - - pub fn guild(self) -> Option<Arc<RwLock<GuildChannel>>> { + pub fn guild(self) -> Option<Rc<RefCell<GuildChannel>>> { match self { Channel::Guild(lock) => Some(lock), _ => None, @@ -152,8 +145,7 @@ impl Channel { /// } /// # } /// ``` - - pub fn private(self) -> Option<Arc<RwLock<PrivateChannel>>> { + pub fn private(self) -> Option<Rc<RefCell<PrivateChannel>>> { match self { Channel::Private(lock) => Some(lock), _ => None, @@ -182,8 +174,7 @@ impl Channel { /// } /// # } /// ``` - - pub fn category(self) -> Option<Arc<RwLock<ChannelCategory>>> { + pub fn category(self) -> Option<Rc<RefCell<ChannelCategory>>> { match self { Channel::Category(lock) => Some(lock), _ => None, @@ -205,9 +196,13 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - self.id().create_reaction(message_id, reaction_type) + pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) + -> FutureResult<()> where M: Into<MessageId>, R: Into<ReactionType> { + ftryopt!(ftry!(self.client())).http.create_reaction( + self.id().0, + message_id.into().0, + &reaction_type.into(), + ) } /// Deletes the inner channel. @@ -217,23 +212,21 @@ impl Channel { /// /// [`Group`]: struct.Group.html #[cfg(feature = "model")] - pub fn delete(&self) -> Result<()> { + pub fn delete(&self) -> FutureResult<()> { match *self { Channel::Group(ref group) => { - let _ = group.read().leave()?; + Box::new(group.borrow().leave()) }, Channel::Guild(ref public_channel) => { - let _ = public_channel.read().delete()?; + Box::new(public_channel.borrow().delete().map(|_| ())) }, Channel::Private(ref private_channel) => { - let _ = private_channel.read().delete()?; + Box::new(private_channel.borrow().delete().map(|_| ())) }, Channel::Category(ref category) => { - category.read().delete()?; + Box::new(category.borrow().delete()) }, } - - Ok(()) } /// Deletes a [`Message`] given its Id. @@ -249,8 +242,12 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn delete_message<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - self.id().delete_message(message_id) + pub fn delete_message<M>(&self, message_id: M) -> FutureResult<()> + where M: Into<MessageId> { + ftryopt!(ftry!(self.client())).http.delete_message( + self.id().0, + message_id.into().0, + ) } /// Deletes the given [`Reaction`] from the channel. @@ -263,14 +260,18 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn delete_reaction<M, R>(&self, - message_id: M, - user_id: Option<UserId>, - reaction_type: R) - -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - self.id() - .delete_reaction(message_id, user_id, reaction_type) + pub fn delete_reaction<M, R>( + &self, + message_id: M, + user_id: Option<UserId>, + reaction_type: R, + ) -> FutureResult<()> where M: Into<MessageId>, R: Into<ReactionType> { + ftryopt!(ftry!(self.client())).http.delete_reaction( + self.id().0, + message_id.into().0, + user_id.map(|x| x.0), + &reaction_type.into(), + ) } /// Edits a [`Message`] in the channel given its Id. @@ -295,9 +296,14 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn edit_message<F, M>(&self, message_id: M, f: F) -> Result<Message> + pub fn edit_message<F, M>(&self, message_id: M, f: F) + -> FutureResult<Message> where F: FnOnce(EditMessage) -> EditMessage, M: Into<MessageId> { - self.id().edit_message(message_id, f) + ftryopt!(ftry!(self.client())).http.edit_message( + self.id().0, + message_id.into().0, + f, + ) } /// Determines if the channel is NSFW. @@ -309,8 +315,8 @@ impl Channel { #[inline] pub fn is_nsfw(&self) -> bool { match *self { - Channel::Guild(ref channel) => channel.with(|c| c.is_nsfw()), - Channel::Category(ref category) => category.with(|c| c.is_nsfw()), + Channel::Guild(ref channel) => channel.borrow().is_nsfw(), + Channel::Category(ref category) => category.borrow().is_nsfw(), Channel::Group(_) | Channel::Private(_) => false, } } @@ -323,32 +329,12 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn message<M: Into<MessageId>>(&self, message_id: M) -> Result<Message> { - self.id().message(message_id) - } - - /// Gets messages from the channel. - /// - /// Requires the [Read Message History] permission. - /// - /// # Examples - /// - /// ```rust,ignore - /// use serenity::model::MessageId; - /// - /// let id = MessageId(81392407232380928); - /// - /// // Maximum is 100. - /// let _messages = channel.messages(|g| g.after(id).limit(100)); - /// ``` - /// - /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html - #[cfg(feature = "model")] - #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] - #[inline] - pub fn messages<F>(&self, f: F) -> Result<Vec<Message>> - where F: FnOnce(GetMessages) -> GetMessages { - self.id().messages(f) + pub fn message<M>(&self, message_id: M) -> FutureResult<Message> + where M: Into<MessageId> { + ftryopt!(ftry!(self.client())).http.get_message( + self.id().0, + message_id.into().0, + ) } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a @@ -375,10 +361,15 @@ impl Channel { reaction_type: R, limit: Option<u8>, after: U, - ) -> Result<Vec<User>> where M: Into<MessageId>, - R: Into<ReactionType>, - U: Into<Option<UserId>> { - self.id().reaction_users(message_id, reaction_type, limit, after) + ) -> FutureResult<Vec<User>> + where M: Into<MessageId>, R: Into<ReactionType>, U: Into<Option<UserId>> { + ftryopt!(ftry!(self.client())).http.get_reaction_users( + self.id().0, + message_id.into().0, + &reaction_type.into(), + limit, + after.into().map(|x| x.0), + ) } /// Retrieves the Id of the inner [`Group`], [`GuildChannel`], or @@ -389,10 +380,10 @@ impl Channel { /// [`PrivateChannel`]: struct.PrivateChannel.html pub fn id(&self) -> ChannelId { match *self { - Channel::Group(ref group) => group.with(|g| g.channel_id), - Channel::Guild(ref ch) => ch.with(|c| c.id), - Channel::Private(ref ch) => ch.with(|c| c.id), - Channel::Category(ref category) => category.with(|c| c.id), + Channel::Group(ref group) => group.borrow().channel_id, + Channel::Guild(ref ch) => ch.borrow().id, + Channel::Private(ref ch) => ch.borrow().id, + Channel::Category(ref category) => category.borrow().id, } } @@ -409,7 +400,10 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn say(&self, content: &str) -> Result<Message> { self.id().say(content) } + pub fn say(&self, content: &str) -> FutureResult<Message> { + ftryopt!(ftry!(self.client())).http.send_message(self.id().0, |f| + f.content(content)) + } /// Sends (a) file(s) along with optional message contents. /// @@ -429,13 +423,17 @@ impl Channel { /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[cfg(feature = "model")] - #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] - #[inline] - 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>> { - self.id().send_files(files, f) - } + // todo + // #[cfg(feature = "model")] + // #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] + // #[inline] + // pub fn send_files<'a, F, T, It>(&self, files: It, f: F) + // -> FutureResult<Message> + // where F: FnOnce(CreateMessage) -> CreateMessage, + // T: Into<AttachmentType<'a>>, + // It: IntoIterator<Item = T> { + // ftryopt!(ftry!(self.client())).http.send_files(self.id(), files, f) + // } /// Sends a message to the channel. /// @@ -459,9 +457,9 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn send_message<F>(&self, f: F) -> Result<Message> + pub fn send_message<F>(&self, f: F) -> FutureResult<Message> where F: FnOnce(CreateMessage) -> CreateMessage { - self.id().send_message(f) + ftryopt!(ftry!(self.client())).http.send_message(self.id().0, f) } /// Unpins a [`Message`] in the channel given by its Id. @@ -473,8 +471,20 @@ impl Channel { #[cfg(feature = "model")] #[deprecated(since = "0.4.2", note = "Use the inner channel's method")] #[inline] - pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - self.id().unpin(message_id) + pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<()> { + let retrieve = ftry!(self.client()); + let obtained = ftryopt!(retrieve); + + obtained.http.unpin_message(self.id().0, message_id.into().0) + } + + fn client(&self) -> Result<WrappedClient> { + Ok(match *self { + Channel::Category(ref c) => c.try_borrow()?.client.as_ref().map(Rc::clone), + Channel::Group(ref c) => c.try_borrow()?.client.as_ref().map(Rc::clone), + Channel::Guild(ref c) => c.try_borrow()?.client.as_ref().map(Rc::clone), + Channel::Private(ref c) => c.try_borrow()?.client.as_ref().map(Rc::clone), + }) } } @@ -489,16 +499,16 @@ impl<'de> Deserialize<'de> for Channel { match kind { 0 | 2 => serde_json::from_value::<GuildChannel>(Value::Object(v)) - .map(|x| Channel::Guild(Arc::new(RwLock::new(x)))) + .map(|x| Channel::Guild(Rc::new(RefCell::new(x)))) .map_err(DeError::custom), 1 => serde_json::from_value::<PrivateChannel>(Value::Object(v)) - .map(|x| Channel::Private(Arc::new(RwLock::new(x)))) + .map(|x| Channel::Private(Rc::new(RefCell::new(x)))) .map_err(DeError::custom), 3 => serde_json::from_value::<Group>(Value::Object(v)) - .map(|x| Channel::Group(Arc::new(RwLock::new(x)))) + .map(|x| Channel::Group(Rc::new(RefCell::new(x)))) .map_err(DeError::custom), 4 => serde_json::from_value::<ChannelCategory>(Value::Object(v)) - .map(|x| Channel::Category(Arc::new(RwLock::new(x)))) + .map(|x| Channel::Category(Rc::new(RefCell::new(x)))) .map_err(DeError::custom), _ => Err(DeError::custom("Unknown channel type")), } @@ -510,16 +520,16 @@ impl Serialize for Channel { where S: Serializer { match *self { Channel::Category(ref c) => { - ChannelCategory::serialize(&*c.read(), serializer) + ChannelCategory::serialize(&*c.borrow(), serializer) }, Channel::Group(ref c) => { - Group::serialize(&*c.read(), serializer) + Group::serialize(&*c.borrow(), serializer) }, Channel::Guild(ref c) => { - GuildChannel::serialize(&*c.read(), serializer) + GuildChannel::serialize(&*c.borrow(), serializer) }, Channel::Private(ref c) => { - PrivateChannel::serialize(&*c.read(), serializer) + PrivateChannel::serialize(&*c.borrow(), serializer) }, } } @@ -542,15 +552,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().name(), f), - Channel::Guild(ref ch) => Display::fmt(&ch.read().id.mention(), f), + Channel::Group(ref group) => Display::fmt(&group.borrow().name(), f), + Channel::Guild(ref ch) => Display::fmt(&ch.borrow().id.mention(), f), Channel::Private(ref ch) => { - let channel = ch.read(); - let recipient = channel.recipient.read(); + let channel = ch.borrow(); + let recipient = channel.recipient.borrow(); Display::fmt(&recipient.name, f) }, - Channel::Category(ref category) => Display::fmt(&category.read().name, f), + Channel::Category(ref category) => Display::fmt(&category.borrow().name, f), } } } diff --git a/src/model/channel/private_channel.rs b/src/model/channel/private_channel.rs index 7c8b1e8..48d887b 100644 --- a/src/model/channel/private_channel.rs +++ b/src/model/channel/private_channel.rs @@ -1,14 +1,15 @@ use chrono::{DateTime, FixedOffset}; +use futures::Future; use model::prelude::*; +use std::cell::RefCell; use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::rc::Rc; +use super::super::WrappedClient; use super::deserialize_single_recipient; +use ::FutureResult; #[cfg(feature = "model")] use builder::{CreateMessage, EditMessage, GetMessages}; -#[cfg(feature = "model")] -use http::AttachmentType; -#[cfg(feature = "model")] -use internal::RwLockExt; /// A Direct Message text channel with another user. #[derive(Clone, Debug, Deserialize, Serialize)] @@ -33,14 +34,18 @@ pub struct PrivateChannel { /// The recipient to the private channel. #[serde(deserialize_with = "deserialize_single_recipient", rename = "recipients", - serialize_with = "serialize_sync_user")] - pub recipient: Arc<RwLock<User>>, + serialize_with = "serialize_user")] + pub recipient: Rc<RefCell<User>>, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] impl PrivateChannel { /// Broadcasts that the current user is typing to the recipient. - pub fn broadcast_typing(&self) -> Result<()> { self.id.broadcast_typing() } + pub fn broadcast_typing(&self) -> FutureResult<()> { + ftryopt!(self.client).http.broadcast_typing(self.id.0) + } /// React to a [`Message`] with a custom [`Emoji`] or unicode character. /// @@ -54,16 +59,22 @@ impl PrivateChannel { /// [`Message`]: struct.Message.html /// [`Message::react`]: struct.Message.html#method.react /// [Add Reactions]: permissions/constant.ADD_REACTIONS.html - pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - self.id.create_reaction(message_id, reaction_type) + pub fn create_reaction<M, R>(&self, message_id: M, reaction_type: R) + -> FutureResult<()> where M: Into<MessageId>, R: Into<ReactionType> { + ftryopt!(self.client).http.create_reaction( + self.id.0, + message_id.into().0, + &reaction_type.into(), + ) } /// Deletes the channel. This does not delete the contents of the channel, /// and is equivalent to closing a private channel on the client, which can /// be re-opened. #[inline] - pub fn delete(&self) -> Result<Channel> { self.id.delete() } + pub fn delete(&self) -> FutureResult<Channel> { + ftryopt!(self.client).http.delete_channel(self.id.0) + } /// Deletes all messages by Ids from the given vector in the channel. /// @@ -83,8 +94,11 @@ impl PrivateChannel { /// [`ModelError::BulkDeleteAmount`]: ../enum.ModelError.html#variant.BulkDeleteAmount /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn delete_messages<T: AsRef<MessageId>, It: IntoIterator<Item=T>>(&self, message_ids: It) -> Result<()> { - self.id.delete_messages(message_ids) + pub fn delete_messages<T: AsRef<MessageId>, It: IntoIterator<Item=T>>( + &self, + message_ids: It, + ) -> FutureResult<()> { + ftryopt!(self.client).http.delete_messages(self.id.0, message_ids) } /// Deletes all permission overrides in the channel from a member @@ -94,8 +108,14 @@ impl PrivateChannel { /// /// [Manage Channel]: permissions/constant.MANAGE_CHANNELS.html #[inline] - pub fn delete_permission(&self, permission_type: PermissionOverwriteType) -> Result<()> { - self.id.delete_permission(permission_type) + pub fn delete_permission(&self, permission_type: PermissionOverwriteType) + -> FutureResult<()> { + let overwrite_id = match permission_type { + PermissionOverwriteType::Member(id) => id.0, + PermissionOverwriteType::Role(id) => id.0, + }; + + ftryopt!(self.client).http.delete_permission(self.id.0, overwrite_id) } /// Deletes the given [`Reaction`] from the channel. @@ -106,13 +126,18 @@ impl PrivateChannel { /// [`Reaction`]: struct.Reaction.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn delete_reaction<M, R>(&self, - message_id: M, - user_id: Option<UserId>, - reaction_type: R) - -> Result<()> - where M: Into<MessageId>, R: Into<ReactionType> { - self.id.delete_reaction(message_id, user_id, reaction_type) + pub fn delete_reaction<M: Into<MessageId>, R: Into<ReactionType>>( + &self, + message_id: M, + user_id: Option<UserId>, + reaction_type: R, + ) -> FutureResult<()> { + ftryopt!(self.client).http.delete_reaction( + self.id.0, + message_id.into().0, + user_id.map(|x| x.0), + &reaction_type.into(), + ) } /// Edits a [`Message`] in the channel given its Id. @@ -135,9 +160,14 @@ impl PrivateChannel { /// [`Message`]: struct.Message.html /// [`the limit`]: ../builder/struct.EditMessage.html#method.content #[inline] - pub fn edit_message<F, M>(&self, message_id: M, f: F) -> Result<Message> + pub fn edit_message<F, M>(&self, message_id: M, f: F) + -> FutureResult<Message> where F: FnOnce(EditMessage) -> EditMessage, M: Into<MessageId> { - self.id.edit_message(message_id, f) + ftryopt!(self.client).http.edit_message( + self.id.0, + message_id.into().0, + f, + ) } /// Determines if the channel is NSFW. @@ -157,8 +187,9 @@ impl PrivateChannel { /// /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn message<M: Into<MessageId>>(&self, message_id: M) -> Result<Message> { - self.id.message(message_id) + pub fn message<M: Into<MessageId>>(&self, message_id: M) + -> FutureResult<Message> { + ftryopt!(self.client).http.get_message(self.id.0, message_id.into().0) } /// Gets messages from the channel. @@ -170,13 +201,15 @@ impl PrivateChannel { /// [`Channel::messages`]: enum.Channel.html#method.messages /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn messages<F>(&self, f: F) -> Result<Vec<Message>> - where F: FnOnce(GetMessages) -> GetMessages { - self.id.messages(f) + pub fn messages<'a, F: FnOnce(GetMessages) -> GetMessages>(&'a self, f: F) + -> Box<Future<Item = Vec<Message>, Error = Error> + 'a> { + ftryopt!(self.client).http.get_messages(self.id.0, f) } /// Returns "DM with $username#discriminator". - pub fn name(&self) -> String { format!("DM with {}", self.recipient.with(|r| r.tag())) } + pub fn name(&self) -> String { + format!("DM with {}", self.recipient.borrow().tag()) + } /// Gets the list of [`User`]s who have reacted to a [`Message`] with a /// certain [`Emoji`]. @@ -191,27 +224,37 @@ impl PrivateChannel { /// [`User`]: struct.User.html /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn reaction_users<M, R, U>(&self, + pub fn reaction_users<M, R, U>( + &self, message_id: M, reaction_type: R, limit: Option<u8>, after: U, - ) -> Result<Vec<User>> where M: Into<MessageId>, - R: Into<ReactionType>, - U: Into<Option<UserId>> { - self.id.reaction_users(message_id, reaction_type, limit, after) + ) -> FutureResult<Vec<User>> + where M: Into<MessageId>, R: Into<ReactionType>, U: Into<Option<UserId>> { + ftryopt!(self.client).http.get_reaction_users( + self.id.0, + message_id.into().0, + &reaction_type.into(), + limit, + after.into().map(|x| x.0), + ) } /// Pins a [`Message`] to the channel. /// /// [`Message`]: struct.Message.html #[inline] - pub fn pin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { self.id.pin(message_id) } + pub fn pin<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<()> { + ftryopt!(self.client).http.pin_message(self.id.0, message_id.into().0) + } /// Retrieves the list of messages that have been pinned in the private /// channel. #[inline] - pub fn pins(&self) -> Result<Vec<Message>> { self.id.pins() } + pub fn pins(&self) -> FutureResult<Vec<Message>> { + ftryopt!(self.client).http.get_pins(self.id.0) + } /// Sends a message with just the given message content in the channel. /// @@ -224,7 +267,12 @@ impl PrivateChannel { /// [`ChannelId`]: ../model/id/struct.ChannelId.html /// [`ModelError::MessageTooLong`]: enum.ModelError.html#variant.MessageTooLong #[inline] - pub fn say<D: ::std::fmt::Display>(&self, content: D) -> Result<Message> { self.id.say(content) } + pub fn say<D: ::std::fmt::Display>(&self, content: D) + -> FutureResult<Message> { + ftryopt!(self.client) + .http + .send_message(self.id.0, |m| m.content(content)) + } /// Sends (a) file(s) along with optional message contents. /// @@ -244,11 +292,15 @@ impl PrivateChannel { /// [`ClientError::MessageTooLong`]: ../client/enum.ClientError.html#variant.MessageTooLong /// [Attach Files]: permissions/constant.ATTACH_FILES.html /// [Send Messages]: permissions/constant.SEND_MESSAGES.html - #[inline] - 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>> { - self.id.send_files(files, f) - } + // todo + // #[inline] + // pub fn send_files<'a, F, T, It>(&self, files: It, f: F) + // -> FutureResult<Message> + // where F: FnOnce(CreateMessage) -> CreateMessage, + // T: Into<AttachmentType<'a>>, + // It: IntoIterator<Item=T> { + // ftryopt!(self.client).http.send_files(self.id.0, files, f) + // } /// Sends a message to the channel with the given content. /// @@ -265,8 +317,9 @@ impl PrivateChannel { /// [`CreateMessage`]: ../builder/struct.CreateMessage.html /// [`Message`]: struct.Message.html #[inline] - pub fn send_message<F: FnOnce(CreateMessage) -> CreateMessage>(&self, f: F) -> Result<Message> { - self.id.send_message(f) + pub fn send_message<F: FnOnce(CreateMessage) -> CreateMessage>(&self, f: F) + -> FutureResult<Message> { + ftryopt!(self.client).http.send_message(self.id.0, f) } /// Unpins a [`Message`] in the channel given by its Id. @@ -276,14 +329,14 @@ impl PrivateChannel { /// [`Message`]: struct.Message.html /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html #[inline] - pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> Result<()> { - self.id.unpin(message_id) + pub fn unpin<M: Into<MessageId>>(&self, message_id: M) -> FutureResult<()> { + ftryopt!(self.client).http.unpin_message(self.id.0, message_id.into().0) } } 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().name) + f.write_str(&self.recipient.borrow().name) } } diff --git a/src/model/channel/reaction.rs b/src/model/channel/reaction.rs index 42c3578..28ddb8d 100644 --- a/src/model/channel/reaction.rs +++ b/src/model/channel/reaction.rs @@ -1,16 +1,14 @@ +use futures::future; use model::prelude::*; use serde::de::{Deserialize, Error as DeError, MapAccess, Visitor}; use serde::ser::{SerializeMap, Serialize, Serializer}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult, Write as FmtWrite}; use std::str::FromStr; +use super::super::WrappedClient; +use ::FutureResult; use internal::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; -#[cfg(feature = "model")] -use http; - /// An emoji reaction to a message. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Reaction { @@ -29,6 +27,8 @@ pub struct Reaction { /// /// [`User`]: struct.User.html pub user_id: UserId, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -43,8 +43,8 @@ impl Reaction { /// /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html #[inline] - pub fn channel(&self) -> Result<Channel> { - self.channel_id.get() + pub fn channel(&self) -> FutureResult<Channel> { + ftryopt!(self.client).http.get_channel(self.channel_id.0) } /// Deletes the reaction, but only if the current user is the user who made @@ -62,36 +62,45 @@ impl Reaction { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Messages]: permissions/constant.MANAGE_MESSAGES.html /// [permissions]: permissions - pub fn delete(&self) -> Result<()> { - let user_id = feature_cache! { - { - let user = if self.user_id == CACHE.read().user.id { - None - } else { - Some(self.user_id.0) - }; - - // If the reaction is one _not_ made by the current user, then ensure - // that the current user has permission* to delete the reaction. - // - // Normally, users can only delete their own reactions. - // - // * The `Manage Messages` permission. - if user.is_some() { - let req = Permissions::MANAGE_MESSAGES; - - if !utils::user_has_perms(self.channel_id, req).unwrap_or(true) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } + pub fn delete(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + + let user_id = feature_cache! {{ + let cache = ftry!(client.cache.try_borrow()); - user + let user = if self.user_id == cache.user.id { + None } else { Some(self.user_id.0) + }; + + // If the reaction is one _not_ made by the current user, then ensure + // that the current user has permission* to delete the reaction. + // + // Normally, users can only delete their own reactions. + // + // * The `Manage Messages` permission. + if user.is_some() { + let req = Permissions::MANAGE_MESSAGES; + + if !cache.user_has_perms(self.channel_id, req).unwrap_or(true) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); + } } - }; - http::delete_reaction(self.channel_id.0, self.message_id.0, user_id, &self.emoji) + user + } else { + Some(self.user_id.0) + }}; + + ftryopt!(self.client).http.delete_reaction( + self.channel_id.0, + self.message_id.0, + user_id, + &self.emoji, + ) } /// Retrieves the [`Message`] associated with this reaction. @@ -105,8 +114,11 @@ impl Reaction { /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html /// [`Message`]: struct.Message.html #[inline] - pub fn message(&self) -> Result<Message> { - self.channel_id.message(self.message_id) + pub fn message(&self) -> FutureResult<Message> { + ftryopt!(self.client).http.get_message( + self.channel_id.0, + self.message_id.0, + ) } /// Retrieves the user that made the reaction. @@ -115,8 +127,8 @@ impl Reaction { /// If not - or the user was not found - this will perform a request over /// the REST API for the user. #[inline] - pub fn user(&self) -> Result<User> { - self.user_id.get() + pub fn user(&self) -> FutureResult<User> { + ftryopt!(self.client).http.get_user(self.user_id.0) } /// Retrieves the list of [`User`]s who have reacted to a [`Message`] with a @@ -144,17 +156,18 @@ impl Reaction { /// [`User`]: struct.User.html /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html /// [permissions]: permissions - pub fn users<R, U>(&self, - reaction_type: R, - limit: Option<u8>, - after: Option<U>) - -> Result<Vec<User>> + pub fn users<R, U>( + &self, + reaction_type: R, + limit: Option<u8>, + after: Option<U>, + ) -> FutureResult<Vec<User>> where R: Into<ReactionType>, U: Into<UserId> { - http::get_reaction_users( + ftryopt!(self.client).http.get_reaction_users( self.channel_id.0, self.message_id.0, &reaction_type.into(), - limit.unwrap_or(50), + limit, after.map(|u| u.into().0), ) } diff --git a/src/model/error.rs b/src/model/error.rs index ea919bc..58fe647 100644 --- a/src/model/error.rs +++ b/src/model/error.rs @@ -73,6 +73,9 @@ pub enum Error { /// When attempting to delete below or above the minimum and maximum allowed /// number of messages. BulkDeleteAmount, + /// The client wasn't present on the model when it was expected to be, e.g. + /// when performing a cache or HTTP operation on it. + ClientNotPresent, /// When attempting to delete a number of days' worth of messages that is /// not allowed. DeleteMessageDaysAmount(u8), @@ -131,6 +134,7 @@ impl StdError for Error { fn description(&self) -> &str { match *self { Error::BulkDeleteAmount => "Too few/many messages to bulk delete", + Error::ClientNotPresent => "The client wasn't present on the model", Error::DeleteMessageDaysAmount(_) => "Invalid delete message days", Error::EmbedTooLarge(_) => "Embed too large", Error::GuildNotFound => "Guild not found in the cache", diff --git a/src/model/event.rs b/src/model/event.rs index a47d36b..fe710da 100644 --- a/src/model/event.rs +++ b/src/model/event.rs @@ -10,15 +10,6 @@ use super::prelude::*; use constants::{OpCode, VoiceOpCode}; use internal::prelude::*; -#[cfg(feature = "cache")] -use cache::{Cache, CacheUpdate}; -#[cfg(feature = "cache")] -use internal::RwLockExt; -#[cfg(feature = "cache")] -use std::collections::hash_map::Entry; -#[cfg(feature = "cache")] -use std::mem; - /// Event data for the channel creation event. /// /// This is fired when: @@ -52,107 +43,11 @@ impl Serialize for ChannelCreateEvent { } } -#[cfg(feature = "cache")] -impl CacheUpdate for ChannelCreateEvent { - type Output = Channel; - - fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { - match self.channel { - Channel::Group(ref group) => { - let group = Arc::clone(group); - - let channel_id = group.with_mut(|writer| { - for (recipient_id, recipient) in &mut writer.recipients { - cache.update_user_entry(&recipient.read()); - - *recipient = Arc::clone(&cache.users[recipient_id]); - } - - writer.channel_id - }); - - let ch = cache.groups.insert(channel_id, group); - - ch.map(Channel::Group) - }, - Channel::Guild(ref channel) => { - let (guild_id, channel_id) = channel.with(|channel| (channel.guild_id, channel.id)); - - cache.channels.insert(channel_id, Arc::clone(channel)); - - cache - .guilds - .get_mut(&guild_id) - .and_then(|guild| { - guild - .with_mut(|guild| guild.channels.insert(channel_id, Arc::clone(channel))) - }) - .map(Channel::Guild) - }, - Channel::Private(ref channel) => { - if let Some(channel) = cache.private_channels.get(&channel.with(|c| c.id)) { - return Some(Channel::Private(Arc::clone(&(*channel)))); - } - - let channel = Arc::clone(channel); - - let id = channel.with_mut(|writer| { - let user_id = writer.recipient.with_mut(|user| { - cache.update_user_entry(user); - - user.id - }); - - writer.recipient = Arc::clone(&cache.users[&user_id]); - writer.id - }); - - let ch = cache.private_channels.insert(id, Arc::clone(&channel)); - ch.map(Channel::Private) - }, - Channel::Category(ref category) => cache - .categories - .insert(category.read().id, Arc::clone(category)) - .map(Channel::Category), - } - } -} - #[derive(Clone, Debug)] pub struct ChannelDeleteEvent { pub channel: Channel, } -#[cfg(feature = "cache")] -impl CacheUpdate for ChannelDeleteEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - match self.channel { - Channel::Guild(ref channel) => { - let (guild_id, channel_id) = channel.with(|channel| (channel.guild_id, channel.id)); - - cache.channels.remove(&channel_id); - - cache - .guilds - .get_mut(&guild_id) - .and_then(|guild| guild.with_mut(|g| g.channels.remove(&channel_id))); - }, - Channel::Category(ref category) => { - let channel_id = category.with(|cat| cat.id); - - cache.categories.remove(&channel_id); - }, - // We ignore these two due to the fact that the delete event for dms/groups - // will _not_ fire anymore. - Channel::Private(_) | Channel::Group(_) => unreachable!(), - }; - - None - } -} - impl<'de> Deserialize<'de> for ChannelDeleteEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -174,143 +69,23 @@ pub struct ChannelPinsUpdateEvent { pub last_pin_timestamp: Option<DateTime<FixedOffset>>, } -#[cfg(feature = "cache")] -impl CacheUpdate for ChannelPinsUpdateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - if let Some(channel) = cache.channels.get(&self.channel_id) { - channel.with_mut(|c| { - c.last_pin_timestamp = self.last_pin_timestamp; - }); - - return None; - } - - if let Some(channel) = cache.private_channels.get_mut(&self.channel_id) { - channel.with_mut(|c| { - c.last_pin_timestamp = self.last_pin_timestamp; - }); - - return None; - } - - if let Some(group) = cache.groups.get_mut(&self.channel_id) { - group.with_mut(|c| { - c.last_pin_timestamp = self.last_pin_timestamp; - }); - - return None; - } - - None - } -} - - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ChannelRecipientAddEvent { pub channel_id: ChannelId, pub user: User, } -#[cfg(feature = "cache")] -impl CacheUpdate for ChannelRecipientAddEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.update_user_entry(&self.user); - let user = Arc::clone(&cache.users[&self.user.id]); - - cache.groups.get_mut(&self.channel_id).map(|group| { - group.write().recipients.insert(self.user.id, user); - }); - - None - } -} - - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ChannelRecipientRemoveEvent { pub channel_id: ChannelId, pub user: User, } -#[cfg(feature = "cache")] -impl CacheUpdate for ChannelRecipientRemoveEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.groups.get_mut(&self.channel_id).map(|group| { - group.with_mut(|g| g.recipients.remove(&self.user.id)) - }); - - None - } -} - #[derive(Clone, Debug)] pub struct ChannelUpdateEvent { pub channel: Channel, } -#[cfg(feature = "cache")] -impl CacheUpdate for ChannelUpdateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - match self.channel { - Channel::Group(ref group) => { - let (ch_id, no_recipients) = - group.with(|g| (g.channel_id, g.recipients.is_empty())); - - match cache.groups.entry(ch_id) { - Entry::Vacant(e) => { - e.insert(Arc::clone(group)); - }, - Entry::Occupied(mut e) => { - let mut dest = e.get_mut().write(); - - if no_recipients { - let recipients = mem::replace(&mut dest.recipients, HashMap::new()); - - dest.clone_from(&group.read()); - - dest.recipients = recipients; - } else { - dest.clone_from(&group.read()); - } - }, - } - }, - Channel::Guild(ref channel) => { - let (guild_id, channel_id) = channel.with(|channel| (channel.guild_id, channel.id)); - - cache.channels.insert(channel_id, Arc::clone(channel)); - cache.guilds.get_mut(&guild_id).map(|guild| { - guild - .with_mut(|g| g.channels.insert(channel_id, Arc::clone(channel))) - }); - }, - Channel::Private(ref channel) => { - cache - .private_channels - .get_mut(&channel.read().id) - .map(|private| private.clone_from(channel)); - }, - Channel::Category(ref category) => { - cache - .categories - .get_mut(&category.read().id) - .map(|c| c.clone_from(category)); - }, - } - - None - } -} - impl<'de> Deserialize<'de> for ChannelUpdateEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -343,31 +118,6 @@ pub struct GuildCreateEvent { pub guild: Guild, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildCreateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.unavailable_guilds.remove(&self.guild.id); - - let mut guild = self.guild.clone(); - - for (user_id, member) in &mut guild.members { - cache.update_user_entry(&member.user.read()); - let user = Arc::clone(&cache.users[user_id]); - - member.user = Arc::clone(&user); - } - - cache.channels.extend(guild.channels.clone()); - cache - .guilds - .insert(self.guild.id, Arc::new(RwLock::new(guild))); - - None - } -} - impl<'de> Deserialize<'de> for GuildCreateEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -388,22 +138,6 @@ pub struct GuildDeleteEvent { pub guild: PartialGuild, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildDeleteEvent { - type Output = Arc<RwLock<Guild>>; - - 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().channels.keys() { - cache.channels.remove(channel_id); - } - - guild - }) - } -} - impl<'de> Deserialize<'de> for GuildDeleteEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -425,21 +159,6 @@ pub struct GuildEmojisUpdateEvent { pub guild_id: GuildId, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildEmojisUpdateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.guilds.get_mut(&self.guild_id).map(|guild| { - guild.with_mut(|g| { - g.emojis.clone_from(&self.emojis) - }); - }); - - None - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GuildIntegrationsUpdateEvent { pub guild_id: GuildId, @@ -451,28 +170,6 @@ pub struct GuildMemberAddEvent { pub member: Member, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildMemberAddEvent { - type Output = (); - - 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()); - - // Always safe due to being inserted above. - self.member.user = Arc::clone(&cache.users[&user_id]); - - cache.guilds.get_mut(&self.guild_id).map(|guild| { - guild.with_mut(|guild| { - guild.member_count += 1; - guild.members.insert(user_id, self.member.clone()); - }) - }); - - None - } -} - impl<'de> Deserialize<'de> for GuildMemberAddEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { let map = JsonMap::deserialize(deserializer)?; @@ -496,20 +193,6 @@ pub struct GuildMemberRemoveEvent { pub user: User, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildMemberRemoveEvent { - type Output = Member; - - fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { - cache.guilds.get_mut(&self.guild_id).and_then(|guild| { - guild.with_mut(|guild| { - guild.member_count -= 1; - guild.members.remove(&self.user.id) - }) - }) - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GuildMemberUpdateEvent { pub guild_id: GuildId, @@ -518,77 +201,12 @@ pub struct GuildMemberUpdateEvent { pub user: User, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildMemberUpdateEvent { - type Output = Member; - - fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { - cache.update_user_entry(&self.user); - - if let Some(guild) = cache.guilds.get_mut(&self.guild_id) { - let mut guild = guild.write(); - - let mut found = false; - - let item = if let Some(member) = guild.members.get_mut(&self.user.id) { - let item = Some(member.clone()); - - member.nick.clone_from(&self.nick); - member.roles.clone_from(&self.roles); - member.user.write().clone_from(&self.user); - - found = true; - - item - } else { - None - }; - - if !found { - guild.members.insert( - self.user.id, - Member { - deaf: false, - guild_id: self.guild_id, - joined_at: None, - mute: false, - nick: self.nick.clone(), - roles: self.roles.clone(), - user: Arc::new(RwLock::new(self.user.clone())), - }, - ); - } - - item - } else { - None - } - } -} - #[derive(Clone, Debug, Serialize)] pub struct GuildMembersChunkEvent { pub guild_id: GuildId, pub members: HashMap<UserId, Member>, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildMembersChunkEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - for member in self.members.values() { - cache.update_user_entry(&member.user.read()); - } - - cache.guilds.get_mut(&self.guild_id).map(|guild| { - guild.with_mut(|g| g.members.extend(self.members.clone())) - }); - - None - } -} - impl<'de> Deserialize<'de> for GuildMembersChunkEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { let mut map = JsonMap::deserialize(deserializer)?; @@ -627,105 +245,28 @@ pub struct GuildRoleCreateEvent { pub role: Role, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildRoleCreateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.guilds.get_mut(&self.guild_id).map(|guild| { - guild - .write() - .roles - .insert(self.role.id, self.role.clone()) - }); - - None - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GuildRoleDeleteEvent { pub guild_id: GuildId, pub role_id: RoleId, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildRoleDeleteEvent { - type Output = Role; - - fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { - cache - .guilds - .get_mut(&self.guild_id) - .and_then(|guild| guild.with_mut(|g| g.roles.remove(&self.role_id))) - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GuildRoleUpdateEvent { pub guild_id: GuildId, pub role: Role, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildRoleUpdateEvent { - type Output = Role; - - fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { - cache.guilds.get_mut(&self.guild_id).and_then(|guild| { - guild.with_mut(|g| { - g.roles - .get_mut(&self.role.id) - .map(|role| mem::replace(role, self.role.clone())) - }) - }) - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GuildUnavailableEvent { #[serde(rename = "id")] pub guild_id: GuildId, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildUnavailableEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.unavailable_guilds.insert(self.guild_id); - cache.guilds.remove(&self.guild_id); - - None - } -} - #[derive(Clone, Debug)] pub struct GuildUpdateEvent { pub guild: PartialGuild, } -#[cfg(feature = "cache")] -impl CacheUpdate for GuildUpdateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.guilds.get_mut(&self.guild.id).map(|guild| { - let mut guild = guild.write(); - - guild.afk_timeout = self.guild.afk_timeout; - guild.afk_channel_id.clone_from(&self.guild.afk_channel_id); - guild.icon.clone_from(&self.guild.icon); - guild.name.clone_from(&self.guild.name); - guild.owner_id.clone_from(&self.guild.owner_id); - guild.region.clone_from(&self.guild.region); - guild.roles.clone_from(&self.guild.roles); - guild.verification_level = self.guild.verification_level; - }); - - None - } -} - impl<'de> Deserialize<'de> for GuildUpdateEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -799,62 +340,6 @@ pub struct PresenceUpdateEvent { pub roles: Option<Vec<RoleId>>, } -#[cfg(feature = "cache")] -impl CacheUpdate for PresenceUpdateEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - let user_id = self.presence.user_id; - - if let Some(user) = self.presence.user.as_mut() { - 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(); - - // If the member went offline, remove them from the presence list. - if self.presence.status == OnlineStatus::Offline { - guild.presences.remove(&self.presence.user_id); - } else { - guild - .presences - .insert(self.presence.user_id, self.presence.clone()); - } - - // Create a partial member instance out of the presence update - // data. This includes everything but `deaf`, `mute`, and - // `joined_at`. - if !guild.members.contains_key(&self.presence.user_id) { - if let Some(user) = self.presence.user.as_ref() { - let roles = self.roles.clone().unwrap_or_default(); - - guild.members.insert(self.presence.user_id, Member { - deaf: false, - guild_id: guild_id, - joined_at: None, - mute: false, - nick: self.presence.nick.clone(), - user: Arc::clone(&user), - roles, - }); - } - } - } - } else if self.presence.status == OnlineStatus::Offline { - cache.presences.remove(&self.presence.user_id); - } else { - cache - .presences - .insert(self.presence.user_id, self.presence.clone()); - } - - None - } -} - impl<'de> Deserialize<'de> for PresenceUpdateEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { let mut map = JsonMap::deserialize(deserializer)?; @@ -885,25 +370,6 @@ pub struct PresencesReplaceEvent { pub presences: Vec<Presence>, } -#[cfg(feature = "cache")] -impl CacheUpdate for PresencesReplaceEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - cache.presences.extend({ - let mut p: HashMap<UserId, Presence> = HashMap::default(); - - for presence in &self.presences { - p.insert(presence.user_id, presence.clone()); - } - - p - }); - - None - } -} - impl<'de> Deserialize<'de> for PresencesReplaceEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { let presences: Vec<Presence> = Deserialize::deserialize(deserializer)?; @@ -979,46 +445,6 @@ pub struct ReadyEvent { pub ready: Ready, } -#[cfg(feature = "cache")] -impl CacheUpdate for ReadyEvent { - type Output = (); - - fn update(&mut self, cache: &mut Cache) -> Option<()> { - let mut ready = self.ready.clone(); - - for guild in ready.guilds { - match guild { - GuildStatus::Offline(unavailable) => { - cache.guilds.remove(&unavailable.id); - cache.unavailable_guilds.insert(unavailable.id); - }, - GuildStatus::OnlineGuild(guild) => { - cache.unavailable_guilds.remove(&guild.id); - cache.guilds.insert(guild.id, Arc::new(RwLock::new(guild))); - }, - GuildStatus::OnlinePartialGuild(_) => {}, - } - } - - // `ready.private_channels` will always be empty, and possibly be removed in the future. - // So don't handle it at all. - - for (user_id, presence) in &mut ready.presences { - if let Some(ref user) = presence.user { - cache.update_user_entry(&user.read()); - } - - presence.user = cache.users.get(user_id).cloned(); - } - - cache.presences.extend(ready.presences); - cache.shard_count = ready.shard.map_or(1, |s| s[1]); - cache.user = ready.user; - - None - } -} - impl<'de> Deserialize<'de> for ReadyEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -1057,15 +483,6 @@ pub struct UserUpdateEvent { pub current_user: CurrentUser, } -#[cfg(feature = "cache")] -impl CacheUpdate for UserUpdateEvent { - type Output = CurrentUser; - - fn update(&mut self, cache: &mut Cache) -> Option<Self::Output> { - Some(mem::replace(&mut cache.user, self.current_user.clone())) - } -} - impl<'de> Deserialize<'de> for UserUpdateEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { Ok(Self { @@ -1095,43 +512,6 @@ pub struct VoiceStateUpdateEvent { pub voice_state: VoiceState, } -#[cfg(feature = "cache")] -impl CacheUpdate for VoiceStateUpdateEvent { - type Output = (); - - 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(); - - if self.voice_state.channel_id.is_some() { - // Update or add to the voice state list - { - let finding = guild.voice_states.get_mut(&self.voice_state.user_id); - - if let Some(srv_state) = finding { - srv_state.clone_from(&self.voice_state); - - return None; - } - } - - guild - .voice_states - .insert(self.voice_state.user_id, self.voice_state.clone()); - } else { - // Remove the user from the voice state list - guild.voice_states.remove(&self.voice_state.user_id); - } - } - - return None; - } - - None - } -} - impl<'de> Deserialize<'de> for VoiceStateUpdateEvent { fn deserialize<D: Deserializer<'de>>(deserializer: D) -> StdResult<Self, D::Error> { let map = JsonMap::deserialize(deserializer)?; diff --git a/src/model/gateway.rs b/src/model/gateway.rs index 6ad2cda..ad30bab 100644 --- a/src/model/gateway.rs +++ b/src/model/gateway.rs @@ -1,10 +1,10 @@ //! Models pertaining to the gateway. -use parking_lot::RwLock; use serde::de::Error as DeError; use serde::ser::{SerializeStruct, Serialize, Serializer}; use serde_json; -use std::sync::Arc; +use std::cell::RefCell; +use std::rc::Rc; use super::utils::*; use super::prelude::*; @@ -209,7 +209,7 @@ pub struct Presence { /// date. pub user_id: UserId, /// The associated user instance. - pub user: Option<Arc<RwLock<User>>>, + pub user: Option<Rc<RefCell<User>>>, } impl<'de> Deserialize<'de> for Presence { @@ -224,7 +224,7 @@ impl<'de> Deserialize<'de> for Presence { let user = User::deserialize(Value::Object(user_map)) .map_err(DeError::custom)?; - (user.id, Some(Arc::new(RwLock::new(user)))) + (user.id, Some(Rc::new(RefCell::new(user)))) } else { let user_id = user_map .remove("id") @@ -280,7 +280,7 @@ impl Serialize for Presence { state.serialize_field("status", &self.status)?; if let Some(ref user) = self.user { - state.serialize_field("user", &*user.read())?; + state.serialize_field("user", &*user.borrow())?; } else { state.serialize_field("user", &UserId { id: self.user_id.0, @@ -295,10 +295,14 @@ impl Serialize for Presence { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Ready { pub guilds: Vec<GuildStatus>, - #[serde(default, deserialize_with = "deserialize_presences")] - pub presences: HashMap<UserId, Presence>, - #[serde(default, deserialize_with = "deserialize_private_channels")] - pub private_channels: HashMap<ChannelId, Channel>, + #[serde(default, + deserialize_with = "deserialize_presences", + serialize_with = "serialize_gen_rc_map")] + pub presences: HashMap<UserId, Rc<RefCell<Presence>>>, + #[serde(default, + deserialize_with = "deserialize_private_channels", + serialize_with = "serialize_gen_rc_map")] + pub private_channels: HashMap<ChannelId, Rc<RefCell<Channel>>>, pub session_id: String, pub shard: Option<[u64; 2]>, #[serde(default, rename = "_trace")] diff --git a/src/model/guild/emoji.rs b/src/model/guild/emoji.rs index 23a174d..48d9b7d 100644 --- a/src/model/guild/emoji.rs +++ b/src/model/guild/emoji.rs @@ -1,16 +1,17 @@ use std::fmt::{Display, Formatter, Result as FmtResult, Write as FmtWrite}; use super::super::id::{EmojiId, RoleId}; +use super::super::WrappedClient; +#[cfg(feature = "cache")] +use futures::{Future, future}; #[cfg(all(feature = "cache", feature = "model"))] use internal::prelude::*; #[cfg(all(feature = "cache", feature = "model"))] -use std::mem; -#[cfg(all(feature = "cache", feature = "model"))] use super::super::ModelError; #[cfg(all(feature = "cache", feature = "model"))] use super::super::id::GuildId; -#[cfg(all(feature = "cache", feature = "model"))] -use {CACHE, http}; +#[cfg(feature = "cache")] +use ::FutureResult; /// Represents a custom guild emoji, which can either be created using the API, /// or via an integration. Emojis created using the API only work within the @@ -37,6 +38,8 @@ pub struct Emoji { /// /// [`Role`]: struct.Role.html pub roles: Vec<RoleId>, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -73,11 +76,15 @@ impl Emoji { /// } /// ``` #[cfg(feature = "cache")] - pub fn delete(&self) -> Result<()> { - match self.find_guild_id() { - Some(guild_id) => http::delete_emoji(guild_id.0, self.id.0), - None => Err(Error::Model(ModelError::ItemMissing)), - } + pub fn delete(&self) -> FutureResult<()> { + let guild_id = match self.find_guild_id() { + Some(guild_id) => guild_id, + None => return Box::new(future::err(Error::Model( + ModelError::ItemMissing, + ))), + }; + + ftryopt!(self.client).http.delete_emoji(guild_id.0, self.id.0) } /// Edits the emoji by updating it with a new name. @@ -110,24 +117,16 @@ impl Emoji { /// assert_eq!(emoji.name, "blobuwu"); /// ``` #[cfg(feature = "cache")] - pub fn edit(&mut self, name: &str) -> Result<()> { - match self.find_guild_id() { - Some(guild_id) => { - let map = json!({ - "name": name, - }); - - match http::edit_emoji(guild_id.0, self.id.0, &map) { - Ok(emoji) => { - mem::replace(self, emoji); + pub fn edit<'a>(&'a mut self, name: &'a str) + -> Box<Future<Item = Emoji, Error = Error> + 'a> { + let guild_id = match self.find_guild_id() { + Some(guild_id) => guild_id, + None => return Box::new(future::err(Error::Model( + ModelError::ItemMissing, + ))), + }; - Ok(()) - }, - Err(why) => Err(why), - } - }, - None => Err(Error::Model(ModelError::ItemMissing)), - } + ftryopt!(self.client).http.edit_emoji(guild_id.0, self.id.0, name) } /// Finds the [`Guild`] that owns the emoji by looking through the Cache. @@ -158,8 +157,8 @@ impl Emoji { /// ``` #[cfg(feature = "cache")] pub fn find_guild_id(&self) -> Option<GuildId> { - for guild in CACHE.read().guilds.values() { - let guild = guild.read(); + for guild in self.client.as_ref()?.cache.borrow().guilds.values() { + let guild = guild.borrow(); if guild.emojis.contains_key(&self.id) { return Some(guild.id); diff --git a/src/model/guild/guild_id.rs b/src/model/guild/guild_id.rs index adfac05..ea095a0 100644 --- a/src/model/guild/guild_id.rs +++ b/src/model/guild/guild_id.rs @@ -1,16 +1,5 @@ use model::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; -#[cfg(feature = "model")] -use builder::{EditGuild, EditMember, EditRole}; -#[cfg(feature = "model")] -use internal::prelude::*; -#[cfg(feature = "model")] -use model::guild::BanOptions; -#[cfg(feature = "model")] -use {http, utils}; - #[cfg(feature = "model")] impl GuildId { /// Converts the guild Id into the default channel's Id. @@ -18,459 +7,10 @@ impl GuildId { #[deprecated(note = "The concept of default channels is no more, use \ `Guild::default_channel{_guaranteed}` to simulate the concept.")] - pub fn as_channel_id(&self) -> ChannelId { ChannelId(self.0) } - - /// Ban a [`User`] from the guild. All messages by the - /// user within the last given number of days given will be deleted. - /// - /// Refer to the documentation for [`Guild::ban`] for more information. - /// - /// **Note**: Requires the [Ban Members] permission. - /// - /// # Examples - /// - /// Ban a member and remove all messages they've sent in the last 4 days: - /// - /// ```rust,ignore - /// use serenity::model::GuildId; - /// - /// // assuming a `user` has already been bound - /// let _ = GuildId(81384788765712384).ban(user, 4); - /// ``` - /// - /// # Errors - /// - /// Returns a [`ModelError::DeleteMessageDaysAmount`] if the number of - /// days' worth of messages to delete is over the maximum. - /// - /// [`ModelError::DeleteMessageDaysAmount`]: - /// enum.ModelError.html#variant.DeleteMessageDaysAmount - /// [`Guild::ban`]: struct.Guild.html#method.ban - /// [`User`]: struct.User.html - /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - pub fn ban<U, BO>(&self, user: U, ban_options: &BO) -> Result<()> - where U: Into<UserId>, BO: BanOptions { - let dmd = ban_options.dmd(); - if dmd > 7 { - return Err(Error::Model(ModelError::DeleteMessageDaysAmount(dmd))); - } - - let reason = ban_options.reason(); - - if reason.len() > 512 { - return Err(Error::ExceededLimit(reason.to_string(), 512)); - } - - http::ban_user(self.0, user.into().0, dmd, &*reason) - } - - /// Gets a list of the guild's bans. - /// - /// Requires the [Ban Members] permission. - /// - /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - #[inline] - pub fn bans(&self) -> Result<Vec<Ban>> { http::get_bans(self.0) } - - /// Gets a list of the guild's audit log entries - #[inline] - 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. - /// - /// [`Guild`]: struct.Guild.html - pub fn channels(&self) -> Result<HashMap<ChannelId, GuildChannel>> { - let mut channels = HashMap::new(); - - for channel in http::get_channels(self.0)? { - channels.insert(channel.id, channel); - } - - Ok(channels) - } - - /// Creates a [`GuildChannel`] in the the guild. - /// - /// Refer to [`http::create_channel`] for more information. - /// - /// Requires the [Manage Channels] permission. - /// - /// # Examples - /// - /// Create a voice channel in a guild with the name `test`: - /// - /// ```rust,ignore - /// use serenity::model::{ChannelType, GuildId}; - /// - /// let _channel = GuildId(7).create_channel("test", ChannelType::Voice, None); - /// ``` - /// - /// [`GuildChannel`]: struct.GuildChannel.html - /// [`http::create_channel`]: ../http/fn.create_channel.html - /// [Manage Channels]: permissions/constant.MANAGE_CHANNELS.html - pub fn create_channel<C>(&self, name: &str, kind: ChannelType, category: C) -> Result<GuildChannel> - where C: Into<Option<ChannelId>> { - let map = json!({ - "name": name, - "type": kind as u8, - "parent_id": category.into().map(|c| c.0) - }); - - http::create_channel(self.0, &map) - } - - /// Creates an emoji in the guild with a name and base64-encoded image. - /// - /// Refer to the documentation for [`Guild::create_emoji`] for more - /// information. - /// - /// Requires the [Manage Emojis] permission. - /// - /// # Examples - /// - /// See the [`EditProfile::avatar`] example for an in-depth example as to - /// how to read an image from the filesystem and encode it as base64. Most - /// of the example can be applied similarly for this method. - /// - /// [`EditProfile::avatar`]: ../builder/struct.EditProfile.html#method.avatar - /// [`Guild::create_emoji`]: struct.Guild.html#method.create_emoji - /// [`utils::read_image`]: ../utils/fn.read_image.html - /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html - pub fn create_emoji(&self, name: &str, image: &str) -> Result<Emoji> { - let map = json!({ - "name": name, - "image": image, - }); - - http::create_emoji(self.0, &map) - } - - /// Creates an integration for the guild. - /// - /// Requires the [Manage Guild] permission. - /// - /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - pub fn create_integration<I>(&self, integration_id: I, kind: &str) -> Result<()> - where I: Into<IntegrationId> { - let integration_id = integration_id.into(); - let map = json!({ - "id": integration_id.0, - "type": kind, - }); - - http::create_guild_integration(self.0, integration_id.0, &map) - } - - /// Creates a new role in the guild with the data set, if any. - /// - /// See the documentation for [`Guild::create_role`] on how to use this. - /// - /// **Note**: Requires the [Manage Roles] permission. - /// - /// [`Guild::create_role`]: struct.Guild.html#method.create_role - /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html - #[inline] - pub fn create_role<F: FnOnce(EditRole) -> EditRole>(&self, f: F) -> Result<Role> { - let map = utils::vecmap_to_json_map(f(EditRole::default()).0); - - let role = http::create_role(self.0, &map)?; - - if let Some(position) = map.get("position").and_then(Value::as_u64) { - self.edit_role_position(role.id, position)?; - } - - Ok(role) - } - - /// Deletes the current guild if the current account is the owner of the - /// guild. - /// - /// Refer to [`Guild::delete`] for more information. - /// - /// **Note**: Requires the current user to be the owner of the guild. - /// - /// [`Guild::delete`]: struct.Guild.html#method.delete - #[inline] - pub fn delete(&self) -> Result<PartialGuild> { http::delete_guild(self.0) } - - /// Deletes an [`Emoji`] from the guild. - /// - /// Requires the [Manage Emojis] permission. - /// - /// [`Emoji`]: struct.Emoji.html - /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html - #[inline] - pub fn delete_emoji<E: Into<EmojiId>>(&self, emoji_id: E) -> Result<()> { - http::delete_emoji(self.0, emoji_id.into().0) - } - - /// Deletes an integration by Id from the guild. - /// - /// Requires the [Manage Guild] permission. - /// - /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - #[inline] - pub fn delete_integration<I: Into<IntegrationId>>(&self, integration_id: I) -> Result<()> { - http::delete_guild_integration(self.0, integration_id.into().0) - } - - /// Deletes a [`Role`] by Id from the guild. - /// - /// Also see [`Role::delete`] if you have the `cache` and `methods` features - /// enabled. - /// - /// Requires the [Manage Roles] permission. - /// - /// [`Role`]: struct.Role.html - /// [`Role::delete`]: struct.Role.html#method.delete - /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html - #[inline] - pub fn delete_role<R: Into<RoleId>>(&self, role_id: R) -> Result<()> { - http::delete_role(self.0, role_id.into().0) - } - - /// Edits the current guild with new data where specified. - /// - /// Refer to [`Guild::edit`] for more information. - /// - /// **Note**: Requires the current user to have the [Manage Guild] - /// permission. - /// - /// [`Guild::edit`]: struct.Guild.html#method.edit - /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - #[inline] - pub fn edit<F: FnOnce(EditGuild) -> EditGuild>(&mut self, f: F) -> Result<PartialGuild> { - let map = utils::vecmap_to_json_map(f(EditGuild::default()).0); - - http::edit_guild(self.0, &map) - } - - /// Edits an [`Emoji`]'s name in the guild. - /// - /// Also see [`Emoji::edit`] if you have the `cache` and `methods` features - /// enabled. - /// - /// Requires the [Manage Emojis] permission. - /// - /// [`Emoji`]: struct.Emoji.html - /// [`Emoji::edit`]: struct.Emoji.html#method.edit - /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html - pub fn edit_emoji<E: Into<EmojiId>>(&self, emoji_id: E, name: &str) -> Result<Emoji> { - let map = json!({ - "name": name, - }); - - http::edit_emoji(self.0, emoji_id.into().0, &map) - } - - /// Edits the properties of member of the guild, such as muting or - /// nicknaming them. - /// - /// Refer to `EditMember`'s documentation for a full list of methods and - /// permission restrictions. - /// - /// # Examples - /// - /// Mute a member and set their roles to just one role with a predefined Id: - /// - /// ```rust,ignore - /// guild.edit_member(user_id, |m| m.mute(true).roles(&vec![role_id])); - /// ``` - #[inline] - pub fn edit_member<F, U>(&self, user_id: U, f: F) -> Result<()> - where F: FnOnce(EditMember) -> EditMember, U: Into<UserId> { - let map = utils::vecmap_to_json_map(f(EditMember::default()).0); - - http::edit_member(self.0, user_id.into().0, &map) + pub fn as_channel_id(&self) -> ChannelId { + ChannelId(self.0) } - /// Edits the current user's nickname for the guild. - /// - /// Pass `None` to reset the nickname. - /// - /// Requires the [Change Nickname] permission. - /// - /// [Change Nickname]: permissions/constant.CHANGE_NICKNAME.html - #[inline] - pub fn edit_nickname(&self, new_nickname: Option<&str>) -> Result<()> { - http::edit_nickname(self.0, new_nickname) - } - - /// Edits a [`Role`], optionally setting its new fields. - /// - /// Requires the [Manage Roles] permission. - /// - /// # Examples - /// - /// Make a role hoisted: - /// - /// ```rust,ignore - /// use serenity::model::{GuildId, RoleId}; - /// - /// GuildId(7).edit_role(RoleId(8), |r| r.hoist(true)); - /// ``` - /// - /// [`Role`]: struct.Role.html - /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html - #[inline] - pub fn edit_role<F, R>(&self, role_id: R, f: F) -> Result<Role> - where F: FnOnce(EditRole) -> EditRole, R: Into<RoleId> { - let map = utils::vecmap_to_json_map(f(EditRole::default()).0); - - http::edit_role(self.0, role_id.into().0, &map) - } - - /// Edits the order of [`Role`]s - /// Requires the [Manage Roles] permission. - /// - /// # Examples - /// - /// Change the order of a role: - /// - /// ```rust,ignore - /// use serenity::model::{GuildId, RoleId}; - /// GuildId(7).edit_role_position(RoleId(8), 2); - /// ``` - /// - /// [`Role`]: struct.Role.html - /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html - #[inline] - pub fn edit_role_position<R>(&self, role_id: R, position: u64) -> Result<Vec<Role>> - where R: Into<RoleId> { - http::edit_role_position(self.0, role_id.into().0, position) - } - - - /// Search the cache for the guild. - #[cfg(feature = "cache")] - pub fn find(&self) -> Option<Arc<RwLock<Guild>>> { CACHE.read().guild(*self) } - - /// Requests the guild over REST. - /// - /// Note that this will not be a complete guild, as REST does not send - /// all data with a guild retrieval. - #[inline] - pub fn get(&self) -> Result<PartialGuild> { http::get_guild(self.0) } - - /// Gets all integration of the guild. - /// - /// This performs a request over the REST API. - #[inline] - pub fn integrations(&self) -> Result<Vec<Integration>> { http::get_guild_integrations(self.0) } - - /// Gets all of the guild's invites. - /// - /// Requires the [Manage Guild] permission. - /// - /// [Manage Guild]: permissions/struct.MANAGE_GUILD.html - #[inline] - pub fn invites(&self) -> Result<Vec<RichInvite>> { http::get_guild_invites(self.0) } - - /// Kicks a [`Member`] from the guild. - /// - /// Requires the [Kick Members] permission. - /// - /// [`Member`]: struct.Member.html - /// [Kick Members]: permissions/constant.KICK_MEMBERS.html - #[inline] - pub fn kick<U: Into<UserId>>(&self, user_id: U) -> Result<()> { - http::kick_member(self.0, user_id.into().0) - } - - /// Leaves the guild. - #[inline] - pub fn leave(&self) -> Result<()> { http::leave_guild(self.0) } - - /// Gets a user's [`Member`] for the guild by Id. - /// - /// [`Guild`]: struct.Guild.html - /// [`Member`]: struct.Member.html - #[inline] - pub fn member<U: Into<UserId>>(&self, user_id: U) -> Result<Member> { - http::get_member(self.0, user_id.into().0) - } - - /// Gets a list of the guild's members. - /// - /// Optionally pass in the `limit` to limit the number of results. Maximum - /// value is 1000. Optionally pass in `after` to offset the results by a - /// [`User`]'s Id. - /// - /// [`User`]: struct.User.html - #[inline] - pub fn members<U>(&self, limit: Option<u64>, after: Option<U>) -> Result<Vec<Member>> - where U: Into<UserId> { - http::get_guild_members(self.0, limit, after.map(|x| x.into().0)) - } - - /// Moves a member to a specific voice channel. - /// - /// Requires the [Move Members] permission. - /// - /// [Move Members]: permissions/constant.MOVE_MEMBERS.html - pub fn move_member<C, U>(&self, user_id: U, channel_id: C) -> Result<()> - where C: Into<ChannelId>, U: Into<UserId> { - let mut map = Map::new(); - map.insert( - "channel_id".to_string(), - Value::Number(Number::from(channel_id.into().0)), - ); - - http::edit_member(self.0, user_id.into().0, &map) - } - - /// Gets the number of [`Member`]s that would be pruned with the given - /// number of days. - /// - /// Requires the [Kick Members] permission. - /// - /// [`Member`]: struct.Member.html - /// [Kick Members]: permissions/constant.KICK_MEMBERS.html - pub fn prune_count(&self, days: u16) -> Result<GuildPrune> { - let map = json!({ - "days": days, - }); - - http::get_guild_prune_count(self.0, &map) - } - - /// Re-orders the channels of the guild. - /// - /// Accepts an iterator of a tuple of the channel ID to modify and its new - /// position. - /// - /// Although not required, you should specify all channels' positions, - /// regardless of whether they were updated. Otherwise, positioning can - /// sometimes get weird. - pub fn reorder_channels<It>(&self, channels: It) -> Result<()> - where It: IntoIterator<Item = (ChannelId, u64)> { - let items = channels.into_iter().map(|(id, pos)| json!({ - "id": id, - "position": pos, - })).collect(); - - http::edit_guild_channel_positions(self.0, &Value::Array(items)) - } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total - /// number of shards. - /// - /// **Note**: When the cache is enabled, this function unlocks the cache to - /// retrieve the total number of shards in use. If you already have the - /// total, consider using [`utils::shard_id`]. - /// - /// [`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().shard_count) } - /// Returns the Id of the shard associated with the guild. /// /// When the cache is enabled this will automatically retrieve the total @@ -492,56 +32,11 @@ impl GuildId { /// /// assert_eq!(guild_id.shard_id(17), 7); /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] - #[inline] - pub fn shard_id(&self, shard_count: u64) -> u64 { ::utils::shard_id(self.0, shard_count) } - - /// Starts an integration sync for the given integration Id. - /// - /// Requires the [Manage Guild] permission. - /// - /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - #[inline] - pub fn start_integration_sync<I: Into<IntegrationId>>(&self, integration_id: I) -> Result<()> { - http::start_integration_sync(self.0, integration_id.into().0) - } - - /// Starts a prune of [`Member`]s. - /// - /// See the documentation on [`GuildPrune`] for more information. - /// - /// **Note**: Requires the [Kick Members] permission. - /// - /// [`GuildPrune`]: struct.GuildPrune.html - /// [`Member`]: struct.Member.html - /// [Kick Members]: permissions/constant.KICK_MEMBERS.html + #[cfg(feature = "utils")] #[inline] - pub fn start_prune(&self, days: u16) -> Result<GuildPrune> { - let map = json!({ - "days": days, - }); - - http::start_guild_prune(self.0, &map) + pub fn shard_id(&self, shard_count: u64) -> u64 { + ::utils::shard_id(self.0, shard_count) } - - /// Unbans a [`User`] from the guild. - /// - /// Requires the [Ban Members] permission. - /// - /// [`User`]: struct.User.html - /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - #[inline] - pub fn unban<U: Into<UserId>>(&self, user_id: U) -> Result<()> { - http::remove_ban(self.0, user_id.into().0) - } - - /// Retrieves the guild's webhooks. - /// - /// **Note**: Requires the [Manage Webhooks] permission. - /// - /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html - #[inline] - pub fn webhooks(&self) -> Result<Vec<Webhook>> { http::get_guild_webhooks(self.0) } } impl From<PartialGuild> for GuildId { diff --git a/src/model/guild/member.rs b/src/model/guild/member.rs index cbc6066..0c22597 100644 --- a/src/model/guild/member.rs +++ b/src/model/guild/member.rs @@ -1,7 +1,10 @@ -use model::prelude::*; use chrono::{DateTime, FixedOffset}; -use std::fmt::{Display, Formatter, Result as FmtResult}; -use super::deserialize_sync_user; +use futures::future; +use model::prelude::*; +use std::cell::RefCell; +use super::super::WrappedClient; +use super::deserialize_user; +use ::FutureResult; #[cfg(all(feature = "builder", feature = "cache", feature = "model"))] use builder::EditMember; @@ -11,8 +14,6 @@ use internal::prelude::*; use std::borrow::Cow; #[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 { @@ -66,60 +67,53 @@ pub struct Member { /// Vector of Ids of [`Role`]s given to the member. pub roles: Vec<RoleId>, /// Attached User struct. - #[serde(deserialize_with = "deserialize_sync_user", - serialize_with = "serialize_sync_user")] - pub user: Arc<RwLock<User>>, + #[serde(deserialize_with = "deserialize_user", + serialize_with = "serialize_user")] + pub user: Rc<RefCell<User>>, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] impl Member { - /// Adds a [`Role`] to the member, editing its roles in-place if the request - /// was successful. + /// Adds a [`Role`] to the member. /// /// **Note**: Requires the [Manage Roles] permission. /// /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[cfg(feature = "cache")] - pub fn add_role<R: Into<RoleId>>(&mut self, role_id: R) -> Result<()> { + pub fn add_role<R: Into<RoleId>>(&mut self, role_id: R) + -> FutureResult<()> { let role_id = role_id.into(); if self.roles.contains(&role_id) { - return Ok(()); + return Box::new(future::ok(())); } - match http::add_member_role(self.guild_id.0, self.user.read().id.0, role_id.0) { - Ok(()) => { - self.roles.push(role_id); - - Ok(()) - }, - Err(why) => Err(why), - } + ftryopt!(self.client).http.add_member_role( + self.guild_id.0, + self.user.borrow().id.0, + role_id.0, + ) } - /// Adds one or multiple [`Role`]s to the member, editing - /// its roles in-place if the request was successful. + /// Adds one or multiple [`Role`]s to the member. /// /// **Note**: Requires the [Manage Roles] permission. /// /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[cfg(feature = "cache")] - pub fn add_roles(&mut self, role_ids: &[RoleId]) -> Result<()> { - self.roles.extend_from_slice(role_ids); - - let builder = EditMember::default().roles(&self.roles); - let map = utils::vecmap_to_json_map(builder.0); - - 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)); + pub fn add_roles(&mut self, role_ids: &[RoleId]) -> FutureResult<()> { + let mut roles = self.roles.clone(); + roles.extend(role_ids); - Err(why) - }, - } + ftryopt!(self.client).http.edit_member( + self.guild_id.0, + self.user.borrow().id.0, + |f| f.roles(roles), + ) } /// Ban the member from its guild, deleting the last X number of @@ -136,21 +130,27 @@ impl Member { /// /// [Ban Members]: permissions/constant.BAN_MEMBERS.html #[cfg(feature = "cache")] - pub fn ban<BO: BanOptions>(&self, ban_options: &BO) -> Result<()> { + pub fn ban<BO: BanOptions>(&self, ban_options: &BO) -> FutureResult<()> { let dmd = ban_options.dmd(); + if dmd > 7 { - return Err(Error::Model(ModelError::DeleteMessageDaysAmount(dmd))); + return Box::new(future::err(Error::Model( + ModelError::DeleteMessageDaysAmount(dmd), + ))); } let reason = ban_options.reason(); if reason.len() > 512 { - return Err(Error::ExceededLimit(reason.to_string(), 512)); + return Box::new(future::err(Error::ExceededLimit( + reason.to_string(), + 512, + ))); } - http::ban_user( + ftryopt!(self.client).http.ban_user( self.guild_id.0, - self.user.read().id.0, + self.user.borrow().id.0, dmd, &*reason, ) @@ -159,38 +159,37 @@ impl Member { /// Determines the member's colour. #[cfg(all(feature = "cache", feature = "utils"))] pub fn colour(&self) -> Option<Colour> { - let cache = CACHE.read(); - let guild = cache.guilds.get(&self.guild_id)?.read(); + let client = self.client.as_ref()?; + let cache = client.cache.try_borrow().ok()?; + let guild = cache.guild(self.guild_id)?; + let guild = guild.borrow(); let mut roles = self.roles .iter() .filter_map(|role_id| guild.roles.get(role_id)) - .collect::<Vec<&Role>>(); + .collect::<Vec<&Rc<RefCell<Role>>>>(); roles.sort_by(|a, b| b.cmp(a)); let default = Colour::default(); roles .iter() - .find(|r| r.colour.0 != default.0) - .map(|r| r.colour) + .find(|r| r.borrow().colour.0 != default.0) + .map(|r| r.borrow().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(); + pub fn default_channel(&self) -> Option<Rc<RefCell<GuildChannel>>> { + let cache = self.client.as_ref()?.cache.borrow(); + let guild = cache.guild(self.guild_id)?; + let reader = guild.borrow(); for (cid, channel) in &reader.channels { - if reader.permissions_in(*cid, self.user.read().id).read_messages() { - return Some(Arc::clone(channel)); + if reader.permissions_in(*cid, self.user.borrow().id).read_messages() { + return Some(Rc::clone(channel)); } } @@ -205,17 +204,23 @@ impl Member { self.nick .as_ref() .map(Cow::Borrowed) - .unwrap_or_else(|| Cow::Owned(self.user.read().name.clone())) + .unwrap_or_else(|| { + Cow::Owned(unsafe { (*self.user.as_ptr()).name.clone() }) + }) } /// Returns the DiscordTag of a Member, taking possible nickname into account. #[inline] pub fn distinct(&self) -> String { - format!( - "{}#{}", - self.display_name(), - self.user.read().discriminator - ) + unsafe { + let user = &*self.user.as_ptr(); + + format!( + "{}#{}", + self.display_name(), + user.discriminator, + ) + } } /// Edits the member with the given data. See [`Guild::edit_member`] for @@ -227,10 +232,13 @@ impl Member { /// [`Guild::edit_member`]: ../model/guild/struct.Guild.html#method.edit_member /// [`EditMember`]: ../builder/struct.EditMember.html #[cfg(feature = "cache")] - pub fn edit<F: FnOnce(EditMember) -> EditMember>(&self, f: F) -> Result<()> { - let map = utils::vecmap_to_json_map(f(EditMember::default()).0); - - http::edit_member(self.guild_id.0, self.user.read().id.0, &map) + pub fn edit<F: FnOnce(EditMember) -> EditMember>(&self, f: F) + -> FutureResult<()> { + ftryopt!(self.client).http.edit_member( + self.guild_id.0, + self.user.borrow().id.0, + f, + ) } /// Retrieves the ID and position of the member's highest role in the @@ -249,13 +257,18 @@ impl Member { /// guild. #[cfg(feature = "cache")] pub fn highest_role_info(&self) -> Option<(RoleId, i64)> { - let guild = self.guild_id.find()?; - let reader = guild.read(); + let cache = self.client.as_ref()?.cache.try_borrow().ok()?; + let guild = cache.guild(self.guild_id)?; + let reader = guild.borrow(); let mut highest = None; for role_id in &self.roles { - if let Some(role) = reader.roles.get(&role_id) { + let role = reader.roles + .get(&role_id) + .and_then(|x| x.try_borrow().ok()); + + if let Some(role) = role { // Skip this role if this role in iteration has: // // - a position less than the recorded highest @@ -306,24 +319,30 @@ impl Member { /// [`ModelError::GuildNotFound`]: enum.ModelError.html#variant.GuildNotFound /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Kick Members]: permissions/constant.KICK_MEMBERS.html - pub fn kick(&self) -> Result<()> { + pub fn kick(&self) -> FutureResult<()> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { - let cache = CACHE.read(); + let cache = ftry!(client.cache.try_borrow()); if let Some(guild) = cache.guilds.get(&self.guild_id) { let req = Permissions::KICK_MEMBERS; - let reader = guild.read(); + let reader = ftry!(guild.try_borrow()); if !reader.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } - reader.check_hierarchy(self.user.read().id)?; + ftry!(reader.check_hierarchy(ftry!(self.user.try_borrow()).id)); } } - self.guild_id.kick(self.user.read().id) + let user_id = ftry!(self.user.try_borrow()).id.0; + + ftryopt!(self.client).http.kick_member(self.guild_id.0, user_id) } /// Returns the guild-level permissions for the member. @@ -348,39 +367,39 @@ impl Member { /// [`ModelError::ItemMissing`]: enum.ModelError.html#variant.ItemMissing #[cfg(feature = "cache")] pub fn permissions(&self) -> Result<Permissions> { - let guild = match self.guild_id.find() { + let client = self.client.as_ref().ok_or_else(|| { + Error::Model(ModelError::ClientNotPresent) + })?; + let cache = client.cache.try_borrow()?; + let guild = match cache.guilds.get(&self.guild_id) { Some(guild) => guild, None => return Err(From::from(ModelError::GuildNotFound)), }; - let reader = guild.read(); + let guild = guild.try_borrow()?; - Ok(reader.member_permissions(self.user.read().id)) + Ok(guild.member_permissions(self.user.try_borrow()?.id)) } - /// Removes a [`Role`] from the member, editing its roles in-place if the - /// request was successful. + /// Removes a [`Role`] from the member. /// /// **Note**: Requires the [Manage Roles] permission. /// /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[cfg(feature = "cache")] - pub fn remove_role<R: Into<RoleId>>(&mut self, role_id: R) -> Result<()> { + pub fn remove_role<R: Into<RoleId>>(&mut self, role_id: R) -> FutureResult<()> { let role_id = role_id.into(); if !self.roles.contains(&role_id) { - return Ok(()); + return Box::new(future::ok(())); } - 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); - - Ok(()) - }, - Err(why) => Err(why), - } + ftryopt!(self.client).http.remove_member_role( + self.guild_id.0, + self.user.borrow().id.0, + role_id.0, + ) } /// Removes one or multiple [`Role`]s from the member. @@ -390,20 +409,15 @@ impl Member { /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[cfg(feature = "cache")] - pub fn remove_roles(&mut self, role_ids: &[RoleId]) -> Result<()> { - self.roles.retain(|r| !role_ids.contains(r)); - - let builder = EditMember::default().roles(&self.roles); - let map = utils::vecmap_to_json_map(builder.0); - - 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); + pub fn remove_roles(&mut self, role_ids: &[RoleId]) -> FutureResult<()> { + let mut roles = self.roles.clone(); + roles.retain(|r| !role_ids.contains(r)); - Err(why) - }, - } + ftryopt!(self.client).http.edit_member( + self.guild_id.0, + self.user.borrow().id.0, + |f| f.roles(roles), + ) } /// Retrieves the full role data for the user's roles. @@ -412,17 +426,29 @@ impl Member { /// /// If role data can not be found for the member, then `None` is returned. #[cfg(feature = "cache")] - pub fn roles(&self) -> Option<Vec<Role>> { - self - .guild_id - .find() - .map(|g| g - .read() + pub fn roles(&self) -> Option<Vec<Rc<RefCell<Role>>>> { + let client = self.client.as_ref()?; + let cache = client.cache.try_borrow().ok()?; + + cache.guilds.get(&self.guild_id).and_then(|guild| { + let guild = guild.try_borrow().ok()?; + + let roles = guild .roles .values() - .filter(|role| self.roles.contains(&role.id)) + .filter(|role| { + let role = match role.try_borrow() { + Ok(role) => role, + Err(_) => return false, + }; + + self.roles.contains(&role.id) + }) .cloned() - .collect()) + .collect(); + + Some(roles) + }) } /// Unbans the [`User`] from the guild. @@ -438,23 +464,10 @@ impl Member { /// [`User`]: struct.User.html /// [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().id.0) - } -} - -impl Display for Member { - /// Mentions the user so that they receive a notification. - /// - /// # Examples - /// - /// ```rust,ignore - /// // assumes a `member` has already been bound - /// println!("{} is a member!", member); - /// ``` - /// - // This is in the format of `<@USER_ID>`. - fn fmt(&self, f: &mut Formatter) -> FmtResult { - Display::fmt(&self.user.read().mention(), f) + pub fn unban(&self) -> FutureResult<()> { + ftryopt!(self.client).http.remove_ban( + self.guild_id.0, + self.user.borrow().id.0, + ) } } diff --git a/src/model/guild/mod.rs b/src/model/guild/mod.rs index 142fc73..6c0fa82 100644 --- a/src/model/guild/mod.rs +++ b/src/model/guild/mod.rs @@ -17,15 +17,16 @@ pub use self::role::*; pub use self::audit_log::*; use chrono::{DateTime, FixedOffset}; +use futures::{Future, future}; use model::prelude::*; use serde::de::Error as DeError; use serde_json; +use std::cell::RefCell; +use std::rc::Rc; use super::utils::*; +use super::WrappedClient; +use ::FutureResult; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; -#[cfg(feature = "model")] -use http; #[cfg(feature = "model")] use builder::{EditGuild, EditMember, EditRole}; #[cfg(feature = "model")] @@ -58,8 +59,8 @@ pub struct Guild { /// /// This contains all channels regardless of permissions (i.e. the ability /// of the bot to read from or connect to them). - #[serde(serialize_with = "serialize_gen_locked_map")] - pub channels: HashMap<ChannelId, Arc<RwLock<GuildChannel>>>, + #[serde(serialize_with = "serialize_gen_rc_map")] + pub channels: HashMap<ChannelId, Rc<RefCell<GuildChannel>>>, /// Indicator of whether notifications for all messages are enabled by /// default in the guild. pub default_message_notifications: DefaultMessageNotificationLevel, @@ -102,8 +103,8 @@ pub struct Guild { /// the library. /// /// [`ReadyEvent`]: events/struct.ReadyEvent.html - #[serde(serialize_with = "serialize_gen_map")] - pub members: HashMap<UserId, Member>, + #[serde(serialize_with = "serialize_gen_rc_map")] + pub members: HashMap<UserId, Rc<RefCell<Member>>>, /// Indicator of whether the guild requires multi-factor authentication for /// [`Role`]s or [`User`]s with moderation permissions. /// @@ -119,13 +120,13 @@ pub struct Guild { /// A mapping of [`User`]s' Ids to their current presences. /// /// [`User`]: struct.User.html - #[serde(serialize_with = "serialize_gen_map")] - pub presences: HashMap<UserId, Presence>, + #[serde(serialize_with = "serialize_gen_rc_map")] + pub presences: HashMap<UserId, Rc<RefCell<Presence>>>, /// The region that the voice servers that the guild uses are located in. pub region: String, /// A mapping of the guild's roles. - #[serde(serialize_with = "serialize_gen_map")] - pub roles: HashMap<RoleId, Role>, + #[serde(serialize_with = "serialize_gen_rc_map")] + pub roles: HashMap<RoleId, Rc<RefCell<Role>>>, /// An identifying hash of the guild's splash icon. /// /// If the [`InviteSplash`] feature is enabled, this can be used to generate @@ -142,13 +143,19 @@ pub struct Guild { /// [`User`]: struct.User.html #[serde(serialize_with = "serialize_gen_map")] pub voice_states: HashMap<UserId, VoiceState>, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] impl Guild { #[cfg(feature = "cache")] fn check_hierarchy(&self, other_user: UserId) -> Result<()> { - let current_id = CACHE.read().user.id; + let client = self.client.as_ref().ok_or_else(|| { + Error::Model(ModelError::ClientNotPresent) + })?; + + let current_id = client.cache.try_borrow()?.user.id; if let Some(higher) = self.greater_member_hierarchy(other_user, current_id) { if higher != current_id { @@ -162,10 +169,10 @@ impl Guild { /// 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, uid: UserId) -> Option<Arc<RwLock<GuildChannel>>> { + pub fn default_channel(&self, uid: UserId) -> Option<Rc<RefCell<GuildChannel>>> { for (cid, channel) in &self.channels { if self.permissions_in(*cid, uid).read_messages() { - return Some(Arc::clone(channel)); + return Some(Rc::clone(channel)); } } @@ -177,11 +184,11 @@ 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<Arc<RwLock<GuildChannel>>> { + pub fn default_channel_guaranteed(&self) -> Option<Rc<RefCell<GuildChannel>>> { for (cid, channel) in &self.channels { for memid in self.members.keys() { if self.permissions_in(*cid, *memid).read_messages() { - return Some(Arc::clone(channel)); + return Some(Rc::clone(channel)); } } } @@ -191,7 +198,16 @@ impl Guild { #[cfg(feature = "cache")] fn has_perms(&self, mut permissions: Permissions) -> bool { - let user_id = CACHE.read().user.id; + let client = match self.client.as_ref() { + Some(client) => client, + None => return true, + }; + let cache = match client.cache.try_borrow() { + Ok(cache) => cache, + Err(_) => return true, + }; + + let user_id = cache.user.id; let perms = self.member_permissions(user_id); permissions.remove(perms); @@ -229,7 +245,8 @@ impl Guild { /// [`Guild::ban`]: struct.Guild.html#method.ban /// [`User`]: struct.User.html /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - pub fn ban<U: Into<UserId>, BO: BanOptions>(&self, user: U, options: &BO) -> Result<()> { + pub fn ban<U: Into<UserId>, BO: BanOptions>(&self, user: U, options: &BO) + -> FutureResult<()> { let user = user.into(); #[cfg(feature = "cache")] @@ -237,13 +254,33 @@ impl Guild { let req = Permissions::BAN_MEMBERS; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } - self.check_hierarchy(user)?; + ftry!(self.check_hierarchy(user)); + } + + let dmd = options.dmd(); + if dmd > 7 { + return Box::new(future::err(Error::Model( + ModelError::DeleteMessageDaysAmount(dmd), + ))); } - self.id.ban(user, options) + let reason = options.reason(); + + if reason.len() > 512 { + return Box::new(future::err(Error::ExceededLimit( + reason.to_string(), + 512, + ))); + } + + ftryopt!(self.client) + .http + .ban_user(self.id.0, user.0, dmd, reason) } /// Retrieves a list of [`Ban`]s for the guild. @@ -258,69 +295,60 @@ impl Guild { /// [`Ban`]: struct.Ban.html /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - pub fn bans(&self) -> Result<Vec<Ban>> { + pub fn bans(&self) -> FutureResult<Vec<Ban>> { #[cfg(feature = "cache")] { let req = Permissions::BAN_MEMBERS; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.bans() + ftryopt!(self.client).http.get_bans(self.id.0) } /// Retrieves a list of [`AuditLogs`] for the guild. /// /// [`AuditLogs`]: audit_log/struct.AuditLogs.html #[inline] - 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) + pub fn audit_logs( + &self, + action_type: Option<u8>, + user_id: Option<UserId>, + before: Option<AuditLogEntryId>, + limit: Option<u8>, + ) -> FutureResult<AuditLogs> { + ftryopt!(self.client).http.get_audit_logs( + self.id.0, + action_type, + user_id.map(|x| x.0), + before.map(|x| x.0), + limit, + ) } /// Gets all of the guild's channels over the REST API. /// /// [`Guild`]: struct.Guild.html #[inline] - pub fn channels(&self) -> Result<HashMap<ChannelId, GuildChannel>> { self.id.channels() } + pub fn channels(&self) -> FutureResult<HashMap<ChannelId, GuildChannel>> { + let done = ftryopt!(self.client) + .http + .get_channels(self.id.0) + .map(|channels| { + let mut map = HashMap::with_capacity(channels.len()); + + for channel in channels { + map.insert(channel.id, channel); + } - /// Creates a guild with the data provided. - /// - /// Only a [`PartialGuild`] will be immediately returned, and a full - /// [`Guild`] will be received over a [`Shard`]. - /// - /// **Note**: This endpoint is usually only available for user accounts. - /// Refer to Discord's information for the endpoint [here][whitelist] for - /// more information. If you require this as a bot, re-think what you are - /// doing and if it _really_ needs to be doing this. - /// - /// # Examples - /// - /// Create a guild called `"test"` in the [US West region] with no icon: - /// - /// ```rust,ignore - /// use serenity::model::{Guild, Region}; - /// - /// let _guild = Guild::create_guild("test", Region::UsWest, None); - /// ``` - /// - /// [`Guild`]: struct.Guild.html - /// [`PartialGuild`]: struct.PartialGuild.html - /// [`Shard`]: ../gateway/struct.Shard.html - /// [US West region]: enum.Region.html#variant.UsWest - /// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild - pub fn create(name: &str, region: Region, icon: Option<&str>) -> Result<PartialGuild> { - let map = json!({ - "icon": icon, - "name": name, - "region": region.name(), - }); - - http::create_guild(&map) + map + }); + + Box::new(done) } /// Creates a new [`Channel`] in the guild. @@ -345,18 +373,25 @@ impl Guild { /// [`Channel`]: struct.Channel.html /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Channels]: permissions/constant.MANAGE_CHANNELS.html - pub fn create_channel<C>(&self, name: &str, kind: ChannelType, category: C) -> Result<GuildChannel> - where C: Into<Option<ChannelId>> { + pub fn create_channel<C>(&self, name: &str, kind: ChannelType, category: C) + -> FutureResult<GuildChannel> where C: Into<Option<ChannelId>> { #[cfg(feature = "cache")] { let req = Permissions::MANAGE_CHANNELS; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.create_channel(name, kind, category) + ftryopt!(self.client).http.create_channel( + self.id.0, + name, + kind, + category.into().map(|x| x.0), + ) } /// Creates an emoji in the guild with a name and base64-encoded image. The @@ -379,8 +414,8 @@ impl Guild { /// [`utils::read_image`]: ../fn.read_image.html /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html #[inline] - pub fn create_emoji(&self, name: &str, image: &str) -> Result<Emoji> { - self.id.create_emoji(name, image) + pub fn create_emoji(&self, name: &str, image: &str) -> FutureResult<Emoji> { + ftryopt!(self.client).http.create_emoji(self.id.0, name, image) } /// Creates an integration for the guild. @@ -389,9 +424,13 @@ impl Guild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn create_integration<I>(&self, integration_id: I, kind: &str) -> Result<()> - where I: Into<IntegrationId> { - self.id.create_integration(integration_id, kind) + pub fn create_integration<I>(&self, integration_id: I, kind: &str) + -> FutureResult<()> where I: Into<IntegrationId> { + ftryopt!(self.client).http.create_guild_integration( + self.id.0, + integration_id.into().0, + kind, + ) } /// Creates a new role in the guild with the data set, if any. @@ -416,18 +455,20 @@ impl Guild { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html - pub fn create_role<F>(&self, f: F) -> Result<Role> + pub fn create_role<F>(&self, f: F) -> FutureResult<Role> where F: FnOnce(EditRole) -> EditRole { #[cfg(feature = "cache")] { let req = Permissions::MANAGE_ROLES; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.create_role(f) + ftryopt!(self.client).http.create_role(self.id.0, f) } /// Deletes the current guild if the current user is the owner of the @@ -441,17 +482,23 @@ impl Guild { /// if the current user is not the guild owner. /// /// [`ModelError::InvalidUser`]: enum.ModelError.html#variant.InvalidUser - pub fn delete(&self) -> Result<PartialGuild> { + pub fn delete(&self) -> FutureResult<PartialGuild> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { - if self.owner_id != CACHE.read().user.id { + let cache = ftry!(client.cache.try_borrow()); + + if self.owner_id != cache.user.id { let req = Permissions::MANAGE_GUILD; - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.delete() + ftryopt!(self.client).http.delete_guild(self.id.0) } /// Deletes an [`Emoji`] from the guild. @@ -461,8 +508,9 @@ impl Guild { /// [`Emoji`]: struct.Emoji.html /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html #[inline] - pub fn delete_emoji<E: Into<EmojiId>>(&self, emoji_id: E) -> Result<()> { - self.id.delete_emoji(emoji_id) + pub fn delete_emoji<E: Into<EmojiId>>(&self, emoji_id: E) + -> FutureResult<()> { + ftryopt!(self.client).http.delete_emoji(self.id.0, emoji_id.into().0) } /// Deletes an integration by Id from the guild. @@ -471,8 +519,12 @@ impl Guild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn delete_integration<I: Into<IntegrationId>>(&self, integration_id: I) -> Result<()> { - self.id.delete_integration(integration_id) + pub fn delete_integration<I: Into<IntegrationId>>(&self, integration_id: I) + -> FutureResult<()> { + ftryopt!(self.client).http.delete_guild_integration( + self.id.0, + integration_id.into().0, + ) } /// Deletes a [`Role`] by Id from the guild. @@ -486,8 +538,8 @@ impl Guild { /// [`Role::delete`]: struct.Role.html#method.delete /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[inline] - pub fn delete_role<R: Into<RoleId>>(&self, role_id: R) -> Result<()> { - self.id.delete_role(role_id) + pub fn delete_role<R: Into<RoleId>>(&self, role_id: R) -> FutureResult<()> { + ftryopt!(self.client).http.delete_role(self.id.0, role_id.into().0) } /// Edits the current guild with new data where specified. @@ -518,37 +570,20 @@ impl Guild { /// /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - pub fn edit<F>(&mut self, f: F) -> Result<()> - where F: FnOnce(EditGuild) -> EditGuild { + pub fn edit<F: FnOnce(EditGuild) -> EditGuild>(&self, f: F) + -> FutureResult<PartialGuild> { #[cfg(feature = "cache")] { let req = Permissions::MANAGE_GUILD; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - match self.id.edit(f) { - Ok(guild) => { - self.afk_channel_id = guild.afk_channel_id; - self.afk_timeout = guild.afk_timeout; - self.default_message_notifications = guild.default_message_notifications; - self.emojis = guild.emojis; - self.features = guild.features; - self.icon = guild.icon; - self.mfa_level = guild.mfa_level; - self.name = guild.name; - self.owner_id = guild.owner_id; - self.region = guild.region; - self.roles = guild.roles; - self.splash = guild.splash; - self.verification_level = guild.verification_level; - - Ok(()) - }, - Err(why) => Err(why), - } + ftryopt!(self.client).http.edit_guild(self.id.0, f) } /// Edits an [`Emoji`]'s name in the guild. @@ -562,8 +597,11 @@ impl Guild { /// [`Emoji::edit`]: struct.Emoji.html#method.edit /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html #[inline] - pub fn edit_emoji<E: Into<EmojiId>>(&self, emoji_id: E, name: &str) -> Result<Emoji> { - self.id.edit_emoji(emoji_id, name) + pub fn edit_emoji<E: Into<EmojiId>>(&self, emoji_id: E, name: &str) + -> FutureResult<Emoji> { + let emoji_id = emoji_id.into().0; + + ftryopt!(self.client).http.edit_emoji(self.id.0, emoji_id, name) } /// Edits the properties of member of the guild, such as muting or @@ -580,9 +618,9 @@ impl Guild { /// guild.edit_member(user_id, |m| m.mute(true).roles(&vec![role_id])); /// ``` #[inline] - pub fn edit_member<F, U>(&self, user_id: U, f: F) -> Result<()> + pub fn edit_member<F, U>(&self, user_id: U, f: F) -> FutureResult<()> where F: FnOnce(EditMember) -> EditMember, U: Into<UserId> { - self.id.edit_member(user_id, f) + ftryopt!(self.client).http.edit_member(self.id.0, user_id.into().0, f) } /// Edits the current user's nickname for the guild. @@ -599,17 +637,20 @@ impl Guild { /// /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Change Nickname]: permissions/constant.CHANGE_NICKNAME.html - pub fn edit_nickname(&self, new_nickname: Option<&str>) -> Result<()> { + pub fn edit_nickname(&self, new_nickname: Option<&str>) + -> FutureResult<()> { #[cfg(feature = "cache")] { let req = Permissions::CHANGE_NICKNAME; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.edit_nickname(new_nickname) + ftryopt!(self.client).http.edit_nickname(self.id.0, new_nickname) } /// Edits a role, optionally setting its fields. @@ -626,9 +667,9 @@ impl Guild { /// /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[inline] - pub fn edit_role<F, R>(&self, role_id: R, f: F) -> Result<Role> + pub fn edit_role<F, R>(&self, role_id: R, f: F) -> FutureResult<Role> where F: FnOnce(EditRole) -> EditRole, R: Into<RoleId> { - self.id.edit_role(role_id, f) + ftryopt!(self.client).http.edit_role(self.id.0, role_id.into().0, f) } /// Edits the order of [`Role`]s @@ -646,17 +687,13 @@ impl Guild { /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[inline] - pub fn edit_role_position<R>(&self, role_id: R, position: u64) -> Result<Vec<Role>> - where R: Into<RoleId> { - self.id.edit_role_position(role_id, position) + pub fn edit_role_position<R>(&self, role_id: R, position: u64) + -> FutureResult<Vec<Role>> where R: Into<RoleId> { + ftryopt!(self.client) + .http + .edit_role_position(self.id.0, role_id.into().0, position) } - /// Gets a partial amount of guild data by its Id. - /// - /// Requires that the current user be in the guild. - #[inline] - pub fn get<G: Into<GuildId>>(guild_id: G) -> Result<PartialGuild> { guild_id.into().get() } - /// Returns which of two [`User`]s has a higher [`Member`] hierarchy. /// /// Hierarchy is essentially who has the [`Role`] with the highest @@ -687,9 +724,11 @@ impl Guild { } let lhs = self.members.get(&lhs_id)? + .borrow() .highest_role_info() .unwrap_or((RoleId(0), 0)); let rhs = self.members.get(&rhs_id)? + .borrow() .highest_role_info() .unwrap_or((RoleId(0), 0)); @@ -731,7 +770,9 @@ impl Guild { /// /// This performs a request over the REST API. #[inline] - pub fn integrations(&self) -> Result<Vec<Integration>> { self.id.integrations() } + pub fn integrations(&self) -> FutureResult<Vec<Integration>> { + ftryopt!(self.client).http.get_guild_integrations(self.id.0) + } /// Retrieves the active invites for the guild. /// @@ -744,23 +785,27 @@ impl Guild { /// /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - pub fn invites(&self) -> Result<Vec<RichInvite>> { + pub fn invites(&self) -> FutureResult<Vec<RichInvite>> { #[cfg(feature = "cache")] { let req = Permissions::MANAGE_GUILD; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.invites() + ftryopt!(self.client).http.get_guild_invites(self.id.0) } /// Checks if the guild is 'large'. A guild is considered large if it has /// more than 250 members. #[inline] - pub fn is_large(&self) -> bool { self.members.len() > LARGE_THRESHOLD as usize } + pub fn is_large(&self) -> bool { + self.members.len() > LARGE_THRESHOLD as usize + } /// Kicks a [`Member`] from the guild. /// @@ -769,18 +814,24 @@ impl Guild { /// [`Member`]: struct.Member.html /// [Kick Members]: permissions/constant.KICK_MEMBERS.html #[inline] - pub fn kick<U: Into<UserId>>(&self, user_id: U) -> Result<()> { self.id.kick(user_id) } + pub fn kick<U: Into<UserId>>(&self, user_id: U) -> FutureResult<()> { + ftryopt!(self.client).http.kick_member(self.id.0, user_id.into().0) + } /// Leaves the guild. #[inline] - pub fn leave(&self) -> Result<()> { self.id.leave() } + pub fn leave(&self) -> FutureResult<()> { + ftryopt!(self.client).http.leave_guild(self.id.0) + } /// Gets a user's [`Member`] for the guild by Id. /// /// [`Guild`]: struct.Guild.html /// [`Member`]: struct.Member.html #[inline] - pub fn member<U: Into<UserId>>(&self, user_id: U) -> Result<Member> { self.id.member(user_id) } + pub fn member<U: Into<UserId>>(&self, user_id: U) -> FutureResult<Member> { + ftryopt!(self.client).http.get_member(self.id.0, user_id.into().0) + } /// Gets a list of the guild's members. /// @@ -790,18 +841,21 @@ impl Guild { /// /// [`User`]: struct.User.html #[inline] - pub fn members<U>(&self, limit: Option<u64>, after: Option<U>) -> Result<Vec<Member>> - where U: Into<UserId> { - self.id.members(limit, after) + pub fn members<U>(&self, limit: Option<u64>, after: Option<U>) + -> FutureResult<Vec<Member>> where U: Into<UserId> { + let after = after.map(Into::into).map(|x| x.0); + + ftryopt!(self.client).http.get_guild_members(self.id.0, limit, after) } /// Gets a list of all the members (satisfying the status provided to the function) in this /// guild. - pub fn members_with_status(&self, status: OnlineStatus) -> Vec<&Member> { + pub fn members_with_status(&self, status: OnlineStatus) + -> Vec<&Rc<RefCell<Member>>> { let mut members = vec![]; for (&id, member) in &self.members { - match self.presences.get(&id) { + match self.presences.get(&id).and_then(|x| x.try_borrow().ok()) { Some(presence) => if status == presence.status { members.push(member); }, @@ -828,7 +882,7 @@ impl Guild { /// - **username and discriminator**: "zey#5479" /// /// [`Member`]: struct.Member.html - pub fn member_named(&self, name: &str) -> Option<&Member> { + pub fn member_named(&self, name: &str) -> Option<&Rc<RefCell<Member>>> { let (name, discrim) = if let Some(pos) = name.rfind('#') { let split = name.split_at(pos + 1); @@ -851,9 +905,18 @@ impl Guild { self.members .values() .find(|member| { - let name_matches = member.user.read().name == name; + let member = match member.try_borrow().ok() { + Some(member) => member, + None => return false, + }; + let user = match member.user.try_borrow().ok() { + Some(user) => user, + None => return false, + }; + + let name_matches = user.name == name; let discrim_matches = match discrim { - Some(discrim) => member.user.read().discriminator == discrim, + Some(discrim) => user.discriminator == discrim, None => true, }; @@ -862,7 +925,7 @@ impl Guild { .or_else(|| { self.members .values() - .find(|member| member.nick.as_ref().map_or(false, |nick| nick == name)) + .find(|member| member.borrow().nick.as_ref().map_or(false, |nick| nick == name)) }) } @@ -876,18 +939,23 @@ impl Guild { /// - "zeya", "zeyaa", "zeyla", "zeyzey", "zeyzeyzey" /// /// [`Member`]: struct.Member.html - pub fn members_starting_with(&self, prefix: &str, case_sensitive: bool, sorted: bool) -> Vec<&Member> { - let mut members: Vec<&Member> = self.members + pub fn members_starting_with(&self, prefix: &str, case_sensitive: bool, sorted: bool) -> Vec<&Rc<RefCell<Member>>> { + let mut members: Vec<&Rc<RefCell<Member>>> = self.members .values() .filter(|member| - if case_sensitive { - member.user.read().name.starts_with(prefix) + let member = member.borrow(); + let user = member.user.borrow(); + + user.name.starts_with(prefix) } else { - starts_with_case_insensitive(&member.user.read().name, prefix) + let member = member.borrow(); + let user = member.user.borrow(); + + starts_with_case_insensitive(&user.name, prefix) } - || member.nick.as_ref() + || member.borrow().nick.as_ref() .map_or(false, |nick| if case_sensitive { @@ -899,26 +967,28 @@ impl Guild { if sorted { members .sort_by(|a, b| { + let (a, b) = (a.borrow(), b.borrow()); + let name_a = match a.nick { Some(ref nick) => { - if contains_case_insensitive(&a.user.read().name[..], prefix) { - Cow::Owned(a.user.read().name.clone()) + if contains_case_insensitive(&a.user.borrow().name[..], prefix) { + Cow::Owned(a.user.borrow().name.clone()) } else { Cow::Borrowed(nick) } }, - None => Cow::Owned(a.user.read().name.clone()), + None => Cow::Owned(a.user.borrow().name.clone()), }; let name_b = match b.nick { Some(ref nick) => { - if contains_case_insensitive(&b.user.read().name[..], prefix) { - Cow::Owned(b.user.read().name.clone()) + if contains_case_insensitive(&b.user.borrow().name[..], prefix) { + Cow::Owned(b.user.borrow().name.clone()) } else { Cow::Borrowed(nick) } }, - None => Cow::Owned(b.user.read().name.clone()), + None => Cow::Owned(b.user.borrow().name.clone()), }; closest_to_origin(prefix, &name_a[..], &name_b[..]) @@ -951,18 +1021,24 @@ impl Guild { /// as both fields have to be considered again for sorting. /// /// [`Member`]: struct.Member.html - pub fn members_containing(&self, substring: &str, case_sensitive: bool, sorted: bool) -> Vec<&Member> { - let mut members: Vec<&Member> = self.members + pub fn members_containing(&self, substring: &str, case_sensitive: bool, sorted: bool) -> Vec<&Rc<RefCell<Member>>> { + let mut members: Vec<&Rc<RefCell<Member>>> = self.members .values() .filter(|member| if case_sensitive { - member.user.read().name.contains(substring) + let member = member.borrow(); + let user = member.user.borrow(); + + user.name.contains(substring) } else { - contains_case_insensitive(&member.user.read().name, substring) + let member = member.borrow(); + let user = member.user.borrow(); + + contains_case_insensitive(&user.name, substring) } - || member.nick.as_ref() + || member.borrow().nick.as_ref() .map_or(false, |nick| { if case_sensitive { @@ -975,26 +1051,28 @@ impl Guild { if sorted { members .sort_by(|a, b| { + let (a, b) = (a.borrow(), b.borrow()); + let name_a = match a.nick { Some(ref nick) => { - if contains_case_insensitive(&a.user.read().name[..], substring) { - Cow::Owned(a.user.read().name.clone()) + if contains_case_insensitive(&a.user.borrow().name[..], substring) { + Cow::Owned(a.user.borrow().name.clone()) } else { Cow::Borrowed(nick) } }, - None => Cow::Owned(a.user.read().name.clone()), + None => Cow::Owned(a.user.borrow().name.clone()), }; let name_b = match b.nick { Some(ref nick) => { - if contains_case_insensitive(&b.user.read().name[..], substring) { - Cow::Owned(b.user.read().name.clone()) + if contains_case_insensitive(&b.user.borrow().name[..], substring) { + Cow::Owned(b.user.borrow().name.clone()) } else { Cow::Borrowed(nick) } }, - None => Cow::Owned(b.user.read().name.clone()), + None => Cow::Owned(b.user.borrow().name.clone()), }; closest_to_origin(substring, &name_a[..], &name_b[..]) @@ -1021,22 +1099,34 @@ impl Guild { /// - "zey", "azey", "zeyla", "zeylaa", "zeyzeyzey" /// /// [`Member`]: struct.Member.html - pub fn members_username_containing(&self, substring: &str, case_sensitive: bool, sorted: bool) -> Vec<&Member> { - let mut members: Vec<&Member> = self.members + pub fn members_username_containing(&self, substring: &str, case_sensitive: bool, sorted: bool) -> Vec<&Rc<RefCell<Member>>> { + let mut members: Vec<&Rc<RefCell<Member>>> = self.members .values() .filter(|member| { + let member = match member.try_borrow().ok() { + Some(member) => member, + None => return false, + }; + let user = match member.user.try_borrow().ok() { + Some(user) => user, + None => return false, + }; + if case_sensitive { - member.user.read().name.contains(substring) + user.name.contains(substring) } else { - contains_case_insensitive(&member.user.read().name, substring) + contains_case_insensitive(&user.name, substring) } }).collect(); if sorted { members .sort_by(|a, b| { - let name_a = &a.user.read().name; - let name_b = &b.user.read().name; + let (a, b) = (a.borrow(), b.borrow()); + let (a_user, b_user) = (a.user.borrow(), b.user.borrow()); + + let name_a = &a_user.name; + let name_b = &b_user.name; closest_to_origin(substring, &name_a[..], &name_b[..]) }); members @@ -1064,35 +1154,42 @@ impl Guild { /// a nick, the username will be used (this should never happen). /// /// [`Member`]: struct.Member.html - pub fn members_nick_containing(&self, substring: &str, case_sensitive: bool, sorted: bool) -> Vec<&Member> { - let mut members: Vec<&Member> = self.members + pub fn members_nick_containing(&self, substring: &str, case_sensitive: bool, sorted: bool) -> Vec<&Rc<RefCell<Member>>> { + let mut members = self.members .values() - .filter(|member| + .filter(|member| { + let member = match member.try_borrow() { + Ok(member) => member, + Err(_) => return false, + }; + member.nick.as_ref() .map_or(false, |nick| { - if case_sensitive { nick.contains(substring) } else { contains_case_insensitive(nick, substring) } - })).collect(); + }) + }).collect::<Vec<&Rc<RefCell<Member>>>>(); if sorted { members .sort_by(|a, b| { + let (a, b) = (a.borrow(), b.borrow()); + let name_a = match a.nick { Some(ref nick) => { Cow::Borrowed(nick) }, - None => Cow::Owned(a.user.read().name.clone()), + None => Cow::Owned(a.user.borrow().name.clone()), }; let name_b = match b.nick { Some(ref nick) => { Cow::Borrowed(nick) }, - None => Cow::Owned(b.user.read().name.clone()), + None => Cow::Owned(b.user.borrow().name.clone()), }; closest_to_origin(substring, &name_a[..], &name_b[..]) @@ -1114,7 +1211,7 @@ impl Guild { return Permissions::all(); } - let everyone = match self.roles.get(&RoleId(self.id.0)) { + let everyone = match self.roles.get(&RoleId(self.id.0)).and_then(|x| x.try_borrow().ok()) { Some(everyone) => everyone, None => { error!( @@ -1127,7 +1224,7 @@ impl Guild { }, }; - let member = match self.members.get(&user_id) { + let member = match self.members.get(&user_id).and_then(|x| x.try_borrow().ok()) { Some(member) => member, None => return everyone.permissions, }; @@ -1135,7 +1232,7 @@ impl Guild { let mut permissions = everyone.permissions; for role in &member.roles { - if let Some(role) = self.roles.get(role) { + if let Some(role) = self.roles.get(role).and_then(|x| x.try_borrow().ok()) { if role.permissions.contains(Permissions::ADMINISTRATOR) { return Permissions::all(); } @@ -1144,7 +1241,7 @@ impl Guild { } else { warn!( "(╯°□°)╯︵ ┻━┻ {} on {} has non-existent role {:?}", - member.user.read().id, + member.user.borrow().id, self.id, role, ); @@ -1160,9 +1257,13 @@ impl Guild { /// /// [Move Members]: permissions/constant.MOVE_MEMBERS.html #[inline] - pub fn move_member<C, U>(&self, user_id: U, channel_id: C) -> Result<()> - where C: Into<ChannelId>, U: Into<UserId> { - self.id.move_member(user_id, channel_id) + pub fn move_member<C, U>(&self, user_id: U, channel_id: C) + -> FutureResult<()> where C: Into<ChannelId>, U: Into<UserId> { + ftryopt!(self.client).http.edit_member( + self.id.0, + user_id.into().0, + |f| f.voice_channel(channel_id.into().0), + ) } /// Alias for [`permissions_in`]. @@ -1191,7 +1292,7 @@ impl Guild { let channel_id = channel_id.into(); // Start by retrieving the @everyone role's permissions. - let everyone = match self.roles.get(&RoleId(self.id.0)) { + let everyone = match self.roles.get(&RoleId(self.id.0)).and_then(|x| x.try_borrow().ok()) { Some(everyone) => everyone, None => { error!( @@ -1207,18 +1308,18 @@ impl Guild { // Create a base set of permissions, starting with `@everyone`s. let mut permissions = everyone.permissions; - let member = match self.members.get(&user_id) { + let member = match self.members.get(&user_id).and_then(|x| x.try_borrow().ok()) { Some(member) => member, None => return everyone.permissions, }; for &role in &member.roles { - if let Some(role) = self.roles.get(&role) { + if let Some(role) = self.roles.get(&role).and_then(|x| x.try_borrow().ok()) { permissions |= role.permissions; } else { warn!( "(╯°□°)╯︵ ┻━┻ {} on {} has non-existent role {:?}", - member.user.read().id, + member.user.borrow().id, self.id, role ); @@ -1231,7 +1332,7 @@ impl Guild { } if let Some(channel) = self.channels.get(&channel_id) { - let channel = channel.read(); + let channel = channel.borrow(); // If this is a text channel, then throw out voice permissions. if channel.kind == ChannelType::Text { @@ -1259,7 +1360,7 @@ impl Guild { continue; } - if let Some(role) = self.roles.get(&role) { + if let Some(role) = self.roles.get(&role).and_then(|x| x.try_borrow().ok()) { data.push((role.position, overwrite.deny, overwrite.allow)); } } @@ -1332,17 +1433,19 @@ impl Guild { /// [`GuildPrune`]: struct.GuildPrune.html /// [`Member`]: struct.Member.html /// [Kick Members]: permissions/constant.KICK_MEMBERS.html - pub fn prune_count(&self, days: u16) -> Result<GuildPrune> { + pub fn prune_count(&self, days: u16) -> FutureResult<GuildPrune> { #[cfg(feature = "cache")] { let req = Permissions::KICK_MEMBERS; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.prune_count(days) + ftryopt!(self.client).http.get_guild_prune_count(self.id.0, days) } /// Re-orders the channels of the guild. @@ -1350,9 +1453,12 @@ impl Guild { /// Although not required, you should specify all channels' positions, /// regardless of whether they were updated. Otherwise, positioning can /// sometimes get weird. - pub fn reorder_channels<It>(&self, channels: It) -> Result<()> + pub fn reorder_channels<It>(&self, channels: It) -> FutureResult<()> where It: IntoIterator<Item = (ChannelId, u64)> { - self.id.reorder_channels(channels) + ftryopt!(self.client).http.edit_guild_channel_positions( + self.id.0, + channels, + ) } /// Returns the Id of the shard associated with the guild. @@ -1360,20 +1466,6 @@ impl Guild { /// When the cache is enabled this will automatically retrieve the total /// number of shards. /// - /// **Note**: When the cache is enabled, this function unlocks the cache to - /// retrieve the total number of shards in use. If you already have the - /// total, consider using [`utils::shard_id`]. - /// - /// [`utils::shard_id`]: ../utils/fn.shard_id.html - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - pub fn shard_id(&self) -> u64 { self.id.shard_id() } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total - /// number of shards. - /// /// When the cache is not enabled, the total number of shards being used /// will need to be passed. /// @@ -1389,7 +1481,7 @@ impl Guild { /// /// assert_eq!(guild.shard_id(17), 7); /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] + #[cfg(feature = "utils")] #[inline] pub fn shard_id(&self, shard_count: u64) -> u64 { self.id.shard_id(shard_count) } @@ -1406,8 +1498,12 @@ impl Guild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn start_integration_sync<I: Into<IntegrationId>>(&self, integration_id: I) -> Result<()> { - self.id.start_integration_sync(integration_id) + pub fn start_integration_sync<I>(&self, integration_id: I) + -> FutureResult<()> where I: Into<IntegrationId> { + ftryopt!(self.client).http.start_integration_sync( + self.id.0, + integration_id.into().0, + ) } /// Starts a prune of [`Member`]s. @@ -1425,17 +1521,19 @@ impl Guild { /// [`GuildPrune`]: struct.GuildPrune.html /// [`Member`]: struct.Member.html /// [Kick Members]: permissions/constant.KICK_MEMBERS.html - pub fn start_prune(&self, days: u16) -> Result<GuildPrune> { + pub fn start_prune(&self, days: u16) -> FutureResult<GuildPrune> { #[cfg(feature = "cache")] { let req = Permissions::KICK_MEMBERS; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.start_prune(days) + ftryopt!(self.client).http.start_guild_prune(self.id.0, days) } /// Unbans the given [`User`] from the guild. @@ -1450,17 +1548,19 @@ impl Guild { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [`User`]: struct.User.html /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - pub fn unban<U: Into<UserId>>(&self, user_id: U) -> Result<()> { + pub fn unban<U: Into<UserId>>(&self, user_id: U) -> FutureResult<()> { #[cfg(feature = "cache")] { let req = Permissions::BAN_MEMBERS; if !self.has_perms(req) { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - self.id.unban(user_id) + ftryopt!(self.client).http.remove_ban(self.id.0, user_id.into().0) } /// Retrieves the guild's webhooks. @@ -1469,7 +1569,9 @@ impl Guild { /// /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[inline] - pub fn webhooks(&self) -> Result<Vec<Webhook>> { self.id.webhooks() } + pub fn webhooks(&self) -> FutureResult<Vec<Webhook>> { + ftryopt!(self.client).http.get_guild_webhooks(self.id.0) + } /// Obtain a reference to a role by its name. /// @@ -1502,8 +1604,15 @@ impl Guild { /// /// client.start().unwrap(); /// ``` - pub fn role_by_name(&self, role_name: &str) -> Option<&Role> { - self.roles.values().find(|role| role_name == role.name) + pub fn role_by_name(&self, role_name: &str) -> Option<&Rc<RefCell<Role>>> { + self.roles.values().find(|role| { + let role = match role.try_borrow().ok() { + Some(role) => role, + None => return false, + }; + + role_name == role.name + }) } } @@ -1643,6 +1752,7 @@ impl<'de> Deserialize<'de> for Guild { application_id: application_id, afk_timeout: afk_timeout, channels: channels, + client: None, default_message_notifications: default_message_notifications, emojis: emojis, explicit_content_filter: explicit_content_filter, diff --git a/src/model/guild/partial_guild.rs b/src/model/guild/partial_guild.rs index aebd6e7..6b0b3d2 100644 --- a/src/model/guild/partial_guild.rs +++ b/src/model/guild/partial_guild.rs @@ -1,5 +1,10 @@ +use futures::{Future, future}; use model::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; use super::super::utils::{deserialize_emojis, deserialize_roles}; +use super::super::WrappedClient; +use ::FutureResult; #[cfg(feature = "model")] use builder::{EditGuild, EditMember, EditRole}; @@ -28,9 +33,13 @@ pub struct PartialGuild { pub name: String, pub owner_id: UserId, pub region: String, - #[serde(deserialize_with = "deserialize_roles")] pub roles: HashMap<RoleId, Role>, + #[serde(deserialize_with = "deserialize_roles", + serialize_with = "serialize_gen_rc_map")] + pub roles: HashMap<RoleId, Rc<RefCell<Role>>>, pub splash: Option<String>, pub verification_level: VerificationLevel, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -59,14 +68,21 @@ impl PartialGuild { /// enum.ModelError.html#variant.DeleteMessageDaysAmount /// [`User`]: struct.User.html /// [Ban Members]: permissions/constant.BAN_MEMBERS.html - pub fn ban<U: Into<UserId>>(&self, user: U, delete_message_days: u8) -> Result<()> { + // todo: add ban reason + pub fn ban<U: Into<UserId>>(&self, user_id: U, delete_message_days: u8) + -> FutureResult<()> { if delete_message_days > 7 { - return Err(Error::Model( + return Box::new(future::err(Error::Model( ModelError::DeleteMessageDaysAmount(delete_message_days), - )); + ))); } - self.id.ban(user, &delete_message_days) + Box::new(ftryopt!(self.client).http.ban_user( + self.id.0, + user_id.into().0, + delete_message_days, + "", + )) } /// Gets a list of the guild's bans. @@ -75,13 +91,30 @@ impl PartialGuild { /// /// [Ban Members]: permissions/constant.BAN_MEMBERS.html #[inline] - pub fn bans(&self) -> Result<Vec<Ban>> { self.id.bans() } + pub fn bans(&self) -> FutureResult<Vec<Ban>> { + ftryopt!(self.client).http.get_bans(self.id.0) + } /// Gets all of the guild's channels over the REST API. /// /// [`Guild`]: struct.Guild.html #[inline] - pub fn channels(&self) -> Result<HashMap<ChannelId, GuildChannel>> { self.id.channels() } + pub fn channels(&self) -> FutureResult<HashMap<ChannelId, GuildChannel>> { + let done = ftryopt!(self.client) + .http + .get_channels(self.id.0) + .map(|channels| { + let mut map = HashMap::with_capacity(channels.len()); + + for channel in channels { + map.insert(channel.id, channel); + } + + map + }); + + Box::new(done) + } /// Creates a [`GuildChannel`] in the guild. /// @@ -103,9 +136,14 @@ impl PartialGuild { /// [`http::create_channel`]: ../http/fn.create_channel.html /// [Manage Channels]: permissions/constant.MANAGE_CHANNELS.html #[inline] - pub fn create_channel<C>(&self, name: &str, kind: ChannelType, category: C) -> Result<GuildChannel> - where C: Into<Option<ChannelId>> { - self.id.create_channel(name, kind, category) + pub fn create_channel<C>(&self, name: &str, kind: ChannelType, category: C) + -> FutureResult<GuildChannel> where C: Into<Option<ChannelId>> { + ftryopt!(self.client).http.create_channel( + self.id.0, + name, + kind, + category.into().map(|x| x.0), + ) } /// Creates an emoji in the guild with a name and base64-encoded image. @@ -126,8 +164,8 @@ impl PartialGuild { /// [`utils::read_image`]: ../utils/fn.read_image.html /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html #[inline] - pub fn create_emoji(&self, name: &str, image: &str) -> Result<Emoji> { - self.id.create_emoji(name, image) + pub fn create_emoji(&self, name: &str, image: &str) -> FutureResult<Emoji> { + ftryopt!(self.client).http.create_emoji(self.id.0, name, image) } /// Creates an integration for the guild. @@ -136,9 +174,13 @@ impl PartialGuild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn create_integration<I>(&self, integration_id: I, kind: &str) -> Result<()> - where I: Into<IntegrationId> { - self.id.create_integration(integration_id, kind) + pub fn create_integration<I>(&self, integration_id: I, kind: &str) + -> FutureResult<()> where I: Into<IntegrationId> { + ftryopt!(self.client).http.create_guild_integration( + self.id.0, + integration_id.into().0, + kind, + ) } /// Creates a new role in the guild with the data set, if any. @@ -156,8 +198,9 @@ impl PartialGuild { /// [`Guild::create_role`]: struct.Guild.html#method.create_role /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[inline] - pub fn create_role<F: FnOnce(EditRole) -> EditRole>(&self, f: F) -> Result<Role> { - self.id.create_role(f) + pub fn create_role<F: FnOnce(EditRole) -> EditRole>(&self, f: F) + -> FutureResult<Role> { + ftryopt!(self.client).http.create_role(self.id.0, f) } /// Deletes the current guild if the current user is the owner of the @@ -165,7 +208,9 @@ impl PartialGuild { /// /// **Note**: Requires the current user to be the owner of the guild. #[inline] - pub fn delete(&self) -> Result<PartialGuild> { self.id.delete() } + pub fn delete(&self) -> FutureResult<PartialGuild> { + ftryopt!(self.client).http.delete_guild(self.id.0) + } /// Deletes an [`Emoji`] from the guild. /// @@ -174,8 +219,9 @@ impl PartialGuild { /// [`Emoji`]: struct.Emoji.html /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html #[inline] - pub fn delete_emoji<E: Into<EmojiId>>(&self, emoji_id: E) -> Result<()> { - self.id.delete_emoji(emoji_id) + pub fn delete_emoji<E: Into<EmojiId>>(&self, emoji_id: E) + -> FutureResult<()> { + ftryopt!(self.client).http.delete_emoji(self.id.0, emoji_id.into().0) } /// Deletes an integration by Id from the guild. @@ -184,8 +230,12 @@ impl PartialGuild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn delete_integration<I: Into<IntegrationId>>(&self, integration_id: I) -> Result<()> { - self.id.delete_integration(integration_id) + pub fn delete_integration<I: Into<IntegrationId>>(&self, integration_id: I) + -> FutureResult<()> { + ftryopt!(self.client).http.delete_guild_integration( + self.id.0, + integration_id.into().0, + ) } /// Deletes a [`Role`] by Id from the guild. @@ -199,8 +249,10 @@ impl PartialGuild { /// [`Role::delete`]: struct.Role.html#method.delete /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[inline] - pub fn delete_role<R: Into<RoleId>>(&self, role_id: R) -> Result<()> { - self.id.delete_role(role_id) + pub fn delete_role<R: Into<RoleId>>(&self, role_id: R) -> FutureResult<()> { + let role_id = role_id.into().0; + + ftryopt!(self.client).http.delete_role(self.id.0, role_id) } /// Edits the current guild with new data where specified. @@ -209,28 +261,9 @@ impl PartialGuild { /// permission. /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html - pub fn edit<F>(&mut self, f: F) -> Result<()> - where F: FnOnce(EditGuild) -> EditGuild { - match self.id.edit(f) { - Ok(guild) => { - self.afk_channel_id = guild.afk_channel_id; - self.afk_timeout = guild.afk_timeout; - self.default_message_notifications = guild.default_message_notifications; - self.emojis = guild.emojis; - self.features = guild.features; - self.icon = guild.icon; - self.mfa_level = guild.mfa_level; - self.name = guild.name; - self.owner_id = guild.owner_id; - self.region = guild.region; - self.roles = guild.roles; - self.splash = guild.splash; - self.verification_level = guild.verification_level; - - Ok(()) - }, - Err(why) => Err(why), - } + pub fn edit<'a, F: FnOnce(EditGuild) -> EditGuild>(&self, f: F) + -> FutureResult<PartialGuild> { + ftryopt!(self.client).http.edit_guild(self.id.0, f) } /// Edits an [`Emoji`]'s name in the guild. @@ -244,8 +277,11 @@ impl PartialGuild { /// [`Emoji::edit`]: struct.Emoji.html#method.edit /// [Manage Emojis]: permissions/constant.MANAGE_EMOJIS.html #[inline] - pub fn edit_emoji<E: Into<EmojiId>>(&self, emoji_id: E, name: &str) -> Result<Emoji> { - self.id.edit_emoji(emoji_id, name) + pub fn edit_emoji<E: Into<EmojiId>>(&self, emoji_id: E, name: &str) + -> FutureResult<Emoji> { + ftryopt!(self.client) + .http + .edit_emoji(self.id.0, emoji_id.into().0, name) } /// Edits the properties of member of the guild, such as muting or @@ -264,9 +300,9 @@ impl PartialGuild { /// GuildId(7).edit_member(user_id, |m| m.mute(true).roles(&vec![role_id])); /// ``` #[inline] - pub fn edit_member<F, U>(&self, user_id: U, f: F) -> Result<()> + pub fn edit_member<F, U>(&self, user_id: U, f: F) -> FutureResult<()> where F: FnOnce(EditMember) -> EditMember, U: Into<UserId> { - self.id.edit_member(user_id, f) + ftryopt!(self.client).http.edit_member(self.id.0, user_id.into().0, f) } /// Edits the current user's nickname for the guild. @@ -284,16 +320,11 @@ impl PartialGuild { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Change Nickname]: permissions/constant.CHANGE_NICKNAME.html #[inline] - pub fn edit_nickname(&self, new_nickname: Option<&str>) -> Result<()> { - self.id.edit_nickname(new_nickname) + pub fn edit_nickname(&self, new_nickname: Option<&str>) + -> FutureResult<()> { + ftryopt!(self.client).http.edit_nickname(self.id.0, new_nickname) } - /// Gets a partial amount of guild data by its Id. - /// - /// Requires that the current user be in the guild. - #[inline] - pub fn get<G: Into<GuildId>>(guild_id: G) -> Result<PartialGuild> { guild_id.into().get() } - /// Kicks a [`Member`] from the guild. /// /// Requires the [Kick Members] permission. @@ -301,7 +332,9 @@ impl PartialGuild { /// [`Member`]: struct.Member.html /// [Kick Members]: permissions/constant.KICK_MEMBERS.html #[inline] - pub fn kick<U: Into<UserId>>(&self, user_id: U) -> Result<()> { self.id.kick(user_id) } + pub fn kick<U: Into<UserId>>(&self, user_id: U) -> FutureResult<()> { + ftryopt!(self.client).http.kick_member(self.id.0, user_id.into().0) + } /// Returns a formatted URL of the guild's icon, if the guild has an icon. pub fn icon_url(&self) -> Option<String> { @@ -314,7 +347,9 @@ impl PartialGuild { /// /// This performs a request over the REST API. #[inline] - pub fn integrations(&self) -> Result<Vec<Integration>> { self.id.integrations() } + pub fn integrations(&self) -> FutureResult<Vec<Integration>> { + ftryopt!(self.client).http.get_guild_integrations(self.id.0) + } /// Gets all of the guild's invites. /// @@ -322,17 +357,23 @@ impl PartialGuild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn invites(&self) -> Result<Vec<RichInvite>> { self.id.invites() } + pub fn invites(&self) -> FutureResult<Vec<RichInvite>> { + ftryopt!(self.client).http.get_guild_invites(self.id.0) + } /// Leaves the guild. #[inline] - pub fn leave(&self) -> Result<()> { self.id.leave() } + pub fn leave(&self) -> FutureResult<()> { + ftryopt!(self.client).http.leave_guild(self.id.0) + } /// Gets a user's [`Member`] for the guild by Id. /// /// [`Guild`]: struct.Guild.html /// [`Member`]: struct.Member.html - pub fn member<U: Into<UserId>>(&self, user_id: U) -> Result<Member> { self.id.member(user_id) } + pub fn member<U: Into<UserId>>(&self, user_id: U) -> FutureResult<Member> { + ftryopt!(self.client).http.get_member(self.id.0, user_id.into().0) + } /// Gets a list of the guild's members. /// @@ -341,9 +382,11 @@ impl PartialGuild { /// [`User`]'s Id. /// /// [`User`]: struct.User.html - pub fn members<U>(&self, limit: Option<u64>, after: Option<U>) -> Result<Vec<Member>> - where U: Into<UserId> { - self.id.members(limit, after) + pub fn members<U: Into<UserId>>(&self, limit: Option<u64>, after: Option<U>) + -> FutureResult<Vec<Member>> { + let after = after.map(Into::into).map(|x| x.0); + + ftryopt!(self.client).http.get_guild_members(self.id.0, limit, after) } /// Moves a member to a specific voice channel. @@ -352,9 +395,12 @@ impl PartialGuild { /// /// [Move Members]: permissions/constant.MOVE_MEMBERS.html #[inline] - pub fn move_member<C, U>(&self, user_id: U, channel_id: C) -> Result<()> - where C: Into<ChannelId>, U: Into<UserId> { - self.id.move_member(user_id, channel_id) + pub fn move_member<C, U>(&self, user_id: U, channel_id: C) + -> FutureResult<()> where C: Into<ChannelId>, U: Into<UserId> { + ftryopt!(self.client) + .http + .edit_member(self.id.0, user_id.into().0, |f| f + .voice_channel(channel_id.into().0)) } /// Gets the number of [`Member`]s that would be pruned with the given @@ -365,21 +411,9 @@ impl PartialGuild { /// [`Member`]: struct.Member.html /// [Kick Members]: permissions/constant.KICK_MEMBERS.html #[inline] - pub fn prune_count(&self, days: u16) -> Result<GuildPrune> { self.id.prune_count(days) } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total - /// number of shards. - /// - /// **Note**: When the cache is enabled, this function unlocks the cache to - /// retrieve the total number of shards in use. If you already have the - /// total, consider using [`utils::shard_id`]. - /// - /// [`utils::shard_id`]: ../utils/fn.shard_id.html - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - pub fn shard_id(&self) -> u64 { self.id.shard_id() } + pub fn prune_count(&self, days: u16) -> FutureResult<GuildPrune> { + ftryopt!(self.client).http.get_guild_prune_count(self.id.0, days) + } /// Returns the Id of the shard associated with the guild. /// @@ -401,7 +435,7 @@ impl PartialGuild { /// /// assert_eq!(guild.shard_id(17), 7); /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] + #[cfg(feature = "utils")] #[inline] pub fn shard_id(&self, shard_count: u64) -> u64 { self.id.shard_id(shard_count) } @@ -418,8 +452,11 @@ impl PartialGuild { /// /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html #[inline] - pub fn start_integration_sync<I: Into<IntegrationId>>(&self, integration_id: I) -> Result<()> { - self.id.start_integration_sync(integration_id) + pub fn start_integration_sync<I>(&self, integration_id: I) + -> FutureResult<()> where I: Into<IntegrationId> { + ftryopt!(self.client) + .http + .start_integration_sync(self.id.0, integration_id.into().0) } /// Unbans a [`User`] from the guild. @@ -429,7 +466,9 @@ impl PartialGuild { /// [`User`]: struct.User.html /// [Ban Members]: permissions/constant.BAN_MEMBERS.html #[inline] - pub fn unban<U: Into<UserId>>(&self, user_id: U) -> Result<()> { self.id.unban(user_id) } + pub fn unban<U: Into<UserId>>(&self, user_id: U) -> FutureResult<()> { + ftryopt!(self.client).http.remove_ban(self.id.0, user_id.into().0) + } /// Retrieves the guild's webhooks. /// @@ -437,7 +476,9 @@ impl PartialGuild { /// /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[inline] - pub fn webhooks(&self) -> Result<Vec<Webhook>> { self.id.webhooks() } + pub fn webhooks(&self) -> FutureResult<Vec<Webhook>> { + ftryopt!(self.client).http.get_guild_webhooks(self.id.0) + } /// Obtain a reference to a role by its name. /// @@ -471,7 +512,7 @@ impl PartialGuild { /// /// client.start().unwrap(); /// ``` - pub fn role_by_name(&self, role_name: &str) -> Option<&Role> { - self.roles.values().find(|role| role_name == role.name) + pub fn role_by_name(&self, role_name: &str) -> Option<&Rc<RefCell<Role>>> { + self.roles.values().find(|role| role_name == role.borrow().name) } } diff --git a/src/model/guild/role.rs b/src/model/guild/role.rs index 53ec478..bcb142a 100644 --- a/src/model/guild/role.rs +++ b/src/model/guild/role.rs @@ -1,12 +1,12 @@ use model::prelude::*; use std::cmp::Ordering; +use super::super::WrappedClient; +use ::FutureResult; #[cfg(all(feature = "builder", feature = "cache", feature = "model"))] use builder::EditRole; #[cfg(all(feature = "cache", feature = "model"))] use internal::prelude::*; -#[cfg(all(feature = "cache", feature = "model"))] -use {CACHE, http}; /// Information about a role within a guild. A role represents a set of /// permissions, and can be attached to one or multiple users. A role has @@ -57,6 +57,8 @@ pub struct Role { /// /// The `@everyone` role is usually either `-1` or `0`. pub position: i64, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -68,7 +70,11 @@ impl Role { /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[cfg(feature = "cache")] #[inline] - pub fn delete(&self) -> Result<()> { http::delete_role(self.find_guild()?.0, self.id.0) } + pub fn delete(&self) -> FutureResult<()> { + let guild_id = ftry!(self.find_guild()); + + ftryopt!(self.client).http.delete_role(guild_id.0, self.id.0) + } /// Edits a [`Role`], optionally setting its new fields. /// @@ -89,9 +95,11 @@ impl Role { /// [`Role`]: struct.Role.html /// [Manage Roles]: permissions/constant.MANAGE_ROLES.html #[cfg(all(feature = "builder", feature = "cache"))] - pub fn edit<F: FnOnce(EditRole) -> EditRole>(&self, f: F) -> Result<Role> { - self.find_guild() - .and_then(|guild_id| guild_id.edit_role(self.id, f)) + pub fn edit<F: FnOnce(EditRole) -> EditRole>(&self, f: F) + -> FutureResult<Role> { + let guild_id = ftry!(self.find_guild()); + + ftryopt!(self.client).http.edit_role(guild_id.0, self.id.0, f) } /// Searches the cache for the guild that owns the role. @@ -104,8 +112,12 @@ impl Role { /// [`ModelError::GuildNotFound`]: enum.ModelError.html#variant.GuildNotFound #[cfg(feature = "cache")] pub fn find_guild(&self) -> Result<GuildId> { - for guild in CACHE.read().guilds.values() { - let guild = guild.read(); + let client = self.client.as_ref().ok_or_else(|| { + Error::Model(ModelError::ClientNotPresent) + })?; + + for guild in client.cache.borrow().guilds.values() { + let guild = guild.borrow(); if guild.roles.contains_key(&RoleId(self.id.0)) { return Ok(guild.id); @@ -161,29 +173,6 @@ impl PartialOrd for Role { fn partial_cmp(&self, other: &Role) -> Option<Ordering> { Some(self.cmp(other)) } } -#[cfg(feature = "model")] -impl RoleId { - /// Search the cache for the role. - #[cfg(feature = "cache")] - pub fn find(&self) -> Option<Role> { - let cache = CACHE.read(); - - for guild in cache.guilds.values() { - let guild = guild.read(); - - if !guild.roles.contains_key(self) { - continue; - } - - if let Some(role) = guild.roles.get(self) { - return Some(role.clone()); - } - } - - None - } -} - impl From<Role> for RoleId { /// Gets the Id of a role. fn from(role: Role) -> RoleId { role.id } diff --git a/src/model/invite.rs b/src/model/invite.rs index 6573e35..3be7f47 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -1,16 +1,13 @@ //! Models for server and channel invites. use chrono::{DateTime, FixedOffset}; +use futures::future; use super::prelude::*; +use super::WrappedClient; +use ::FutureResult; -#[cfg(feature = "model")] -use builder::CreateInvite; -#[cfg(feature = "model")] -use internal::prelude::*; #[cfg(all(feature = "cache", feature = "model"))] -use super::{Permissions, utils as model_utils}; -#[cfg(feature = "model")] -use {http, utils}; +use super::Permissions; /// Information about an invite code. /// @@ -40,46 +37,12 @@ pub struct Invite { /// a representation of the minimal amount of information needed about the /// [`Guild`] being invited to. pub guild: InviteGuild, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] impl Invite { - /// Creates an invite for a [`GuildChannel`], providing a builder so that - /// fields may optionally be set. - /// - /// See the documentation for the [`CreateInvite`] builder for information - /// on how to use this and the default values that it provides. - /// - /// Requires the [Create Invite] permission. - /// - /// # Errors - /// - /// If the `cache` is enabled, returns a [`ModelError::InvalidPermissions`] - /// if the current user does not have the required [permission]. - /// - /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions - /// [`CreateInvite`]: ../builder/struct.CreateInvite.html - /// [`GuildChannel`]: struct.GuildChannel.html - /// [Create Invite]: permissions/constant.CREATE_INVITE.html - /// [permission]: permissions/index.html - pub fn create<C, F>(channel_id: C, f: F) -> Result<RichInvite> - where C: Into<ChannelId>, F: FnOnce(CreateInvite) -> CreateInvite { - let channel_id = channel_id.into(); - - #[cfg(feature = "cache")] - { - let req = Permissions::CREATE_INVITE; - - if !model_utils::user_has_perms(channel_id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); - } - } - - let map = utils::vecmap_to_json_map(f(CreateInvite::default()).0); - - http::create_invite(channel_id.0, &map) - } - /// Deletes the invite. /// /// **Note**: Requires the [Manage Guild] permission. @@ -92,30 +55,23 @@ impl Invite { /// [`ModelError::InvalidPermissions`]: enum.ModelError.html#variant.InvalidPermissions /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html /// [permission]: permissions/index.html - pub fn delete(&self) -> Result<Invite> { + pub fn delete(&self) -> FutureResult<Invite> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { + let cache = ftry!(client.cache.try_borrow()); + let req = Permissions::MANAGE_GUILD; - if !model_utils::user_has_perms(self.channel.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(cache.user_has_perms(self.channel.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - http::delete_invite(&self.code) - } - - /// Gets the information about an invite. - #[allow(unused_mut)] - pub fn get(code: &str, stats: bool) -> Result<Invite> { - let mut invite = code; - - #[cfg(feature = "utils")] - { - invite = ::utils::parse_invite(invite); - } - - http::get_invite(invite, stats) + ftryopt!(self.client).http.delete_invite(&self.code) } /// Returns a URL to use for the invite. @@ -177,20 +133,6 @@ impl InviteGuild { /// When the cache is enabled this will automatically retrieve the total /// number of shards. /// - /// **Note**: When the cache is enabled, this function unlocks the cache to - /// retrieve the total number of shards in use. If you already have the - /// total, consider using [`utils::shard_id`]. - /// - /// [`utils::shard_id`]: ../utils/fn.shard_id.html - #[cfg(all(feature = "cache", feature = "utils"))] - #[inline] - pub fn shard_id(&self) -> u64 { self.id.shard_id() } - - /// Returns the Id of the shard associated with the guild. - /// - /// When the cache is enabled this will automatically retrieve the total - /// number of shards. - /// /// When the cache is not enabled, the total number of shards being used /// will need to be passed. /// @@ -206,9 +148,11 @@ impl InviteGuild { /// /// assert_eq!(guild.shard_id(17), 7); /// ``` - #[cfg(all(feature = "utils", not(feature = "cache")))] + #[cfg(feature = "utils")] #[inline] - pub fn shard_id(&self, shard_count: u64) -> u64 { self.id.shard_id(shard_count) } + pub fn shard_id(&self, shard_count: u64) -> u64 { + self.id.shard_id(shard_count) + } } /// Detailed information about an invite. @@ -250,6 +194,8 @@ pub struct RichInvite { pub temporary: bool, /// The amount of times that an invite has been used. pub uses: u64, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -271,17 +217,23 @@ impl RichInvite { /// [`http::delete_invite`]: ../http/fn.delete_invite.html /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html /// [permission]: permissions/index.html - pub fn delete(&self) -> Result<Invite> { + pub fn delete(&self) -> FutureResult<Invite> { + let client = ftryopt!(self.client); + #[cfg(feature = "cache")] { + let cache = ftry!(client.cache.try_borrow()); + let req = Permissions::MANAGE_GUILD; - if !model_utils::user_has_perms(self.channel.id, req)? { - return Err(Error::Model(ModelError::InvalidPermissions(req))); + if !ftry!(cache.user_has_perms(self.channel.id, req)) { + return Box::new(future::err(Error::Model( + ModelError::InvalidPermissions(req), + ))); } } - http::delete_invite(&self.code) + ftryopt!(self.client).http.delete_invite(&self.code) } /// Returns a URL to use for the invite. diff --git a/src/model/misc.rs b/src/model/misc.rs index 183ddc3..898ce6f 100644 --- a/src/model/misc.rs +++ b/src/model/misc.rs @@ -1,7 +1,6 @@ //! Miscellaneous helper traits, enums, and structs for models. use super::prelude::*; -use internal::RwLockExt; #[cfg(all(feature = "model", feature = "utils"))] use std::error::Error as StdError; @@ -28,9 +27,9 @@ impl Mentionable for ChannelId { impl Mentionable for Channel { fn mention(&self) -> String { match *self { - Channel::Guild(ref x) => format!("<#{}>", x.with(|x| x.id.0)), - Channel::Private(ref x) => format!("<#{}>", x.with(|x| x.id.0)), - Channel::Group(ref x) => format!("<#{}>", x.with(|x| x.channel_id.0)), + Channel::Guild(ref x) => format!("<#{}>", x.borrow().id.0), + Channel::Private(ref x) => format!("<#{}>", x.borrow().id.0), + Channel::Group(ref x) => format!("<#{}>", x.borrow().channel_id.0), Channel::Category(_) => panic!("Categories can't be mentioned"), } } @@ -40,10 +39,6 @@ impl Mentionable for Emoji { fn mention(&self) -> String { format!("<:{}:{}>", self.name, self.id.0) } } -impl Mentionable for Member { - fn mention(&self) -> String { format!("<@{}>", self.user.with(|u| u.id.0)) } -} - impl Mentionable for RoleId { fn mention(&self) -> String { format!("<@&{}>", self.0) } } @@ -84,20 +79,6 @@ impl StdError for UserParseError { } } -#[cfg(all(feature = "model", feature = "utils"))] -impl FromStr for User { - type Err = UserParseError; - - fn from_str(s: &str) -> StdResult<Self, Self::Err> { - match utils::parse_username(s) { - Some(x) => UserId(x as u64) - .get() - .map_err(|e| UserParseError::Rest(Box::new(e))), - _ => Err(UserParseError::InvalidUsername), - } - } -} - macro_rules! impl_from_str { (id: $($id:tt, $err:ident;)*) => { $( @@ -136,49 +117,6 @@ macro_rules! impl_from_str { } )* }; - - (struct: $($struct:ty, $id:tt, $err:ident, $invalid_variant:tt, $parse_fn:ident, $desc:expr;)*) => { - $( - #[cfg(all(feature = "cache", feature = "model", feature = "utils"))] - #[derive(Debug)] - pub enum $err { - NotPresentInCache, - $invalid_variant, - } - - #[cfg(all(feature = "cache", feature = "model", feature = "utils"))] - impl fmt::Display for $err { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.description()) } - } - - #[cfg(all(feature = "cache", feature = "model", feature = "utils"))] - impl StdError for $err { - fn description(&self) -> &str { - use self::$err::*; - - match *self { - NotPresentInCache => "not present in cache", - $invalid_variant => $desc, - } - } - } - - #[cfg(all(feature = "cache", feature = "model", feature = "utils"))] - impl FromStr for $struct { - type Err = $err; - - fn from_str(s: &str) -> StdResult<Self, Self::Err> { - match utils::$parse_fn(s) { - Some(x) => match $id(x).find() { - Some(user) => Ok(user), - _ => Err($err::NotPresentInCache), - }, - _ => Err($err::$invalid_variant), - } - } - } - )* - }; } impl_from_str! { id: @@ -187,11 +125,6 @@ impl_from_str! { id: ChannelId, ChannelIdParseError; } -impl_from_str! { struct: - Channel, ChannelId, ChannelParseError, InvalidChannel, parse_channel, "invalid channel"; - Role, RoleId, RoleParseError, InvalidRole, parse_role, "invalid role"; -} - /// A version of an emoji used only when solely the Id and name are known. #[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct EmojiIdentifier { diff --git a/src/model/mod.rs b/src/model/mod.rs index 5e35e92..44a4a3e 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -40,16 +40,17 @@ pub mod webhook; pub use self::error::Error as ModelError; pub use self::permissions::Permissions; +use client::Client; use internal::prelude::*; -use parking_lot::RwLock; use self::utils::*; use serde::de::Visitor; +use serde::{Deserialize, Deserializer}; use std::collections::HashMap; use std::fmt::{Display, Formatter, Result as FmtResult}; -use std::sync::Arc; +use std::rc::Rc; +use std::result::Result as StdResult; #[cfg(feature = "utils")] use utils::Colour; -use serde::{Deserialize, Deserializer}; -use std::result::Result as StdResult; +type WrappedClient = Option<Rc<Client>>; diff --git a/src/model/user.rs b/src/model/user.rs index 227fadf..efc880a 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -1,29 +1,24 @@ //! User information-related models. +use futures::{Future, future}; use serde_json; use std::fmt; use super::utils::deserialize_u16; use super::prelude::*; +use super::WrappedClient; +use ::FutureResult; use internal::prelude::*; use model::misc::Mentionable; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; #[cfg(feature = "model")] use builder::{CreateMessage, EditProfile}; #[cfg(feature = "model")] use chrono::NaiveDateTime; #[cfg(feature = "model")] -use http::{self, GuildPagination}; -#[cfg(all(feature = "cache", feature = "model"))] -use parking_lot::RwLock; +use http::GuildPagination; #[cfg(feature = "model")] use std::fmt::Write; #[cfg(feature = "model")] -use std::mem; -#[cfg(all(feature = "cache", feature = "model"))] -use std::sync::Arc; -#[cfg(feature = "model")] use utils::{self, VecMap}; /// Information about the current user. @@ -37,6 +32,8 @@ pub struct CurrentUser { pub mfa_enabled: bool, #[serde(rename = "username")] pub name: String, pub verified: bool, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -88,8 +85,8 @@ impl CurrentUser { /// /// CACHE.write().user.edit(|p| p.avatar(Some(&avatar))); /// ``` - pub fn edit<F>(&mut self, f: F) -> Result<()> - where F: FnOnce(EditProfile) -> EditProfile { + pub fn edit<F: FnOnce(EditProfile) -> EditProfile>(&mut self, f: F) + -> Box<Future<Item = CurrentUser, Error = Error>> { let mut map = VecMap::new(); map.insert("username", Value::String(self.name.clone())); @@ -98,15 +95,9 @@ impl CurrentUser { } let map = utils::vecmap_to_json_map(f(EditProfile(map)).0); + let value = Value::Object(map); - match http::edit_profile(&map) { - Ok(new) => { - let _ = mem::replace(self, new); - - Ok(()) - }, - Err(why) => Err(why), - } + ftryopt!(self.client).http.edit_profile(&value) } /// Retrieves the URL to the current user's avatar, falling back to the @@ -142,8 +133,10 @@ impl CurrentUser { /// } /// } /// ``` - pub fn guilds(&self) -> Result<Vec<GuildInfo>> { - http::get_guilds(&GuildPagination::After(GuildId(1)), 100) + pub fn guilds(&self) -> FutureResult<Vec<GuildInfo>> { + ftryopt!(self.client) + .http + .get_guilds(&GuildPagination::After(GuildId(1)), 100) } /// Returns the invite url for the bot with the given permissions. @@ -211,20 +204,26 @@ impl CurrentUser { /// /// [`Error::Format`]: ../enum.Error.html#variant.Format /// [`HttpError::InvalidRequest`]: ../http/enum.HttpError.html#variant.InvalidRequest - pub fn invite_url(&self, permissions: Permissions) -> Result<String> { + pub fn invite_url(&self, permissions: Permissions) -> FutureResult<String> { let bits = permissions.bits(); - let client_id = http::get_current_application_info().map(|v| v.id)?; - let mut url = format!( - "https://discordapp.com/api/oauth2/authorize?client_id={}&scope=bot", - client_id - ); + let done = ftryopt!(self.client) + .http + .get_current_application_info() + .map(move |app| { + let mut url = format!( + "https://discordapp.com/api/oauth2/authorize?client_id={}&scope=bot", + app.id, + ); + + if bits != 0 { + let _ = write!(url, "&permissions={}", bits); + } - if bits != 0 { - write!(url, "&permissions={}", bits)?; - } + url + }); - Ok(url) + Box::new(done) } /// Returns a static formatted URL of the user's icon, if one exists. @@ -357,6 +356,8 @@ pub struct User { /// change if the username+discriminator pair becomes non-unique. #[serde(rename = "username")] pub name: String, + #[serde(skip)] + pub(crate) client: WrappedClient, } use std::hash::{Hash, Hasher}; @@ -388,7 +389,9 @@ impl User { /// /// [current user]: struct.CurrentUser.html #[inline] - pub fn create_dm_channel(&self) -> Result<PrivateChannel> { self.id.create_dm_channel() } + pub fn create_dm_channel(&self) -> FutureResult<PrivateChannel> { + ftryopt!(self.client).http.create_private_channel(self.id.0) + } /// Retrieves the time that this user was created at. #[inline] @@ -477,45 +480,56 @@ impl User { // (AKA: Clippy is wrong and so we have to mark as allowing this lint.) #[allow(let_and_return)] #[cfg(feature = "builder")] - pub fn direct_message<F>(&self, f: F) -> Result<Message> - where F: FnOnce(CreateMessage) -> CreateMessage { + pub fn direct_message<'a, F: 'a + FnOnce(CreateMessage) -> CreateMessage>( + &'a self, + f: F, + ) -> Box<Future<Item = Message, Error = Error> + 'a> { if self.bot { - return Err(Error::Model(ModelError::MessagingBot)); + return Box::new(future::err(Error::Model( + ModelError::MessagingBot, + ))); } - let private_channel_id = feature_cache! { - { - let finding = { - let cache = CACHE.read(); + let client = ftryopt!(self.client); - let finding = cache.private_channels - .values() - .map(|ch| ch.read()) - .find(|ch| ch.recipient.read().id == self.id) - .map(|ch| ch.id); + let private_channel_id = feature_cache! {{ + let finding = { + let cache = client.cache.borrow(); - finding - }; + let finding = cache.private_channels + .values() + .find(|ch| { + let ch = ch.borrow(); + let recipient = ch.recipient.borrow(); - if let Some(finding) = finding { - finding - } else { - let map = json!({ - "recipient_id": self.id.0, - }); + recipient.id == self.id + }) + .map(|ch| ch.borrow().id); - http::create_private_channel(&map)?.id - } + finding + }; + + if let Some(finding) = finding { + return Box::new(client.http.send_message(finding.0, f)); } else { - let map = json!({ - "recipient_id": self.id.0, - }); + let done = client + .http + .create_private_channel(self.id.0) + .map(|channel| channel.id); - http::create_private_channel(&map)?.id + Box::new(done) } - }; + } else { + let done = ftryopt!(self.client) + .http + .create_private_channel(self.id.0) + .map(|channel| channel.id); + + Box::new(done) + }}; - private_channel_id.send_message(f) + Box::new(private_channel_id + .and_then(move |id| client.http.send_message(id.0, f))) } /// This is an alias of [direct_message]. @@ -539,7 +553,10 @@ impl User { /// [direct_message]: #method.direct_message #[cfg(feature = "builder")] #[inline] - pub fn dm<F: FnOnce(CreateMessage) -> CreateMessage>(&self, f: F) -> Result<Message> { + pub fn dm<'a, F: 'a + FnOnce(CreateMessage) -> CreateMessage>( + &'a self, + f: F, + ) -> Box<Future<Item = Message, Error = Error> + 'a> { self.direct_message(f) } @@ -586,12 +603,19 @@ impl User { GuildContainer::Guild(guild) => guild.roles.contains_key(&role_id), GuildContainer::Id(_guild_id) => { feature_cache! {{ - CACHE.read() + let client = match self.client.as_ref() { + Some(client) => client, + None => return false, + }; + + let cache = client.cache.borrow(); + + cache .guilds .get(&_guild_id) .map(|g| { - g.read().members.get(&self.id) - .map(|m| m.roles.contains(&role_id)) + g.borrow().members.get(&self.id) + .map(|m| m.borrow().roles.contains(&role_id)) .unwrap_or(false) }) .unwrap_or(false) @@ -602,69 +626,6 @@ impl User { } } - /// Refreshes the information about the user. - /// - /// Replaces the instance with the data retrieved over the REST API. - /// - /// # Examples - /// - /// If maintaing a very long-running bot, you may want to periodically - /// refresh information about certain users if the state becomes - /// out-of-sync: - /// - /// ```rust,no_run - /// # use serenity::prelude::*; - /// # use serenity::model::prelude::*; - /// # - /// struct Handler; - /// - /// impl EventHandler for Handler { - /// fn message(&self, _: Context, _: Message) { - /// // normal message handling here - /// } - /// } - /// - /// let mut client = Client::new("token", Handler).unwrap(); - /// # - /// use serenity::model::id::UserId; - /// use serenity::CACHE; - /// use std::thread; - /// use std::time::Duration; - /// - /// let special_users = vec![UserId(114941315417899012), UserId(87600987040120832)]; - /// - /// // start a new thread to periodically refresh the special users' data - /// // every 12 hours - /// let handle = thread::spawn(move || { - /// // 12 hours in seconds - /// let duration = Duration::from_secs(43200); - /// - /// loop { - /// thread::sleep(duration); - /// - /// let cache = CACHE.read(); - /// - /// for id in &special_users { - /// if let Some(user) = cache.user(*id) { - /// if let Err(why) = user.write().refresh() { - /// println!("Error refreshing {}: {:?}", id, why); - /// } - /// } - /// } - /// } - /// }); - /// - /// println!("{:?}", client.start()); - /// ``` - pub fn refresh(&mut self) -> Result<()> { - self.id.get().map(|replacement| { - mem::replace(self, replacement); - - () - }) - } - - /// Returns a static formatted URL of the user's icon, if one exists. /// /// This will always produce a WEBP image URL. @@ -718,40 +679,6 @@ impl fmt::Display for User { } } -#[cfg(feature = "model")] -impl UserId { - /// Creates a direct message channel between the [current user] and the - /// user. This can also retrieve the channel if one already exists. - /// - /// [current user]: struct.CurrentUser.html - pub fn create_dm_channel(&self) -> Result<PrivateChannel> { - let map = json!({ - "recipient_id": self.0, - }); - - http::create_private_channel(&map) - } - - /// Search the cache for the user with the Id. - #[cfg(feature = "cache")] - pub fn find(&self) -> Option<Arc<RwLock<User>>> { CACHE.read().user(*self) } - - /// Gets a user by its Id over the REST API. - /// - /// **Note**: The current user must be a bot user. - #[inline] - pub fn get(&self) -> Result<User> { - #[cfg(feature = "cache")] - { - if let Some(user) = CACHE.read().user(*self) { - return Ok(user.read().clone()); - } - } - - http::get_user(self.0) - } -} - impl From<CurrentUser> for UserId { /// Gets the Id of a `CurrentUser` struct. fn from(current_user: CurrentUser) -> UserId { current_user.id } @@ -762,16 +689,6 @@ impl<'a> From<&'a CurrentUser> for UserId { fn from(current_user: &CurrentUser) -> UserId { current_user.id } } -impl From<Member> for UserId { - /// Gets the Id of a `Member`. - 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().id } -} - impl From<User> for UserId { /// Gets the Id of a `User`. fn from(user: User) -> UserId { user.id } diff --git a/src/model/utils.rs b/src/model/utils.rs index 981b61b..9ed34e1 100644 --- a/src/model/utils.rs +++ b/src/model/utils.rs @@ -1,18 +1,19 @@ -use parking_lot::RwLock; use serde::de::Error as DeError; use serde::ser::{SerializeSeq, Serialize, Serializer}; +use std::cell::RefCell; use std::collections::HashMap; use std::hash::Hash; -use std::sync::Arc; +use std::rc::Rc; use super::prelude::*; +#[cfg(all(feature = "cache", feature = "model"))] +use cache::Cache; + #[cfg(feature = "cache")] use internal::prelude::*; #[cfg(all(feature = "cache", feature = "model"))] use super::permissions::Permissions; -#[cfg(all(feature = "cache", feature = "model"))] -use CACHE; pub fn default_true() -> bool { true @@ -33,12 +34,12 @@ pub fn deserialize_emojis<'de, D: Deserializer<'de>>( pub fn deserialize_guild_channels<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<HashMap<ChannelId, Arc<RwLock<GuildChannel>>>, D::Error> { + -> StdResult<HashMap<ChannelId, Rc<RefCell<GuildChannel>>>, D::Error> { let vec: Vec<GuildChannel> = Deserialize::deserialize(deserializer)?; let mut map = HashMap::new(); for channel in vec { - map.insert(channel.id, Arc::new(RwLock::new(channel))); + map.insert(channel.id, Rc::new(RefCell::new(channel))); } Ok(map) @@ -46,14 +47,14 @@ pub fn deserialize_guild_channels<'de, D: Deserializer<'de>>( pub fn deserialize_members<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<HashMap<UserId, Member>, D::Error> { + -> StdResult<HashMap<UserId, Rc<RefCell<Member>>>, D::Error> { let vec: Vec<Member> = Deserialize::deserialize(deserializer)?; let mut members = HashMap::new(); for member in vec { - let user_id = member.user.read().id; + let user_id = member.user.borrow().id; - members.insert(user_id, member); + members.insert(user_id, Rc::new(RefCell::new(member))); } Ok(members) @@ -61,12 +62,12 @@ pub fn deserialize_members<'de, D: Deserializer<'de>>( pub fn deserialize_presences<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<HashMap<UserId, Presence>, D::Error> { + -> StdResult<HashMap<UserId, Rc<RefCell<Presence>>>, D::Error> { let vec: Vec<Presence> = Deserialize::deserialize(deserializer)?; let mut presences = HashMap::new(); for presence in vec { - presences.insert(presence.user_id, presence); + presences.insert(presence.user_id, Rc::new(RefCell::new(presence))); } Ok(presences) @@ -74,19 +75,19 @@ pub fn deserialize_presences<'de, D: Deserializer<'de>>( pub fn deserialize_private_channels<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<HashMap<ChannelId, Channel>, D::Error> { + -> StdResult<HashMap<ChannelId, Rc<RefCell<Channel>>>, D::Error> { let vec: Vec<Channel> = Deserialize::deserialize(deserializer)?; let mut private_channels = HashMap::new(); for private_channel in vec { let id = match private_channel { - Channel::Group(ref group) => group.read().channel_id, - Channel::Private(ref channel) => channel.read().id, + Channel::Group(ref group) => group.borrow().channel_id, + Channel::Private(ref channel) => channel.borrow().id, Channel::Guild(_) => unreachable!("Guild private channel decode"), Channel::Category(_) => unreachable!("Channel category private channel decode"), }; - private_channels.insert(id, private_channel); + private_channels.insert(id, Rc::new(RefCell::new(private_channel))); } Ok(private_channels) @@ -94,12 +95,12 @@ pub fn deserialize_private_channels<'de, D: Deserializer<'de>>( pub fn deserialize_roles<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<HashMap<RoleId, Role>, D::Error> { + -> StdResult<HashMap<RoleId, Rc<RefCell<Role>>>, D::Error> { let vec: Vec<Role> = Deserialize::deserialize(deserializer)?; let mut roles = HashMap::new(); for role in vec { - roles.insert(role.id, role); + roles.insert(role.id, Rc::new(RefCell::new(role))); } Ok(roles) @@ -107,7 +108,7 @@ pub fn deserialize_roles<'de, D: Deserializer<'de>>( pub fn deserialize_single_recipient<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<Arc<RwLock<User>>, D::Error> { + -> StdResult<Rc<RefCell<User>>, D::Error> { let mut users: Vec<User> = Deserialize::deserialize(deserializer)?; let user = if users.is_empty() { return Err(DeError::custom("Expected a single recipient")); @@ -115,42 +116,42 @@ pub fn deserialize_single_recipient<'de, D: Deserializer<'de>>( users.remove(0) }; - Ok(Arc::new(RwLock::new(user))) + Ok(Rc::new(RefCell::new(user))) } -pub fn deserialize_sync_user<'de, D>(deserializer: D) - -> StdResult<Arc<RwLock<User>>, D::Error> where D: Deserializer<'de> { - Ok(Arc::new(RwLock::new(User::deserialize(deserializer)?))) +pub fn deserialize_user<'de, D>(deserializer: D) + -> StdResult<Rc<RefCell<User>>, D::Error> where D: Deserializer<'de> { + Ok(Rc::new(RefCell::new(User::deserialize(deserializer)?))) } -pub fn serialize_sync_user<S: Serializer>( - user: &Arc<RwLock<User>>, +pub fn serialize_user<S: Serializer>( + user: &Rc<RefCell<User>>, serializer: S, ) -> StdResult<S::Ok, S::Error> { - User::serialize(&*user.read(), serializer) + User::serialize(&*user.borrow(), serializer) } pub fn deserialize_users<'de, D: Deserializer<'de>>( deserializer: D) - -> StdResult<HashMap<UserId, Arc<RwLock<User>>>, D::Error> { + -> StdResult<HashMap<UserId, Rc<RefCell<User>>>, D::Error> { let vec: Vec<User> = Deserialize::deserialize(deserializer)?; let mut users = HashMap::new(); for user in vec { - users.insert(user.id, Arc::new(RwLock::new(user))); + users.insert(user.id, Rc::new(RefCell::new(user))); } Ok(users) } pub fn serialize_users<S: Serializer>( - users: &HashMap<UserId, Arc<RwLock<User>>>, + users: &HashMap<UserId, Rc<RefCell<User>>>, serializer: S ) -> StdResult<S::Ok, S::Error> { let mut seq = serializer.serialize_seq(Some(users.len()))?; for user in users.values() { - seq.serialize_element(&*user.read())?; + seq.serialize_element(&*user.borrow())?; } seq.end() @@ -190,58 +191,82 @@ pub fn serialize_gen_map<K: Eq + Hash, S: Serializer, V: Serialize>( seq.end() } -pub fn serialize_gen_locked_map<K: Eq + Hash, S: Serializer, V: Serialize>( - map: &HashMap<K, Arc<RwLock<V>>>, +pub fn serialize_gen_rc_map<K: Eq + Hash, S: Serializer, V: Serialize>( + map: &HashMap<K, Rc<RefCell<V>>>, serializer: S, ) -> StdResult<S::Ok, S::Error> { let mut seq = serializer.serialize_seq(Some(map.len()))?; for value in map.values() { - seq.serialize_element(&*value.read())?; + if let Ok(item) = value.try_borrow() { + seq.serialize_element(&*item)?; + } } seq.end() } #[cfg(all(feature = "cache", feature = "model"))] -pub fn user_has_perms(channel_id: ChannelId, mut permissions: Permissions) -> Result<bool> { - let cache = CACHE.read(); - let current_user = &cache.user; +pub trait PermissionCheck { + fn user_has_perms(&self, channel_id: ChannelId, permissions: Permissions) + -> Result<bool>; +} - let channel = match cache.channel(channel_id) { - Some(channel) => channel, - None => return Err(Error::Model(ModelError::ItemMissing)), - }; +#[cfg(all(feature = "cache", feature = "model"))] +impl PermissionCheck for Cache { + fn user_has_perms( + &self, + channel_id: ChannelId, + mut permissions: Permissions, + ) -> Result<bool> { + let current_user = &self.user; + + let channel = match self.channel(channel_id) { + Some(channel) => channel, + None => return Err(Error::Model(ModelError::ItemMissing)), + }; - let guild_id = match channel { - 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 - // permissions. - // - // The only exception to this is when the current user is blocked by - // the recipient in a DM channel, which results in the current user - // not being able to send messages. - // - // Since serenity can't _reasonably_ check and keep track of these, - // just assume that all permissions are granted and return `true`. - return Ok(true); - }, - }; + let guild_id = match channel { + Channel::Guild(channel) => channel.borrow().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 + // permissions. + // + // The only exception to this is when the current user is blocked by + // the recipient in a DM channel, which results in the current user + // not being able to send messages. + // + // Since serenity can't _reasonably_ check and keep track of these, + // just assume that all permissions are granted and return `true`. + return Ok(true); + }, + }; - let guild = match cache.guild(guild_id) { - Some(guild) => guild, - None => return Err(Error::Model(ModelError::ItemMissing)), - }; + let guild = match self.guilds.get(&guild_id) { + Some(guild) => guild, + None => return Err(Error::Model(ModelError::ItemMissing)), + }; - let perms = guild - .read() - .permissions_in(channel_id, current_user.id); + let perms = guild + .borrow() + .permissions_in(channel_id, current_user.id); - permissions.remove(perms); + permissions.remove(perms); - Ok(permissions.is_empty()) + Ok(permissions.is_empty()) + } +} + +macro_rules! ftryopt { + ($code:expr) => { + match $code { + Some(ref v) => v, + None => return Box::new(::futures::future::err(::Error::Model( + ::model::ModelError::ClientNotPresent, + ))), + } + }; } macro_rules! num_visitors { diff --git a/src/model/webhook.rs b/src/model/webhook.rs index 0d6b7c9..f3003e7 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -1,18 +1,17 @@ //! Webhook model and implementations. +use futures::Future; use super::id::{ChannelId, GuildId, WebhookId}; use super::user::User; +use super::WrappedClient; +use ::FutureResult; #[cfg(feature = "model")] use builder::ExecuteWebhook; #[cfg(feature = "model")] use internal::prelude::*; #[cfg(feature = "model")] -use std::mem; -#[cfg(feature = "model")] use super::channel::Message; -#[cfg(feature = "model")] -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 @@ -45,6 +44,8 @@ pub struct Webhook { /// /// **Note**: This is not received when getting a webhook by its token. pub user: Option<User>, + #[serde(skip)] + pub(crate) client: WrappedClient, } #[cfg(feature = "model")] @@ -56,7 +57,13 @@ impl Webhook { /// /// [`http::delete_webhook_with_token`]: ../http/fn.delete_webhook_with_token.html #[inline] - pub fn delete(&self) -> Result<()> { http::delete_webhook_with_token(self.id.0, &self.token) } + pub fn delete(&self) -> FutureResult<()> { + let done = ftryopt!(self.client) + .http + .delete_webhook_with_token(self.id.0, &self.token); + + Box::new(done) + } /// /// Edits the webhook in-place. All fields are optional. @@ -104,36 +111,13 @@ impl Webhook { /// /// [`http::edit_webhook`]: ../http/fn.edit_webhook.html /// [`http::edit_webhook_with_token`]: ../http/fn.edit_webhook_with_token.html - pub fn edit(&mut self, name: Option<&str>, avatar: Option<&str>) -> Result<()> { - if name.is_none() && avatar.is_none() { - return Ok(()); - } - - let mut map = Map::new(); + pub fn edit(&mut self, name: Option<&str>, avatar: Option<&str>) + -> Box<Future<Item = Webhook, Error = Error>> { + let done = ftryopt!(self.client) + .http + .edit_webhook_with_token(self.id.0, &self.token, name, avatar); - if let Some(avatar) = avatar { - map.insert( - "avatar".to_string(), - if avatar.is_empty() { - Value::Null - } else { - Value::String(avatar.to_string()) - }, - ); - } - - if let Some(name) = name { - map.insert("name".to_string(), Value::String(name.to_string())); - } - - match http::edit_webhook_with_token(self.id.0, &self.token, &map) { - Ok(replacement) => { - mem::replace(self, replacement); - - Ok(()) - }, - Err(why) => Err(why), - } + Box::new(done) } /// Executes a webhook with the fields set via the given builder. @@ -184,41 +168,15 @@ impl Webhook { /// .expect("Error executing"); /// ``` #[inline] - pub fn execute<F: FnOnce(ExecuteWebhook) -> ExecuteWebhook>(&self, - wait: bool, - f: F) - -> Result<Option<Message>> { - let map = utils::vecmap_to_json_map(f(ExecuteWebhook::default()).0); - - http::execute_webhook(self.id.0, &self.token, wait, &map) - } + pub fn execute<'a, F: FnOnce(ExecuteWebhook) -> ExecuteWebhook>( + &'a self, + wait: bool, + f: F, + ) -> Box<Future<Item = Option<Message>, Error = Error> + 'a> { + let done = ftryopt!(self.client) + .http + .execute_webhook(self.id.0, &self.token, wait, f); - /// Retrieves the latest information about the webhook, editing the - /// webhook in-place. - /// - /// As this calls the [`http::get_webhook_with_token`] function, - /// authentication is not required. - /// - /// [`http::get_webhook_with_token`]: ../http/fn.get_webhook_with_token.html - pub fn refresh(&mut self) -> Result<()> { - match http::get_webhook_with_token(self.id.0, &self.token) { - Ok(replacement) => { - let _ = mem::replace(self, replacement); - - Ok(()) - }, - Err(why) => Err(why), - } + Box::new(done) } } - -#[cfg(feature = "model")] -impl WebhookId { - /// Retrieves the webhook by the Id. - /// - /// **Note**: Requires the [Manage Webhooks] permission. - /// - /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html - #[inline] - pub fn get(&self) -> Result<Webhook> { http::get_webhook(self.0) } -} diff --git a/src/prelude.rs b/src/prelude.rs index 7b987ef..7e9c26b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -16,10 +16,10 @@ pub use error::Error as SerenityError; pub use model::misc::Mentionable; -pub use parking_lot::{Mutex, RwLock}; +// todo re-add EventHandler, Context re-exports #[cfg(feature = "client")] -pub use client::{Client, ClientError as ClientError, Context, EventHandler}; +pub use client::{Client, ClientError as ClientError}; #[cfg(feature = "gateway")] pub use gateway::GatewayError; #[cfg(feature = "http")] diff --git a/src/utils/mod.rs b/src/utils/mod.rs index de75632..0235a26 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -20,11 +20,6 @@ use std::hash::{BuildHasher, Hash}; use std::io::Read; use std::path::Path; -#[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<H, T>(map: HashMap<T, Value, H>) -> Map<String, Value> where H: BuildHasher, T: Eq + Hash + ToString { @@ -461,45 +456,3 @@ pub fn parse_quotes(s: &str) -> Vec<String> { /// ``` #[inline] pub fn shard_id(guild_id: u64, shard_count: u64) -> u64 { (guild_id >> 22) % shard_count } - -/// A function for doing automatic `read`ing (and the releasing of the guard as well) -/// This is particularly useful if you just want to use the cache for this one time, -/// or don't want to be messing with the `RwLock` directly. -/// -/// # Examples -/// -/// Return the bot's id -/// -/// ```rust,ignore -/// use serenity::utils; -/// -/// // assuming that the id is `1234`: -/// assert_eq!(1234, utils::with_cache(|cache| cache.user.id)); -/// ``` -#[cfg(feature = "cache")] -pub fn with_cache<T, F>(f: F) -> T - where F: Fn(&Cache) -> T { - let cache = CACHE.read(); - f(&cache) -} - -/// Like [`with_cache`] but as the name says, allows for modifications to be done. -/// -/// # Examples -/// -/// Return the bot's id, and changes the shard count -/// -/// ```rust,ignore -/// use serenity::utils; -/// -/// // assuming that the id is `1234`: -/// assert_eq!(1234, utils::with_cache_mut(|cache| { cache.shard_count = 8; cache.user.id })); -/// ``` -/// -/// [`with_cache`]: #fn.with_cache -#[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(); - f(&mut cache) -} |