aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAustin Hellyer <[email protected]>2016-11-10 20:25:32 -0700
committerAustin Hellyer <[email protected]>2016-12-29 11:55:10 -0800
commit0359f512a8aada5ae0371049eb7c66ecd8d68d84 (patch)
treef88dd9b362a2d349d0cdcd13b0859c4cc3329f46 /src
parentRework some event handles (diff)
downloadserenity-0359f512a8aada5ae0371049eb7c66ecd8d68d84.tar.xz
serenity-0359f512a8aada5ae0371049eb7c66ecd8d68d84.zip
Add guild and channel search
Diffstat (limited to 'src')
-rw-r--r--src/client/context.rs83
-rw-r--r--src/client/error.rs3
-rw-r--r--src/client/rest/mod.rs69
-rw-r--r--src/client/rest/ratelimiting.rs2
-rw-r--r--src/constants.rs92
-rw-r--r--src/model/channel.rs117
-rw-r--r--src/model/guild.rs158
-rw-r--r--src/model/mod.rs24
-rw-r--r--src/model/utils.rs30
-rw-r--r--src/utils/builder/mod.rs2
-rw-r--r--src/utils/builder/search.rs379
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())
+ }
+}