diff options
| author | Austin Hellyer <[email protected]> | 2016-11-10 20:25:32 -0700 |
|---|---|---|
| committer | Austin Hellyer <[email protected]> | 2016-12-29 11:55:10 -0800 |
| commit | 0359f512a8aada5ae0371049eb7c66ecd8d68d84 (patch) | |
| tree | f88dd9b362a2d349d0cdcd13b0859c4cc3329f46 /src | |
| parent | Rework some event handles (diff) | |
| download | serenity-0359f512a8aada5ae0371049eb7c66ecd8d68d84.tar.xz serenity-0359f512a8aada5ae0371049eb7c66ecd8d68d84.zip | |
Add guild and channel search
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/context.rs | 83 | ||||
| -rw-r--r-- | src/client/error.rs | 3 | ||||
| -rw-r--r-- | src/client/rest/mod.rs | 69 | ||||
| -rw-r--r-- | src/client/rest/ratelimiting.rs | 2 | ||||
| -rw-r--r-- | src/constants.rs | 92 | ||||
| -rw-r--r-- | src/model/channel.rs | 117 | ||||
| -rw-r--r-- | src/model/guild.rs | 158 | ||||
| -rw-r--r-- | src/model/mod.rs | 24 | ||||
| -rw-r--r-- | src/model/utils.rs | 30 | ||||
| -rw-r--r-- | src/utils/builder/mod.rs | 2 | ||||
| -rw-r--r-- | src/utils/builder/search.rs | 379 |
11 files changed, 949 insertions, 10 deletions
diff --git a/src/client/context.rs b/src/client/context.rs index 8bbdfbe..d6d9ef9 100644 --- a/src/client/context.rs +++ b/src/client/context.rs @@ -16,7 +16,8 @@ use ::utils::builder::{ EditMember, EditProfile, EditRole, - GetMessages + GetMessages, + Search, }; use ::internal::prelude::*; use ::model::*; @@ -1444,6 +1445,86 @@ impl Context { } } + /// Searches a [`Channel`]'s messages by providing query parameters via the + /// search builder. + /// + /// Refer to the documentation for the [`Search`] builder for restrictions + /// and defaults parameters, as well as potentially advanced usage. + /// + /// **Note**: Bot users can not search. + /// + /// # Examples + /// + /// Refer to the [`Search`] builder's documentation for examples, + /// specifically the section on [searching a channel][search channel]. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Channel`]: ../model/enum.Channel.html + /// [`Search`]: ../utils/builder/struct.Search.html + /// [search channel]: ../../utils/builder/struct.Search.html#searching-a-channel + pub fn search_channel<C, F>(&self, channel_id: C, f: F) + -> Result<SearchResult> where C: Into<ChannelId>, + F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + let map = f(Search::default()).0; + + rest::search_channel_messages(channel_id.into().0, map) + } + + /// Searches a [`Guild`]'s messages by providing query parameters via the + /// search builder, with the ability to narrow down channels to search. + /// + /// Refer to the documentation for the [`Search`] builder for restrictions + /// and default parameters, as well as potentially advanced usage. + /// + /// **Note**: Bot users can not search. + /// + /// # Examples + /// + /// Refer to the [`Search`] builder's documentation for more examples, + /// specifically the section on + /// [searching a guild's channels][search guild]. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Guild`]: ../model/struct.Guild.html + /// [`Search`]: ../utils/builder/struct.Search.html + /// [search guild]: ../../utils/builder/struct.Search.html#searching-a-guilds-channels + pub fn search_guild<F, G>(&self, + guild_id: G, + channel_ids: Vec<ChannelId>, + f: F) + -> Result<SearchResult> + where F: FnOnce(Search) -> Search, + G: Into<GuildId> { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + let map = f(Search::default()).0; + let ids = channel_ids.iter().map(|ch| ch.0).collect::<Vec<u64>>(); + + rest::search_guild_messages(guild_id.into().0, &ids, map) + } + /// Sends a file along with optional message contents. The filename _must_ /// be specified. /// diff --git a/src/client/error.rs b/src/client/error.rs index 0ed3bd8..14818d9 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -1,4 +1,5 @@ use hyper::status::StatusCode; +use ::constants::ErrorCode; use ::model::{ChannelType, Permissions}; /// An error returned from the [`Client`] or the [`Context`], or model instance. @@ -46,6 +47,7 @@ use ::model::{ChannelType, Permissions}; /// [`Context`]: struct.Context.html /// [`Context::ban`]: struct.Context.html#method.ban /// [`Error::Client`]: ../enum.Error.html#variant.Client +#[allow(enum_variant_names)] #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum Error { /// When attempting to delete below or above the minimum and maximum allowed @@ -54,6 +56,7 @@ pub enum Error { /// When attempting to delete a number of days' worth of messages that is /// not allowed. DeleteMessageDaysAmount(u8), + ErrorCode(ErrorCode), /// When there was an error retrieving the gateway URI from the REST API. Gateway, /// An indication that a [guild][`Guild`] could not be found by diff --git a/src/client/rest/mod.rs b/src/client/rest/mod.rs index 5a03f37..1af067d 100644 --- a/src/client/rest/mod.rs +++ b/src/client/rest/mod.rs @@ -44,7 +44,7 @@ use std::default::Default; use std::fmt::Write as FmtWrite; use std::io::{ErrorKind as IoErrorKind, Read}; use std::sync::{Arc, Mutex}; -use ::constants; +use ::constants::{self, ErrorCode}; use ::internal::prelude::*; use ::model::*; use ::utils::decode_array; @@ -1429,6 +1429,73 @@ pub fn remove_group_recipient(group_id: u64, user_id: u64) user_id)) } +/// Searches a [`Channel`] for [`Message`]s that meet provided requirements. +/// +/// **Note**: Bot users can not search. +/// +/// [`Channel`]: ../../model/enum.Channel.html +/// [`Message`]: ../../model/struct.Message.html +pub fn search_channel_messages(channel_id: u64, map: BTreeMap<&str, String>) + -> Result<SearchResult> { + let mut uri = format!("/channels/{}/messages/search?", channel_id); + + for (k, v) in map { + uri.push('&'); + uri.push_str(k); + uri.push('='); + uri.push_str(&v); + } + + let response = request!(Route::ChannelsIdMessagesSearch(channel_id), + get, + "{}", + uri); + + if response.status == StatusCode::Accepted { + return Err(Error::Client(ClientError::ErrorCode(ErrorCode::SearchIndexUnavailable))); + } + + let content = try!(serde_json::from_reader(response)); + + SearchResult::decode(content) +} + +/// Searches a [`Guild`] - and optionally specific [channel][`GuildChannel`]s +/// within it - for messages that meet provided requirements. +/// +/// **Note**: Bot users can not search. +/// +/// [`Guild`]: ../../model/struct.Guild.html +/// [`GuildChannel`]: ../../model/struct.GuildChannel.html +pub fn search_guild_messages(guild_id: u64, + channel_ids: &[u64], + map: BTreeMap<&str, String>) + -> Result<SearchResult> { + let mut uri = format!("/guilds/{}/messages/search?", guild_id); + + for (k, v) in map { + uri.push('&'); + uri.push_str(k); + uri.push('='); + uri.push_str(&v); + } + + for channel_id in channel_ids { + write!(uri, "&channel_id={}", channel_id)?; + } + + let response = request!(Route::GuildsIdMessagesSearch(guild_id), + get, + "{}", + uri); + + if response.status == StatusCode::Accepted { + return Err(Error::Client(ClientError::ErrorCode(ErrorCode::SearchIndexUnavailable))); + } + + SearchResult::decode(try!(serde_json::from_reader(response))) +} + /// Sends a file to a channel. pub fn send_file<R: Read>(channel_id: u64, mut file: R, diff --git a/src/client/rest/ratelimiting.rs b/src/client/rest/ratelimiting.rs index ac085eb..a404310 100644 --- a/src/client/rest/ratelimiting.rs +++ b/src/client/rest/ratelimiting.rs @@ -64,6 +64,7 @@ pub enum Route { ChannelsIdMessagesIdAck(u64), ChannelsIdMessagesIdReactions(u64), ChannelsIdMessagesIdReactionsUserIdType(u64), + ChannelsIdMessagesSearch(u64), ChannelsIdPermissionsOverwriteId(u64), ChannelsIdPins(u64), ChannelsIdPinsMessageId(u64), @@ -87,6 +88,7 @@ pub enum Route { GuildsIdMembersId(u64), GuildsIdMembersIdRolesId(u64), GuildsIdMembersMeNick(u64), + GuildsIdMessagesSearch(u64), GuildsIdPrune(u64), GuildsIdRegions(u64), GuildsIdRoles(u64), diff --git a/src/constants.rs b/src/constants.rs index 50dd722..bb2589a 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -8,6 +8,98 @@ pub const MESSAGE_CODE_LIMIT: u16 = 2000; /// [UserAgent]: ../hyper/header/struct.UserAgent.html pub const USER_AGENT: &'static str = concat!("DiscordBot (https://github.com/zeyla/serenity.rs, ", env!("CARGO_PKG_VERSION"), ")"); +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ErrorCode { + BotsCannotUse, + CannotSendEmptyMessage, + CannotSendMessagesInVoice, + CannotSendMessagesToUser, + ChannelVerificationTooHigh, + EditByOtherAuthor, + EmbedDisabled, + InvalidAccountType, + InvalidAuthToken, + InvalidBulkDeleteCount, + InvalidDMChannelAction, + InvalidOauthState, + InvalidPinChannel, + MaxFriendsReached, + MaxGuildsReached, + MaxPinsReached, + MaxRolesReached, + MissingAccess, + MissingPermissions, + NoteTooLong, + Oauth2ApplicationLacksBot, + Oauth2ApplicationLimitReached, + OnlyBotsCanUse, + ReactionBlocked, + SearchIndexUnavailable, + TooManyReactions, + Unauthorized, + UnknownAccount, + UnknownApplication, + UnknownChannel, + UnknownGuild, + UnknownEmoji, + UnknownIntegration, + UnknownInvite, + UnknownMember, + UnknownMessage, + UnknownOverwrite, + UnknownProvider, + UnknownRole, + UnknownToken, + UnknownUser, +} + +/* +map_nums! { ErrorCode; + BotsCannotUse 20001, + CannotSendEmptyMessage 50006, + CannotSendMessagesInVoice 50008, + CannotSendMessagesToUser 50007, + ChannelVerificationTooHigh 50009, + EditByOtherAuthor 50005, + EmbedDisabled 50004, + InvalidAccountType 50002, + InvalidAuthToken 50014, + InvalidBulkDeleteCount 50016, + InvalidDMChannelAction 50003, + InvalidOauthState 50012, + InvalidPinChannel 50019, + MaxFriendsReached 30002, + MaxGuildsReached 30001, + MaxPinsReached 30003, + MaxRolesReached 30005, + MissingAccess 50001, + MissingPermissions 500013, + NoteTooLong 50015, + Oauth2ApplicationLacksBot 50010, + Oauth2ApplicationLimitReached 50011, + OnlyBotsCanUse 20002, + ReactionBlocked 90001, + SearchIndexUnavailable 110000, + TooManyReactions 30010, + Unauthorized 40001, + UnknownAccount 10001, + UnknownApplication 10002, + UnknownChannel 10003, + UnknownEmoji 10014, + UnknownGuild 10004, + UnknownIntegration 10005, + UnknownInvite 10006, + UnknownMember 10007, + UnknownMessage 10008, + UnknownOverwrite 10009, + UnknownProvider 10010, + UnknownRole 10011, + UnknownToken 10012, + UnknownUser 10013, +} +*/ + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum OpCode { Event, diff --git a/src/model/channel.rs b/src/model/channel.rs index c56ebbe..097587e 100644 --- a/src/model/channel.rs +++ b/src/model/channel.rs @@ -27,14 +27,14 @@ use std::path::{Path, PathBuf}; #[cfg(all(feature="cache", feature="methods"))] use super::utils; -#[cfg(feature="methods")] -use ::utils::builder::{CreateEmbed, CreateInvite, EditChannel}; #[cfg(all(feature="cache", feature="methods"))] use ::client::CACHE; #[cfg(all(feature="methods"))] use ::client::rest; #[cfg(all(feature="cache", feature="methods"))] use ::ext::cache::ChannelRef; +#[cfg(feature = "methods")] +use ::utils::builder::{CreateEmbed, CreateInvite, EditChannel, Search}; impl Attachment { /// If this attachment is an image, then a tuple of the width and height @@ -243,6 +243,41 @@ impl Channel { Channel::Private(ref channel) => channel.id, } } + + /// Performs a search request to the API for the inner channel's + /// [`Message`]s. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + #[cfg(feature = "methods")] + pub fn search<F>(&self, f: F) -> Result<SearchResult> + where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + let id = match *self { + Channel::Group(ref group) => group.channel_id.0, + Channel::Guild(ref channel) => channel.id.0, + Channel::Private(ref channel) => channel.id.0, + }; + + rest::search_channel_messages(id, f(Search::default()).0) + } } impl fmt::Display for Channel { @@ -400,6 +435,28 @@ impl Group { rest::remove_group_recipient(self.channel_id.0, user.0) } + /// Performs a search request to the API for the group's channel's + /// [`Message`]s. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + #[cfg(feature = "methods")] + pub fn search<F>(&self, f: F) -> Result<SearchResult> + where F: FnOnce(Search) -> Search { + rest::search_channel_messages(self.channel_id.0, f(Search::default()).0) + } + /// Sends a message to the group with the given content. /// /// Note that an @everyone mention will not be applied. @@ -822,6 +879,34 @@ impl PrivateChannel { rest::get_pins(self.id.0) } + /// Performs a search request to the API for the channel's [`Message`]s. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + #[cfg(feature = "methods")] + pub fn search<F>(&self, f: F) -> Result<SearchResult> + where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + rest::search_channel_messages(self.id.0, f(Search::default()).0) + } + /// Sends a message to the channel with the given content. /// /// **Note**: This will only work when a [`Message`] is received. @@ -1004,6 +1089,34 @@ impl GuildChannel { rest::get_pins(self.id.0) } + /// Performs a search request for the channel's [`Message`]s. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + #[cfg(feature = "methods")] + pub fn search<F>(&self, f: F) -> Result<SearchResult> + where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + rest::search_channel_messages(self.id.0, f(Search::default()).0) + } + /// Sends a message to the channel with the given content. /// /// **Note**: This will only work when a [`Message`] is received. diff --git a/src/model/guild.rs b/src/model/guild.rs index ec492d3..763c610 100644 --- a/src/model/guild.rs +++ b/src/model/guild.rs @@ -20,10 +20,8 @@ use ::utils::decode_array; use serde_json::builder::ObjectBuilder; #[cfg(all(feature="cache", feature = "methods"))] use std::mem; -#[cfg(feature = "methods")] -use ::utils::builder::{EditGuild, EditRole}; #[cfg(all(feature="cache", feature="methods"))] -use ::utils::builder::EditMember; +use ::utils::builder::{EditGuild, EditMember, EditRole, Search}; #[cfg(feature = "methods")] use ::client::rest; @@ -179,6 +177,83 @@ impl PartialGuild { format!(cdn!("/splashes/{}/{}.jpg"), self.id, icon)) } + /// Performs a search request to the API for the guild's [`Message`]s. + /// + /// This will search all of the guild's [`Channel`]s at once, that you have + /// the [Read Message History] permission to. Use [`search_channels`] to + /// specify a list of [channel][`GuildChannel`]s to search, where all other + /// channels will be excluded. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Channel`]: enum.Channel.html + /// [`GuildChannel`]: struct.GuildChannel.html + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + /// [`search_channels`]: #method.search_channels + /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html + #[cfg(feature = "methods")] + pub fn search<F>(&self, f: F) -> Result<SearchResult> + where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + rest::search_guild_messages(self.id.0, &[], f(Search::default()).0) + } + + /// Performs a search request to the API for the guild's [`Message`]s in + /// given channels. + /// + /// This will search all of the messages in the guild's provided + /// [`Channel`]s by Id that you have the [Read Message History] permission + /// to. Use [`search`] to search all of a guild's [channel][`GuildChannel`]s + /// at once. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Channel`]: enum.Channel.html + /// [`GuildChannel`]: struct.GuildChannel.html + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + /// [`search`]: #method.search + /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html + #[cfg(feature = "methods")] + pub fn search_channels<F>(&self, channel_ids: &[ChannelId], f: F) + -> Result<SearchResult> where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + let ids = channel_ids.iter().map(|x| x.0).collect::<Vec<u64>>(); + + rest::search_guild_messages(self.id.0, &ids, f(Search::default()).0) + } + /// Retrieves the guild's webhooks. /// /// **Note**: Requires the [Manage Webhooks] permission. @@ -756,6 +831,83 @@ impl Guild { format!(cdn!("/splashes/{}/{}.jpg"), self.id, icon)) } + /// Performs a search request to the API for the guild's [`Message`]s. + /// + /// This will search all of the guild's [`Channel`]s at once, that you have + /// the [Read Message History] permission to. Use [`search_channels`] to + /// specify a list of [channel][`GuildChannel`]s to search, where all other + /// channels will be excluded. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Channel`]: enum.Channel.html + /// [`GuildChannel`]: struct.GuildChannel.html + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + /// [`search_channels`]: #method.search_channels + /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html + #[cfg(feature = "methods")] + pub fn search<F>(&self, f: F) -> Result<SearchResult> + where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + rest::search_guild_messages(self.id.0, &[], f(Search::default()).0) + } + + /// Performs a search request to the API for the guild's [`Message`]s in + /// given channels. + /// + /// This will search all of the messages in the guild's provided + /// [`Channel`]s by Id that you have the [Read Message History] permission + /// to. Use [`search`] to search all of a guild's [channel][`GuildChannel`]s + /// at once. + /// + /// Refer to the documentation for the [`Search`] builder for examples and + /// more information. + /// + /// **Note**: Bot users can not search. + /// + /// # Errors + /// + /// If the `cache` is enabled, returns a + /// [`ClientError::InvalidOperationAsBot`] if the current user is a bot. + /// + /// [`ClientError::InvalidOperationAsBot`]: ../client/enum.ClientError.html#variant.InvalidOperationAsBot + /// [`Channel`]: enum.Channel.html + /// [`GuildChannel`]: struct.GuildChannel.html + /// [`Message`]: struct.Message.html + /// [`Search`]: ../utils/builder/struct.Search.html + /// [`search`]: #method.search + /// [Read Message History]: permissions/constant.READ_MESSAGE_HISTORY.html + #[cfg(feature = "methods")] + pub fn search_channels<F>(&self, channel_ids: &[ChannelId], f: F) + -> Result<SearchResult> where F: FnOnce(Search) -> Search { + #[cfg(feature="cache")] + { + if CACHE.read().unwrap().user.bot { + return Err(Error::Client(ClientError::InvalidOperationAsBot)); + } + } + + let ids = channel_ids.iter().map(|x| x.0).collect::<Vec<u64>>(); + + rest::search_guild_messages(self.id.0, &ids, f(Search::default()).0) + } + /// Starts a prune of [`Member`]s. /// /// See the documentation on [`GuildPrune`] for more information. diff --git a/src/model/mod.rs b/src/model/mod.rs index 70fdd75..5aad290 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -128,7 +128,7 @@ id! { } /// A container for any channel. -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub enum Channel { /// A group. A group comprises of only one channel. Group(Group), @@ -159,7 +159,7 @@ pub enum GuildContainer { /// This is for use with methods such as `Context::create_permission`. /// /// [`Context::create_permission`]: ../client/ -#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum PermissionOverwriteType { /// A member which is having its permission overwrites edited. Member(UserId), @@ -168,7 +168,7 @@ pub enum PermissionOverwriteType { } /// A guild which may or may not currently be available. -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub enum PossibleGuild<T> { /// An indicator that a guild is currently unavailable for at least one of /// a variety of reasons. @@ -176,3 +176,21 @@ pub enum PossibleGuild<T> { /// An indicator that a guild is currently available. Online(T), } + +#[derive(Copy, Clone, Debug)] +pub enum SearchTarget { + Channel(ChannelId), + Guild(GuildId), +} + +impl From<ChannelId> for SearchTarget { + fn from(channel_id: ChannelId) -> SearchTarget { + SearchTarget::Channel(channel_id) + } +} + +impl From<GuildId> for SearchTarget { + fn from(guild_id: GuildId) -> SearchTarget { + SearchTarget::Guild(guild_id) + } +} diff --git a/src/model/utils.rs b/src/model/utils.rs index 897eb62..e499fd2 100644 --- a/src/model/utils.rs +++ b/src/model/utils.rs @@ -5,6 +5,7 @@ use super::{ Emoji, EmojiId, Member, + Message, Presence, ReadState, Relationship, @@ -173,6 +174,35 @@ pub fn decode_roles(value: Value) -> Result<HashMap<RoleId, Role>> { Ok(roles) } +pub fn decode_search_results(value: Value) -> Result<Vec<Vec<Message>>> { + let array = match value { + Value::Array(v) => v, + value => return Err(Error::Decode("Expected message set array", value)), + }; + + let mut sets: Vec<Vec<Message>> = vec![]; + + for arr in array { + let arr = match arr { + Value::Array(v) => v, + value => return Err(Error::Decode("Expected message set array", value)), + }; + + let mut messages: Vec<Message> = vec![]; + + for item in arr { + messages.push(match item { + Value::Object(v) => try!(Message::decode(Value::Object(v))), + value => return Err(Error::Decode("Expected search message", value)), + }); + } + + sets.push(messages); + } + + Ok(sets) +} + pub fn decode_shards(value: Value) -> Result<[u64; 2]> { let array = into_array(value)?; diff --git a/src/utils/builder/mod.rs b/src/utils/builder/mod.rs index 3fc0f05..9ded8b8 100644 --- a/src/utils/builder/mod.rs +++ b/src/utils/builder/mod.rs @@ -15,6 +15,7 @@ mod edit_profile; mod edit_role; mod execute_webhook; mod get_messages; +mod search; pub use self::create_embed::{ CreateEmbed, @@ -31,3 +32,4 @@ pub use self::edit_profile::EditProfile; pub use self::edit_role::EditRole; pub use self::execute_webhook::ExecuteWebhook; pub use self::get_messages::GetMessages; +pub use self::search::{Search, SortingMode, SortingOrder}; diff --git a/src/utils/builder/search.rs b/src/utils/builder/search.rs new file mode 100644 index 0000000..1aadec4 --- /dev/null +++ b/src/utils/builder/search.rs @@ -0,0 +1,379 @@ +use std::collections::BTreeMap; +use ::model::{MessageId, UserId}; + +/// An indicator of the type of sorting mode to use when searching for +/// [`Message`]s via the [`Search`] builder. +/// +/// [`Message`]: ../../model/struct.Message.html +/// [`Search`]: struct.Search.html +pub enum SortingMode { + /// Search by messages' relevance to parameters. + Relevance, + /// Search by messages' timestamp, where results will match according to + /// parameters. This is used in conjunction in the [`Search::sort_order`] + /// method, and is used in conjunction with [`SortingOrder`]. + /// + /// [`Search::sort_order`]: struct.Search.html#method.sort_order + /// [`SortingOrder`]: enum.SortingOrder.html + Timestamp, +} + +impl SortingMode { + /// Retrieves the name of the sorting mode. This is equivilant to a + /// lowercase string version of each variant. + pub fn name(&self) -> &str { + match *self { + SortingMode::Relevance => "relevance", + SortingMode::Timestamp => "timestamp", + } + } +} + +/// An indicator of how to sort results when searching for [`Message`]s via the +/// [`Search`] builder. +/// +/// [`Message`]: ../../model/struct.Message.html +/// [`Search`]: struct.Search.html +pub enum SortingOrder { + /// Search message results in ascending order. + /// + /// In the case of [`SortingMode::Relevance`], this will search from the + /// least relevant to the most relevant. + /// + /// In the case of [`SortingMode::Timestamp`], this will indicate to search + /// from the least recent to the most recent. + /// + /// [`SortingMode::Relevance`]: enum.SortingMode.html#variant.Relevance + /// [`SortingMode::Timestamp`]: enum.SortingMode.html#variant.Timestamp + Ascending, + /// Search message results in descending order. + /// + /// In the case of [`SortingMode::Relevance`], this will search from the + /// most relevant to least relevant. + /// + /// In the case of [`SortingMode::Timestamp`], this will search from the + /// most recent to least recent. + /// + /// [`SortingMode::Relevance`]: enum.SortingMode.html#variant.Relevance + /// [`SortingMode::Timestamp`]: enum.SortingMode.html#variant.Timestamp + Descending, +} + +impl SortingOrder { + /// Retrieves the name of the sorting order. This is equivilant to a + /// lowercase string version of each variant. + pub fn name(&self) -> &str { + match *self { + SortingOrder::Ascending => "asc", + SortingOrder::Descending => "desc", + } + } +} + +/// A builder used to query a [`Channel`] or [`Guild`] for its [`Message`]s, +/// specifying certain parameters to narrow down the returned messages. +/// +/// Many methods are provided to narrow down the results, such as [`sort_by`] - +/// which is used with the [`SortingMode`] enum to sort the results - or +/// [`limit`], which can be used in conjunction with [`offset`] to paginate +/// results. +/// +/// # Examples +/// +/// Provided are multiple in-depth examples for searching through different +/// means. Also see [example 08] for a fully runnable bot. +/// +/// ### Searching a Channel +/// +/// Search for messages via [`Context::search_channel`] with the content +/// `"rust"`, which have no embed, no attachment, searching by relevance in +/// ascending order, and limiting to 5 results: +/// +/// ```rust,ignore +/// // assuming you are in a context +/// +/// let res = context.search_channel(message.channel_id, |s| s +/// .content("rust") +/// .has_embed(false) +/// .has_attachment(false) +/// .limit(5) +/// .sort_by(SortingMode::Relevance) +/// .sort_order(SortingOrder::Ascending)); +/// ``` +/// +/// ### Searching a Guild's Channels +/// +/// Search for messages with a query provided by a user, which have an +/// embed, have no attachment, searching by timestamp in descending order, +/// limiting to 2 results, and only searching channels that have a name +/// prefixed with `"search-"`: +/// +/// ```rust,no_run +/// use serenity::client::{Client, Context}; +/// use serenity::model::Message; +/// use serenity::utils::builder::{SortingMode, SortingOrder}; +/// use std::env; +/// +/// let client = Client::login_bot(&env::var("DISCORD_BOT_TOKEN").unwrap()); +/// +/// client.with_framework(|f| f +/// .configure(|c| c.prefix("~").on_mention(true)) +/// .on("search", search)); +/// +/// fn search(context: Context, message: Message, args: Vec<String>) { +/// let query = args.join(" "); +/// +/// if query.is_empty() { +/// let _ = context.say("You must provide a query"); +/// +/// return; +/// } +/// +/// let guild = match message.guild().unwrap(); +/// +/// let channel_ids = guild.iter() +/// .channels +/// .values() +/// .filter(|c| c.name.starts_with("search-")) +/// .map(|c| c.id) +/// .collect(); +/// +/// let search = context.search_guild(guild.id, channel_ids, |s| s +/// .content(&query) +/// .context_size(0) +/// .has_attachment(true) +/// .has_embed(true) +/// .max_id(message.id.0 - 1) +/// .sort_by(SortingMode::Timestamp) +/// .sort_order(SortingOrder::Descending)); +/// +/// let messages = match search { +/// Ok(messages) => messages, +/// Err(why) => { +/// println!("Error performing search '{}': {:?}", query, why); +/// +/// let _ = context.say("Error occurred while searching"); +/// +/// return; +/// }, +/// }; +/// +/// let _ = context.send_message(message.channel_id, |m| m +/// .content(&format!("Found {} total results", messages.total)) +/// .embed(|e| { +/// for (i, messages) in messages.results.iter().enumerate() { +/// let mut message = messages[0]; +/// message.content.truncate(1000); +/// +/// e.field(|f| f +/// .name(&format!("Result {}", i)) +/// .value(&message.content)); +/// } +/// +/// e +/// })); +/// } +/// ``` +/// +/// [`Channel`]: ../../model/enum.Channel.html +/// [`Context::search_channel`]: ../../client/struct.Context.html#method.search_channel +/// [`Guild`]: ../../model/struct.Guild.html +/// [`Message`]: ../../model/struct.Message.html +/// [`SortingMode`]: enum.SortingMode.html +/// [`limit`]: #method.limit +/// [`offset`]: #method.offset +/// [`sort_by`]: #method.sort_by +/// [example 08]: https://github.com/zeyla/serenity.rs/tree/master/examples/08_search +pub struct Search<'a>(pub BTreeMap<&'a str, String>); + +impl<'a> Search<'a> { + /// Sets the list of attachment extensions to search by. + /// + /// When providing a vector of extensions, do _not_ include the period (`.`) + /// character as part of the search. + /// + /// This is sent to Discord as a comma-separated value list of extension + /// names. + pub fn attachment_extensions(mut self, attachment_extensions: &[&str]) -> Self { + let list = attachment_extensions.join(" "); + + self.0.insert("attachment_extensions", list); + + self + } + + /// Sets the filename of the attachments to search for. + pub fn attachment_filename(mut self, attachment_filename: &str) -> Self { + self.0.insert("attachment_filename", attachment_filename.to_owned()); + + self + } + + /// Sets the Id of the author of [`Message`]s to search for. This excludes + /// all messages by other [`User`]s. + /// + /// [`Message`]: ../../model/struct.Message.html + /// [`User`]: ../../model/struct.User.html + pub fn author_id<U: Into<UserId>>(mut self, author_id: U) -> Self { + self.0.insert("author_id", author_id.into().0.to_string()); + + self + } + + /// Sets the content of the [`Message`] to search for. This is a fuzzy + /// search, and can partially match the given query content. + /// + /// [`Message`]: ../../model/struct.Message.html + pub fn content(mut self, content: &str) -> Self { + self.0.insert("content", content.to_owned()); + + self + } + + /// Sets the amount of "context" [`Message`]s to provide, at maximum. This + /// is the number of messages to provide around each side + /// (ascending+descending) of the "hit" (aka found) message. + /// + /// The default value is `2`. The minimum value is `0`. The maximum value is + /// `2`. + pub fn context_size(mut self, context_size: u8) -> Self { + self.0.insert("context_size", context_size.to_string()); + + self + } + + /// Sets the embed providers to search by. + /// + /// This is a list of the providers' names. + /// + /// This is sent to Discord as a comma-separated value list of provider + /// names. + pub fn embed_providers(mut self, embed_providers: &[&str]) -> Self { + self.0.insert("embed_providers", embed_providers.join(" ")); + + self + } + + /// Sets the type of [`Embed`]s to search by. + /// + /// An example of an [embed type][`Embed::kind`] is `"rich"`. + /// + /// [`Embed`]: ../../model/struct.Embed.html + /// [`Embed::kind`]: ../../model/struct.Embed.html#structfield.kind + pub fn embed_types(mut self, embed_types: &[&str]) -> Self { + self.0.insert("embed_types", embed_types.join(" ")); + + self + } + + /// Sets whether to search for methods that do - or do not - have an + /// attachment. + /// + /// Do not specify to search for both. + pub fn has_attachment(mut self, has_attachment: bool) -> Self { + self.0.insert("has_attachment", has_attachment.to_string()); + + self + } + + /// Sets whether to search for methods that do - or do not - have an embed. + /// + /// Do not specify to search for both. + pub fn has_embed(mut self, has_embed: bool) -> Self { + self.0.insert("has_embed", has_embed.to_string()); + + self + } + + /// Sets the number of messages to retrieve _at maximum_. This can be used + /// in conjunction with [`offset`]. + /// + /// The minimum value is `1`. The maximum value is `25`. + /// + /// [`offset`]: #method.offset + pub fn limit(mut self, limit: u8) -> Self { + self.0.insert("limit", limit.to_string()); + + self + } + + /// Set the maximum [`Message`] Id to search up to. All messages with an Id + /// greater than the given value will be ignored. + /// + /// [`Message`]: ../../model/struct.Message.html + pub fn max_id<M: Into<MessageId>>(mut self, message_id: M) -> Self { + self.0.insert("max_id", message_id.into().0.to_string()); + + self + } + + /// Set the minimum [`Message`]s Id to search down to. All messages with an + /// Id less than the given value will be ignored. + /// + /// [`Message`]: ../../model/struct.Message.html + pub fn min_id<M: Into<MessageId>>(mut self, message_id: M) -> Self { + self.0.insert("min_id", message_id.into().0.to_string()); + + self + } + + /// Set the offset of [`Message`]s to return. This can be used in + /// conjunction with [`limit`]. + /// + /// The minimum value is `0`. The maximum value is `5000`. + /// + /// [`Message`]: ../../model/struct.Message.html + /// [`limit`]: fn.limit.html + pub fn offset(mut self, offset: u16) -> Self { + self.0.insert("offset", offset.to_string()); + + self + } + + /// The sorting mode to use. + /// + /// Refer to [`SortingMode`] for more information. + /// + /// [`SortingMode`]: enum.SortingMode.html + pub fn sort_by(mut self, sorting_mode: SortingMode) -> Self { + self.0.insert("sort_by", sorting_mode.name().to_string()); + + self + } + + /// The order to sort results by. + /// + /// Refer to the documentation for [`SortingOrder`] for more information. + /// + /// [`SortingOrder`]: enum.SortingOrder.html + pub fn sort_order(mut self, sorting_order: SortingOrder) -> Self { + self.0.insert("sort_order", sorting_order.name().to_string()); + + self + } +} + +impl<'a> Default for Search<'a> { + /// Creates a new builder for searching for [`Message`]s. Refer to each + /// method to learn what minimum and maximum values are available for each + /// field, as well as restrictions and other useful information. + /// + /// The library does not provide defaults differently than what Discord + /// itself defaults to. + /// + /// This list of defaults is: + /// + /// - [`context_size`]: 2 + /// - [`limit`]: 25 + /// - [`offset`]: 0 + /// - [`sort_by`]: [`SortingMode::Timestamp`] + /// + /// [`SortingMode::Timestamp`]: enum.SortingMode.html#variant.Timestamp + /// [`context_size`]: #method.context_size + /// [`limit`]: #method.limit + /// [`offset`]: #method.offset + /// [`sort_by`]: #method.sort_by + fn default<'b>() -> Search<'b> { + Search(BTreeMap::default()) + } +} |