diff options
| author | Zeyla Hellyer <[email protected]> | 2017-05-22 17:02:00 -0700 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2017-05-22 17:02:00 -0700 |
| commit | 9969be60cf320797c37b317da24d9a08fd5eafa5 (patch) | |
| tree | f27bf7a57af95bbc11990b1edcea9cca99276964 /src/ext | |
| parent | Reasonably derive Debug on items (diff) | |
| download | serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.tar.xz serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.zip | |
Restructure modules
Modules are now separated into a fashion where the library can be used
for most use cases, without needing to compile the rest.
The core of serenity, with no features enabled, contains only the
struct (model) definitions, constants, and prelude. Models do not have
most functions compiled in, as that is separated into the `model`
feature.
The `client` module has been split into 3 modules: `client`, `gateway`,
and `http`.
`http` contains functions to interact with the REST API. `gateway`
contains the Shard to interact with the gateway, requiring `http` for
retrieving the gateway URL. `client` requires both of the other features
and acts as an abstracted interface over both the gateway and REST APIs,
handling the event loop.
The `builder` module has been separated from `utils`, and can now be
optionally compiled in. It and the `http` feature are required by the
`model` feature due to a large number of methods requiring access to
them.
`utils` now contains a number of utilities, such as the Colour struct, the
`MessageBuilder`, and mention parsing functions.
Each of the original `ext` modules are still featured, with `cache` not
requiring any feature to be enabled, `framework` requiring the `client`,
`model`, and `utils`, and `voice` requiring `gateway`.
In total the features and their requirements are:
- `builder`: none
- `cache`: none
- `client`: `gateway`, `http`
- `framework`: `client`, `model`, `utils`
- `gateway`: `http`
- `http`: none
- `model`: `builder`, `http`
- `utils`: none
- `voice`: `gateway`
The default features are `builder`, `cache`, `client`, `framework`,
`gateway`, `model`, `http`, and `utils`.
To help with forwards compatibility, modules have been re-exported from
their original locations.
Diffstat (limited to 'src/ext')
| -rw-r--r-- | src/ext/cache/mod.rs | 1023 | ||||
| -rw-r--r-- | src/ext/framework/buckets.rs | 60 | ||||
| -rw-r--r-- | src/ext/framework/command.rs | 164 | ||||
| -rw-r--r-- | src/ext/framework/configuration.rs | 261 | ||||
| -rw-r--r-- | src/ext/framework/create_command.rs | 226 | ||||
| -rw-r--r-- | src/ext/framework/create_group.rs | 68 | ||||
| -rw-r--r-- | src/ext/framework/help_commands.rs | 285 | ||||
| -rw-r--r-- | src/ext/framework/mod.rs | 665 | ||||
| -rw-r--r-- | src/ext/mod.rs | 18 | ||||
| -rw-r--r-- | src/ext/voice/audio.rs | 16 | ||||
| -rw-r--r-- | src/ext/voice/connection.rs | 477 | ||||
| -rw-r--r-- | src/ext/voice/connection_info.rs | 10 | ||||
| -rw-r--r-- | src/ext/voice/error.rs | 34 | ||||
| -rw-r--r-- | src/ext/voice/handler.rs | 433 | ||||
| -rw-r--r-- | src/ext/voice/manager.rs | 126 | ||||
| -rw-r--r-- | src/ext/voice/mod.rs | 29 | ||||
| -rw-r--r-- | src/ext/voice/payload.rs | 50 | ||||
| -rw-r--r-- | src/ext/voice/streamer.rs | 152 | ||||
| -rw-r--r-- | src/ext/voice/threading.rs | 94 |
19 files changed, 0 insertions, 4191 deletions
diff --git a/src/ext/cache/mod.rs b/src/ext/cache/mod.rs deleted file mode 100644 index f1326d4..0000000 --- a/src/ext/cache/mod.rs +++ /dev/null @@ -1,1023 +0,0 @@ -//! A cache of events received over a `Shard`, where storing at least some -//! data from the event is possible. -//! -//! This acts as a cache, to avoid making requests over the REST API through -//! the [`rest`] module where possible. All fields are public, and do not have -//! getters, to allow you more flexibility with the stored data. However, this -//! allows data to be "corrupted", and _may or may not_ cause misfunctions -//! within the library. Mutate data at your own discretion. -//! -//! A "globally available" instance of the Cache is available at -//! [`client::CACHE`]. This is the instance that is updated by the library, -//! meaning you should _not_ need to maintain updating it yourself in any case. -//! -//! # Use by the Context -//! -//! The [`Context`] will automatically attempt to pull from the cache for you. -//! For example, the [`Context::get_channel`] method will attempt to find the -//! channel in the cache. If it can not find it, it will perform a request -//! through the REST API, and then insert a clone of the channel - if found - -//! into the Cache, giving you the original. -//! -//! This allows you to save a step, by only needing to perform the -//! [`Context::get_channel`] call and not need to first search through the cache -//! - and if not found - _then_ perform an HTTP request through the Context or -//! [`rest`] module. -//! -//! Additionally, note that some information received through events can _not_ -//! be retrieved through the REST API. This is information such as [`Role`]s in -//! [`LiveGuild`]s. -//! -//! # Use by Models -//! -//! Most models of Discord objects, such as the [`Message`], [`PublicChannel`], -//! or [`Emoji`], have methods for interacting with that single instance. This -//! feature is only compiled if the `methods` feature is enabled. An example of -//! this is [`LiveGuild::edit`], which performs a check to ensure that the -//! current user is the owner of the guild, prior to actually performing the -//! HTTP request. The cache is involved due to the function's use of unlocking -//! the cache and retrieving the Id of the current user, and comparing it to -//! the Id of the user that owns the guild. This is an inexpensive method of -//! being able to access data required by these sugary methods. -//! -//! # Do I need the Cache? -//! -//! If you're asking this, the answer is likely "definitely yes" or -//! "definitely no"; any in-between tends to be "yes". If you are low on RAM, -//! and need to run on only a couple MB, then the answer is "definitely no". If -//! you do not care about RAM and want your bot to be able to access data -//! while needing to hit the REST API as little as possible, then the answer -//! is "yes". -//! -//! [`Context`]: ../../client/struct.Context.html -//! [`Context::get_channel`]: ../../client/struct.Context.html#method.get_channel -//! [`Emoji`]: ../../model/struct.Emoji.html -//! [`Group`]: ../../model/struct.Group.html -//! [`LiveGuild`]: ../../model/struct.LiveGuild.html -//! [`LiveGuild::edit`]: ../../model/struct.LiveGuild.html#method.edit -//! [`Message`]: ../../model/struct.Message.html -//! [`PublicChannel`]: ../../model/struct.PublicChannel.html -//! [`Role`]: ../../model/struct.Role.html -//! [`client::CACHE`]: ../../client/struct.CACHE.html -//! [`rest`]: ../../client/rest/index.html - -use std::collections::hash_map::Entry; -use std::collections::{HashMap, HashSet}; -use std::default::Default; -use std::sync::{Arc, RwLock}; -use std::mem; -use ::model::*; -use ::model::event::*; - -/// A cache of all events received over a [`Connection`], where storing at least -/// some data from the event is possible. -/// -/// This acts as a cache, to avoid making requests over the REST API through the -/// [`rest`] module where possible. All fields are public, and do not have -/// getters, to allow you more flexibility with the stored data. However, this -/// allows data to be "corrupted", and _may or may not_ cause misfunctions -/// within the library. Mutate data at your own discretion. -/// -/// # Use by the Context -/// -/// The [`Context`] will automatically attempt to pull from the cache for you. -/// For example, the [`Context::get_channel`] method will attempt to find the -/// channel in the cache. If it can not find it, it will perform a request -/// through the REST API, and then insert a clone of the channel - if found - -/// into the Cache. -/// -/// This allows you to only need to perform the `Context::get_channel` call, -/// and not need to first search through the cache - and if not found - _then_ -/// perform an HTTP request through the Context or `rest` module. -/// -/// Additionally, note that some information received through events can _not_ -/// be retrieved through the REST API. This is information such as [`Role`]s in -/// [`Guild`]s. -/// -/// [`Connection`]: ../../client/struct.Connection.html -/// [`Context`]: ../../client/struct.Context.html -/// [`Context::get_channel`]: ../../client/struct.Context.html#method.get_channel -/// [`Guild`]: ../../model/struct.Guild.html -/// [`Role`]: ../../model/struct.Role.html -/// [`rest`]: ../../client/rest/index.html -#[derive(Clone, Debug)] -pub struct Cache { - /// A map of channels in [`Guild`]s that the current user has received data - /// for. - /// - /// When a [`Event::GuildDelete`] or [`Event::GuildUnavailable`] is - /// received and processed by the cache, the relevant channels are also - /// removed from this map. - /// - /// [`Event::GuildDelete`]: ../../model/event/struct.GuildDeleteEvent.html - /// [`Event::GuildUnavailable`]: ../../model/event/struct.GuildUnavailableEvent.html - /// [`Guild`]: ../../model/struct.Guild.html - pub channels: HashMap<ChannelId, Arc<RwLock<GuildChannel>>>, - /// 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>>>, - /// 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/struct.Emoji.html - /// [`Role`]: ../../model/struct.Role.html - pub guilds: HashMap<GuildId, Arc<RwLock<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 - /// note is equivalent to deleting a note. - /// - /// This will always be empty for bot users. - pub notes: HashMap<UserId, String>, - /// 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>, - /// A map of direct message channels that the current user has open with - /// other users. - pub private_channels: HashMap<ChannelId, Arc<RwLock<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 - /// [`Event::GuildUnavailable`] for more information on when this can occur. - /// - /// Additionally, guilds are always unavailable for bot users when a Ready - /// is received. Guilds are "sent in" over time through the receiving of - /// [`Event::GuildCreate`]s. - /// - /// [`Event::GuildCreate`]: ../../model/enum.Event.html#variant.GuildCreate - /// [`Event::GuildUnavailable`]: ../../model/enum.Event.html#variant.GuildUnavailable - pub unavailable_guilds: HashSet<GuildId>, - /// The current user "logged in" and for which events are being received - /// for. - /// - /// The current user contains information that a regular [`User`] does not, - /// such as whether it is a bot, whether the user is verified, etc. - /// - /// Refer to the documentation for [`CurrentUser`] for more information. - /// - /// [`CurrentUser`]: ../../model/struct.CurrentUser.html - /// [`User`]: ../../model/struct.User.html - pub user: CurrentUser, - /// A map of users that the current user sees. - /// - /// Users are added to - and updated from - this map via the following - /// received events: - /// - /// - [`ChannelRecipientAdd`][`ChannelRecipientAddEvent`] - /// - [`GuildMemberAdd`][`GuildMemberAddEvent`] - /// - [`GuildMemberRemove`][`GuildMemberRemoveEvent`] - /// - [`GuildMembersChunk`][`GuildMembersChunkEvent`] - /// - [`GuildSync`][`GuildSyncEvent`] - /// - [`PresenceUpdate`][`PresenceUpdateEvent`] - /// - [`Ready`][`ReadyEvent`] - /// - /// Note, however, that users are _not_ removed from the map on removal - /// events such as [`GuildMemberRemove`][`GuildMemberRemoveEvent`], as other - /// structs such as members or recipients may still exist. - /// - /// [`ChannelRecipientAddEvent`]: ../../model/event/struct.ChannelRecipientAddEvent.html - /// [`GuildMemberAddEvent`]: ../../model/event/struct.GuildMemberAddEvent.html - /// [`GuildMemberRemoveEvent`]: ../../model/event/struct.GuildMemberRemoveEvent.html - /// [`GuildMemberUpdateEvent`]: ../../model/event/struct.GuildMemberUpdateEvent.html - /// [`GuildMembersChunkEvent`]: ../../model/event/struct.GuildMembersChunkEvent.html - /// [`GuildSyncEvent`]: ../../model/event/struct.GuildSyncEvent.html - /// [`PresenceUpdateEvent`]: ../../model/event/struct.PresenceUpdateEvent.html - /// [`Ready`]: ../../model/event/struct.ReadyEvent.html - pub users: HashMap<UserId, Arc<RwLock<User>>>, -} - -impl Cache { - /// Fetches the number of [`Member`]s that have not had data received. - /// - /// The important detail to note here is that this is the number of - /// _member_s that have not had data downloaded. A single [`User`] may have - /// multiple associated member objects that have not been received. - /// - /// This can be used in combination with [`Shard::chunk_guilds`], and can be - /// used to determine how many members have not yet been downloaded. - /// - /// [`Member`]: ../../model/struct.Member.html - /// [`User`]: ../../model/struct.User.html - pub fn unknown_members(&self) -> u64 { - let mut total = 0; - - for guild in self.guilds.values() { - let guild = guild.read().unwrap(); - - let members = guild.members.len() as u64; - - if guild.member_count > members { - total += guild.member_count - members; - } - } - - total - } - - /// Fetches a vector of all [`PrivateChannel`] and [`Group`] Ids that are - /// stored in the cache. - /// - /// # Examples - /// - /// If there are 6 private channels and 2 groups in the cache, then `8` Ids - /// will be returned. - /// - /// [`Group`]: ../../model/struct.Group.html - /// [`PrivateChannel`]: ../../model/struct.PrivateChannel.html - pub fn all_private_channels(&self) -> Vec<ChannelId> { - self.groups - .keys() - .cloned() - .chain(self.private_channels.keys().cloned()) - .collect() - } - - /// Fetches a vector of all [`Guild`]s' Ids that are stored in the cache. - /// - /// Note that if you are utilizing multiple [`Shard`]s, then the guilds - /// retrieved over all shards are included in this count -- not just the - /// current [`Context`]'s shard, if accessing from one. - /// - /// [`Context`]: ../../client/struct.Context.html - /// [`Guild`]: ../../model/struct.Guild.html - /// [`Shard`]: ../../client/gateway/struct.Shard.html - pub fn all_guilds(&self) -> Vec<GuildId> { - self.guilds - .values() - .map(|g| g.read().unwrap().id) - .chain(self.unavailable_guilds.iter().cloned()) - .collect() - } - - /// Retrieves a [`Channel`] from the cache based on the given Id. - /// - /// This will search the [`channels`] map, the [`private_channels`] map, and - /// then the map of [`groups`] to find the channel. - /// - /// If you know what type of channel you're looking for, you should instead - /// manually retrieve from one of the respective maps or methods: - /// - /// - [`GuildChannel`]: [`get_guild_channel`] or [`channels`] - /// - [`PrivateChannel`]: [`get_private_channel`] or [`private_channels`] - /// - [`Group`]: [`get_group`] or [`groups`] - /// - /// [`Channel`]: ../../model/enum.Channel.html - /// [`Group`]: ../../model/struct.Group.html - /// [`Guild`]: ../../model/struct.Guild.html - /// [`channels`]: #structfield.channels - /// [`get_group`]: #method.get_group - /// [`get_guild_channel`]: #method.get_guild_channel - /// [`get_private_channel`]: #method.get_private_channel - /// [`groups`]: #structfield.groups - /// [`private_channels`]: #structfield.private_channels - pub fn channel<C: Into<ChannelId>>(&self, id: C) -> Option<Channel> { - let id = id.into(); - - if let Some(channel) = self.channels.get(&id) { - return Some(Channel::Guild(channel.clone())); - } - - if let Some(private_channel) = self.private_channels.get(&id) { - return Some(Channel::Private(private_channel.clone())); - } - - if let Some(group) = self.groups.get(&id) { - return Some(Channel::Group(group.clone())); - } - - None - } - - /// Retrieves a guild from the cache based on the given Id. - /// - /// The only advantage of this method is that you can pass in anything that - /// is indirectly a [`GuildId`]. - /// - /// [`GuildId`]: ../../model/struct.GuildId.html - #[inline] - pub fn guild<G: Into<GuildId>>(&self, id: G) -> Option<Arc<RwLock<Guild>>> { - self.guilds.get(&id.into()).cloned() - } - - /// Retrieves a reference to a [`Guild`]'s channel. Unlike [`get_channel`], - /// this will only search guilds for the given channel. - /// - /// The only advantage of this method is that you can pass in anything that - /// is indirectly a [`ChannelId`]. - /// - /// # Examples - /// - /// Getting a guild's channel via the Id of the message received through a - /// [`Client::on_message`] event dispatch: - /// - /// ```rust,ignore - /// use serenity::client::CACHE; - /// - /// let cache = CACHE.read().unwrap(); - /// - /// let channel = match cache.get_guild_channel(message.channel_id) { - /// Some(channel) => channel, - /// None => { - /// if let Err(why) = message.channel_id.say("Could not find guild's channel data") { - /// println!("Error sending message: {:?}", why); - /// } - /// - /// return; - /// }, - /// }; - /// ``` - /// - /// [`ChannelId`]: ../../model/struct.ChannelId.html - /// [`Client::on_message`]: ../../client/struct.Client.html#method.on_message - /// [`Guild`]: ../../model/struct.Guild.html - /// [`get_channel`]: #method.get_channel - #[inline] - pub fn guild_channel<C: Into<ChannelId>>(&self, id: C) -> Option<Arc<RwLock<GuildChannel>>> { - self.channels.get(&id.into()).cloned() - } - - /// Retrieves a reference to a [`Group`] from the cache based on the given - /// associated channel Id. - /// - /// The only advantage of this method is that you can pass in anything that - /// is indirectly a [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/struct.ChannelId.html - /// [`Group`]: ../../model/struct.Group.html - #[inline] - pub fn group<C: Into<ChannelId>>(&self, id: C) -> Option<Arc<RwLock<Group>>> { - self.groups.get(&id.into()).cloned() - } - - /// Retrieves a [`Guild`]'s member from the cache based on the guild's and - /// user's given Ids. - /// - /// **Note**: This will clone the entire member. Instead, retrieve the guild - /// and retrieve from the guild's [`members`] map to avoid this. - /// - /// # Examples - /// - /// Retrieving the member object of the user that posted a message, in a - /// [`Client::on_message`] context: - /// - /// ```rust,ignore - /// use serenity::client::CACHE; - /// - /// let cache = CACHE.read().unwrap(); - /// let member = { - /// let channel = match cache.get_guild_channel(message.channel_id) { - /// Some(channel) => channel, - /// None => { - /// if let Err(why) = message.channel_id.say("Error finding channel data") { - /// println!("Error sending message: {:?}", why); - /// } - /// }, - /// }; - /// - /// match cache.get_member(channel.guild_id, message.author.id) { - /// Some(member) => member, - /// None => { - /// if let Err(why) = message.channel_id.say("Error finding member data") { - /// println!("Error sending message: {:?}", why); - /// } - /// }, - /// } - /// }; - /// - /// let msg = format!("You have {} roles", member.roles.len()); - /// - /// if let Err(why) = message.channel_id.say(&msg) { - /// println!("Error sending message: {:?}", why); - /// } - /// ``` - /// - /// [`Client::on_message`]: ../../client/struct.Client.html#method.on_message - /// [`Guild`]: ../../model/struct.Guild.html - /// [`members`]: ../../model/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> { - self.guilds - .get(&guild_id.into()) - .and_then(|guild| guild.write().unwrap().members.get(&user_id.into()).cloned()) - } - - /// Retrieves a [`PrivateChannel`] from the cache's [`private_channels`] - /// map, if it exists. - /// - /// The only advantage of this method is that you can pass in anything that - /// is indirectly a [`ChannelId`]. - #[inline] - pub fn private_channel<C: Into<ChannelId>>(&self, channel_id: C) - -> Option<Arc<RwLock<PrivateChannel>>> { - self.private_channels.get(&channel_id.into()).cloned() - } - - /// Retrieves a [`Guild`]'s role by their Ids. - /// - /// **Note**: This will clone the entire role. Instead, retrieve the guild - /// and retrieve from the guild's [`roles`] map to avoid this. - /// - /// [`Guild`]: ../../model/struct.Guild.html - /// [`roles`]: ../../model/struct.Guild.html#structfield.roles - pub fn role<G, R>(&self, guild_id: G, role_id: R) -> Option<Role> - where G: Into<GuildId>, R: Into<RoleId> { - self.guilds - .get(&guild_id.into()) - .and_then(|g| g.read().unwrap().roles.get(&role_id.into()).cloned()) - } - - /// Retrieves a `User` from the cache's [`users`] map, if it exists. - /// - /// The only advantage of this method is that you can pass in anything that - /// is indirectly a [`UserId`]. - /// - /// [`UserId`]: ../../model/struct.UserId.html - /// [`users`]: #structfield.users - #[inline] - pub fn user<U: Into<UserId>>(&self, user_id: U) -> Option<Arc<RwLock<User>>> { - self.users.get(&user_id.into()).cloned() - } - - /// Alias of [`channel`]. - /// - /// [`channel`]: #method.channel - #[deprecated(since="0.1.5", note="Use `channel` instead.")] - #[inline] - pub fn get_channel<C: Into<ChannelId>>(&self, id: C) -> Option<Channel> { - self.channel(id) - } - - /// Alias of [`guild`]. - /// - /// [`guild`]: #method.guild - #[deprecated(since="0.1.5", note="Use `guild` instead.")] - #[inline] - pub fn get_guild<G: Into<GuildId>>(&self, id: G) -> Option<Arc<RwLock<Guild>>> { - self.guild(id) - } - - /// Alias of [`guild_channel`]. - /// - /// [`guild_channel`]: #method.guild_channel - #[deprecated(since="0.1.5", note="Use `guild_channel` instead.")] - #[inline] - pub fn get_guild_channel<C: Into<ChannelId>>(&self, id: C) - -> Option<Arc<RwLock<GuildChannel>>> { - self.guild_channel(id) - } - - /// Alias of [`member`]. - /// - /// [`member`]: #method.member - #[deprecated(since="0.1.5", note="Use `member` instead.")] - #[inline] - pub fn get_member<G, U>(&self, guild_id: G, user_id: U) -> Option<Member> - where G: Into<GuildId>, U: Into<UserId> { - self.member(guild_id, user_id) - } - - /// Alias of [`private_channel`]. - /// - /// [`private_channel`]: #method.private_channel - #[deprecated(since="0.1.5", note="Use `private_channel` instead.")] - #[inline] - pub fn get_private_channel<C: Into<ChannelId>>(&self, id: C) - -> Option<Arc<RwLock<PrivateChannel>>> { - self.private_channel(id) - } - - /// Alias of [`role`]. - /// - /// [`role`]: #method.role - #[deprecated(since="0.1.5", note="Use `role` instead.")] - #[inline] - pub fn get_role<G, R>(&self, guild_id: G, role_id: R) -> Option<Role> - where G: Into<GuildId>, R: Into<RoleId> { - self.role(guild_id, role_id) - } - - /// Alias of [`user`]. - /// - /// [`user`]: #method.user - #[deprecated(since="0.1.5", note="Use `user` instead.")] - #[inline] - pub fn get_user<U: Into<UserId>>(&self, id: U) -> Option<Arc<RwLock<User>>> { - self.user(id) - } - - #[doc(hidden)] - pub fn update_with_channel_create(&mut self, event: &ChannelCreateEvent) -> Option<Channel> { - match event.channel { - Channel::Group(ref group) => { - let group = group.clone(); - - let channel_id = { - let writer = group.write().unwrap(); - - for (recipient_id, recipient) in &mut group.write().unwrap().recipients { - self.update_user_entry(&recipient.read().unwrap()); - - *recipient = self.users[recipient_id].clone(); - } - - writer.channel_id - }; - - let ch = self.groups.insert(channel_id, group); - - ch.map(Channel::Group) - }, - Channel::Guild(ref channel) => { - let (guild_id, channel_id) = { - let channel = channel.read().unwrap(); - - (channel.guild_id, channel.id) - }; - - self.channels.insert(channel_id, channel.clone()); - - self.guilds - .get_mut(&guild_id) - .and_then(|guild| { - guild.write().unwrap().channels.insert(channel_id, channel.clone()) - }).map(Channel::Guild) - }, - Channel::Private(ref channel) => { - let channel = channel.clone(); - - let mut channel_writer = channel.write().unwrap(); - - let user_id = { - let user_reader = channel_writer.recipient.read().unwrap(); - - self.update_user_entry(&user_reader); - - user_reader.id - }; - - channel_writer.recipient = self.users[&user_id].clone(); - - let ch = self.private_channels.insert(channel_writer.id, channel.clone()); - ch.map(Channel::Private) - }, - } - } - - #[doc(hidden)] - pub fn update_with_channel_delete(&mut self, event: &ChannelDeleteEvent) -> Option<Channel> { - match event.channel { - Channel::Group(ref group) => { - self.groups.remove(&group.read().unwrap().channel_id).map(Channel::Group) - }, - Channel::Private(ref channel) => { - self.private_channels.remove(&channel.read().unwrap().id).map(Channel::Private) - }, - Channel::Guild(ref channel) => { - let (channel_id, guild_id) = { - let channel = channel.read().unwrap(); - - (channel.id, channel.guild_id) - }; - - self.channels.remove(&channel_id); - - self.guilds - .get_mut(&guild_id) - .and_then(|guild| guild.write().unwrap().channels.remove(&channel_id)) - .map(Channel::Guild) - }, - } - } - - #[doc(hidden)] - pub fn update_with_channel_pins_update(&mut self, event: &ChannelPinsUpdateEvent) { - if let Some(channel) = self.channels.get(&event.channel_id) { - channel.write().unwrap().last_pin_timestamp = event.last_pin_timestamp.clone(); - - return; - } - - if let Some(channel) = self.private_channels.get_mut(&event.channel_id) { - channel.write().unwrap().last_pin_timestamp = event.last_pin_timestamp.clone(); - - return; - } - - if let Some(group) = self.groups.get_mut(&event.channel_id) { - group.write().unwrap().last_pin_timestamp = event.last_pin_timestamp.clone(); - - return; - } - } - - #[doc(hidden)] - pub fn update_with_channel_recipient_add(&mut self, event: &mut ChannelRecipientAddEvent) { - self.update_user_entry(&event.user); - let user = self.users[&event.user.id].clone(); - - self.groups - .get_mut(&event.channel_id) - .map(|group| { - group.write() - .unwrap() - .recipients - .insert(event.user.id, user); - }); - } - - #[doc(hidden)] - pub fn update_with_channel_recipient_remove(&mut self, event: &ChannelRecipientRemoveEvent) { - self.groups - .get_mut(&event.channel_id) - .map(|group| group.write().unwrap().recipients.remove(&event.user.id)); - } - - #[doc(hidden)] - pub fn update_with_channel_update(&mut self, event: &ChannelUpdateEvent) { - match event.channel { - Channel::Group(ref group) => { - let (ch_id, no_recipients) = { - let group = group.read().unwrap(); - - (group.channel_id, group.recipients.is_empty()) - }; - - match self.groups.entry(ch_id) { - Entry::Vacant(e) => { - e.insert(group.clone()); - }, - Entry::Occupied(mut e) => { - let mut dest = e.get_mut().write().unwrap(); - - if no_recipients { - let recipients = mem::replace(&mut dest.recipients, HashMap::new()); - - dest.clone_from(&group.read().unwrap()); - - dest.recipients = recipients; - } else { - dest.clone_from(&group.read().unwrap()); - } - }, - } - }, - Channel::Guild(ref channel) => { - let (channel_id, guild_id) = { - let channel = channel.read().unwrap(); - - (channel.id, channel.guild_id) - }; - - self.channels.insert(channel_id, channel.clone()); - self.guilds - .get_mut(&guild_id) - .map(|guild| { - guild.write() - .unwrap() - .channels - .insert(channel_id, channel.clone()) - }); - }, - Channel::Private(ref channel) => { - self.private_channels - .get_mut(&channel.read().unwrap().id) - .map(|private| private.clone_from(channel)); - }, - } - } - - #[doc(hidden)] - pub fn update_with_guild_create(&mut self, event: &GuildCreateEvent) { - self.unavailable_guilds.remove(&event.guild.id); - - let mut guild = event.guild.clone(); - - for (user_id, member) in &mut guild.members { - self.update_user_entry(&member.user.read().unwrap()); - let user = self.users[user_id].clone(); - - member.user = user.clone(); - } - - self.channels.extend(guild.channels.clone()); - self.guilds.insert(event.guild.id, Arc::new(RwLock::new(guild))); - } - - #[doc(hidden)] - pub fn update_with_guild_delete(&mut self, event: &GuildDeleteEvent) - -> Option<Arc<RwLock<Guild>>> { - // Remove channel entries for the guild if the guild is found. - self.guilds.remove(&event.guild.id).map(|guild| { - for channel_id in guild.read().unwrap().channels.keys() { - self.channels.remove(channel_id); - } - - guild - }) - } - - #[doc(hidden)] - pub fn update_with_guild_emojis_update(&mut self, event: &GuildEmojisUpdateEvent) { - self.guilds - .get_mut(&event.guild_id) - .map(|guild| guild.write().unwrap().emojis.extend(event.emojis.clone())); - } - - #[doc(hidden)] - pub fn update_with_guild_member_add(&mut self, event: &mut GuildMemberAddEvent) { - let user_id = event.member.user.read().unwrap().id; - self.update_user_entry(&event.member.user.read().unwrap()); - - // Always safe due to being inserted above. - event.member.user = self.users[&user_id].clone(); - - self.guilds - .get_mut(&event.guild_id) - .map(|guild| { - let mut guild = guild.write().unwrap(); - - guild.member_count += 1; - guild.members.insert(user_id, event.member.clone()); - }); - } - - #[doc(hidden)] - pub fn update_with_guild_member_remove(&mut self, event: &GuildMemberRemoveEvent) - -> Option<Member> { - self.guilds - .get_mut(&event.guild_id) - .and_then(|guild| { - let mut guild = guild.write().unwrap(); - - guild.member_count -= 1; - guild.members.remove(&event.user.id) - }) - } - - #[doc(hidden)] - pub fn update_with_guild_member_update(&mut self, event: &GuildMemberUpdateEvent) - -> Option<Member> { - self.update_user_entry(&event.user); - - if let Some(guild) = self.guilds.get_mut(&event.guild_id) { - let mut guild = guild.write().unwrap(); - - let mut found = false; - - let item = if let Some(member) = guild.members.get_mut(&event.user.id) { - let item = Some(member.clone()); - - member.nick.clone_from(&event.nick); - member.roles.clone_from(&event.roles); - member.user.write().unwrap().clone_from(&event.user); - - found = true; - - item - } else { - None - }; - - if !found { - guild.members.insert(event.user.id, Member { - deaf: false, - guild_id: Some(event.guild_id), - joined_at: String::default(), - mute: false, - nick: event.nick.clone(), - roles: event.roles.clone(), - user: Arc::new(RwLock::new(event.user.clone())), - }); - } - - item - } else { - None - } - } - - #[doc(hidden)] - pub fn update_with_guild_members_chunk(&mut self, event: &GuildMembersChunkEvent) { - for member in event.members.values() { - self.update_user_entry(&member.user.read().unwrap()); - } - - self.guilds - .get_mut(&event.guild_id) - .map(|guild| guild.write().unwrap().members.extend(event.members.clone())); - } - - #[doc(hidden)] - pub fn update_with_guild_role_create(&mut self, event: &GuildRoleCreateEvent) { - self.guilds - .get_mut(&event.guild_id) - .map(|guild| guild.write().unwrap().roles.insert(event.role.id, event.role.clone())); - } - - #[doc(hidden)] - pub fn update_with_guild_role_delete(&mut self, event: &GuildRoleDeleteEvent) -> Option<Role> { - self.guilds - .get_mut(&event.guild_id) - .and_then(|guild| guild.write().unwrap().roles.remove(&event.role_id)) - } - - #[doc(hidden)] - pub fn update_with_guild_role_update(&mut self, event: &GuildRoleUpdateEvent) -> Option<Role> { - self.guilds - .get_mut(&event.guild_id) - .and_then(|guild| { - guild.write() - .unwrap() - .roles - .get_mut(&event.role.id) - .map(|role| mem::replace(role, event.role.clone())) - }) - } - - #[doc(hidden)] - pub fn update_with_guild_unavailable(&mut self, event: &GuildUnavailableEvent) { - self.unavailable_guilds.insert(event.guild_id); - self.guilds.remove(&event.guild_id); - } - - #[doc(hidden)] - pub fn update_with_guild_update(&mut self, event: &GuildUpdateEvent) { - self.guilds - .get_mut(&event.guild.id) - .map(|guild| { - let mut guild = guild.write().unwrap(); - - guild.afk_timeout = event.guild.afk_timeout; - guild.afk_channel_id.clone_from(&event.guild.afk_channel_id); - guild.icon.clone_from(&event.guild.icon); - guild.name.clone_from(&event.guild.name); - guild.owner_id.clone_from(&event.guild.owner_id); - guild.region.clone_from(&event.guild.region); - guild.roles.clone_from(&event.guild.roles); - guild.verification_level = event.guild.verification_level; - }); - } - - #[doc(hidden)] - pub fn update_with_presences_replace(&mut self, event: &PresencesReplaceEvent) { - self.presences.extend({ - let mut p: HashMap<UserId, Presence> = HashMap::default(); - - for presence in &event.presences { - p.insert(presence.user_id, presence.clone()); - } - - p - }); - } - - #[doc(hidden)] - pub fn update_with_presence_update(&mut self, event: &mut PresenceUpdateEvent) { - let user_id = event.presence.user_id; - - if let Some(user) = event.presence.user.as_mut() { - self.update_user_entry(&user.read().unwrap()); - *user = self.users[&user_id].clone(); - } - - if let Some(guild_id) = event.guild_id { - if let Some(guild) = self.guilds.get_mut(&guild_id) { - let mut guild = guild.write().unwrap(); - - // If the member went offline, remove them from the presence list. - if event.presence.status == OnlineStatus::Offline { - guild.presences.remove(&event.presence.user_id); - } else { - guild.presences.insert(event.presence.user_id, event.presence.clone()); - } - } - } else if event.presence.status == OnlineStatus::Offline { - self.presences.remove(&event.presence.user_id); - } else { - self.presences.insert(event.presence.user_id, event.presence.clone()); - } - } - - #[doc(hidden)] - pub fn update_with_ready(&mut self, event: &ReadyEvent) { - let mut ready = event.ready.clone(); - - for guild in ready.guilds { - match guild { - GuildStatus::Offline(unavailable) => { - self.guilds.remove(&unavailable.id); - self.unavailable_guilds.insert(unavailable.id); - }, - GuildStatus::OnlineGuild(guild) => { - self.unavailable_guilds.remove(&guild.id); - self.guilds.insert(guild.id, Arc::new(RwLock::new(guild))); - }, - GuildStatus::OnlinePartialGuild(_) => {}, - } - } - - // The private channels sent in the READY contains both the actual - // private channels and the groups. - for (channel_id, channel) in ready.private_channels { - match channel { - Channel::Group(group) => { - self.groups.insert(channel_id, group); - }, - Channel::Private(channel) => { - self.private_channels.insert(channel_id, channel); - }, - Channel::Guild(guild) => warn!("Got a guild in DMs: {:?}", guild), - } - } - - for (user_id, presence) in &mut ready.presences { - if let Some(ref user) = presence.user { - self.update_user_entry(&user.read().unwrap()); - } - - presence.user = self.users.get(user_id).cloned(); - } - - self.presences.extend(ready.presences); - self.shard_count = ready.shard.map_or(1, |s| s[1]); - self.user = ready.user; - } - - #[doc(hidden)] - pub fn update_with_user_update(&mut self, event: &UserUpdateEvent) -> CurrentUser { - mem::replace(&mut self.user, event.current_user.clone()) - } - - #[doc(hidden)] - pub fn update_with_voice_state_update(&mut self, event: &VoiceStateUpdateEvent) { - if let Some(guild_id) = event.guild_id { - if let Some(guild) = self.guilds.get_mut(&guild_id) { - let mut guild = guild.write().unwrap(); - - if event.voice_state.channel_id.is_some() { - // Update or add to the voice state list - { - let finding = guild.voice_states.get_mut(&event.voice_state.user_id); - - if let Some(srv_state) = finding { - srv_state.clone_from(&event.voice_state); - - return; - } - } - - guild.voice_states.insert(event.voice_state.user_id, event.voice_state.clone()); - } else { - // Remove the user from the voice state list - guild.voice_states.remove(&event.voice_state.user_id); - } - } - - return; - } - } - - // Adds or updates a user entry in the [`users`] map with a received user. - // - // [`users`]: #structfield.users - 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()))); - }, - Entry::Occupied(mut e) => { - e.get_mut().write().unwrap().clone_from(user); - } - } - } -} - -impl Default for Cache { - fn default() -> Cache { - Cache { - channels: HashMap::default(), - groups: HashMap::default(), - guilds: HashMap::default(), - notes: HashMap::default(), - presences: HashMap::default(), - private_channels: HashMap::default(), - shard_count: 1, - unavailable_guilds: HashSet::default(), - user: CurrentUser { - avatar: None, - bot: false, - discriminator: 0, - email: None, - id: UserId(0), - mfa_enabled: false, - name: String::default(), - verified: false, - }, - users: HashMap::default(), - } - } -} diff --git a/src/ext/framework/buckets.rs b/src/ext/framework/buckets.rs deleted file mode 100644 index 02cd658..0000000 --- a/src/ext/framework/buckets.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::collections::HashMap; -use std::default::Default; -use time; - -#[doc(hidden)] -pub struct Ratelimit { - pub delay: i64, - pub limit: Option<(i64, i32)>, -} - -#[doc(hidden)] -pub struct MemberRatelimit { - pub tickets: i32, - pub last_time: i64, - pub set_time: i64, -} - -impl Default for MemberRatelimit { - fn default() -> Self { - MemberRatelimit { - tickets: 0, - last_time: 0, - set_time: 0, - } - } -} - -#[doc(hidden)] -pub struct Bucket { - pub ratelimit: Ratelimit, - pub users: HashMap<u64, MemberRatelimit>, -} - -impl Bucket { - pub fn take(&mut self, user_id: u64) -> i64 { - let time = time::get_time().sec; - let user = self.users.entry(user_id) - .or_insert_with(MemberRatelimit::default); - - if let Some((timespan, limit)) = self.ratelimit.limit { - if (user.tickets + 1) > limit { - if time < (user.set_time + timespan) { - return (user.set_time + timespan) - time; - } else { - user.tickets = 0; - user.set_time = time; - } - } - } - - if time < user.last_time + self.ratelimit.delay { - (user.last_time + self.ratelimit.delay) - time - } else { - user.tickets += 1; - user.last_time = time; - - 0 - } - } -} diff --git a/src/ext/framework/command.rs b/src/ext/framework/command.rs deleted file mode 100644 index 5f43284..0000000 --- a/src/ext/framework/command.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::sync::Arc; -use super::Configuration; -use ::client::Context; -use ::model::Message; -use ::model::Permissions; -use std::collections::HashMap; - -pub type Check = Fn(&mut Context, &Message) -> bool + Send + Sync + 'static; -pub type Exec = Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static; -pub type Help = Fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, &[String]) -> Result<(), String> + Send + Sync + 'static; -pub type BeforeHook = Fn(&mut Context, &Message, &String) -> bool + Send + Sync + 'static; -pub type AfterHook = Fn(&mut Context, &Message, &String, Result<(), String>) + Send + Sync + 'static; -#[doc(hidden)] -pub type InternalCommand = Arc<Command>; -pub type PrefixCheck = Fn(&mut Context) -> Option<String> + Send + Sync + 'static; - -#[doc(hidden)] -pub enum CommandOrAlias { - Alias(String), - Command(InternalCommand), -} - -/// Command function type. Allows to access internal framework things inside -/// your commands. -pub enum CommandType { - StringResponse(String), - Basic(Box<Exec>), - WithCommands(Box<Help>), -} - -#[derive(Default)] -pub struct CommandGroup { - pub prefix: Option<String>, - pub commands: HashMap<String, CommandOrAlias>, -} - -/// Command struct used to store commands internally. -pub struct Command { - /// A set of checks to be called prior to executing the command. The checks - /// will short-circuit on the first check that returns `false`. - pub checks: Vec<Box<Check>>, - /// Function called when the command is called. - pub exec: CommandType, - /// Ratelimit bucket. - pub bucket: Option<String>, - /// Command description, used by other commands. - pub desc: Option<String>, - /// Example arguments, used by other commands. - pub example: Option<String>, - /// Command usage schema, used by other commands. - pub usage: Option<String>, - /// Whether arguments should be parsed using quote parser or not. - pub use_quotes: bool, - /// Minumum amount of arguments that should be passed. - pub min_args: Option<i32>, - /// Maximum amount of arguments that can be passed. - pub max_args: Option<i32>, - /// Permissions required to use this command. - pub required_permissions: Permissions, - /// Whether command should be displayed in help list or not, used by other commands. - pub help_available: bool, - /// Whether command can be used only privately or not. - pub dm_only: bool, - /// Whether command can be used only in guilds or not. - pub guild_only: bool, - /// Whether command can only be used by owners or not. - pub owners_only: bool, - #[doc(hidden)] - pub aliases: Vec<String>, -} - -impl Command { - pub fn new<F>(f: F) -> Self - where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static { - Command { - aliases: Vec::new(), - checks: Vec::default(), - exec: CommandType::Basic(Box::new(f)), - desc: None, - usage: None, - example: None, - use_quotes: false, - dm_only: false, - bucket: None, - guild_only: false, - help_available: true, - min_args: None, - max_args: None, - owners_only: false, - required_permissions: Permissions::empty(), - } - } -} - -pub fn positions(ctx: &mut Context, content: &str, conf: &Configuration) -> Option<Vec<usize>> { - if !conf.prefixes.is_empty() || conf.dynamic_prefix.is_some() { - // Find out if they were mentioned. If not, determine if the prefix - // was used. If not, return None. - let mut positions: Vec<usize> = vec![]; - - if let Some(mention_end) = find_mention_end(content, conf) { - positions.push(mention_end); - } else if let Some(ref func) = conf.dynamic_prefix { - if let Some(x) = func(ctx) { - if content.starts_with(&x) { - positions.push(x.len()); - } - } else { - for n in conf.prefixes.clone() { - if content.starts_with(&n) { - positions.push(n.len()); - } - } - } - } else { - for n in conf.prefixes.clone() { - if content.starts_with(&n) { - positions.push(n.len()); - } - } - }; - - if positions.is_empty() { - return None; - } - - if conf.allow_whitespace { - let pos = *unsafe { positions.get_unchecked(0) }; - - positions.insert(0, pos + 1); - } - - Some(positions) - } else if conf.on_mention.is_some() { - match find_mention_end(content, conf) { - Some(mention_end) => { - let mut positions = vec![mention_end]; - - if conf.allow_whitespace { - positions.insert(0, mention_end + 1); - } - - Some(positions) - }, - None => None, - } - } else { - None - } -} - -fn find_mention_end(content: &str, conf: &Configuration) -> Option<usize> { - if let Some(ref mentions) = conf.on_mention { - for mention in mentions { - if !content.starts_with(&mention[..]) { - continue; - } - - return Some(mention.len()); - } - } - - None -} diff --git a/src/ext/framework/configuration.rs b/src/ext/framework/configuration.rs deleted file mode 100644 index 4f73359..0000000 --- a/src/ext/framework/configuration.rs +++ /dev/null @@ -1,261 +0,0 @@ -use std::collections::HashSet; -use std::default::Default; -use super::command::PrefixCheck; -use ::client::{Context, rest}; -use ::model::{GuildId, UserId}; - -/// The configuration to use for a [`Framework`] associated with a [`Client`] -/// instance. -/// -/// This allows setting configurations like the depth to search for commands, -/// whether to treat mentions like a command prefix, etc. -/// -/// # Examples -/// -/// Responding to mentions and setting a command prefix of `"~"`: -/// -/// ```rust,no_run -/// use serenity::Client; -/// use std::env; -/// -/// let mut client = Client::login(&env::var("DISCORD_BOT_TOKEN").unwrap()); -/// -/// client.with_framework(|f| f -/// .configure(|c| c.on_mention(true).prefix("~"))); -/// ``` -/// -/// [`Client`]: ../../client/struct.Client.html -/// [`Framework`]: struct.Framework.html -pub struct Configuration { - #[doc(hidden)] - pub depth: usize, - #[doc(hidden)] - pub on_mention: Option<Vec<String>>, - #[doc(hidden)] - pub allow_whitespace: bool, - #[doc(hidden)] - pub prefixes: Vec<String>, - #[doc(hidden)] - pub dynamic_prefix: Option<Box<PrefixCheck>>, - #[doc(hidden)] - pub ignore_bots: bool, - #[doc(hidden)] - pub blocked_users: HashSet<UserId>, - #[doc(hidden)] - pub blocked_guilds: HashSet<GuildId>, - #[doc(hidden)] - pub owners: HashSet<UserId>, - #[doc(hidden)] - pub disabled_commands: HashSet<String>, - #[doc(hidden)] - pub allow_dm: bool, - #[doc(hidden)] - pub ignore_webhooks: bool, -} - -impl Configuration { - /// Whether the bot should respond to other bots. - /// - /// For example, if this is set to false, then the bot will respond to any other bots including itself. - pub fn ignore_bots(mut self, ignore_bots: bool) -> Self { - self.ignore_bots = ignore_bots; - - self - } - - /// Whether to allow whitespace being optional between a mention/prefix and - /// a command. - /// - /// **Note**: Defaults to `false`. - /// - /// # Examples - /// - /// Setting this to `false` will _only_ allow this scenario to occur: - /// - /// ```ignore - /// <@245571012924538880> about - /// !about - /// - /// // bot processes and executes the "about" command if it exists - /// ``` - /// - /// while setting this to `true` will _also_ allow this scenario to occur: - /// - /// ```ignore - /// <@245571012924538880>about - /// ! about - /// - /// // bot processes and executes the "about" command if it exists - /// ``` - pub fn allow_whitespace(mut self, allow_whitespace: bool) -> Self { - self.allow_whitespace = allow_whitespace; - - self - } - - /// If set to false, bot will ignore any private messages. - pub fn allow_dm(mut self, allow_dm: bool) -> Self { - self.allow_dm = allow_dm; - - self - } - - /// If set to true, bot will ignore all commands called by webhooks. - /// True by default. - pub fn ignore_webhooks(mut self, ignore_webhooks: bool) -> Self { - self.ignore_webhooks = ignore_webhooks; - - self - } - - /// HashSet of user Ids whose commands will be ignored. - /// Guilds owned by user Ids will also be ignored. - pub fn blocked_users(mut self, users: HashSet<UserId>) -> Self { - self.blocked_users = users; - - self - } - - /// HashSet of guild Ids where commands will be ignored. - pub fn blocked_guilds(mut self, guilds: HashSet<GuildId>) -> Self { - self.blocked_guilds = guilds; - - self - } - - /// The default depth of the message to check for commands. Defaults to 5. - /// This determines how "far" into a message to check for a valid command. - /// - /// # Examples - /// - /// If you set a depth of `1`, and make a command of `"music play"`, but - /// not a `"music"` command, then the former command will never be - /// triggered, as its "depth" is `2`. - pub fn depth(mut self, depth: u8) -> Self { - self.depth = depth as usize; - - self - } - - /// HashSet of command names that won't be run. - pub fn disabled_commands(mut self, commands: HashSet<String>) -> Self { - self.disabled_commands = commands; - - self - } - - /// Sets the prefix to respond to dynamically based on conditions. - /// - /// Return `None` to not have a special prefix for the dispatch, and to - /// instead use the inherited prefix. - /// - /// # Examples - /// - /// If the Id of the channel is divisible by 5, return a prefix of `"!"`, - /// otherwise return a prefix of `"~"`. - /// - /// ```rust,no_run - /// # use serenity::Client; - /// # - /// # let mut client = Client::login("token"); - /// client.with_framework(|f| f - /// .command("ping", |c| c.exec_str("Pong!")) - /// .configure(|c| c.dynamic_prefix(|ctx| { - /// Some(if ctx.channel_id.unwrap().0 % 5 == 0 { - /// "!" - /// } else { - /// "~" - /// }.to_owned()) - /// }))); - /// ``` - pub fn dynamic_prefix<F>(mut self, dynamic_prefix: F) -> Self - where F: Fn(&mut Context) -> Option<String> + Send + Sync + 'static { - self.dynamic_prefix = Some(Box::new(dynamic_prefix)); - - self - } - - /// Whether or not to respond to commands initiated with a mention. Note - /// that this can be used in conjunction with [`prefix`]. - /// - /// By default this is set to `false`. - /// - /// # Examples - /// - /// Setting this to `true` will allow the following types of mentions to be - /// responded to: - /// - /// ```ignore - /// <@245571012924538880> about - /// <@!245571012924538880> about - /// ``` - /// - /// The former is a direct mention, while the latter is a nickname mention, - /// which aids mobile devices in determining whether to display a user's - /// nickname. It has no real meaning for your bot, and the library - /// encourages you to ignore differentiating between the two. - /// - /// [`prefix`]: #method.prefix - pub fn on_mention(mut self, on_mention: bool) -> Self { - if !on_mention { - return self; - } - - if let Ok(current_user) = rest::get_current_user() { - self.on_mention = Some(vec![ - format!("<@{}>", current_user.id), // Regular mention - format!("<@!{}>", current_user.id), // Nickname mention - ]); - } - - self - } - - /// A `HashSet` of user Ids checks won't apply to. - pub fn owners(mut self, user_ids: HashSet<UserId>) -> Self { - self.owners = user_ids; - - self - } - - /// Sets the prefix to respond to. This can either be a single-char or - /// multi-char string. - pub fn prefix(mut self, prefix: &str) -> Self { - self.prefixes = vec![prefix.to_owned()]; - - self - } - - /// Sets the prefixes to respond to. Those can either be single-chararacter or - /// multi-chararacter strings. - pub fn prefixes(mut self, prefixes: Vec<&str>) -> Self { - self.prefixes = prefixes.iter().map(|x| x.to_string()).collect(); - - self - } -} - -impl Default for Configuration { - /// Builds a default framework configuration, setting the following: - /// - /// - **allow_whitespace** to `false` - /// - **depth** to `5` - /// - **on_mention** to `false` (basically) - /// - **prefix** to `None` - fn default() -> Configuration { - Configuration { - depth: 5, - on_mention: None, - dynamic_prefix: None, - allow_whitespace: false, - prefixes: vec![], - ignore_bots: true, - owners: HashSet::default(), - blocked_users: HashSet::default(), - blocked_guilds: HashSet::default(), - disabled_commands: HashSet::default(), - allow_dm: true, - ignore_webhooks: true, - } - } -} diff --git a/src/ext/framework/create_command.rs b/src/ext/framework/create_command.rs deleted file mode 100644 index 512e82c..0000000 --- a/src/ext/framework/create_command.rs +++ /dev/null @@ -1,226 +0,0 @@ -pub use super::{Command, CommandType, CommandGroup}; - -use std::collections::HashMap; -use std::default::Default; -use std::sync::Arc; -use ::client::Context; -use ::model::{Message, Permissions}; - -pub struct CreateCommand(pub Command); - -impl CreateCommand { - /// Adds a ratelimit bucket. - pub fn bucket(mut self, bucket: &str) -> Self { - self.0.bucket = Some(bucket.to_owned()); - - self - } - - /// Adds an alias, allowing users to use the command under a different name. - pub fn known_as(mut self, name: &str) -> Self { - self.0.aliases.push(name.to_owned()); - - self - } - - /// Adds multiple aliases. - pub fn batch_known_as(mut self, names: Vec<&str>) -> Self { - for n in names { - self.0.aliases.push(n.to_owned()); - } - - self - } - - /// Adds a "check" to a command, which checks whether or not the command's - /// function should be called. - /// - /// # Examples - /// - /// Ensure that the user who created a message, calling a "ping" command, - /// is the owner. - /// - /// ```rust,no_run - /// use serenity::client::{Client, Context}; - /// use serenity::model::Message; - /// use std::env; - /// - /// let mut client = Client::login(&env::var("DISCORD_TOKEN").unwrap()); - /// - /// client.with_framework(|f| f - /// .configure(|c| c.prefix("~")) - /// .command("ping", |c| c - /// .check(owner_check) - /// .desc("Replies to a ping with a pong") - /// .exec(ping))); - /// - /// fn ping(_context: &mut Context, message: &Message, _args: Vec<String>) -> Result<(), String> { - /// let _ = message.channel_id.say("Pong!"); - /// - /// Ok(()) - /// } - /// - /// fn owner_check(_context: &mut Context, message: &Message) -> bool { - /// // replace with your user ID - /// message.author.id == 7 - /// } - /// ``` - pub fn check<F>(mut self, check: F) -> Self - where F: Fn(&mut Context, &Message) -> bool + Send + Sync + 'static { - self.0.checks.push(Box::new(check)); - - self - } - - /// Description, used by other commands. - pub fn desc(mut self, desc: &str) -> Self { - self.0.desc = Some(desc.to_owned()); - - self - } - - /// Whether command can be used only privately or not. - pub fn dm_only(mut self, dm_only: bool) -> Self { - self.0.dm_only = dm_only; - - self - } - - /// Example arguments, used by other commands. - pub fn example(mut self, example: &str) -> Self { - self.0.example = Some(example.to_owned()); - - self - } - - /// A function that can be called when a command is received. - /// You can return `Err(string)` if there's an error. - /// - /// See [`exec_str`] if you _only_ need to return a string on command use. - /// - /// [`exec_str`]: #method.exec_str - pub fn exec<F>(mut self, func: F) -> Self - where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static { - self.0.exec = CommandType::Basic(Box::new(func)); - - self - } - - /// Sets a function that's called when a command is called that can access - /// the internal HashMap of commands, used specifically for creating a help - /// command. - /// - /// You can return `Err(string)` if there's an error. - pub fn exec_help<F>(mut self, f: F) -> Self - where F: Fn(&mut Context, &Message, HashMap<String, Arc<CommandGroup>>, &[String]) -> Result<(), String> + Send + Sync + 'static { - self.0.exec = CommandType::WithCommands(Box::new(f)); - - self - } - - /// Sets a string to be sent in the channel of context on command. This can - /// be useful for an `about`, `invite`, `ping`, etc. command. - /// - /// # Examples - /// - /// Create a command named "ping" that returns "Pong!": - /// - /// ```rust,ignore - /// client.with_framework(|f| f - /// .command("ping", |c| c.exec_str("Pong!"))); - /// ``` - pub fn exec_str(mut self, content: &str) -> Self { - self.0.exec = CommandType::StringResponse(content.to_owned()); - - self - } - - /// Whether command can be used only in guilds or not. - pub fn guild_only(mut self, guild_only: bool) -> Self { - self.0.guild_only = guild_only; - - self - } - - /// Whether command should be displayed in help list or not, used by other commands. - pub fn help_available(mut self, help_available: bool) -> Self { - self.0.help_available = help_available; - - self - } - - /// Maximum amount of arguments that can be passed. - pub fn max_args(mut self, max_args: i32) -> Self { - self.0.max_args = Some(max_args); - - self - } - - /// Minumum amount of arguments that should be passed. - pub fn min_args(mut self, min_args: i32) -> Self { - self.0.min_args = Some(min_args); - - self - } - - /// Whether command can be used only privately or not. - pub fn owners_only(mut self, owners_only: bool) -> Self { - self.0.owners_only = owners_only; - - self - } - - /// The permissions that a user must have in the contextual channel in order - /// for the command to be processed. - pub fn required_permissions(mut self, permissions: Permissions) -> Self { - self.0.required_permissions = permissions; - - self - } - - /// Command usage schema, used by other commands. - pub fn usage(mut self, usage: &str) -> Self { - self.0.usage = Some(usage.to_owned()); - - self - } - - /// Whether or not arguments should be parsed using the quotation parser. - /// - /// Enabling this will parse `~command "this is arg 1" "this is arg 2"` as - /// two arguments: `this is arg 1` and `this is arg 2`. - /// - /// Disabling this will parse `~command "this is arg 1" "this is arg 2"` as - /// eight arguments: `"this`, `is`, `arg`, `1"`, `"this`, `is`, `arg`, `2"`. - /// - /// Refer to [`utils::parse_quotes`] for information on the parser. - /// - /// [`utils::parse_quotes`]: ../../utils/fn.parse_quotes.html - pub fn use_quotes(mut self, use_quotes: bool) -> Self { - self.0.use_quotes = use_quotes; - - self - } -} - -impl Default for Command { - fn default() -> Command { - Command { - aliases: Vec::new(), - checks: Vec::default(), - exec: CommandType::Basic(Box::new(|_, _, _| Ok(()))), - desc: None, - usage: None, - example: None, - use_quotes: false, - min_args: None, - bucket: None, - max_args: None, - required_permissions: Permissions::empty(), - dm_only: false, - guild_only: false, - help_available: true, - owners_only: false, - } - } -} diff --git a/src/ext/framework/create_group.rs b/src/ext/framework/create_group.rs deleted file mode 100644 index 03fc33e..0000000 --- a/src/ext/framework/create_group.rs +++ /dev/null @@ -1,68 +0,0 @@ -pub use ext::framework::command::{Command, CommandType, CommandGroup, CommandOrAlias}; -pub use ext::framework::create_command::CreateCommand; - -use std::default::Default; -use std::sync::Arc; -use ::client::Context; -use ::model::Message; - -#[derive(Default)] -pub struct CreateGroup(pub CommandGroup); - -/// Used to create command groups -/// -/// # Examples -/// -/// Create group named Information where all commands are prefixed with info, -/// and add one command named "name". For example, if prefix is "~", we say "~info name" -/// to call the "name" command. -/// -/// ```rust,ignore -/// framework.group("Information", |g| g -/// .prefix("info") -/// .command("name", |c| c -/// .exec_str("Hakase"))) -/// ``` -impl CreateGroup { - /// Adds a command to group. - pub fn command<F>(mut self, command_name: &str, f: F) -> Self - where F: FnOnce(CreateCommand) -> CreateCommand { - let cmd = f(CreateCommand(Command::default())).0; - - for n in &cmd.aliases { - if let Some(ref prefix) = self.0.prefix { - self.0.commands.insert(format!("{} {}", prefix, n.to_owned()), CommandOrAlias::Alias(format!("{} {}", prefix, command_name.to_string()))); - } else { - self.0.commands.insert(n.to_owned(), CommandOrAlias::Alias(command_name.to_string())); - } - } - - self.0.commands.insert(command_name.to_owned(), CommandOrAlias::Command(Arc::new(cmd))); - - self - } - - /// Adds a command to group with simplified API. - /// You can return Err(string) if there's an error. - pub fn on<F>(mut self, command_name: &str, f: F) -> Self - where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static { - let cmd = Arc::new(Command::new(f)); - - self.0.commands.insert(command_name.to_owned(), CommandOrAlias::Command(cmd)); - - self - } - - /// If prefix is set, it will be required before all command names. - /// For example, if bot prefix is "~" and group prefix is "image" - /// we'd call a subcommand named "hibiki" by sending "~image hibiki". - /// - /// **Note**: serenity automatically puts a space after group prefix. - /// - /// **Note**: It's suggested to call this first when making a group. - pub fn prefix(mut self, desc: &str) -> Self { - self.0.prefix = Some(desc.to_owned()); - - self - } -} diff --git a/src/ext/framework/help_commands.rs b/src/ext/framework/help_commands.rs deleted file mode 100644 index 1f38bd5..0000000 --- a/src/ext/framework/help_commands.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! A collection of default help commands for the framework. -//! -//! # Example -//! -//! Using the [`with_embeds`] function to have the framework's help message use -//! embeds: -//! -//! ```rs,no_run -//! use serenity::ext::framework::help_commands; -//! use serenity::Client; -//! use std::env; -//! -//! let mut client = Client::login(&env::var("DISCORD_TOKEN").unwrap()); -//! client.with_framework(|f| f -//! .command("help", |c| c.exec_help(help_commands::with_embeds))); -//! ``` -//! -//! The same can be accomplished with no embeds by substituting `with_embeds` -//! with the [`plain`] function. -//! -//! [`plain`]: fn.plain.html -//! [`with_embeds`]: fn.with_embeds.html - -use std::collections::HashMap; -use std::sync::Arc; -use std::fmt::Write; -use super::command::InternalCommand; -use super::{Command, CommandGroup, CommandOrAlias}; -use ::client::Context; -use ::model::Message; -use ::utils::Colour; - -fn error_embed(ctx: &mut Context, input: &str) { - let _ = ctx.channel_id - .unwrap() - .send_message(|m| m - .embed(|e| e - .colour(Colour::dark_red()) - .description(input))); -} - -fn remove_aliases(cmds: &HashMap<String, CommandOrAlias>) -> HashMap<&String, &InternalCommand> { - let mut result = HashMap::new(); - - for (n, v) in cmds { - if let CommandOrAlias::Command(ref cmd) = *v { - result.insert(n, cmd); - } - } - - result -} - -pub fn with_embeds(ctx: &mut Context, - _: &Message, - groups: HashMap<String, Arc<CommandGroup>>, - args: &[String]) -> Result<(), String> { - if !args.is_empty() { - let name = args.join(" "); - - for (group_name, group) in groups { - let mut found: Option<(&String, &InternalCommand)> = None; - - for (command_name, command) in &group.commands { - let with_prefix = if let Some(ref prefix) = group.prefix { - format!("{} {}", prefix, command_name) - } else { - command_name.to_owned() - }; - - if name == with_prefix || name == *command_name { - match *command { - CommandOrAlias::Command(ref cmd) => { - found = Some((command_name, cmd)); - }, - CommandOrAlias::Alias(ref name) => { - error_embed(ctx, &format!("Did you mean \"{}\"?", name)); - return Ok(()); - } - } - } - } - - if let Some((command_name, command)) = found { - if !command.help_available { - error_embed(ctx, "**Error**: No help available."); - - return Ok(()); - } - - let _ = ctx.channel_id.unwrap().send_message(|m| { - m.embed(|e| { - let mut embed = e.colour(Colour::rosewater()) - .title(command_name); - if let Some(ref desc) = command.desc { - embed = embed.description(desc); - } - - if let Some(ref usage) = command.usage { - embed = embed.field(|f| f - .name("Usage") - .value(&format!("`{} {}`", command_name, usage))); - } - - if let Some(ref example) = command.example { - embed = embed.field(|f| f - .name("Sample usage") - .value(&format!("`{} {}`", command_name, example))); - } - - if group_name != "Ungrouped" { - embed = embed.field(|f| f - .name("Group") - .value(&group_name)); - } - - let available = if command.dm_only { - "Only in DM" - } else if command.guild_only { - "Only in guilds" - } else { - "In DM and guilds" - }; - - embed = embed.field(|f| f - .name("Available") - .value(available)); - - embed - }) - }); - - return Ok(()); - } - } - - let error_msg = format!("**Error**: Command `{}` not found.", name); - error_embed(ctx, &error_msg); - - return Ok(()); - } - - let _ = ctx.channel_id.unwrap().send_message(|m| m - .embed(|mut e| { - e = e.colour(Colour::rosewater()) - .description("To get help with an individual command, pass its \ - name as an argument to this command."); - - for (group_name, group) in groups { - let mut desc = String::new(); - - if let Some(ref x) = group.prefix { - let _ = write!(desc, "Prefix: {}\n", x); - } - - let mut no_commands = true; - - for (n, cmd) in remove_aliases(&group.commands) { - if cmd.help_available { - let _ = write!(desc, "`{}`\n", n); - - no_commands = false; - } - } - - if no_commands { - let _ = write!(desc, "*[No commands]*"); - } - - e = e.field(|f| f.name(&group_name).value(&desc)); - } - - e - })); - - Ok(()) -} - -pub fn plain(ctx: &mut Context, - _: &Message, - groups: HashMap<String, Arc<CommandGroup>>, - args: &[String]) -> Result<(), String> { - if !args.is_empty() { - let name = args.join(" "); - - for (group_name, group) in groups { - let mut found: Option<(&String, &Command)> = None; - - for (command_name, command) in &group.commands { - let with_prefix = if let Some(ref prefix) = group.prefix { - format!("{} {}", prefix, command_name) - } else { - command_name.to_owned() - }; - - if name == with_prefix || name == *command_name { - match *command { - CommandOrAlias::Command(ref cmd) => { - found = Some((command_name, cmd)); - }, - CommandOrAlias::Alias(ref name) => { - let _ = ctx.channel_id.unwrap().say(&format!("Did you mean {:?}?", name)); - return Ok(()); - } - } - } - } - - if let Some((command_name, command)) = found { - if !command.help_available { - let _ = ctx.channel_id.unwrap().say("**Error**: No help available."); - return Ok(()); - } - - let mut result = format!("**{}**\n", command_name); - - if let Some(ref desc) = command.desc { - let _ = write!(result, "**Description:** {}\n", desc); - } - - if let Some(ref usage) = command.usage { - let _ = write!(result, "**Usage:** `{} {}`\n", command_name, usage); - } - - if let Some(ref example) = command.example { - let _ = write!(result, "**Sample usage:** `{} {}`\n", command_name, example); - } - - if group_name != "Ungrouped" { - let _ = write!(result, "**Group:** {}\n", group_name); - } - - result.push_str("**Available:** "); - result.push_str(if command.dm_only { - "Only in DM" - } else if command.guild_only { - "Only in guilds" - } else { - "In DM and guilds" - }); - result.push_str("\n"); - - let _ = ctx.channel_id.unwrap().say(&result); - - return Ok(()); - } - } - - let _ = ctx.channel_id.unwrap().say(&format!("**Error**: Command `{}` not found.", name)); - - return Ok(()); - } - - let mut result = "**Commands**\nTo get help with an individual command, pass its \ - name as an argument to this command.\n\n" - .to_string(); - - for (group_name, group) in groups { - let _ = write!(result, "**{}:** ", group_name); - - if let Some(ref x) = group.prefix { - let _ = write!(result, "(prefix: `{}`): ", x); - } - - let mut no_commands = true; - - for (n, cmd) in remove_aliases(&group.commands) { - if cmd.help_available { - let _ = write!(result, "`{}` ", n); - - no_commands = false; - } - } - - if no_commands { - result.push_str("*[No Commands]*"); - } - - result.push('\n'); - } - - let _ = ctx.channel_id.unwrap().say(&result); - - Ok(()) -} diff --git a/src/ext/framework/mod.rs b/src/ext/framework/mod.rs deleted file mode 100644 index cde10b2..0000000 --- a/src/ext/framework/mod.rs +++ /dev/null @@ -1,665 +0,0 @@ -//! The framework is a customizable method of separating commands. -//! -//! This is used in combination with [`Client::with_framework`]. -//! -//! The framework has a number of configurations, and can have any number of -//! commands bound to it. The primary purpose of it is to offer the utility of -//! not needing to manually match message content strings to determine if a -//! message is a command. -//! -//! Additionally, "checks" can be added to commands, to ensure that a certain -//! condition is met prior to calling a command; this could be a check that the -//! user who posted a message owns the bot, for example. -//! -//! Each command has a given named, and an associated function/closure. For -//! example, you might have two commands: `"ping"` and `"weather"`. These each -//! have an associated function that are called if the framework determines -//! that a message is of that command. -//! -//! Assuming a command prefix of `"~"`, then the following would occur with the -//! two previous commands: -//! -//! ```ignore -//! ~ping // calls the ping command's function -//! ~pin // does not -//! ~ ping // _does_ call it _if_ the `allow_whitespace` option is enabled -//! ~~ping // does not -//! ``` -//! -//! # Examples -//! -//! Configuring a Client with a framework, which has a prefix of `"~"` and a -//! ping and about command: -//! -//! ```rust,ignore -//! use serenity::client::{Client, Context}; -//! use serenity::model::Message; -//! use std::env; -//! -//! let mut client = Client::login(&env::var("DISCORD_BOT_TOKEN").unwrap()); -//! -//! client.with_framework(|f| f -//! .configure(|c| c.prefix("~")) -//! .command("about", |c| c.exec_str("A simple test bot")) -//! .command("ping", |c| c.exec(ping))); -//! -//! command!(about(_context, message) { -//! let _ = message.channel_id.say("A simple test bot"); -//! }); -//! -//! command!(ping(_context, message) { -//! let _ = message.channel_id.say("Pong!"); -//! }); -//! ``` -//! -//! [`Client::with_framework`]: ../../client/struct.Client.html#method.with_framework - -pub mod help_commands; - -mod command; -mod configuration; -mod create_command; -mod create_group; -mod buckets; - -pub use self::buckets::{Bucket, MemberRatelimit, Ratelimit}; -pub use self::command::{Command, CommandType, CommandGroup, CommandOrAlias}; -pub use self::configuration::Configuration; -pub use self::create_command::CreateCommand; -pub use self::create_group::CreateGroup; - -use self::command::{AfterHook, BeforeHook}; -use std::collections::HashMap; -use std::default::Default; -use std::sync::Arc; -use std::thread; -use ::client::Context; -use ::model::{Message, UserId}; -use ::model::permissions::Permissions; -use ::utils; - -#[cfg(feature="cache")] -use ::client::CACHE; -#[cfg(feature="cache")] -use ::model::Channel; - -/// A macro to generate "named parameters". This is useful to avoid manually -/// using the "arguments" parameter and manually parsing types. -/// -/// This is meant for use with the command [`Framework`]. -/// -/// # Examples -/// -/// Create a regular `ping` command which takes no arguments: -/// -/// ```rust,ignore -/// command!(ping(_context, message, _args) { -/// if let Err(why) = message.reply("Pong!") { -/// println!("Error sending pong: {:?}", why); -/// } -/// }); -/// ``` -/// -/// Create a command named `multiply` which accepts 2 floats and multiplies -/// them, sending the product as a reply: -/// -/// ```rust,ignore -/// command!(multiply(_context, message, _args, first: f64, second: f64) { -/// let product = first * second; -/// -/// if let Err(why) = message.reply(&product.to_string()) { -/// println!("Error sending product: {:?}", why); -/// } -/// }); -/// ``` -/// -/// [`Framework`]: ext/framework/index.html -#[macro_export] -macro_rules! command { - ($fname:ident($c:ident) $b:block) => { - #[allow(unreachable_code, unused_mut)] - pub fn $fname(mut $c: &mut $crate::client::Context, _: &$crate::model::Message, _: Vec<String>) -> ::std::result::Result<(), String> { - $b - - Ok(()) - } - }; - ($fname:ident($c:ident, $m:ident) $b:block) => { - #[allow(unreachable_code, unused_mut)] - pub fn $fname(mut $c: &mut $crate::client::Context, $m: &$crate::model::Message, _: Vec<String>) -> ::std::result::Result<(), String> { - $b - - Ok(()) - } - }; - ($fname:ident($c:ident, $m:ident, $a:ident) $b:block) => { - #[allow(unreachable_code, unused_mut)] - pub fn $fname(mut $c: &mut $crate::client::Context, $m: &$crate::model::Message, $a: Vec<String>) -> ::std::result::Result<(), String> { - $b - - Ok(()) - } - }; - ($fname:ident($c:ident, $m:ident, $a:ident, $($name:ident: $t:ty),*) $b:block) => { - #[allow(unreachable_code, unreachable_patterns, unused_mut)] - pub fn $fname(mut $c: &mut $crate::client::Context, $m: &$crate::model::Message, $a: Vec<String>) -> ::std::result::Result<(), String> { - let mut i = $a.iter(); - let mut arg_counter = 0; - - $( - arg_counter += 1; - - let $name = match i.next() { - Some(v) => match v.parse::<$t>() { - Ok(v) => v, - Err(_) => return Err(format!("Failed to parse argument #{} of type {:?}", - arg_counter, - stringify!($t))), - }, - None => return Err(format!("Missing argument #{} of type {:?}", - arg_counter, - stringify!($t))), - }; - )* - - drop(i); - - $b - - Ok(()) - } - }; -} - -/// A enum representing all possible fail conditions under which a command won't -/// be executed. -pub enum DispatchError { - /// When a custom function check has failed. - CheckFailed, - /// When the requested command is disabled in bot configuration. - CommandDisabled(String), - /// When the user is blocked in bot configuration. - BlockedUser, - /// When the guild or its owner is blocked in bot configuration. - BlockedGuild, - /// When the command requester lacks specific required permissions. - LackOfPermissions(Permissions), - /// When the command requester has exceeded a ratelimit bucket. The attached - /// value is the time a requester has to wait to run the command again. - RateLimited(i64), - /// When the requested command can only be used in a direct message or group - /// channel. - OnlyForDM, - /// When the requested command can only be ran in guilds, or the bot doesn't - /// support DMs. - OnlyForGuilds, - /// When the requested command can only be used by bot owners. - OnlyForOwners, - /// When there are too few arguments. - NotEnoughArguments { min: i32, given: usize }, - /// When there are too many arguments. - TooManyArguments { max: i32, given: usize }, - /// When the command was requested by a bot user when they are set to be - /// ignored. - IgnoredBot, - /// When the bot ignores webhooks and a command was issued by one. - WebhookAuthor, -} - -type DispatchErrorHook = Fn(Context, Message, DispatchError) + Send + Sync + 'static; - -/// A utility for easily managing dispatches to commands. -/// -/// Refer to the [module-level documentation] for more information. -/// -/// [module-level documentation]: index.html -#[allow(type_complexity)] -#[derive(Default)] -pub struct Framework { - configuration: Configuration, - groups: HashMap<String, Arc<CommandGroup>>, - before: Option<Arc<BeforeHook>>, - dispatch_error_handler: Option<Arc<DispatchErrorHook>>, - buckets: HashMap<String, Bucket>, - after: Option<Arc<AfterHook>>, - /// Whether the framework has been "initialized". - /// - /// The framework is initialized once one of the following occurs: - /// - /// - configuration has been set; - /// - a command handler has been set; - /// - a command check has been set. - /// - /// This is used internally to determine whether or not - in addition to - /// dispatching to the [`Client::on_message`] handler - to have the - /// framework check if a [`Event::MessageCreate`] should be processed by - /// itself. - /// - /// [`Client::on_message`]: ../../client/struct.Client.html#method.on_message - /// [`Event::MessageCreate`]: ../../model/event/enum.Event.html#variant.MessageCreate - pub initialized: bool, - user_info: (u64, bool), -} - -impl Framework { - /// Configures the framework, setting non-default values. All fields are - /// optional. Refer to [`Configuration::default`] for more information on - /// the default values. - /// - /// # Examples - /// - /// Configuring the framework for a [`Client`], setting the [`depth`] to 3, - /// [allowing whitespace], and setting the [`prefix`] to `"~"`: - /// - /// ```rust,no_run - /// use serenity::Client; - /// use std::env; - /// - /// let mut client = Client::login(&env::var("DISCORD_TOKEN").unwrap()); - /// client.with_framework(|f| f - /// .configure(|c| c - /// .depth(3) - /// .allow_whitespace(true) - /// .prefix("~"))); - /// ``` - /// - /// [`Client`]: ../../client/struct.Client.html - /// [`Configuration::default`]: struct.Configuration.html#method.default - /// [`depth`]: struct.Configuration.html#method.depth - /// [`prefix`]: struct.Configuration.html#method.prefix - /// [allowing whitespace]: struct.Configuration.html#method.allow_whitespace - pub fn configure<F>(mut self, f: F) -> Self - where F: FnOnce(Configuration) -> Configuration { - self.configuration = f(self.configuration); - - self - } - - /// Defines a bucket with `delay` between each command, and the `limit` of uses - /// per `time_span`. - pub fn bucket<S>(mut self, s: S, delay: i64, time_span: i64, limit: i32) -> Self - where S: Into<String> { - self.buckets.insert(s.into(), Bucket { - ratelimit: Ratelimit { - delay: delay, - limit: Some((time_span, limit)), - }, - users: HashMap::new(), - }); - - self - } - - /// Defines a bucket with only a `delay` between each command. - pub fn simple_bucket<S>(mut self, s: S, delay: i64) -> Self - where S: Into<String> { - self.buckets.insert(s.into(), Bucket { - ratelimit: Ratelimit { - delay: delay, - limit: None, - }, - users: HashMap::new(), - }); - - self - } - - #[cfg(feature="cache")] - fn is_blocked_guild(&self, message: &Message) -> bool { - if let Some(Channel::Guild(channel)) = CACHE.read().unwrap().channel(message.channel_id) { - let guild_id = channel.read().unwrap().guild_id; - if self.configuration.blocked_guilds.contains(&guild_id) { - return true; - } - - if let Some(guild) = guild_id.find() { - return self.configuration.blocked_users.contains(&guild.read().unwrap().owner_id); - } - } - - false - } - - #[cfg(feature="cache")] - fn has_correct_permissions(&self, command: &Arc<Command>, message: &Message) -> bool { - if !command.required_permissions.is_empty() { - if let Some(guild) = message.guild() { - let perms = guild.read().unwrap().permissions_for(message.channel_id, message.author.id); - - return perms.contains(command.required_permissions); - } - } - - true - } - - fn checks_passed(&self, command: &Arc<Command>, mut context: &mut Context, message: &Message) -> bool { - for check in &command.checks { - if !(check)(&mut context, message) { - return false; - } - } - - true - } - - #[allow(too_many_arguments)] - fn should_fail(&mut self, - mut context: &mut Context, - message: &Message, - command: &Arc<Command>, - args: usize, - to_check: &str, - built: &str) -> Option<DispatchError> { - if self.configuration.ignore_bots && message.author.bot { - Some(DispatchError::IgnoredBot) - } else if self.configuration.ignore_webhooks && message.webhook_id.is_some() { - Some(DispatchError::WebhookAuthor) - } else if self.configuration.owners.contains(&message.author.id) { - None - } else { - if let Some(rate_limit) = command.bucket.clone().map(|x| self.ratelimit_time(x.as_str(), message.author.id.0)) { - if rate_limit > 0i64 { - return Some(DispatchError::RateLimited(rate_limit)); - } - } - - if let Some(x) = command.min_args { - if args < x as usize { - return Some(DispatchError::NotEnoughArguments { - min: x, - given: args - }); - } - } - - if let Some(x) = command.max_args { - if args > x as usize { - return Some(DispatchError::TooManyArguments { - max: x, - given: args - }); - } - } - - #[cfg(feature="cache")] - { - if self.is_blocked_guild(message) { - return Some(DispatchError::BlockedGuild); - } - - if !self.has_correct_permissions(command, message) { - return Some(DispatchError::LackOfPermissions(command.required_permissions)); - } - - if (!self.configuration.allow_dm && message.is_private()) || - (command.guild_only && message.is_private()) { - return Some(DispatchError::OnlyForGuilds); - } - - if command.dm_only && !message.is_private() { - return Some(DispatchError::OnlyForDM); - } - } - - if command.owners_only { - Some(DispatchError::OnlyForOwners) - } else if !self.checks_passed(command, &mut context, message) { - Some(DispatchError::CheckFailed) - } else if self.configuration.blocked_users.contains(&message.author.id) { - Some(DispatchError::BlockedUser) - } else if self.configuration.disabled_commands.contains(to_check) { - Some(DispatchError::CommandDisabled(to_check.to_owned())) - } else if self.configuration.disabled_commands.contains(built) { - Some(DispatchError::CommandDisabled(built.to_owned())) - } else { - None - } - } - } - - #[allow(cyclomatic_complexity)] - #[doc(hidden)] - pub fn dispatch(&mut self, mut context: Context, message: Message) { - let res = command::positions(&mut context, &message.content, &self.configuration); - - let positions = match res { - Some(mut positions) => { - // First, take out the prefixes that are as long as _or_ longer - // than the message, to avoid character boundary violations. - positions.retain(|p| *p < message.content.len()); - - // Ensure that there is _at least one_ position remaining. There - // is no point in continuing if there is not. - if positions.is_empty() { - return; - } - - positions - }, - None => return, - }; - - 'outer: for position in positions { - let mut built = String::new(); - let round = message.content.chars() - .skip(position) - .collect::<String>(); - let round = round.trim() - .split_whitespace() - .collect::<Vec<&str>>(); - - for i in 0..self.configuration.depth { - if i != 0 { - built.push(' '); - } - - built.push_str(match round.get(i) { - Some(piece) => piece, - None => continue 'outer, - }); - - let groups = self.groups.clone(); - - for group in groups.values() { - let command_length = built.len(); - - if let Some(&CommandOrAlias::Alias(ref points_to)) = group.commands.get(&built) { - built = points_to.to_owned(); - } - - let to_check = if let Some(ref prefix) = group.prefix { - if built.starts_with(prefix) && command_length > prefix.len() + 1 { - built[(prefix.len() + 1)..].to_owned() - } else { - continue; - } - } else { - built.clone() - }; - - if let Some(&CommandOrAlias::Command(ref command)) = group.commands.get(&to_check) { - let before = self.before.clone(); - let command = command.clone(); - let after = self.after.clone(); - let groups = self.groups.clone(); - - let args = { - let content = message.content[position..].trim(); - - if command.use_quotes { - utils::parse_quotes(&content[command_length..]) - } else { - content[command_length..] - .split_whitespace() - .map(|arg| arg.to_owned()) - .collect::<Vec<String>>() - } - }; - - if let Some(error) = self.should_fail(&mut context, &message, &command, args.len(), &to_check, &built) { - if let Some(ref handler) = self.dispatch_error_handler { - handler(context, message, error); - } - return; - } - - thread::spawn(move || { - if let Some(before) = before { - if !(before)(&mut context, &message, &built) { - return; - } - } - - let result = match command.exec { - CommandType::StringResponse(ref x) => { - let _ = &mut context.channel_id.unwrap().say(x); - - Ok(()) - }, - CommandType::Basic(ref x) => { - (x)(&mut context, &message, args) - }, - CommandType::WithCommands(ref x) => { - (x)(&mut context, &message, groups, &args) - } - }; - - if let Some(after) = after { - (after)(&mut context, &message, &built, result); - } - }); - - return; - } - } - } - } - } - - /// Adds a function to be associated with a command, which will be called - /// when a command is used in a message. - /// - /// This requires that a check - if one exists - passes, prior to being - /// called. - /// - /// Note that once v0.2.0 lands, you will need to use the command builder - /// via the [`command`] method to set checks. This command will otherwise - /// only be for simple commands. - /// - /// Refer to the [module-level documentation] for more information and - /// usage. - /// - /// [`command`]: #method.command - /// [module-level documentation]: index.html - pub fn on<F, S>(mut self, command_name: S, f: F) -> Self - where F: Fn(&mut Context, &Message, Vec<String>) -> Result<(), String> + Send + Sync + 'static, - S: Into<String> { - { - let ungrouped = self.groups.entry("Ungrouped".to_owned()) - .or_insert_with(|| Arc::new(CommandGroup::default())); - - if let Some(ref mut group) = Arc::get_mut(ungrouped) { - let name = command_name.into(); - - group.commands.insert(name, CommandOrAlias::Command(Arc::new(Command::new(f)))); - } - } - - self.initialized = true; - - self - } - - /// Adds a command using command builder. - /// - /// # Examples - /// - /// ```rust,ignore - /// framework.command("ping", |c| c - /// .description("Responds with 'pong'.") - /// .exec(|ctx, _, _| { - /// let _ = ctx.say("pong"); - /// })); - /// ``` - pub fn command<F, S>(mut self, command_name: S, f: F) -> Self - where F: FnOnce(CreateCommand) -> CreateCommand, - S: Into<String> { - { - let ungrouped = self.groups.entry("Ungrouped".to_owned()) - .or_insert_with(|| Arc::new(CommandGroup::default())); - - if let Some(ref mut group) = Arc::get_mut(ungrouped) { - let cmd = f(CreateCommand(Command::default())).0; - let name = command_name.into(); - - if let Some(ref prefix) = group.prefix { - for v in &cmd.aliases { - group.commands.insert(format!("{} {}", prefix, v.to_owned()), CommandOrAlias::Alias(format!("{} {}", prefix, name))); - } - } else { - for v in &cmd.aliases { - group.commands.insert(v.to_owned(), CommandOrAlias::Alias(name.clone())); - } - } - - group.commands.insert(name, CommandOrAlias::Command(Arc::new(cmd))); - } - } - - self.initialized = true; - - self - } - - pub fn group<F, S>(mut self, group_name: S, f: F) -> Self - where F: FnOnce(CreateGroup) -> CreateGroup, - S: Into<String> { - let group = f(CreateGroup(CommandGroup::default())).0; - - self.groups.insert(group_name.into(), Arc::new(group)); - self.initialized = true; - - self - } - - /// Specify the function that's called in case a command wasn't executed for one reason or another. - /// - /// DispatchError represents all possible fail conditions. - pub fn on_dispatch_error<F>(mut self, f: F) -> Self - where F: Fn(Context, Message, DispatchError) + Send + Sync + 'static { - self.dispatch_error_handler = Some(Arc::new(f)); - - self - } - - /// Specify the function to be called prior to every command's execution. - /// If that function returns true, the command will be executed. - pub fn before<F>(mut self, f: F) -> Self - where F: Fn(&mut Context, &Message, &String) -> bool + Send + Sync + 'static { - self.before = Some(Arc::new(f)); - - self - } - - /// Specify the function to be called after every command's execution. - /// Fourth argument exists if command returned an error which you can handle. - pub fn after<F>(mut self, f: F) -> Self - where F: Fn(&mut Context, &Message, &String, Result<(), String>) + Send + Sync + 'static { - self.after = Some(Arc::new(f)); - - self - } - - #[doc(hidden)] - pub fn update_current_user(&mut self, user_id: UserId, is_bot: bool) { - self.user_info = (user_id.0, is_bot); - } - - fn ratelimit_time(&mut self, bucket_name: &str, user_id: u64) -> i64 { - self.buckets - .get_mut(bucket_name) - .map(|bucket| bucket.take(user_id)) - .unwrap_or(0) - } -} diff --git a/src/ext/mod.rs b/src/ext/mod.rs deleted file mode 100644 index 6254539..0000000 --- a/src/ext/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! A set of extended functionality that is not required for a `Client` and/or -//! `Shard` to properly function. -//! -//! These are flagged behind feature-gates and can be enabled and disabled. -//! -//! See each extension's module-level documentation for more information. -//! -//! Note that the framework module requires the `framework` feature to be -//! enabled (enabled by default), the cache requires the `cache` feature to be -//! enabled (enabled by default), and voice support requires the `voice` feature -//! to be enabled (disabled by default). - -#[cfg(feature="cache")] -pub mod cache; -#[cfg(feature="framework")] -pub mod framework; -#[cfg(feature="voice")] -pub mod voice; diff --git a/src/ext/voice/audio.rs b/src/ext/voice/audio.rs deleted file mode 100644 index ea8c87a..0000000 --- a/src/ext/voice/audio.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub const HEADER_LEN: usize = 12; -pub const SAMPLE_RATE: u32 = 48000; - -/// A readable audio source. -pub trait AudioSource: Send { - fn is_stereo(&mut self) -> bool; - - fn read_frame(&mut self, buffer: &mut [i16]) -> Option<usize>; -} - -/// A receiver for incoming audio. -pub trait AudioReceiver: Send { - fn speaking_update(&mut self, ssrc: u32, user_id: u64, speaking: bool); - - fn voice_packet(&mut self, ssrc: u32, sequence: u16, timestamp: u32, stereo: bool, data: &[i16]); -} diff --git a/src/ext/voice/connection.rs b/src/ext/voice/connection.rs deleted file mode 100644 index 698f469..0000000 --- a/src/ext/voice/connection.rs +++ /dev/null @@ -1,477 +0,0 @@ -use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; -use opus::{ - Channels, - CodingMode, - Decoder as OpusDecoder, - Encoder as OpusEncoder, - packet as opus_packet, -}; -use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; -use std::collections::HashMap; -use std::io::Write; -use std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; -use std::sync::mpsc::{self, Receiver as MpscReceiver, Sender as MpscSender}; -use std::thread::{self, Builder as ThreadBuilder, JoinHandle}; -use std::time::Duration; -use super::audio::{HEADER_LEN, SAMPLE_RATE, AudioReceiver, AudioSource}; -use super::connection_info::ConnectionInfo; -use super::{CRYPTO_MODE, VoiceError, payload}; -use websocket::client::request::Url as WebsocketUrl; -use websocket::client::{ - Client as WsClient, - Receiver as WsReceiver, - Sender as WsSender -}; -use websocket::stream::WebSocketStream; -use ::internal::prelude::*; -use ::internal::ws_impl::{ReceiverExt, SenderExt}; -use ::internal::Timer; -use ::model::event::VoiceEvent; -use ::model::UserId; - -enum ReceiverStatus { - Udp(Vec<u8>), - Websocket(VoiceEvent), -} - -#[allow(dead_code)] -struct ThreadItems { - rx: MpscReceiver<ReceiverStatus>, - udp_close_sender: MpscSender<i32>, - udp_thread: JoinHandle<()>, - ws_close_sender: MpscSender<i32>, - ws_thread: JoinHandle<()>, -} - -#[allow(dead_code)] -pub struct Connection { - audio_timer: Timer, - decoder_map: HashMap<(u32, Channels), OpusDecoder>, - destination: SocketAddr, - encoder: OpusEncoder, - encoder_stereo: bool, - keepalive_timer: Timer, - key: Key, - sender: WsSender<WebSocketStream>, - sequence: u16, - silence_frames: u8, - speaking: bool, - ssrc: u32, - thread_items: ThreadItems, - timestamp: u32, - udp: UdpSocket, - user_id: UserId, -} - -impl Connection { - pub fn new(mut info: ConnectionInfo) -> Result<Connection> { - let url = generate_url(&mut info.endpoint)?; - - let response = WsClient::connect(url)?.send()?; - response.validate()?; - let (mut sender, mut receiver) = response.begin().split(); - - sender.send_json(&payload::build_identify(&info))?; - - let hello = { - let hello; - - loop { - match receiver.recv_json(VoiceEvent::decode)? { - VoiceEvent::Hello(received_hello) => { - hello = received_hello; - - break; - }, - VoiceEvent::Heartbeat(_) => continue, - other => { - debug!("[Voice] Expected hello/heartbeat; got: {:?}", - other); - - return Err(Error::Voice(VoiceError::ExpectedHandshake)); - }, - } - } - - hello - }; - - if !has_valid_mode(&hello.modes) { - return Err(Error::Voice(VoiceError::VoiceModeUnavailable)); - } - - let destination = (&info.endpoint[..], hello.port) - .to_socket_addrs()? - .next() - .ok_or(Error::Voice(VoiceError::HostnameResolve))?; - - // Important to note here: the length of the packet can be of either 4 - // or 70 bytes. If it is 4 bytes, then we need to send a 70-byte packet - // to determine the IP. - // - // Past the initial 4 bytes, the packet _must_ be completely empty data. - // - // The returned packet will be a null-terminated string of the IP, and - // the port encoded in LE in the last two bytes of the packet. - let udp = UdpSocket::bind("0.0.0.0:0")?; - - { - let mut bytes = [0; 70]; - - (&mut bytes[..]).write_u32::<BigEndian>(hello.ssrc)?; - udp.send_to(&bytes, destination)?; - - let mut bytes = [0; 256]; - let (len, _addr) = udp.recv_from(&mut bytes)?; - - // Find the position in the bytes that contains the first byte of 0, - // indicating the "end of the address". - let index = bytes.iter().skip(4).position(|&x| x == 0) - .ok_or(Error::Voice(VoiceError::FindingByte))?; - - let pos = 4 + index; - let addr = String::from_utf8_lossy(&bytes[4..pos]); - let port_pos = len - 2; - let port = (&bytes[port_pos..]).read_u16::<LittleEndian>()?; - - sender.send_json(&payload::build_select_protocol(addr, port))?; - } - - let key = encryption_key(&mut receiver)?; - - let thread_items = start_threads(receiver, &udp)?; - - info!("[Voice] Connected to: {}", info.endpoint); - - let encoder = OpusEncoder::new(SAMPLE_RATE, Channels::Mono, CodingMode::Audio)?; - - Ok(Connection { - audio_timer: Timer::new(1000 * 60 * 4), - decoder_map: HashMap::new(), - destination: destination, - encoder: encoder, - encoder_stereo: false, - key: key, - keepalive_timer: Timer::new(hello.heartbeat_interval), - udp: udp, - sender: sender, - sequence: 0, - silence_frames: 0, - speaking: false, - ssrc: hello.ssrc, - thread_items: thread_items, - timestamp: 0, - user_id: info.user_id, - }) - } - - #[allow(unused_variables)] - pub fn cycle(&mut self, - source: &mut Option<Box<AudioSource>>, - receiver: &mut Option<Box<AudioReceiver>>, - audio_timer: &mut Timer) - -> Result<()> { - let mut buffer = [0i16; 960 * 2]; - let mut packet = [0u8; 512]; - let mut nonce = secretbox::Nonce([0; 24]); - - if let Some(receiver) = receiver.as_mut() { - while let Ok(status) = self.thread_items.rx.try_recv() { - match status { - ReceiverStatus::Udp(packet) => { - let mut handle = &packet[2..]; - let seq = handle.read_u16::<BigEndian>()?; - let timestamp = handle.read_u32::<BigEndian>()?; - let ssrc = handle.read_u32::<BigEndian>()?; - - nonce.0[..HEADER_LEN].clone_from_slice(&packet[..HEADER_LEN]); - - if let Ok(decrypted) = secretbox::open(&packet[HEADER_LEN..], &nonce, &self.key) { - let channels = opus_packet::get_nb_channels(&decrypted)?; - - let entry = self.decoder_map.entry((ssrc, channels)) - .or_insert_with(|| OpusDecoder::new(SAMPLE_RATE, - channels) - .unwrap()); - - let len = entry.decode(&decrypted, &mut buffer, false)?; - - let is_stereo = channels == Channels::Stereo; - - let b = if is_stereo { - len * 2 - } else { - len - }; - - receiver.voice_packet(ssrc, seq, timestamp, is_stereo, &buffer[..b]); - } - }, - ReceiverStatus::Websocket(VoiceEvent::Speaking(ev)) => { - receiver.speaking_update(ev.ssrc, - ev.user_id.0, - ev.speaking); - }, - ReceiverStatus::Websocket(other) => { - info!("[Voice] Received other websocket data: {:?}", - other); - }, - } - } - } else { - loop { - if self.thread_items.rx.try_recv().is_err() { - break; - } - } - } - - // Send the voice websocket keepalive if it's time - if self.keepalive_timer.check() { - self.sender.send_json(&payload::build_keepalive())?; - } - - // Send UDP keepalive if it's time - if self.audio_timer.check() { - let mut bytes = [0; 4]; - (&mut bytes[..]).write_u32::<BigEndian>(self.ssrc)?; - self.udp.send_to(&bytes, self.destination)?; - } - - let len = self.read(source, &mut buffer)?; - - if len == 0 { - self.set_speaking(false)?; - - if self.silence_frames > 0 { - self.silence_frames -= 1; - - for value in &mut buffer[..] { - *value = 0; - } - } else { - audio_timer.await(); - - return Ok(()); - } - } else { - self.silence_frames = 5; - - for value in &mut buffer[len..] { - *value = 0; - } - } - - self.set_speaking(true)?; - let index = self.prep_packet(&mut packet, buffer, nonce)?; - - audio_timer.await(); - self.udp.send_to(&packet[..index], self.destination)?; - self.audio_timer.reset(); - - Ok(()) - } - - fn prep_packet(&mut self, - packet: &mut [u8; 512], - buffer: [i16; 1920], - mut nonce: Nonce) - -> Result<usize> { - { - let mut cursor = &mut packet[..HEADER_LEN]; - cursor.write_all(&[0x80, 0x78])?; - cursor.write_u16::<BigEndian>(self.sequence)?; - cursor.write_u32::<BigEndian>(self.timestamp)?; - cursor.write_u32::<BigEndian>(self.ssrc)?; - } - - nonce.0[..HEADER_LEN].clone_from_slice(&packet[..HEADER_LEN]); - - let sl_index = packet.len() - 16; - let buffer_len = if self.encoder_stereo { - 960 * 2 - } else { - 960 - }; - - let len = self.encoder.encode(&buffer[..buffer_len], - &mut packet[HEADER_LEN..sl_index])?; - let crypted = { - let slice = &packet[HEADER_LEN..HEADER_LEN + len]; - secretbox::seal(slice, &nonce, &self.key) - }; - let index = HEADER_LEN + crypted.len(); - packet[HEADER_LEN..index].clone_from_slice(&crypted); - - self.sequence = self.sequence.wrapping_add(1); - self.timestamp = self.timestamp.wrapping_add(960); - - Ok(HEADER_LEN + crypted.len()) - } - - fn read(&mut self, - source: &mut Option<Box<AudioSource>>, - buffer: &mut [i16; 1920]) - -> Result<usize> { - let mut clear = false; - - let len = match source.as_mut() { - Some(source) => { - let is_stereo = source.is_stereo(); - - if is_stereo != self.encoder_stereo { - let channels = if is_stereo { - Channels::Stereo - } else { - Channels::Mono - }; - self.encoder = OpusEncoder::new(SAMPLE_RATE, - channels, - CodingMode::Audio)?; - self.encoder_stereo = is_stereo; - } - - let buffer_len = if is_stereo { - 960 * 2 - } else { - 960 - }; - - match source.read_frame(&mut buffer[..buffer_len]) { - Some(len) => len, - None => { - clear = true; - - 0 - }, - } - }, - None => 0, - }; - - if clear { - *source = None; - } - - Ok(len) - } - - fn set_speaking(&mut self, speaking: bool) -> Result<()> { - if self.speaking == speaking { - return Ok(()); - } - - self.speaking = speaking; - - self.sender.send_json(&payload::build_speaking(speaking)) - } -} - -impl Drop for Connection { - fn drop(&mut self) { - let _ = self.thread_items.udp_close_sender.send(0); - let _ = self.thread_items.ws_close_sender.send(0); - - info!("[Voice] Disconnected"); - } -} - -fn generate_url(endpoint: &mut String) -> Result<WebsocketUrl> { - if endpoint.ends_with(":80") { - let len = endpoint.len(); - - endpoint.truncate(len - 3); - } - - WebsocketUrl::parse(&format!("wss://{}", endpoint)) - .or(Err(Error::Voice(VoiceError::EndpointUrl))) -} - -#[inline] -fn encryption_key(receiver: &mut WsReceiver<WebSocketStream>) - -> Result<Key> { - loop { - match receiver.recv_json(VoiceEvent::decode)? { - VoiceEvent::Ready(ready) => { - if ready.mode != CRYPTO_MODE { - return Err(Error::Voice(VoiceError::VoiceModeInvalid)); - } - - return Key::from_slice(&ready.secret_key) - .ok_or(Error::Voice(VoiceError::KeyGen)); - }, - VoiceEvent::Unknown(op, value) => { - debug!("[Voice] Expected ready for key; got: op{}/v{:?}", - op.num(), - value); - }, - _ => {}, - } - } -} - -#[inline] -fn has_valid_mode(modes: &[String]) -> bool { - modes.iter().any(|s| s == CRYPTO_MODE) -} - -#[inline] -fn start_threads(mut receiver: WsReceiver<WebSocketStream>, udp: &UdpSocket) - -> Result<ThreadItems> { - let (udp_close_sender, udp_close_reader) = mpsc::channel(); - let (ws_close_sender, ws_close_reader) = mpsc::channel(); - - let current_thread = thread::current(); - let thread_name = current_thread.name().unwrap_or("serenity voice"); - - let (tx, rx) = mpsc::channel(); - let tx_clone = tx.clone(); - let udp_clone = udp.try_clone()?; - - let udp_thread = ThreadBuilder::new() - .name(format!("{} UDP", thread_name)) - .spawn(move || { - let _ = udp_clone.set_read_timeout(Some(Duration::from_millis(250))); - - let mut buffer = [0; 512]; - - loop { - if let Ok((len, _)) = udp_clone.recv_from(&mut buffer) { - let piece = buffer[..len].to_vec(); - let send = tx.send(ReceiverStatus::Udp(piece)); - - if send.is_err() { - return; - } - } else if udp_close_reader.try_recv().is_ok() { - return; - } - } - })?; - - let ws_thread = ThreadBuilder::new() - .name(format!("{} WS", thread_name)) - .spawn(move || { - loop { - while let Ok(msg) = receiver.recv_json(VoiceEvent::decode) { - if tx_clone.send(ReceiverStatus::Websocket(msg)).is_ok() { - return; - } - } - - if ws_close_reader.try_recv().is_ok() { - return; - } - - thread::sleep(Duration::from_millis(25)); - } - })?; - - Ok(ThreadItems { - rx: rx, - udp_close_sender: udp_close_sender, - udp_thread: udp_thread, - ws_close_sender: ws_close_sender, - ws_thread: ws_thread, - }) -} diff --git a/src/ext/voice/connection_info.rs b/src/ext/voice/connection_info.rs deleted file mode 100644 index d0364ce..0000000 --- a/src/ext/voice/connection_info.rs +++ /dev/null @@ -1,10 +0,0 @@ -use ::model::{GuildId, UserId}; - -#[derive(Clone, Debug)] -pub struct ConnectionInfo { - pub endpoint: String, - pub guild_id: GuildId, - pub session_id: String, - pub token: String, - pub user_id: UserId, -} diff --git a/src/ext/voice/error.rs b/src/ext/voice/error.rs deleted file mode 100644 index 55be1f6..0000000 --- a/src/ext/voice/error.rs +++ /dev/null @@ -1,34 +0,0 @@ -use serde_json::Value; -use std::process::Output; - -/// An error returned from the voice module. -// Errors which are not visible to the end user are hidden. -#[derive(Debug)] -pub enum VoiceError { - /// An indicator that an endpoint URL was invalid. - EndpointUrl, - #[doc(hidden)] - ExpectedHandshake, - #[doc(hidden)] - FindingByte, - #[doc(hidden)] - HostnameResolve, - #[doc(hidden)] - KeyGen, - /// An error occurred while checking if a path is stereo. - Streams, - #[doc(hidden)] - VoiceModeInvalid, - #[doc(hidden)] - VoiceModeUnavailable, - /// An error occurred while running `youtube-dl`. - YouTubeDLRun(Output), - /// An error occurred while processing the JSON output from `youtube-dl`. - /// - /// The JSON output is given. - YouTubeDLProcessing(Value), - /// The `url` field of the `youtube-dl` JSON output was not present. - /// - /// The JSON output is given. - YouTubeDLUrl(Value), -} diff --git a/src/ext/voice/handler.rs b/src/ext/voice/handler.rs deleted file mode 100644 index 545befe..0000000 --- a/src/ext/voice/handler.rs +++ /dev/null @@ -1,433 +0,0 @@ -use std::sync::mpsc::{self, Sender as MpscSender}; -use super::{AudioReceiver, AudioSource}; -use super::connection_info::ConnectionInfo; -use super::Status as VoiceStatus; -use ::client::gateway::GatewayStatus; -use ::constants::VoiceOpCode; -use ::model::{ChannelId, GuildId, UserId, VoiceState}; -use super::threading; - -/// The handler is responsible for "handling" a single voice connection, acting -/// as a clean API above the inner connection. -/// -/// Look into the [`Manager`] for a slightly higher-level interface for managing -/// the existence of handlers. -/// -/// **Note**: You should _not_ manually mutate any struct fields. You should -/// _only_ read them. Use methods to mutate them. -/// -/// # Examples -/// -/// Assuming that you already have a `Manager`, most likely retrieved via a -/// [WebSocket connection], you can join a guild's voice channel and deafen -/// yourself like so: -/// -/// ```rust,ignore -/// // assuming a `manager` has already been bound, hopefully retrieved through -/// // a websocket's connection. -/// use serenity::model::{ChannelId, GuildId}; -/// -/// let guild_id = GuildId(81384788765712384); -/// let channel_id = ChannelId(85482585546833920); -/// -/// let handler = manager.join(Some(guild_id), channel_id); -/// handler.deafen(true); -/// ``` -/// -/// [`Manager`]: struct.Manager.html -/// [WebSocket connection]: ../../client/struct.Connection.html -#[derive(Clone, Debug)] -pub struct Handler { - /// The ChannelId to be connected to, if any. - /// - /// Note that when connected to a voice channel, while the `ChannelId` will - /// not be `None`, the [`guild_id`] can, in the event of [`Group`] or - /// 1-on-1 [`Call`]s. - /// - /// **Note**: This _must not_ be manually mutated. Call [`switch_to`] to - /// mutate this value. - /// - /// [`Call`]: ../../model/struct.Call.html - /// [`Group`]: ../../model/struct.Group.html - /// [`guild`]: #structfield.guild - /// [`switch_to`]: #method.switch_to - pub channel_id: Option<ChannelId>, - /// The voice server endpoint. - pub endpoint: Option<String>, - /// The GuildId to be connected to, if any. Can be normally `None` in the - /// event of playing audio to a one-on-one [`Call`] or [`Group`]. - /// - /// [`Call`]: ../../model/struct.Call.html - /// [`Group`]: ../../model/struct.Group.html - pub guild_id: GuildId, - /// Whether the current handler is set to deafen voice connections. - /// - /// **Note**: This _must not_ be manually mutated. Call [`deafen`] to - /// mutate this value. - /// - /// [`deafen`]: #method.deafen - pub self_deaf: bool, - /// Whether the current handler is set to mute voice connections. - /// - /// **Note**: This _must not_ be manually mutated. Call [`mute`] to mutate - /// this value. - /// - /// [`mute`]: #method.mute - pub self_mute: bool, - /// The internal sender to the voice connection monitor thread. - sender: MpscSender<VoiceStatus>, - /// The session Id of the current voice connection, if any. - /// - /// **Note**: This _should_ be set through an [`update_state`] call. - /// - /// [`update_state`]: #method.update_state - pub session_id: Option<String>, - /// The token of the current voice connection, if any. - /// - /// **Note**: This _should_ be set through an [`update_server`] call. - /// - /// [`update_server`]: #method.update_server - pub token: Option<String>, - /// The Id of the current user. - /// - /// This is configured via [`new`] or [`standalone`]. - /// - /// [`new`]: #method.new - /// [`standalone`]: #method.standalone - pub user_id: UserId, - /// Will be set when a `Handler` is made via the [`new`][`Handler::new`] - /// method. - /// - /// When set via [`standalone`][`Handler::standalone`], it will not be - /// present. - ws: Option<MpscSender<GatewayStatus>>, -} - -impl Handler { - /// Creates a new Handler. - /// - /// **Note**: You should never call this yourself, and should instead use - /// [`Manager::join`]. - /// - /// Like, really. Really do not use this. Please. - /// - /// [`Manager::join`]: struct.Manager.html#method.join - #[doc(hidden)] - #[inline] - pub fn new(guild_id: GuildId, ws: MpscSender<GatewayStatus>, user_id: UserId) -> Self { - Self::new_raw(guild_id, Some(ws), user_id) - } - - /// Creates a new, standalone Handler which is not connected to the primary - /// WebSocket to the Gateway. - /// - /// Actions such as muting, deafening, and switching channels will not - /// function through this Handler and must be done through some other - /// method, as the values will only be internally updated. - /// - /// For most use cases you do not want this. Only use it if you are using - /// the voice component standalone from the rest of the library. - #[inline] - pub fn standalone(guild_id: GuildId, user_id: UserId) -> Self { - Self::new_raw(guild_id, None, user_id) - } - - /// Connects to the voice channel if the following are present: - /// - /// - [`endpoint`] - /// - [`session_id`] - /// - [`token`] - /// - /// If they _are_ all present, then `true` is returned. Otherwise, `false` - /// is. - /// - /// This will automatically be called by [`update_server`] or - /// [`update_state`] when all three values become present. - /// - /// [`endpoint`]: #structfield.endpoint - /// [`session_id`]: #structfield.session_id - /// [`token`]: #structfield.token - /// [`update_server`]: #method.update_server - /// [`update_state`]: #method.update_state - pub fn connect(&mut self) -> bool { - if self.endpoint.is_none() || self.session_id.is_none() || self.token.is_none() { - return false; - } - - let endpoint = self.endpoint.clone().unwrap(); - let guild_id = self.guild_id; - let session_id = self.session_id.clone().unwrap(); - let token = self.token.clone().unwrap(); - let user_id = self.user_id; - - // Safe as all of these being present was already checked. - self.send(VoiceStatus::Connect(ConnectionInfo { - endpoint: endpoint, - guild_id: guild_id, - session_id: session_id, - token: token, - user_id: user_id, - })); - - true - } - - /// Sets whether the current connection to be deafened. - /// - /// If there is no live voice connection, then this only acts as a settings - /// update for future connections. - /// - /// **Note**: Unlike in the official client, you _can_ be deafened while - /// not being muted. - /// - /// **Note**: If the `Handler` was created via [`standalone`], then this - /// will _only_ update whether the connection is internally deafened. - /// - /// [`standalone`]: #method.standalone - pub fn deafen(&mut self, deaf: bool) { - self.self_deaf = deaf; - - // Only send an update if there is currently a connected channel. - // - // Otherwise, this can be treated as a "settings" update for a - // connection. - if self.channel_id.is_some() { - self.update(); - } - } - - /// Connect - or switch - to the given voice channel by its Id. - /// - /// **Note**: This is not necessary for [`Group`] or direct [call][`Call`]s. - /// - /// [`Call`]: ../../model/struct.Call.html - /// [`Group`]: ../../model/struct.Group.html - pub fn join(&mut self, channel_id: ChannelId) { - self.channel_id = Some(channel_id); - - self.send_join(); - } - - /// Leaves the current voice channel, disconnecting from it. - /// - /// This does _not_ forget settings, like whether to be self-deafened or - /// self-muted. - /// - /// **Note**: If the `Handler` was created via [`standalone`], then this - /// will _only_ update whether the connection is internally connected to a - /// voice channel. - /// - /// [`standalone`]: #method.standalone - pub fn leave(&mut self) { - // Only send an update if we were in a voice channel. - if self.channel_id.is_some() { - self.channel_id = None; - - self.update(); - } - } - - /// Sets a receiver, i.e. a way to receive audio. Most use cases for bots do - /// not require this. - /// - /// The `receiver` argument can be thought of as an "optional Option". You - /// can pass in just a boxed receiver, and do not need to specify `Some`. - /// - /// Pass `None` to drop the current receiver, if one exists. - pub fn listen<O: Into<Option<Box<AudioReceiver>>>>(&mut self, receiver: O) { - self.send(VoiceStatus::SetReceiver(receiver.into())) - } - - /// Sets whether the current connection is to be muted. - /// - /// If there is no live voice connection, then this only acts as a settings - /// update for future connections. - /// - /// **Note**: If the `Handler` was created via [`standalone`], then this - /// will _only_ update whether the connection is internally muted. - /// - /// [`standalone`]: #method.standalone - pub fn mute(&mut self, mute: bool) { - self.self_mute = mute; - - if self.channel_id.is_some() { - self.update(); - } - } - - /// Plays audio from a source. This can be a source created via - /// [`voice::ffmpeg`] or [`voice::ytdl`]. - /// - /// [`voice::ffmpeg`]: fn.ffmpeg.html - /// [`voice::ytdl`]: fn.ytdl.html - pub fn play(&mut self, source: Box<AudioSource>) { - self.send(VoiceStatus::SetSender(Some(source))) - } - - /// Stops playing audio from a source, if one is set. - pub fn stop(&mut self) { - self.send(VoiceStatus::SetSender(None)) - } - - /// Switches the current connected voice channel to the given `channel_id`. - /// - /// This has 3 separate behaviors: - /// - /// - if the given `channel_id` is equivalent to the current connected - /// `channel_id`, then do nothing; - /// - if the given `channel_id` is _not_ equivalent to the current connected - /// `channel_id`, then switch to the given `channel_id`; - /// - if not currently connected to a voice channel, connect to the given - /// one. - /// - /// If you are dealing with switching from one group to another, then open - /// another handler, and optionally drop this one via [`Manager::remove`]. - /// - /// **Note**: The given `channel_id`, if in a guild, _must_ be in the - /// current handler's associated guild. - /// - /// **Note**: If the `Handler` was created via [`standalone`], then this - /// will _only_ update whether the connection is internally switched to a - /// different channel. - /// - /// [`Manager::remove`]: struct.Manager.html#method.remove - /// [`standalone`]: #method.standalone - pub fn switch_to(&mut self, channel_id: ChannelId) { - match self.channel_id { - Some(current_id) if current_id == channel_id => { - // If already connected to the given channel, do nothing. - return; - }, - _ => { - self.channel_id = Some(channel_id); - - self.update(); - }, - } - } - - /// Updates the voice server data. - /// - /// You should only need to use this if you initialized the `Handler` via - /// [`standalone`]. - /// - /// Refer to the documentation for [`connect`] for when this will - /// automatically connect to a voice channel. - /// - /// [`connect`]: #method.connect - /// [`standalone`]: #method.standalone - pub fn update_server(&mut self, endpoint: &Option<String>, token: &str) { - self.token = Some(token.to_owned()); - - if let Some(endpoint) = endpoint.clone() { - self.endpoint = Some(endpoint); - - if self.session_id.is_some() { - self.connect(); - } - } else { - self.leave(); - } - } - - /// Updates the internal voice state of the current user. - /// - /// You should only need to use this if you initialized the `Handler` via - /// [`standalone`]. - /// - /// refer to the documentation for [`connect`] for when this will - /// automatically connect to a voice channel. - /// - /// [`connect`]: #method.connect - /// [`standalone`]: #method.standalone - pub fn update_state(&mut self, voice_state: &VoiceState) { - if self.user_id != voice_state.user_id.0 { - return; - } - - self.channel_id = voice_state.channel_id; - - if voice_state.channel_id.is_some() { - self.session_id = Some(voice_state.session_id.clone()); - - if self.endpoint.is_some() && self.token.is_some() { - self.connect(); - } - } else { - self.leave(); - } - } - - fn new_raw(guild_id: GuildId, ws: Option<MpscSender<GatewayStatus>>, user_id: UserId) -> Self { - let (tx, rx) = mpsc::channel(); - - threading::start(guild_id, rx); - - Handler { - channel_id: None, - endpoint: None, - guild_id: guild_id, - self_deaf: false, - self_mute: false, - sender: tx, - session_id: None, - token: None, - user_id: user_id, - ws: ws, - } - } - - /// Sends a message to the thread. - fn send(&mut self, status: VoiceStatus) { - // Restart thread if it errored. - if let Err(mpsc::SendError(status)) = self.sender.send(status) { - let (tx, rx) = mpsc::channel(); - - self.sender = tx; - self.sender.send(status).unwrap(); - - threading::start(self.guild_id, rx); - - self.update(); - } - } - - fn send_join(&self) { - // Do _not_ try connecting if there is not at least a channel. There - // does not _necessarily_ need to be a guild. - if self.channel_id.is_none() { - return; - } - - self.update(); - } - - /// Send an update for the current session over WS. - /// - /// Does nothing if initialized via [`standalone`]. - /// - /// [`standalone`]: #method.standalone - fn update(&self) { - if let Some(ref ws) = self.ws { - let map = json!({ - "op": VoiceOpCode::SessionDescription.num(), - "d": { - "channel_id": self.channel_id.map(|c| c.0), - "guild_id": self.guild_id.0, - "self_deaf": self.self_deaf, - "self_mute": self.self_mute, - } - }); - - let _ = ws.send(GatewayStatus::SendMessage(map)); - } - } -} - -impl Drop for Handler { - /// Leaves the current connected voice channel, if connected to one, and - /// forgets all configurations relevant to this Handler. - fn drop(&mut self) { - self.leave(); - } -} diff --git a/src/ext/voice/manager.rs b/src/ext/voice/manager.rs deleted file mode 100644 index e85a5a3..0000000 --- a/src/ext/voice/manager.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::collections::HashMap; -use std::sync::mpsc::Sender as MpscSender; -use super::Handler; -use ::client::gateway::GatewayStatus; -use ::model::{ChannelId, GuildId, UserId}; - -/// A manager is a struct responsible for managing [`Handler`]s which belong to -/// a single [WebSocket connection]. This is a fairly complex key-value store, -/// with a bit of extra utility for easily joining a "target". -/// -/// The "target" used by the Manager is determined based on the `guild_id` and -/// `channel_id` provided. If a `guild_id` is _not_ provided to methods that -/// optionally require it, then the target is a group or 1-on-1 call with a -/// user. The `channel_id` is then used as the target. -/// -/// If a `guild_id` is provided, then the target is the guild, as a user -/// can not be connected to two channels within one guild simultaneously. -/// -/// [`Group`]: ../../model/struct.Group.html -/// [`Handler`]: struct.Handler.html -/// [guild's channel]: ../../model/enum.ChannelType.html#variant.Voice -/// [WebSocket connection]: ../../client/struct.Connection.html -#[derive(Clone, Debug)] -pub struct Manager { - handlers: HashMap<GuildId, Handler>, - user_id: UserId, - ws: MpscSender<GatewayStatus>, -} - -impl Manager { - #[doc(hidden)] - pub fn new(ws: MpscSender<GatewayStatus>, user_id: UserId) -> Manager { - Manager { - handlers: HashMap::new(), - user_id: user_id, - ws: ws, - } - } - - /// Retrieves a mutable handler for the given target, if one exists. - pub fn get<G: Into<GuildId>>(&mut self, guild_id: G) -> Option<&mut Handler> { - self.handlers.get_mut(&guild_id.into()) - } - - /// Connects to a target by retrieving its relevant [`Handler`] and - /// connecting, or creating the handler if required. - /// - /// This can also switch to the given channel, if a handler already exists - /// for the target and the current connected channel is not equal to the - /// given channel. - /// - /// In the case of channel targets, the same channel is used to connect to. - /// - /// In the case of guilds, the provided channel is used to connect to. The - /// channel _must_ be in the provided guild. This is _not_ checked by the - /// library, and will result in an error. If there is already a connected - /// handler for the guild, _and_ the provided channel is different from the - /// channel that the connection is already connected to, then the handler - /// will switch the connection to the provided channel. - /// - /// If you _only_ need to retrieve the handler for a target, then use - /// [`get`]. - /// - /// [`Handler`]: struct.Handler.html - /// [`get`]: #method.get - #[allow(map_entry)] - pub fn join<C, G>(&mut self, guild_id: G, channel_id: C) -> &mut Handler - where C: Into<ChannelId>, G: Into<GuildId> { - let channel_id = channel_id.into(); - let guild_id = guild_id.into(); - - { - let mut found = false; - - if let Some(handler) = self.handlers.get_mut(&guild_id) { - handler.switch_to(channel_id); - - found = true; - } - - if found { - // Actually safe, as the key has already been found above. - return self.handlers.get_mut(&guild_id).unwrap(); - } - } - - let mut handler = Handler::new(guild_id, self.ws.clone(), self.user_id); - handler.join(channel_id); - - self.handlers.insert(guild_id, handler); - - // Actually safe, as the key would have been inserted above. - self.handlers.get_mut(&guild_id).unwrap() - } - - /// Retrieves the [handler][`Handler`] for the given target and leaves the - /// associated voice channel, if connected. - /// - /// This will _not_ drop the handler, and will preserve it and its settings. - /// - /// This is a wrapper around [getting][`get`] a handler and calling - /// [`leave`] on it. - /// - /// [`Handler`]: struct.Handler.html - /// [`get`]: #method.get - /// [`leave`]: struct.Handler.html#method.leave - pub fn leave<G: Into<GuildId>>(&mut self, guild_id: G) { - if let Some(handler) = self.handlers.get_mut(&guild_id.into()) { - handler.leave(); - } - } - - /// Retrieves the [`Handler`] for the given target and leaves the associated - /// voice channel, if connected. - /// - /// The handler is then dropped, removing settings for the target. - /// - /// [`Handler`]: struct.Handler.html - pub fn remove<G: Into<GuildId>>(&mut self, guild_id: G) { - let guild_id = guild_id.into(); - - self.leave(guild_id); - - self.handlers.remove(&guild_id); - } -} diff --git a/src/ext/voice/mod.rs b/src/ext/voice/mod.rs deleted file mode 100644 index 94e3b40..0000000 --- a/src/ext/voice/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! A module for connecting to voice channels. - -mod audio; -mod connection; -mod connection_info; -mod error; -mod manager; -mod handler; -mod payload; -mod streamer; -mod threading; - -pub use self::audio::{AudioReceiver, AudioSource}; -pub use self::error::VoiceError; -pub use self::handler::Handler; -pub use self::manager::Manager; -pub use self::streamer::{ffmpeg, pcm, ytdl}; - -use self::connection_info::ConnectionInfo; - -const CRYPTO_MODE: &'static str = "xsalsa20_poly1305"; - -#[doc(hidden)] -pub enum Status { - Connect(ConnectionInfo), - Disconnect, - SetReceiver(Option<Box<AudioReceiver>>), - SetSender(Option<Box<AudioSource>>), -} diff --git a/src/ext/voice/payload.rs b/src/ext/voice/payload.rs deleted file mode 100644 index c2e7c0c..0000000 --- a/src/ext/voice/payload.rs +++ /dev/null @@ -1,50 +0,0 @@ -use serde_json::Value; -use super::connection_info::ConnectionInfo; -use ::constants::VoiceOpCode; - -#[inline] -pub fn build_identify(info: &ConnectionInfo) -> Value { - json!({ - "op": VoiceOpCode::Identify.num(), - "d": { - "server_id": info.guild_id.0, - "session_id": &info.session_id, - "token": &info.token, - "user_id": info.user_id.0, - } - }) -} - -#[inline] -pub fn build_keepalive() -> Value { - json!({ - "op": VoiceOpCode::KeepAlive.num(), - "d": Value::Null, - }) -} - -#[inline] -pub fn build_select_protocol(address: ::std::borrow::Cow<str>, port: u16) -> Value { - json!({ - "op": VoiceOpCode::SelectProtocol.num(), - "d": { - "protocol": "udp", - "data": { - "address": address, - "mode": super::CRYPTO_MODE, - "port": port, - } - } - }) -} - -#[inline] -pub fn build_speaking(speaking: bool) -> Value { - json!({ - "op": VoiceOpCode::Speaking.num(), - "d": { - "delay": 0, - "speaking": speaking, - } - }) -} diff --git a/src/ext/voice/streamer.rs b/src/ext/voice/streamer.rs deleted file mode 100644 index 4d3b9a9..0000000 --- a/src/ext/voice/streamer.rs +++ /dev/null @@ -1,152 +0,0 @@ -use byteorder::{LittleEndian, ReadBytesExt}; -use serde_json; -use std::ffi::OsStr; -use std::io::{ErrorKind as IoErrorKind, Read, Result as IoResult}; -use std::process::{Child, Command, Stdio}; -use super::{AudioSource, VoiceError}; -use ::internal::prelude::*; - -struct ChildContainer(Child); - -impl Read for ChildContainer { - fn read(&mut self, buffer: &mut [u8]) -> IoResult<usize> { - self.0.stdout.as_mut().unwrap().read(buffer) - } -} - -struct PcmSource<R: Read + Send + 'static>(bool, R); - -impl<R: Read + Send> AudioSource for PcmSource<R> { - fn is_stereo(&mut self) -> bool { - self.0 - } - - fn read_frame(&mut self, buffer: &mut [i16]) -> Option<usize> { - for (i, v) in buffer.iter_mut().enumerate() { - *v = match self.1.read_i16::<LittleEndian>() { - Ok(v) => v, - Err(ref e) => return if e.kind() == IoErrorKind::UnexpectedEof { - Some(i) - } else { - None - }, - } - } - - Some(buffer.len()) - } -} - -/// Opens an audio file through `ffmpeg` and creates an audio source. -pub fn ffmpeg<P: AsRef<OsStr>>(path: P) -> Result<Box<AudioSource>> { - let path = path.as_ref(); - - /// Will fail if the path is not to a file on the fs. Likely a YouTube URI. - let is_stereo = is_stereo(path).unwrap_or(false); - let stereo_val = if is_stereo { - "2" - } else { - "1" - }; - - let args = [ - "-f", - "s16le", - "-ac", - stereo_val, - "-ar", - "48000", - "-acodec", - "pcm_s16le", - "-", - ]; - - let command = Command::new("ffmpeg") - .arg("-i") - .arg(path) - .args(&args) - .stderr(Stdio::null()) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .spawn()?; - - Ok(pcm(is_stereo, ChildContainer(command))) -} - -/// Creates a PCM audio source. -pub fn pcm<R: Read + Send + 'static>(is_stereo: bool, reader: R) - -> Box<AudioSource> { - Box::new(PcmSource(is_stereo, reader)) -} - -/// Creates a streamed audio source with `youtube-dl` and `ffmpeg`. -pub fn ytdl(uri: &str) -> Result<Box<AudioSource>> { - let args = [ - "-f", - "webm[abr>0]/bestaudio/best", - "--no-playlist", - "--print-json", - "--skip-download", - uri, - ]; - - let out = Command::new("youtube-dl") - .args(&args) - .stdin(Stdio::null()) - .output()?; - - if !out.status.success() { - return Err(Error::Voice(VoiceError::YouTubeDLRun(out))); - } - - let value = serde_json::from_reader(&out.stdout[..])?; - let mut obj = match value { - Value::Object(obj) => obj, - other => return Err(Error::Voice(VoiceError::YouTubeDLProcessing(other))), - }; - - let uri = match obj.remove("url") { - Some(v) => match v { - Value::String(uri) => uri, - other => return Err(Error::Voice(VoiceError::YouTubeDLUrl(other))), - }, - None => return Err(Error::Voice(VoiceError::YouTubeDLUrl(Value::Object(obj)))), - }; - - ffmpeg(&uri) -} - -fn is_stereo(path: &OsStr) -> Result<bool> { - let args = [ - "-v", - "quiet", - "-of", - "json", - "-show-streams", - "-i", - ]; - - let out = Command::new("ffprobe") - .args(&args) - .arg(path) - .stdin(Stdio::null()) - .output()?; - - let value: Value = serde_json::from_reader(&out.stdout[..])?; - - let streams = value.as_object() - .and_then(|m| m.get("streams")) - .and_then(|v| v.as_array()) - .ok_or(Error::Voice(VoiceError::Streams))?; - - let check = streams.iter() - .any(|stream| { - let channels = stream.as_object() - .and_then(|m| m.get("channels") - .and_then(|v| v.as_i64())); - - channels == Some(2) - }); - - Ok(check) -} diff --git a/src/ext/voice/threading.rs b/src/ext/voice/threading.rs deleted file mode 100644 index 4777231..0000000 --- a/src/ext/voice/threading.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::sync::mpsc::{Receiver as MpscReceiver, TryRecvError}; -use std::thread::Builder as ThreadBuilder; -use super::connection::Connection; -use super::Status; -use ::internal::Timer; -use ::model::GuildId; - -pub fn start(guild_id: GuildId, rx: MpscReceiver<Status>) { - let name = format!("Serenity Voice (G{})", guild_id); - - ThreadBuilder::new() - .name(name) - .spawn(move || runner(&rx)) - .expect(&format!("[Voice] Error starting guild: {:?}", guild_id)); -} - -fn runner(rx: &MpscReceiver<Status>) { - let mut sender = None; - let mut receiver = None; - let mut connection = None; - let mut timer = Timer::new(20); - - 'runner: loop { - loop { - match rx.try_recv() { - Ok(Status::Connect(info)) => { - connection = match Connection::new(info) { - Ok(connection) => { - Some(connection) - }, - Err(why) => { - warn!("[Voice] Error connecting: {:?}", why); - - None - }, - }; - }, - Ok(Status::Disconnect) => { - connection = None; - }, - Ok(Status::SetReceiver(r)) => { - receiver = r; - }, - Ok(Status::SetSender(s)) => { - sender = s; - }, - Err(TryRecvError::Empty) => { - // If we receieved nothing, then we can perform an update. - break; - }, - Err(TryRecvError::Disconnected) => { - break 'runner; - }, - } - } - - // Overall here, check if there's an error. - // - // If there is a connection, try to send an update. This should not - // error. If there is though for some spurious reason, then set `error` - // to `true`. - // - // Otherwise, wait out the timer and do _not_ error and wait to receive - // another event. - let error = match connection.as_mut() { - Some(connection) => { - let cycle = connection.cycle(&mut sender, - &mut receiver, - &mut timer); - - match cycle { - Ok(()) => false, - Err(why) => { - error!("(╯°□°)╯︵ ┻━┻ Error updating connection: {:?}", - why); - - true - }, - } - }, - None => { - timer.await(); - - false - }, - }; - - // If there was an error, then just reset the connection and try to get - // another. - if error { - connection = None; - } - } -} |