diff options
| author | Zeyla Hellyer <[email protected]> | 2018-05-28 16:34:38 -0700 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2018-05-28 16:34:38 -0700 |
| commit | 6b5f3b98084b86b00e3f7e78b5eb9512e75e78a0 (patch) | |
| tree | 4011d56b63d88999eb8169e332c54f3eafe972ae /src/http | |
| parent | Make Message Builder use &mut self instead of self (diff) | |
| parent | Futures shard manager #298 (WIP) (#300) (diff) | |
| download | serenity-6b5f3b98084b86b00e3f7e78b5eb9512e75e78a0.tar.xz serenity-6b5f3b98084b86b00e3f7e78b5eb9512e75e78a0.zip | |
Merge branch 'futures' into v0.6.x
Diffstat (limited to 'src/http')
| -rw-r--r-- | src/http/constants.rs | 13 | ||||
| -rw-r--r-- | src/http/error.rs | 127 | ||||
| -rw-r--r-- | src/http/macros.rs | 23 | ||||
| -rw-r--r-- | src/http/mod.rs | 3154 | ||||
| -rw-r--r-- | src/http/ratelimiting.rs | 591 | ||||
| -rw-r--r-- | src/http/routing.rs | 1430 | ||||
| -rw-r--r-- | src/http/utils.rs | 12 |
7 files changed, 3295 insertions, 2055 deletions
diff --git a/src/http/constants.rs b/src/http/constants.rs new file mode 100644 index 0000000..ad08754 --- /dev/null +++ b/src/http/constants.rs @@ -0,0 +1,13 @@ +//! A set of constants denoting the URIs that the lib uses and a constant +//! representing the version in use. + +/// The base URI to the REST API. +pub const API_URI_BASE: &str = "https://discordapp.com/api"; +/// The versioned URI to the REST API. +pub const API_URI_VERSIONED: &str = "https://discordapp.com/api/v6"; +/// The status page base URI. +pub const STATUS_URI_BASE: &str = "https://status.discordapp.com/api"; +/// The versioned URI to the status page. +pub const STATUS_URI_VERSIONED: &str = "https://status.discordapp.com/api/v2"; +/// The API version that the library supports and uses. +pub const VERSION: u8 = 6; diff --git a/src/http/error.rs b/src/http/error.rs index 2a5adeb..f185675 100644 --- a/src/http/error.rs +++ b/src/http/error.rs @@ -1,23 +1,51 @@ -use hyper::client::Response; +use futures::Canceled; +use hyper::{ + error::{Error as HyperError, UriError}, + Response, +}; +use native_tls::Error as TlsError; +use serde_json::Error as JsonError; use std::{ + cell::BorrowMutError, error::Error as StdError, - fmt::{ - Display, - Formatter, - Result as FmtResult - } + fmt::{Display, Error as FmtError, Formatter, Result as FmtResult}, + io::Error as IoError, + result::Result as StdResult, }; +use super::ratelimiting::RateLimitError; +use tokio_timer::TimerError; + +pub type Result<T> = StdResult<T, Error>; #[derive(Debug)] pub enum Error { - /// When a non-successful status code was received for a request. - UnsuccessfulRequest(Response), - /// When the decoding of a ratelimit header could not be properly decoded - /// into an `i64`. - RateLimitI64, - /// When the decoding of a ratelimit header could not be properly decoded - /// from UTF-8. - RateLimitUtf8, + /// There was an error mutably borrowing an `std::cell::RefCell`. + BorrowMut(BorrowMutError), + /// A future was canceled. + /// + /// This most likely occurred during a pre-emptive ratelimit. + Canceled(Canceled), + /// An error from the `std::fmt` module. + Format(FmtError), + /// An error from the `hyper` crate. + Hyper(HyperError), + /// When a status code was unexpectedly received for a request's status. + InvalidRequest(Response), + /// An error from the `std::io` module. + Io(IoError), + /// An error from the `serde_json` crate. + Json(JsonError), + /// An error from the `ratelimiting` module. + RateLimit(RateLimitError), + /// An error occurred while creating a timer. + Timer(TimerError), + /// An error from the `native_tls` crate. + Tls(TlsError), + /// When a status is received, but the verification to ensure the response + /// is valid does not recognize the status. + UnknownStatus(u16), + /// A `hyper` error while parsing a Uri. + Uri(UriError), } impl Display for Error { @@ -27,11 +55,72 @@ impl Display for Error { impl StdError for Error { fn description(&self) -> &str { match *self { - Error::UnsuccessfulRequest(_) => { - "A non-successful response status code was received" - }, - Error::RateLimitI64 => "Error decoding a header into an i64", - Error::RateLimitUtf8 => "Error decoding a header from UTF-8", + Error::BorrowMut(ref inner) => inner.description(), + Error::Canceled(ref inner) => inner.description(), + Error::Format(ref inner) => inner.description(), + Error::Hyper(ref inner) => inner.description(), + Error::InvalidRequest(_) => "Received an unexpected status code", + Error::Io(ref inner) => inner.description(), + Error::Json(ref inner) => inner.description(), + Error::RateLimit(ref inner) => inner.description(), + Error::Timer(ref inner) => inner.description(), + Error::Tls(ref inner) => inner.description(), + Error::UnknownStatus(_) => "Verification does not understand status", + Error::Uri(ref inner) => inner.description(), } } } + +impl From<BorrowMutError> for Error { + fn from(err: BorrowMutError) -> Self { + Error::BorrowMut(err) + } +} + +impl From<Canceled> for Error { + fn from(err: Canceled) -> Self { + Error::Canceled(err) + } +} + +impl From<FmtError> for Error { + fn from(err: FmtError) -> Self { + Error::Format(err) + } +} + +impl From<HyperError> for Error { + fn from(err: HyperError) -> Self { + Error::Hyper(err) + } +} + +impl From<IoError> for Error { + fn from(err: IoError) -> Self { + Error::Io(err) + } +} + +impl From<JsonError> for Error { + fn from(err: JsonError) -> Self { + Error::Json(err) + } +} + +impl From<RateLimitError> for Error { + fn from(err: RateLimitError) -> Self { + Error::RateLimit(err) + } +} + +impl From<TimerError> for Error { + fn from(err: TimerError) -> Self { + Error::Timer(err) + } +} + +impl From<TlsError> for Error { + fn from(err: TlsError) -> Self { + Error::Tls(err) + } +} diff --git a/src/http/macros.rs b/src/http/macros.rs new file mode 100644 index 0000000..87596f6 --- /dev/null +++ b/src/http/macros.rs @@ -0,0 +1,23 @@ +macro_rules! try_uri { + ($url:expr) => {{ + match ::hyper::Uri::from_str($url) { + Ok(v) => v, + Err(why) => return Box::new(::futures::future::err(::Error::Uri(why))), + } + }}; +} + +macro_rules! api { + ($e:expr) => { + concat!("https://discordapp.com/api/v6", $e) + }; + ($e:expr, $($rest:tt)*) => { + format!(api!($e), $($rest)*) + }; +} + +macro_rules! status { + ($e:expr) => { + concat!("https://status.discordapp.com/api/v2", $e) + } +} diff --git a/src/http/mod.rs b/src/http/mod.rs index 40b35bb..880809d 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -23,54 +23,43 @@ //! [`Client`]: ../struct.Client.html //! [model]: ../model/index.html +#[macro_use] mod macros; + pub mod ratelimiting; mod error; - -pub use self::error::Error as HttpError; -pub use hyper::status::{StatusClass, StatusCode}; - -use constants; -use hyper::{ - client::{ - Client as HyperClient, - Request, - RequestBuilder, - Response as HyperResponse - }, - header::ContentType, - method::Method, - mime::{Mime, SubLevel, TopLevel}, - net::HttpsConnector, - header, - Error as HyperError, - Result as HyperResult, - Url -}; -use hyper_native_tls::NativeTlsClient; -use internal::prelude::*; +mod routing; +mod utils; + +pub use hyper::StatusCode; +pub use self::error::{Error as HttpError, Result}; +pub use self::routing::{Path, Route}; + +use futures::{Future, Stream, future}; +use hyper::client::{Client as HyperClient, Config as HyperConfig, HttpConnector}; +use hyper::header::{Authorization, ContentType}; +use hyper::{Body, Method, Request, Response}; +use hyper_multipart_rfc7578::client::multipart::{Body as MultipartBody, Form}; +use hyper_tls::HttpsConnector; use model::prelude::*; -use multipart::client::Multipart; -use parking_lot::Mutex; -use self::ratelimiting::Route; -use serde_json; -use std::{ - collections::BTreeMap, - default::Default, - fmt::Write as FmtWrite, - fs::File, - io::ErrorKind as IoErrorKind, - path::{Path, PathBuf}, - sync::Arc -}; +use self::ratelimiting::RateLimiter; +use serde::de::DeserializeOwned; +use serde_json::{self, Number, Value}; +use std::cell::RefCell; +use std::fmt::Write; +use std::fs::File; +use std::io::Cursor; +use std::rc::Rc; +use std::str::FromStr; +use tokio_core::reactor::Handle; +use ::builder::*; +use ::{Error, utils as serenity_utils}; /// An method used for ratelimiting special routes. /// /// This is needed because `hyper`'s `Method` enum does not derive Copy. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum LightMethod { - /// Indicates that a route is for "any" method. - Any, /// Indicates that a route is for the `DELETE` method only. Delete, /// Indicates that a route is for the `GET` method only. @@ -83,1884 +72,1571 @@ pub enum LightMethod { Put, } -lazy_static! { - static ref TOKEN: Arc<Mutex<String>> = Arc::new(Mutex::new(String::default())); +impl LightMethod { + pub fn hyper_method(&self) -> Method { + match *self { + LightMethod::Delete => Method::Delete, + LightMethod::Get => Method::Get, + LightMethod::Patch => Method::Patch, + LightMethod::Post => Method::Post, + LightMethod::Put => Method::Put, + } + } } -/// Sets the token to be used across all requests which require authentication. -/// -/// If you are using the client module, you don't need to use this. If you're -/// using serenity solely for HTTP, you need to use this. -/// -/// # Examples -/// -/// Setting the token from an environment variable: -/// -/// ```rust,no_run -/// # use std::error::Error; -/// # -/// # fn try_main() -> Result<(), Box<Error>> { -/// # -/// use serenity::http; -/// use std::env; -/// -/// http::set_token(&env::var("DISCORD_TOKEN")?); -/// # Ok(()) -/// # } -/// # -/// # fn main() { -/// # try_main().unwrap(); -/// # } -pub fn set_token(token: &str) { TOKEN.lock().clone_from(&token.to_string()); } - -/// Adds a [`User`] as a recipient to a [`Group`]. -/// -/// **Note**: Groups have a limit of 10 recipients, including the current user. -/// -/// [`Group`]: ../model/channel/struct.Group.html -/// [`Group::add_recipient`]: ../model/channel/struct.Group.html#method.add_recipient -/// [`User`]: ../model/user/struct.User.html -pub fn add_group_recipient(group_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::None, - put, - "/channels/{}/recipients/{}", - group_id, - user_id - ), - ) -} +#[derive(Clone, Debug)] +pub struct Client { + pub client: Rc<HyperClient<HttpsConnector<HttpConnector>, Body>>, + pub handle: Handle, + pub multiparter: Rc<HyperClient<HttpsConnector<HttpConnector>, MultipartBody>>, + pub ratelimiter: Rc<RefCell<RateLimiter>>, + pub token: Rc<String>, +} + +impl Client { + pub fn new( + client: Rc<HyperClient<HttpsConnector<HttpConnector>, Body>>, + handle: Handle, + token: Rc<String>, + ) -> Result<Self> { + let connector = HttpsConnector::new(4, &handle)?; + + let multiparter = Rc::new(HyperConfig::default() + .body::<MultipartBody>() + .connector(connector) + .keep_alive(true) + .build(&handle)); + + Ok(Self { + ratelimiter: Rc::new(RefCell::new(RateLimiter::new(handle.clone()))), + client, + handle, + multiparter, + token, + }) + } -/// Adds a single [`Role`] to a [`Member`] in a [`Guild`]. -/// -/// **Note**: Requires the [Manage Roles] permission and respect of role -/// hierarchy. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`Member`]: ../model/guild/struct.Member.html -/// [`Role`]: ../model/guild/struct.Role.html -/// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html -pub fn add_member_role(guild_id: u64, user_id: u64, role_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdMembersIdRolesId(guild_id), - put, - "/guilds/{}/members/{}/roles/{}", - guild_id, - user_id, - role_id - ), - ) -} + pub fn set_token(&mut self, token: Rc<String>) { + self.token = token; + } -/// Bans a [`User`] from a [`Guild`], removing their messages sent in the last -/// X number of days. -/// -/// Passing a `delete_message_days` of `0` is equivalent to not removing any -/// messages. Up to `7` days' worth of messages may be deleted. -/// -/// **Note**: Requires that you have the [Ban Members] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`User`]: ../model/user/struct.User.html -/// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html -pub fn ban_user(guild_id: u64, user_id: u64, delete_message_days: u8, reason: &str) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdBansUserId(guild_id), - put, - "/guilds/{}/bans/{}?delete_message_days={}&reason={}", + /// Adds a [`User`] as a recipient to a [`Group`]. + /// + /// **Note**: Groups have a limit of 10 recipients, including the current + /// user. + /// + /// [`Group`]: ../model/channel/struct.Group.html + /// [`Group::add_recipient`]: ../model/channel/struct.Group.html#method.add_recipient + /// [`User`]: ../model/user/struct.User.html + pub fn add_group_recipient(&self, group_id: u64, user_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::AddGroupRecipient { group_id, user_id }, None) + } + + /// Adds a single [`Role`] to a [`Member`] in a [`Guild`]. + /// + /// **Note**: Requires the [Manage Roles] permission and respect of role + /// hierarchy. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`Member`]: ../model/guild/struct.Member.html + /// [`Role`]: ../model/guild/struct.Role.html + /// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html + pub fn add_member_role( + &self, + guild_id: u64, + user_id: u64, + role_id: u64 + ) -> impl Future<Item = (), Error = Error> { + self.verify(Route::AddMemberRole { guild_id, user_id, role_id }, None) + } + + /// Bans a [`User`] from a [`Guild`], removing their messages sent in the + /// last X number of days. + /// + /// Passing a `delete_message_days` of `0` is equivalent to not removing any + /// messages. Up to `7` days' worth of messages may be deleted. + /// + /// **Note**: Requires that you have the [Ban Members] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`User`]: ../model/user/struct.User.html + /// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html + pub fn ban_user( + &self, + guild_id: u64, + user_id: u64, + delete_message_days: u8, + reason: &str, + ) -> impl Future<Item = (), Error = Error> { + self.verify(Route::GuildBanUser { + delete_message_days: Some(delete_message_days), + reason: Some(reason), guild_id, user_id, - delete_message_days, - reason - ), - ) -} + }, None) + } -/// Ban zeyla from a [`Guild`], removing her messages sent in the last X number -/// of days. -/// -/// Passing a `delete_message_days` of `0` is equivalent to not removing any -/// messages. Up to `7` days' worth of messages may be deleted. -/// -/// **Note**: Requires that you have the [Ban Members] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html -pub fn ban_zeyla(guild_id: u64, delete_message_days: u8, reason: &str) -> Result<()> { - ban_user(guild_id, 114941315417899012, delete_message_days, reason) -} + /// Ban zeyla from a [`Guild`], removing her messages sent in the last X number + /// of days. + /// + /// Passing a `delete_message_days` of `0` is equivalent to not removing any + /// messages. Up to `7` days' worth of messages may be deleted. + /// + /// **Note**: Requires that you have the [Ban Members] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html + pub fn ban_zeyla( + &self, + guild_id: u64, + delete_message_days: u8, + reason: &str, + ) -> impl Future<Item = (), Error = Error> { + self.verify(Route::GuildBanUser { + delete_message_days: Some(delete_message_days), + reason: Some(reason), + guild_id, + user_id: 114941315417899012, + }, None) + } -/// Broadcasts that the current user is typing in the given [`Channel`]. -/// -/// This lasts for about 10 seconds, and will then need to be renewed to -/// indicate that the current user is still typing. -/// -/// This should rarely be used for bots, although it is a good indicator that a -/// long-running command is still being processed. -/// -/// [`Channel`]: ../model/channel/enum.Channel.html -pub fn broadcast_typing(channel_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdTyping(channel_id), - post, - "/channels/{}/typing", - channel_id - ), - ) -} + /// Broadcasts that the current user is typing in the given [`Channel`]. + /// + /// This lasts for about 10 seconds, and will then need to be renewed to + /// indicate that the current user is still typing. + /// + /// This should rarely be used for bots, although it is a good indicator + /// that a long-running command is still being processed. + /// + /// [`Channel`]: ../model/channel/enum.Channel.html + pub fn broadcast_typing(&self, channel_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::BroadcastTyping { channel_id }, None) + } -/// Creates a [`GuildChannel`] in the [`Guild`] given its Id. -/// -/// Refer to the Discord's [docs] for information on what fields this requires. -/// -/// **Note**: Requires the [Manage Channels] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -/// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-channel -/// [Manage Channels]: ../model/permissions/constant.MANAGE_CHANNELS.html -pub fn create_channel(guild_id: u64, map: &Value) -> Result<GuildChannel> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdChannels(guild_id), - post(body), - "/guilds/{}/channels", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildChannel>(response) - .map_err(From::from) -} + /// Creates a [`GuildChannel`] in the [`Guild`] given its Id. + /// + /// Refer to the Discord's [docs] for information on what fields this + /// requires. + /// + /// **Note**: Requires the [Manage Channels] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + /// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-channel + /// [Manage Channels]: ../model/permissions/constant.MANAGE_CHANNELS.html + pub fn create_channel( + &self, + guild_id: u64, + name: &str, + kind: ChannelType, + category_id: Option<u64>, + ) -> impl Future<Item = GuildChannel, Error = Error> { + self.post(Route::CreateChannel { guild_id }, Some(&json!({ + "name": name, + "parent_id": category_id, + "type": kind as u8, + }))) + } -/// Creates an emoji in the given [`Guild`] with the given data. -/// -/// View the source code for [`Context::create_emoji`] to see what fields this -/// requires. -/// -/// **Note**: Requires the [Manage Emojis] permission. -/// -/// [`Context::create_emoji`]: ../struct.Context.html#method.create_emoji -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [Manage Emojis]: ../model/permissions/constant.MANAGE_EMOJIS.html -pub fn create_emoji(guild_id: u64, map: &Value) -> Result<Emoji> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdEmojis(guild_id), - post(body), - "/guilds/{}/emojis", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Emoji>(response) - .map_err(From::from) -} + /// Creates an emoji in the given [`Guild`] with the given data. + /// + /// View the source code for [`Context::create_emoji`] to see what fields + /// this requires. + /// + /// **Note**: Requires the [Manage Emojis] permission. + /// + /// [`Context::create_emoji`]: ../struct.Context.html#method.create_emoji + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [Manage Emojis]: ../model/permissions/constant.MANAGE_EMOJIS.html + pub fn create_emoji(&self, guild_id: u64, name: &str, image: &str) + -> impl Future<Item = Emoji, Error = Error> { + self.post(Route::CreateEmoji { guild_id }, Some(&json!({ + "image": image, + "name": name, + }))) + } -/// Creates a guild with the data provided. -/// -/// Only a [`PartialGuild`] will be immediately returned, and a full [`Guild`] -/// will be received over a [`Shard`], if at least one is running. -/// -/// **Note**: This endpoint is currently limited to 10 active guilds. The -/// limits are raised for whitelisted [GameBridge] applications. See the -/// [documentation on this endpoint] for more info. -/// -/// # Examples -/// -/// Create a guild called `"test"` in the [US West region]: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serde_json::Value; -/// use serenity::http; -/// -/// let map = ObjectBuilder::new() -/// .insert("name", "test") -/// .insert("region", "us-west") -/// .build(); -/// -/// let _result = http::create_guild(map); -/// ``` -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`PartialGuild`]: ../model/guild/struct.PartialGuild.html -/// [`Shard`]: ../gateway/struct.Shard.html -/// [GameBridge]: https://discordapp.com/developers/docs/topics/gamebridge -/// [US West Region]: ../model/guild/enum.Region.html#variant.UsWest -/// [documentation on this endpoint]: -/// https://discordapp.com/developers/docs/resources/guild#create-guild -/// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild -pub fn create_guild(map: &Value) -> Result<PartialGuild> { - let body = map.to_string(); - let response = request!(Route::Guilds, post(body), "/guilds"); - - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Creates a guild with the data provided. + /// + /// Only a [`PartialGuild`] will be immediately returned, and a full + /// [`Guild`] will be received over a [`Shard`], if at least one is running. + /// + /// **Note**: This endpoint is currently limited to 10 active guilds. The + /// limits are raised for whitelisted [GameBridge] applications. See the + /// [documentation on this endpoint] for more info. + /// + /// # Examples + /// + /// Create a guild called `"test"` in the [US West region]: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serde_json::Value; + /// use serenity::http; + /// + /// let map = ObjectBuilder::new() + /// .insert("name", "test") + /// .insert("region", "us-west") + /// .build(); + /// + /// let _result = http::create_guild(map); + /// ``` + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`PartialGuild`]: ../model/guild/struct.PartialGuild.html + /// [`Shard`]: ../gateway/struct.Shard.html + /// [GameBridge]: https://discordapp.com/developers/docs/topics/gamebridge + /// [US West Region]: ../model/enum.Region.html#variant.UsWest + /// [documentation on this endpoint]: + /// https://discordapp.com/developers/docs/resources/guild#create-guild + /// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild + pub fn create_guild(&self, map: &Value) + -> impl Future<Item = PartialGuild, Error = Error> { + self.post(Route::CreateGuild, Some(map)) + } -/// Creates an [`Integration`] for a [`Guild`]. -/// -/// Refer to Discord's [docs] for field information. -/// -/// **Note**: Requires the [Manage Guild] permission. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`Integration`]: ../model/guild/struct.Integration.html -/// [Manage Guild]: ../model/permissions/constant.MANAGE_GUILD.html -/// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-integration -pub fn create_guild_integration(guild_id: u64, integration_id: u64, map: &Value) -> Result<()> { - let body = map.to_string(); - - verify( - 204, - request!( - Route::GuildsIdIntegrations(guild_id), - post(body), - "/guilds/{}/integrations/{}", + /// Creates an [`Integration`] for a [`Guild`]. + /// + /// Refer to Discord's [docs] for field information. + /// + /// **Note**: Requires the [Manage Guild] permission. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`Integration`]: ../model/guild/struct.Integration.html + /// [Manage Guild]: ../model/permissions/constant.MANAGE_GUILD.html + /// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-integration + pub fn create_guild_integration( + &self, + guild_id: u64, + integration_id: u64, + kind: &str, + ) -> impl Future<Item = (), Error = Error> { + let json = json!({ + "id": integration_id, + "type": kind, + }); + self.verify(Route::CreateGuildIntegration { guild_id, - integration_id - ), - ) -} - -/// Creates a [`RichInvite`] for the given [channel][`GuildChannel`]. -/// -/// Refer to Discord's [docs] for field information. -/// -/// All fields are optional. -/// -/// **Note**: Requires the [Create Invite] permission. -/// -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -/// [`RichInvite`]: ../model/guild/struct.RichInvite.html -/// [Create Invite]: ../model/permissions/constant.CREATE_INVITE.html -/// [docs]: https://discordapp.com/developers/docs/resources/channel#create-channel-invite -pub fn create_invite(channel_id: u64, map: &JsonMap) -> Result<RichInvite> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::ChannelsIdInvites(channel_id), - post(body), - "/channels/{}/invites", - channel_id - ); - - serde_json::from_reader::<HyperResponse, RichInvite>(response) - .map_err(From::from) -} + integration_id, + }, Some(&json)) + } -/// Creates a permission override for a member or a role in a channel. -pub fn create_permission(channel_id: u64, target_id: u64, map: &Value) -> Result<()> { - let body = map.to_string(); + /// Creates a [`RichInvite`] for the given [channel][`GuildChannel`]. + /// + /// Refer to Discord's [docs] for field information. + /// + /// All fields are optional. + /// + /// **Note**: Requires the [Create Invite] permission. + /// + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + /// [`RichInvite`]: ../model/guild/struct.RichInvite.html + /// [Create Invite]: ../model/permissions/constant.CREATE_INVITE.html + /// [docs]: https://discordapp.com/developers/docs/resources/channel#create-channel-invite + pub fn create_invite<F>(&self, channel_id: u64, f: F) + -> impl Future<Item = RichInvite, Error = Error> + where F: FnOnce(CreateInvite) -> CreateInvite { + let map = serenity_utils::vecmap_to_json_map(f(CreateInvite::default()).0); + + self.post(Route::CreateInvite { channel_id }, Some(&Value::Object(map))) + } - verify( - 204, - request!( - Route::ChannelsIdPermissionsOverwriteId(channel_id), - put(body), - "/channels/{}/permissions/{}", + /// Creates a permission override for a member or a role in a channel. + pub fn create_permission( + &self, + channel_id: u64, + target: &PermissionOverwrite, + ) -> impl Future<Item = (), Error = Error> { + let (id, kind) = match target.kind { + PermissionOverwriteType::Member(id) => (id.0, "member"), + PermissionOverwriteType::Role(id) => (id.0, "role"), + }; + let map = json!({ + "allow": target.allow.bits(), + "deny": target.deny.bits(), + "id": id, + "type": kind, + }); + + self.verify(Route::CreatePermission { + target_id: id, channel_id, - target_id - ), - ) -} + }, Some(&map)) + } -/// Creates a private channel with a user. -pub fn create_private_channel(map: &Value) -> Result<PrivateChannel> { - let body = map.to_string(); - let response = request!(Route::UsersMeChannels, post(body), "/users/@me/channels"); + /// Creates a private channel with a user. + pub fn create_private_channel(&self, user_id: u64) + -> impl Future<Item = PrivateChannel, Error = Error> { + let map = json!({ + "recipient_id": user_id, + }); - serde_json::from_reader::<HyperResponse, PrivateChannel>(response) - .map_err(From::from) -} + self.post(Route::CreatePrivateChannel, Some(&map)) + } -/// Reacts to a message. -pub fn create_reaction(channel_id: u64, - message_id: u64, - reaction_type: &ReactionType) - -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - put, - "/channels/{}/messages/{}/reactions/{}/@me", + /// Reacts to a message. + pub fn create_reaction( + &self, + channel_id: u64, + message_id: u64, + reaction_type: &ReactionType + ) -> impl Future<Item = (), Error = Error> { + self.verify(Route::CreateReaction { + reaction: &utils::reaction_type_data(reaction_type), channel_id, message_id, - reaction_type.as_data() - ), - ) -} + }, None) + } -/// Creates a role. -pub fn create_role(guild_id: u64, map: &JsonMap) -> Result<Role> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::GuildsIdRoles(guild_id), - post(body), - "/guilds/{}/roles", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Role>(response) - .map_err(From::from) -} + /// Creates a role. + pub fn create_role<F>(&self, guild_id: u64, f: F) -> impl Future<Item = Role, Error = Error> + where F: FnOnce(EditRole) -> EditRole { + let map = serenity_utils::vecmap_to_json_map(f(EditRole::default()).0); -/// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing in -/// the given data. -/// -/// This method requires authentication. -/// -/// The Value is a map with the values of: -/// -/// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar -/// (_optional_); -/// - **name**: the name of the webhook, limited to between 2 and 100 characters -/// long. -/// -/// # Examples -/// -/// Creating a webhook named `test`: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let channel_id = 81384788765712384; -/// let map = ObjectBuilder::new().insert("name", "test").build(); -/// -/// let webhook = http::create_webhook(channel_id, map).expect("Error creating"); -/// ``` -/// -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -pub fn create_webhook(channel_id: u64, map: &Value) -> Result<Webhook> { - let body = map.to_string(); - let response = request!( - Route::ChannelsIdWebhooks(channel_id), - post(body), - "/channels/{}/webhooks", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + self.post(Route::CreateRole { guild_id }, Some(&Value::Object(map))) + } -/// Deletes a private channel or a channel in a guild. -pub fn delete_channel(channel_id: u64) -> Result<Channel> { - let response = request!( - Route::ChannelsId(channel_id), - delete, - "/channels/{}", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Channel>(response) - .map_err(From::from) -} + /// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing + /// in the given data. + /// + /// This method requires authentication. + /// + /// The Value is a map with the values of: + /// + /// - **avatar**: base64-encoded 128x128 image for the webhook's default + /// avatar (_optional_); + /// - **name**: the name of the webhook, limited to between 2 and 100 + /// characters long. + /// + /// # Examples + /// + /// Creating a webhook named `test`: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let channel_id = 81384788765712384; + /// let map = ObjectBuilder::new().insert("name", "test").build(); + /// + /// let webhook = http::create_webhook(channel_id, map) + /// .expect("Error creating"); + /// ``` + /// + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + pub fn create_webhook(&self, channel_id: u64, map: &Value) + -> impl Future<Item = Webhook, Error = Error> { + self.post(Route::CreateWebhook { channel_id }, Some(map)) + } -/// Deletes an emoji from a server. -pub fn delete_emoji(guild_id: u64, emoji_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdEmojisId(guild_id), - delete, - "/guilds/{}/emojis/{}", - guild_id, - emoji_id - ), - ) -} + /// Deletes a private channel or a channel in a guild. + pub fn delete_channel(&self, channel_id: u64) -> impl Future<Item = Channel, Error = Error> { + self.delete(Route::DeleteChannel { channel_id }, None) + } -/// Deletes a guild, only if connected account owns it. -pub fn delete_guild(guild_id: u64) -> Result<PartialGuild> { - let response = request!(Route::GuildsId(guild_id), delete, "/guilds/{}", guild_id); + /// Deletes an emoji from a server. + pub fn delete_emoji(&self, guild_id: u64, emoji_id: u64) + -> impl Future<Item = (), Error = Error> { + self.delete(Route::DeleteEmoji { guild_id, emoji_id }, None) + } - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Deletes a guild, only if connected account owns it. + pub fn delete_guild(&self, guild_id: u64) + -> impl Future<Item = PartialGuild, Error = Error> { + self.delete(Route::DeleteGuild { guild_id }, None) + } -/// Remvoes an integration from a guild. -pub fn delete_guild_integration(guild_id: u64, integration_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdIntegrationsId(guild_id), - delete, - "/guilds/{}/integrations/{}", + /// Remvoes an integration from a guild. + pub fn delete_guild_integration( + &self, + guild_id: u64, + integration_id: u64, + ) -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteGuildIntegration { guild_id, - integration_id - ), - ) -} + integration_id, + }, None) + } -/// Deletes an invite by code. -pub fn delete_invite(code: &str) -> Result<Invite> { - let response = request!(Route::InvitesCode, delete, "/invites/{}", code); + /// Deletes an invite by code. + pub fn delete_invite(&self, code: &str) -> impl Future<Item = Invite, Error = Error> { + self.delete(Route::DeleteInvite { code }, None) + } - serde_json::from_reader::<HyperResponse, Invite>(response) - .map_err(From::from) -} + /// Deletes a message if created by us or we have specific permissions. + pub fn delete_message(&self, channel_id: u64, message_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteMessage { channel_id, message_id }, None) + } -/// Deletes a message if created by us or we have -/// specific permissions. -pub fn delete_message(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdMessagesId(LightMethod::Delete, channel_id), - delete, - "/channels/{}/messages/{}", - channel_id, - message_id - ), - ) -} + /// Deletes a bunch of messages, only works for bots. + pub fn delete_messages<T, It>(&self, channel_id: u64, message_ids: It) + -> impl Future<Item = (), Error = Error> + where T: AsRef<MessageId>, It: IntoIterator<Item=T> { + let ids = message_ids + .into_iter() + .map(|id| id.as_ref().0) + .collect::<Vec<u64>>(); -/// Deletes a bunch of messages, only works for bots. -pub fn delete_messages(channel_id: u64, map: &Value) -> Result<()> { - let body = map.to_string(); - - verify( - 204, - request!( - Route::ChannelsIdMessagesBulkDelete(channel_id), - post(body), - "/channels/{}/messages/bulk-delete", - channel_id - ), - ) -} + let map = json!({ + "messages": ids, + }); -/// Deletes all of the [`Reaction`]s associated with a [`Message`]. -/// -/// # Examples -/// -/// ```rust,no_run -/// use serenity::http; -/// use serenity::model::id::{ChannelId, MessageId}; -/// -/// let channel_id = ChannelId(7); -/// let message_id = MessageId(8); -/// -/// let _ = http::delete_message_reactions(channel_id.0, message_id.0) -/// .expect("Error deleting reactions"); -/// ``` -/// -/// [`Message`]: ../model/channel/struct.Message.html -/// [`Reaction`]: ../model/channel/struct.Reaction.html -pub fn delete_message_reactions(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdMessagesIdReactions(channel_id), - delete, - "/channels/{}/messages/{}/reactions", - channel_id, - message_id - ), - ) -} + self.verify(Route::DeleteMessages { channel_id }, Some(&map)) + } -/// Deletes a permission override from a role or a member in a channel. -pub fn delete_permission(channel_id: u64, target_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdPermissionsOverwriteId(channel_id), - delete, - "/channels/{}/permissions/{}", + /// Deletes all of the [`Reaction`]s associated with a [`Message`]. + /// + /// # Examples + /// + /// ```rust,no_run + /// use serenity::http; + /// use serenity::model::{ChannelId, MessageId}; + /// + /// let channel_id = ChannelId(7); + /// let message_id = MessageId(8); + /// + /// let _ = http::delete_message_reactions(channel_id.0, message_id.0) + /// .expect("Error deleting reactions"); + /// ``` + /// + /// [`Message`]: ../model/channel/struct.Message.html + /// [`Reaction`]: ../model/channel/struct.Reaction.html + pub fn delete_message_reactions(&self, channel_id: u64, message_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteMessageReactions { channel_id, - target_id - ), - ) -} + message_id, + }, None) + } + + /// Deletes a permission override from a role or a member in a channel. + pub fn delete_permission(&self, channel_id: u64, target_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeletePermission { channel_id, target_id }, None) + } -/// Deletes a reaction from a message if owned by us or -/// we have specific permissions. -pub fn delete_reaction(channel_id: u64, - message_id: u64, - user_id: Option<u64>, - reaction_type: &ReactionType) - -> Result<()> { - let user = user_id - .map(|uid| uid.to_string()) - .unwrap_or_else(|| "@me".to_string()); - - verify( - 204, - request!( - Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - delete, - "/channels/{}/messages/{}/reactions/{}/{}", + /// Deletes a reaction from a message if owned by us or + /// we have specific permissions. + pub fn delete_reaction( + &self, + channel_id: u64, + message_id: u64, + user_id: Option<u64>, + reaction_type: &ReactionType, + ) -> impl Future<Item = (), Error = Error> { + let reaction_type = utils::reaction_type_data(reaction_type); + let user = user_id + .map(|uid| uid.to_string()) + .unwrap_or_else(|| "@me".to_string()); + + self.verify(Route::DeleteReaction { + reaction: &reaction_type, + user: &user, channel_id, message_id, - reaction_type.as_data(), - user - ), - ) -} + }, None) + } -/// Deletes a role from a server. Can't remove the default everyone role. -pub fn delete_role(guild_id: u64, role_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdRolesId(guild_id), - delete, - "/guilds/{}/roles/{}", - guild_id, - role_id - ), - ) -} + /// Deletes a role from a server. Can't remove the default everyone role. + pub fn delete_role(&self, guild_id: u64, role_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteRole { guild_id, role_id }, None) + } -/// Deletes a [`Webhook`] given its Id. -/// -/// This method requires authentication, whereas [`delete_webhook_with_token`] -/// does not. -/// -/// # Examples -/// -/// Deletes a webhook given its Id: -/// -/// ```rust,no_run -/// use serenity::{Client, http}; -/// use std::env; -/// -/// // Due to the `delete_webhook` function requiring you to authenticate, you -/// // must have set the token first. -/// http::set_token(&env::var("DISCORD_TOKEN").unwrap()); -/// -/// http::delete_webhook(245037420704169985).expect("Error deleting webhook"); -/// ``` -/// -/// [`Webhook`]: ../model/webhook/struct.Webhook.html -/// [`delete_webhook_with_token`]: fn.delete_webhook_with_token.html -pub fn delete_webhook(webhook_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::WebhooksId(webhook_id), - delete, - "/webhooks/{}", - webhook_id, - ), - ) -} + /// Deletes a [`Webhook`] given its Id. + /// + /// This method requires authentication, whereas + /// [`delete_webhook_with_token`] does not. + /// + /// # Examples + /// + /// Deletes a webhook given its Id: + /// + /// ```rust,no_run + /// use serenity::{Client, http}; + /// use std::env; + /// + /// // Due to the `delete_webhook` function requiring you to authenticate, + /// // you must have set the token first. + /// http::set_token(&env::var("DISCORD_TOKEN").unwrap()); + /// + /// http::delete_webhook(245037420704169985) + /// .expect("Error deleting webhook"); + /// ``` + /// + /// [`Webhook`]: ../model/webhook/struct.Webhook.html + /// [`delete_webhook_with_token`]: fn.delete_webhook_with_token.html + pub fn delete_webhook(&self, webhook_id: u64) -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteWebhook { webhook_id }, None) + } -/// Deletes a [`Webhook`] given its Id and unique token. -/// -/// This method does _not_ require authentication. -/// -/// # Examples -/// -/// Deletes a webhook given its Id and unique token: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// -/// http::delete_webhook_with_token(id, token).expect("Error deleting webhook"); -/// ``` -/// -/// [`Webhook`]: ../model/webhook/struct.Webhook.html -pub fn delete_webhook_with_token(webhook_id: u64, token: &str) -> Result<()> { - let client = request_client!(); - - verify( - 204, - retry(|| { - client - .delete(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - }).map_err(Error::Hyper)?, - ) -} + /// Deletes a [`Webhook`] given its Id and unique token. + /// + /// This method does _not_ require authentication. + /// + /// # Examples + /// + /// Deletes a webhook given its Id and unique token: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// + /// http::delete_webhook_with_token(id, token) + /// .expect("Error deleting webhook"); + /// ``` + /// + /// [`Webhook`]: ../model/webhook/struct.Webhook.html + pub fn delete_webhook_with_token(&self, webhook_id: u64, token: &str) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteWebhookWithToken { webhook_id, token }, None) + } -/// Changes channel information. -pub fn edit_channel(channel_id: u64, map: &JsonMap) -> Result<GuildChannel> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::ChannelsId(channel_id), - patch(body), - "/channels/{}", - channel_id - ); - - serde_json::from_reader::<HyperResponse, GuildChannel>(response) - .map_err(From::from) -} + /// Changes channel information. + pub fn edit_channel<F>(&self, channel_id: u64, f: F) + -> impl Future<Item = GuildChannel, Error = Error> + where F: FnOnce(EditChannel) -> EditChannel { + let channel = f(EditChannel::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(channel)); -/// Changes emoji information. -pub fn edit_emoji(guild_id: u64, emoji_id: u64, map: &Value) -> Result<Emoji> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdEmojisId(guild_id), - patch(body), - "/guilds/{}/emojis/{}", - guild_id, - emoji_id - ); - - serde_json::from_reader::<HyperResponse, Emoji>(response) - .map_err(From::from) -} + self.patch(Route::EditChannel { channel_id }, Some(&map)) + } -/// Changes guild information. -pub fn edit_guild(guild_id: u64, map: &JsonMap) -> Result<PartialGuild> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::GuildsId(guild_id), - patch(body), - "/guilds/{}", - guild_id - ); - - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + /// Changes emoji information. + pub fn edit_emoji(&self, guild_id: u64, emoji_id: u64, name: &str) + -> impl Future<Item = Emoji, Error = Error> { + let map = json!({ + "name": name, + }); -/// Edits the positions of a guild's channels. -pub fn edit_guild_channel_positions(guild_id: u64, value: &Value) - -> Result<()> { - let body = serde_json::to_string(value)?; - - verify( - 204, - request!( - Route::GuildsIdChannels(guild_id), - patch(body), - "/guilds/{}/channels", - guild_id, - ), - ) -} + self.patch(Route::EditEmoji { guild_id, emoji_id }, Some(&map)) + } -/// Edits a [`Guild`]'s embed setting. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn edit_guild_embed(guild_id: u64, map: &Value) -> Result<GuildEmbed> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdEmbed(guild_id), - patch(body), - "/guilds/{}/embed", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildEmbed>(response) - .map_err(From::from) -} + /// Changes guild information. + pub fn edit_guild<F>(&self, guild_id: u64, f: F) + -> impl Future<Item = PartialGuild, Error = Error> + where F: FnOnce(EditGuild) -> EditGuild { + let guild = f(EditGuild::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(guild)); -/// Does specific actions to a member. -pub fn edit_member(guild_id: u64, user_id: u64, map: &JsonMap) -> Result<()> { - let body = serde_json::to_string(map)?; + self.patch(Route::EditGuild { guild_id }, Some(&map)) + } - verify( - 204, - request!( - Route::GuildsIdMembersId(guild_id), - patch(body), - "/guilds/{}/members/{}", - guild_id, - user_id - ), - ) -} + /// Edits the positions of a guild's channels. + pub fn edit_guild_channel_positions<It>(&self, guild_id: u64, channels: It) + -> impl Future<Item = (), Error = Error> where It: IntoIterator<Item = (ChannelId, u64)> { + let items = channels.into_iter().map(|(id, pos)| json!({ + "id": id, + "position": pos, + })).collect(); -/// Edits a message by Id. -/// -/// **Note**: Only the author of a message can modify it. -pub fn edit_message(channel_id: u64, message_id: u64, map: &Value) -> Result<Message> { - let body = map.to_string(); - let response = request!( - Route::ChannelsIdMessagesId(LightMethod::Any, channel_id), - patch(body), - "/channels/{}/messages/{}", - channel_id, - message_id - ); - - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + let map = Value::Array(items); -/// Edits the current user's nickname for the provided [`Guild`] via its Id. -/// -/// Pass `None` to reset the nickname. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn edit_nickname(guild_id: u64, new_nickname: Option<&str>) -> Result<()> { - let map = json!({ "nick": new_nickname }); - let body = map.to_string(); - let response = request!( - Route::GuildsIdMembersMeNick(guild_id), - patch(body), - "/guilds/{}/members/@me/nick", - guild_id - ); - - verify(200, response) -} + self.patch(Route::EditGuildChannels { guild_id }, Some(&map)) + } -/// Edits the current user's profile settings. -/// -/// Refer to Discord's [docs] for field information. -/// -/// [docs]: https://discordapp.com/developers/docs/resources/user#modify-current-user -pub fn edit_profile(map: &JsonMap) -> Result<CurrentUser> { - let body = serde_json::to_string(map)?; - let response = request!(Route::UsersMe, patch(body), "/users/@me"); + /// Edits a [`Guild`]'s embed setting. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + // todo + pub fn edit_guild_embed(&self, guild_id: u64, map: &Value) + -> impl Future<Item = GuildEmbed, Error = Error> { + self.patch(Route::EditGuildEmbed { guild_id }, Some(map)) + } - let mut value = serde_json::from_reader::<HyperResponse, Value>(response)?; + /// Does specific actions to a member. + pub fn edit_member<F>(&self, guild_id: u64, user_id: u64, f: F) + -> impl Future<Item = (), Error = Error> where F: FnOnce(EditMember) -> EditMember { + let member = f(EditMember::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(member)); - if let Some(map) = value.as_object_mut() { - if !TOKEN.lock().starts_with("Bot ") { - if let Some(Value::String(token)) = map.remove("token") { - set_token(&token); - } - } + self.verify(Route::EditMember { guild_id, user_id }, Some(&map)) } - serde_json::from_value::<CurrentUser>(value) - .map_err(From::from) -} + /// Edits a message by Id. + /// + /// **Note**: Only the author of a message can modify it. + pub fn edit_message<F: FnOnce(EditMessage) -> EditMessage>( + &self, + channel_id: u64, + message_id: u64, + f: F, + ) -> impl Future<Item = Message, Error = Error> { + let msg = f(EditMessage::default()); + let map = Value::Object(serenity_utils::vecmap_to_json_map(msg.0)); + + self.patch(Route::EditMessage { channel_id, message_id }, Some(&map)) + } -/// Changes a role in a guild. -pub fn edit_role(guild_id: u64, role_id: u64, map: &JsonMap) -> Result<Role> { - let body = serde_json::to_string(map)?; - let response = request!( - Route::GuildsIdRolesId(guild_id), - patch(body), - "/guilds/{}/roles/{}", - guild_id, - role_id - ); - - serde_json::from_reader::<HyperResponse, Role>(response) - .map_err(From::from) -} + /// Edits the current user's nickname for the provided [`Guild`] via its Id. + /// + /// Pass `None` to reset the nickname. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + pub fn edit_nickname(&self, guild_id: u64, new_nickname: Option<&str>) + -> impl Future<Item = (), Error = Error> { + self.patch(Route::EditNickname { guild_id }, Some(&json!({ + "nick": new_nickname, + }))) + } -/// Changes the position of a role in a guild. -pub fn edit_role_position(guild_id: u64, role_id: u64, position: u64) -> Result<Vec<Role>> { - let body = serde_json::to_string(&json!({ - "id": role_id, - "position": position, - }))?; - let response = request!( - Route::GuildsIdRolesId(guild_id), - patch(body), - "/guilds/{}/roles/{}", - guild_id, - role_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Role>>(response) - .map_err(From::from) -} + /// Edits the current user's profile settings. + pub fn edit_profile(&self, map: &Value) -> impl Future<Item = CurrentUser, Error = Error> { + self.patch(Route::EditProfile, Some(map)) + } -/// Edits a the webhook with the given data. -/// -/// The Value is a map with optional values of: -/// -/// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar -/// (_optional_); -/// - **name**: the name of the webhook, limited to between 2 and 100 characters -/// long. -/// -/// Note that, unlike with [`create_webhook`], _all_ values are optional. -/// -/// This method requires authentication, whereas [`edit_webhook_with_token`] -/// does not. -/// -/// # Examples -/// -/// Edit the image of a webhook given its Id and unique token: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let image = serenity::utils::read_image("./webhook_img.png") -/// .expect("Error reading image"); -/// let map = ObjectBuilder::new().insert("avatar", image).build(); -/// -/// let edited = http::edit_webhook_with_token(id, token, map) -/// .expect("Error editing webhook"); -/// ``` -/// -/// [`create_webhook`]: fn.create_webhook.html -/// [`edit_webhook_with_token`]: fn.edit_webhook_with_token.html -// The tests are ignored, rather than no_run'd, due to rustdoc tests with -// external crates being incredibly messy and misleading in the end user's view. -pub fn edit_webhook(webhook_id: u64, map: &Value) -> Result<Webhook> { - let body = map.to_string(); - let response = request!( - Route::WebhooksId(webhook_id), - patch(body), - "/webhooks/{}", - webhook_id, - ); - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + /// Changes a role in a guild. + pub fn edit_role<F>(&self, guild_id: u64, role_id: u64, f: F) + -> impl Future<Item = Role, Error = Error> where F: FnOnce(EditRole) -> EditRole { + let role = f(EditRole::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(role)); -/// Edits the webhook with the given data. -/// -/// Refer to the documentation for [`edit_webhook`] for more information. -/// -/// This method does _not_ require authentication. -/// -/// # Examples -/// -/// Edit the name of a webhook given its Id and unique token: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let map = ObjectBuilder::new().insert("name", "new name").build(); -/// -/// let edited = http::edit_webhook_with_token(id, token, map) -/// .expect("Error editing webhook"); -/// ``` -/// -/// [`edit_webhook`]: fn.edit_webhook.html -pub fn edit_webhook_with_token(webhook_id: u64, token: &str, map: &JsonMap) -> Result<Webhook> { - let body = serde_json::to_string(map)?; - let client = request_client!(); - - let response = retry(|| { - client - .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - .body(&body) - }).map_err(Error::Hyper)?; - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + self.patch(Route::EditRole { guild_id, role_id }, Some(&map)) + } -/// Executes a webhook, posting a [`Message`] in the webhook's associated -/// [`Channel`]. -/// -/// This method does _not_ require authentication. -/// -/// Pass `true` to `wait` to wait for server confirmation of the message sending -/// before receiving a response. From the [Discord docs]: -/// -/// > waits for server confirmation of message send before response, and returns -/// > the created message body (defaults to false; when false a message that is -/// > not saved does not return an error) -/// -/// The map can _optionally_ contain the following data: -/// -/// - `avatar_url`: Override the default avatar of the webhook with a URL. -/// - `tts`: Whether this is a text-to-speech message (defaults to `false`). -/// - `username`: Override the default username of the webhook. -/// -/// Additionally, _at least one_ of the following must be given: -/// -/// - `content`: The content of the message. -/// - `embeds`: An array of rich embeds. -/// -/// **Note**: For embed objects, all fields are registered by Discord except for -/// `height`, `provider`, `proxy_url`, `type` (it will always be `rich`), -/// `video`, and `width`. The rest will be determined by Discord. -/// -/// # Examples -/// -/// Sending a webhook with message content of `test`: -/// -/// ```rust,ignore -/// extern crate serde_json; -/// extern crate serenity; -/// -/// use serde_json::builder::ObjectBuilder; -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let map = ObjectBuilder::new().insert("content", "test").build(); -/// -/// let message = match http::execute_webhook(id, token, true, map) { -/// Ok(Some(message)) => message, -/// Ok(None) => { -/// println!("Expected a webhook message"); -/// -/// return; -/// }, -/// Err(why) => { -/// println!("Error executing webhook: {:?}", why); -/// -/// return; -/// }, -/// }; -/// ``` -/// -/// [`Channel`]: ../model/channel/enum.Channel.html -/// [`Message`]: ../model/channel/struct.Message.html -/// [Discord docs]: https://discordapp.com/developers/docs/resources/webhook#querystring-params -pub fn execute_webhook(webhook_id: u64, - token: &str, - wait: bool, - map: &JsonMap) - -> Result<Option<Message>> { - let body = serde_json::to_string(map)?; - - let client = request_client!(); - - let response = retry(|| { - client - .post(&format!( - api!("/webhooks/{}/{}?wait={}"), - webhook_id, - token, - wait - )) - .body(&body) - .header(ContentType( - Mime(TopLevel::Application, SubLevel::Json, vec![]), - )) - }).map_err(Error::Hyper)?; - - if response.status == StatusCode::NoContent { - return Ok(None); - } - - serde_json::from_reader::<HyperResponse, Message>(response) - .map(Some) - .map_err(From::from) -} + /// Changes the position of a role in a guild. + pub fn edit_role_position( + &self, + guild_id: u64, + role_id: u64, + position: u64, + ) -> impl Future<Item = Vec<Role>, Error = Error> { + self.patch(Route::EditRole { guild_id, role_id }, Some(&json!({ + "id": role_id, + "position": position, + }))) + } -/// Gets the active maintenances from Discord's Status API. -/// -/// Does not require authentication. -pub fn get_active_maintenances() -> Result<Vec<Maintenance>> { - let client = request_client!(); + /// Edits a the webhook with the given data. + /// + /// The Value is a map with optional values of: + /// + /// - **avatar**: base64-encoded 128x128 image for the webhook's default + /// avatar (_optional_); + /// - **name**: the name of the webhook, limited to between 2 and 100 + /// characters long. + /// + /// Note that, unlike with [`create_webhook`], _all_ values are optional. + /// + /// This method requires authentication, whereas [`edit_webhook_with_token`] + /// does not. + /// + /// # Examples + /// + /// Edit the image of a webhook given its Id and unique token: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let image = serenity::utils::read_image("./webhook_img.png") + /// .expect("Error reading image"); + /// let map = ObjectBuilder::new().insert("avatar", image).build(); + /// + /// let edited = http::edit_webhook_with_token(id, token, map) + /// .expect("Error editing webhook"); + /// ``` + /// + /// [`create_webhook`]: fn.create_webhook.html + /// [`edit_webhook_with_token`]: fn.edit_webhook_with_token.html + // The tests are ignored, rather than no_run'd, due to rustdoc tests with + // external crates being incredibly messy and misleading in the end user's + // view. + pub fn edit_webhook( + &self, + webhook_id: u64, + name: Option<&str>, + avatar: Option<&str>, + ) -> impl Future<Item = Webhook, Error = Error> { + let map = json!({ + "avatar": avatar, + "name": name, + }); + + self.patch(Route::EditWebhook { webhook_id }, Some(&map)) + } - let response = retry(|| { - client.get(status!("/scheduled-maintenances/active.json")) - })?; + /// Edits the webhook with the given data. + /// + /// Refer to the documentation for [`edit_webhook`] for more information. + /// + /// This method does _not_ require authentication. + /// + /// # Examples + /// + /// Edit the name of a webhook given its Id and unique token: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let map = ObjectBuilder::new().insert("name", "new name").build(); + /// + /// let edited = http::edit_webhook_with_token(id, token, map) + /// .expect("Error editing webhook"); + /// ``` + /// + /// [`edit_webhook`]: fn.edit_webhook.html + pub fn edit_webhook_with_token( + &self, + webhook_id: u64, + token: &str, + name: Option<&str>, + avatar: Option<&str>, + ) -> impl Future<Item = Webhook, Error = Error> { + let map = json!({ + "avatar": avatar, + "name": name, + }); + let route = Route::EditWebhookWithToken { + token, + webhook_id, + }; - let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + self.patch(route, Some(&map)) + } - match map.remove("scheduled_maintenances") { - Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) - .map_err(From::from), - None => Ok(vec![]), + /// Executes a webhook, posting a [`Message`] in the webhook's associated + /// [`Channel`]. + /// + /// This method does _not_ require authentication. + /// + /// Pass `true` to `wait` to wait for server confirmation of the message + /// sending before receiving a response. From the [Discord docs]: + /// + /// > waits for server confirmation of message send before response, and + /// > returns the created message body (defaults to false; when false a + /// > message that is not saved does not return an error) + /// + /// The map can _optionally_ contain the following data: + /// + /// - `avatar_url`: Override the default avatar of the webhook with a URL. + /// - `tts`: Whether this is a text-to-speech message (defaults to `false`). + /// - `username`: Override the default username of the webhook. + /// + /// Additionally, _at least one_ of the following must be given: + /// + /// - `content`: The content of the message. + /// - `embeds`: An array of rich embeds. + /// + /// **Note**: For embed objects, all fields are registered by Discord except + /// for `height`, `provider`, `proxy_url`, `type` (it will always be + /// `rich`), `video`, and `width`. The rest will be determined by Discord. + /// + /// # Examples + /// + /// Sending a webhook with message content of `test`: + /// + /// ```rust,ignore + /// extern crate serde_json; + /// extern crate serenity; + /// + /// use serde_json::builder::ObjectBuilder; + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// let map = ObjectBuilder::new().insert("content", "test").build(); + /// + /// let message = match http::execute_webhook(id, token, true, map) { + /// Ok(Some(message)) => message, + /// Ok(None) => { + /// println!("Expected a webhook message"); + /// + /// return; + /// }, + /// Err(why) => { + /// println!("Error executing webhook: {:?}", why); + /// + /// return; + /// }, + /// }; + /// ``` + /// + /// [`Channel`]: ../model/channel/enum.Channel.html + /// [`Message`]: ../model/channel/struct.Message.html + /// [Discord docs]: https://discordapp.com/developers/docs/resources/webhook#querystring-params + pub fn execute_webhook<F: FnOnce(ExecuteWebhook) -> ExecuteWebhook>( + &self, + webhook_id: u64, + token: &str, + wait: bool, + f: F, + ) -> impl Future<Item = Option<Message>, Error = Error> { + let execution = f(ExecuteWebhook::default()).0; + let map = Value::Object(serenity_utils::vecmap_to_json_map(execution)); + + let route = Route::ExecuteWebhook { + token, + wait, + webhook_id, + }; + + if wait { + self.post(route, Some(&map)) + } else { + Box::new(self.verify(route, Some(&map)).map(|_| None)) + } } -} -/// Gets all the users that are banned in specific guild. -pub fn get_bans(guild_id: u64) -> Result<Vec<Ban>> { - let response = request!( - Route::GuildsIdBans(guild_id), - get, - "/guilds/{}/bans", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Ban>>(response) - .map_err(From::from) -} + /// Gets the active maintenances from Discord's Status API. + /// + /// Does not require authentication. + // pub fn get_active_maintenances() -> impl Future<Item = Vec<Maintenance>, Error = Error> { + // let client = request_client!(); + + // let response = retry(|| { + // client.get(status!("/scheduled-maintenances/active.json")) + // })?; + + // let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; -/// Gets all audit logs in a specific guild. -pub fn get_audit_logs(guild_id: u64, - action_type: Option<u8>, - user_id: Option<u64>, - before: Option<u64>, - limit: Option<u8>) -> Result<AuditLogs> { - let mut params = Vec::with_capacity(4); + // match map.remove("scheduled_maintenances") { + // Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) + // .map_err(From::from), + // None => Ok(vec![]), + // } + // } - if let Some(action_type) = action_type { - params.push(format!("action_type={}", action_type)); + /// Gets all the users that are banned in specific guild. + pub fn get_bans(&self, guild_id: u64) -> impl Future<Item = Vec<Ban>, Error = Error> { + self.get(Route::GetBans { guild_id }) } - if let Some(user_id) = user_id { - params.push(format!("user_id={}", user_id)); + + /// Gets all audit logs in a specific guild. + pub fn get_audit_logs( + &self, + guild_id: u64, + action_type: Option<u8>, + user_id: Option<u64>, + before: Option<u64>, + limit: Option<u8>, + ) -> impl Future<Item = AuditLogs, Error = Error> { + self.get(Route::GetAuditLogs { + action_type, + before, + guild_id, + limit, + user_id, + }) } - if let Some(before) = before { - params.push(format!("before={}", before)); + + /// Gets current bot gateway. + pub fn get_bot_gateway(&self) -> impl Future<Item = BotGateway, Error = Error> { + self.get(Route::GetBotGateway) } - if let Some(limit) = limit { - params.push(format!("limit={}", limit)); + + /// Gets all invites for a channel. + pub fn get_channel_invites(&self, channel_id: u64) + -> impl Future<Item = Vec<RichInvite>, Error = Error> { + self.get(Route::GetChannelInvites { channel_id }) } - let mut query_string = params.join("&"); - if !query_string.is_empty() { - query_string.insert(0, '?'); + /// Retrieves the webhooks for the given [channel][`GuildChannel`]'s Id. + /// + /// This method requires authentication. + /// + /// # Examples + /// + /// Retrieve all of the webhooks owned by a channel: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let channel_id = 81384788765712384; + /// + /// let webhooks = http::get_channel_webhooks(channel_id) + /// .expect("Error getting channel webhooks"); + /// ``` + /// + /// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html + pub fn get_channel_webhooks(&self, channel_id: u64) + -> impl Future<Item = Vec<Webhook>, Error = Error> { + self.get(Route::GetChannelWebhooks { channel_id }) } - let response = request!( - Route::GuildsIdAuditLogs(guild_id), - get, - "/guilds/{}/audit-logs{}", - guild_id, - query_string - ); + /// Gets channel information. + pub fn get_channel(&self, channel_id: u64) -> impl Future<Item = Channel, Error = Error> { + self.get(Route::GetChannel { channel_id }) + } - serde_json::from_reader::<HyperResponse, AuditLogs>(response) - .map_err(From::from) -} + /// Gets all channels in a guild. + pub fn get_channels(&self, guild_id: u64) + -> impl Future<Item = Vec<GuildChannel>, Error = Error> { + self.get(Route::GetChannels { guild_id }) + } -/// Gets current bot gateway. -pub fn get_bot_gateway() -> Result<BotGateway> { - let response = request!(Route::GatewayBot, get, "/gateway/bot"); + /// Gets information about the current application. + /// + /// **Note**: Only applications may use this endpoint. + pub fn get_current_application_info(&self) + -> impl Future<Item = CurrentApplicationInfo, Error = Error> { + self.get(Route::GetCurrentApplicationInfo) + } - serde_json::from_reader::<HyperResponse, BotGateway>(response) - .map_err(From::from) -} + /// Gets information about the user we're connected with. + pub fn get_current_user(&self, ) -> impl Future<Item = CurrentUser, Error = Error> { + self.get(Route::GetCurrentUser) + } -/// Gets all invites for a channel. -pub fn get_channel_invites(channel_id: u64) -> Result<Vec<RichInvite>> { - let response = request!( - Route::ChannelsIdInvites(channel_id), - get, - "/channels/{}/invites", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response) - .map_err(From::from) -} + /// Gets current gateway. + pub fn get_gateway(&self) -> impl Future<Item = Gateway, Error = Error> { + self.get(Route::GetGateway) + } -/// Retrieves the webhooks for the given [channel][`GuildChannel`]'s Id. -/// -/// This method requires authentication. -/// -/// # Examples -/// -/// Retrieve all of the webhooks owned by a channel: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let channel_id = 81384788765712384; -/// -/// let webhooks = http::get_channel_webhooks(channel_id) -/// .expect("Error getting channel webhooks"); -/// ``` -/// -/// [`GuildChannel`]: ../model/channel/struct.GuildChannel.html -pub fn get_channel_webhooks(channel_id: u64) -> Result<Vec<Webhook>> { - let response = request!( - Route::ChannelsIdWebhooks(channel_id), - get, - "/channels/{}/webhooks", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response) - .map_err(From::from) -} + /// Gets guild information. + pub fn get_guild(&self, guild_id: u64) -> impl Future<Item = PartialGuild, Error = Error> { + self.get(Route::GetGuild { guild_id }) + } -/// Gets channel information. -pub fn get_channel(channel_id: u64) -> Result<Channel> { - let response = request!( - Route::ChannelsId(channel_id), - get, - "/channels/{}", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Channel>(response) - .map_err(From::from) -} + /// Gets a guild embed information. + pub fn get_guild_embed(&self, guild_id: u64) + -> impl Future<Item = GuildEmbed, Error = Error> { + self.get(Route::GetGuildEmbed { guild_id }) + } -/// Gets all channels in a guild. -pub fn get_channels(guild_id: u64) -> Result<Vec<GuildChannel>> { - let response = request!( - Route::ChannelsId(guild_id), - get, - "/guilds/{}/channels", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<GuildChannel>>(response) - .map_err(From::from) -} + /// Gets integrations that a guild has. + pub fn get_guild_integrations(&self, guild_id: u64) + -> impl Future<Item = Vec<Integration>, Error = Error> { + self.get(Route::GetGuildIntegrations { guild_id }) + } -/// Gets information about the current application. -/// -/// **Note**: Only applications may use this endpoint. -pub fn get_current_application_info() -> Result<CurrentApplicationInfo> { - let response = request!(Route::None, get, "/oauth2/applications/@me"); + /// Gets all invites to a guild. + pub fn get_guild_invites(&self, guild_id: u64) + -> impl Future<Item = Vec<RichInvite>, Error = Error> { + self.get(Route::GetGuildInvites { guild_id }) + } - serde_json::from_reader::<HyperResponse, CurrentApplicationInfo>(response) - .map_err(From::from) -} + /// Gets the members of a guild. Optionally pass a `limit` and the Id of the + /// user to offset the result by. + pub fn get_guild_members( + &self, + guild_id: u64, + limit: Option<u64>, + after: Option<u64> + ) -> impl Future<Item = Vec<Member>, Error = Error> { + let done = self.get(Route::GetGuildMembers { after, guild_id, limit }) + .and_then(move |mut v: Value| { + if let Some(values) = v.as_array_mut() { + let num = Value::Number(Number::from(guild_id)); + + for value in values { + if let Some(element) = value.as_object_mut() { + element.insert("guild_id".to_string(), num.clone()); + } + } + } + + serde_json::from_value::<Vec<Member>>(v).map_err(From::from) + }); + + Box::new(done) + } -/// Gets information about the user we're connected with. -pub fn get_current_user() -> Result<CurrentUser> { - let response = request!(Route::UsersMe, get, "/users/@me"); + /// Gets the amount of users that can be pruned. + pub fn get_guild_prune_count(&self, guild_id: u64, days: u16) + -> impl Future<Item = GuildPrune, Error = Error> { + self.get(Route::GetGuildPruneCount { + days: days as u64, + guild_id, + }) + } - serde_json::from_reader::<HyperResponse, CurrentUser>(response) - .map_err(From::from) -} + /// Gets regions that a guild can use. If a guild has + /// [`Feature::VipRegions`] enabled, then additional VIP-only regions are + /// returned. + /// + /// [`Feature::VipRegions`]: ../model/enum.Feature.html#variant.VipRegions + pub fn get_guild_regions(&self, guild_id: u64) + -> impl Future<Item = Vec<VoiceRegion>, Error = Error> { + self.get(Route::GetGuildRegions { guild_id }) + } -/// Gets current gateway. -pub fn get_gateway() -> Result<Gateway> { - let response = request!(Route::Gateway, get, "/gateway"); + /// Retrieves a list of roles in a [`Guild`]. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + pub fn get_guild_roles(&self, guild_id: u64) + -> impl Future<Item = Vec<Role>, Error = Error> { + self.get(Route::GetGuildRoles { guild_id }) + } - serde_json::from_reader::<HyperResponse, Gateway>(response) - .map_err(From::from) -} + /// Gets a guild's vanity URL if it has one. + pub fn get_guild_vanity_url(&self, guild_id: u64) + -> impl Future<Item = String, Error = Error> { + #[derive(Deserialize)] + struct GuildVanityUrl { + code: String, + } -/// Gets guild information. -pub fn get_guild(guild_id: u64) -> Result<PartialGuild> { - let response = request!(Route::GuildsId(guild_id), get, "/guilds/{}", guild_id); + let done = self.get::<GuildVanityUrl>( + Route::GetGuildVanityUrl { guild_id }, + ).map(|resp| { + resp.code + }); - serde_json::from_reader::<HyperResponse, PartialGuild>(response) - .map_err(From::from) -} + Box::new(done) + } -/// Gets a guild embed information. -pub fn get_guild_embed(guild_id: u64) -> Result<GuildEmbed> { - let response = request!( - Route::GuildsIdEmbed(guild_id), - get, - "/guilds/{}/embeds", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildEmbed>(response) - .map_err(From::from) -} + /// Retrieves the webhooks for the given [guild][`Guild`]'s Id. + /// + /// This method requires authentication. + /// + /// # Examples + /// + /// Retrieve all of the webhooks owned by a guild: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let guild_id = 81384788765712384; + /// + /// let webhooks = http::get_guild_webhooks(guild_id) + /// .expect("Error getting guild webhooks"); + /// ``` + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + pub fn get_guild_webhooks(&self, guild_id: u64) + -> impl Future<Item = Vec<Webhook>, Error = Error> { + self.get(Route::GetGuildWebhooks { guild_id }) + } -/// Gets integrations that a guild has. -pub fn get_guild_integrations(guild_id: u64) -> Result<Vec<Integration>> { - let response = request!( - Route::GuildsIdIntegrations(guild_id), - get, - "/guilds/{}/integrations", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Integration>>(response) - .map_err(From::from) -} + /// Gets a paginated list of the current user's guilds. + /// + /// The `limit` has a maximum value of 100. + /// + /// [Discord's documentation][docs] + /// + /// # Examples + /// + /// Get the first 10 guilds after a certain guild's Id: + /// + /// ```rust,no_run + /// use serenity::http::{GuildPagination, get_guilds}; + /// use serenity::model::GuildId; + /// + /// let guild_id = GuildId(81384788765712384); + /// + /// let guilds = get_guilds(&GuildPagination::After(guild_id), 10).unwrap(); + /// ``` + /// + /// [docs]: https://discordapp.com/developers/docs/resources/user#get-current-user-guilds + pub fn get_guilds(&self, target: &GuildPagination, limit: u64) + -> impl Future<Item = Vec<GuildInfo>, Error = Error> { + let (after, before) = match *target { + GuildPagination::After(v) => (Some(v.0), None), + GuildPagination::Before(v) => (None, Some(v.0)), + }; -/// Gets all invites to a guild. -pub fn get_guild_invites(guild_id: u64) -> Result<Vec<RichInvite>> { - let response = request!( - Route::GuildsIdInvites(guild_id), - get, - "/guilds/{}/invites", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response) - .map_err(From::from) -} + self.get(Route::GetGuilds { after, before, limit }) + } -/// Gets a guild's vanity URL if it has one. -pub fn get_guild_vanity_url(guild_id: u64) -> Result<String> { - #[derive(Deserialize)] - struct GuildVanityUrl { - code: String, + /// Gets information about a specific invite. + pub fn get_invite<'a>(&self, code: &'a str, stats: bool) + -> Box<Future<Item = Invite, Error = Error> + 'a> { + self.get(Route::GetInvite { code, stats }) } - let response = request!( - Route::GuildsIdVanityUrl(guild_id), - get, - "/guilds/{}/vanity-url", - guild_id - ); + /// Gets member of a guild. + pub fn get_member(&self, guild_id: u64, user_id: u64) + -> impl Future<Item = Member, Error = Error> { + let done = self.get::<Value>(Route::GetMember { guild_id, user_id }) + .and_then(move |mut v| { + if let Some(map) = v.as_object_mut() { + map.insert("guild_id".to_string(), Value::Number(Number::from(guild_id))); + } - serde_json::from_reader::<HyperResponse, GuildVanityUrl>(response) - .map(|x| x.code) - .map_err(From::from) -} + serde_json::from_value(v).map_err(From::from) + }); -/// Gets the members of a guild. Optionally pass a `limit` and the Id of the -/// user to offset the result by. -pub fn get_guild_members(guild_id: u64, - limit: Option<u64>, - after: Option<u64>) - -> Result<Vec<Member>> { - let response = request!( - Route::GuildsIdMembers(guild_id), - get, - "/guilds/{}/members?limit={}&after={}", - guild_id, - limit.unwrap_or(500), - after.unwrap_or(0) - ); - - let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?; - - if let Some(values) = v.as_array_mut() { - let num = Value::Number(Number::from(guild_id)); - - for value in values { - if let Some(element) = value.as_object_mut() { - element.insert("guild_id".to_string(), num.clone()); - } - } + Box::new(done) } - serde_json::from_value::<Vec<Member>>(v).map_err(From::from) -} + /// Gets a message by an Id, bots only. + pub fn get_message(&self, channel_id: u64, message_id: u64) + -> impl Future<Item = Message, Error = Error> { + self.get(Route::GetMessage { channel_id, message_id }) + } -/// Gets the amount of users that can be pruned. -pub fn get_guild_prune_count(guild_id: u64, map: &Value) -> Result<GuildPrune> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdPrune(guild_id), - get(body), - "/guilds/{}/prune", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildPrune>(response) - .map_err(From::from) -} + /// Gets X messages from a channel. + pub fn get_messages<'a, F: FnOnce(GetMessages) -> GetMessages>( + &'a self, + channel_id: u64, + f: F, + ) -> Box<Future<Item = Vec<Message>, Error = Error> + 'a> { + let mut map = f(GetMessages::default()).0; -/// Gets regions that a guild can use. If a guild has the `VIP_REGIONS` feature -/// enabled, then additional VIP-only regions are returned. -pub fn get_guild_regions(guild_id: u64) -> Result<Vec<VoiceRegion>> { - let response = request!( - Route::GuildsIdRegions(guild_id), - get, - "/guilds/{}/regions", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response) - .map_err(From::from) -} + let limit = map.remove(&"limit").unwrap_or(50); + let mut query = format!("?limit={}", limit); -/// Retrieves a list of roles in a [`Guild`]. -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn get_guild_roles(guild_id: u64) -> Result<Vec<Role>> { - let response = request!( - Route::GuildsIdRoles(guild_id), - get, - "/guilds/{}/roles", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Role>>(response) - .map_err(From::from) -} + if let Some(after) = map.remove(&"after") { + ftry!(write!(query, "&after={}", after)); + } -/// Retrieves the webhooks for the given [guild][`Guild`]'s Id. -/// -/// This method requires authentication. -/// -/// # Examples -/// -/// Retrieve all of the webhooks owned by a guild: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let guild_id = 81384788765712384; -/// -/// let webhooks = http::get_guild_webhooks(guild_id) -/// .expect("Error getting guild webhooks"); -/// ``` -/// -/// [`Guild`]: ../model/guild/struct.Guild.html -pub fn get_guild_webhooks(guild_id: u64) -> Result<Vec<Webhook>> { - let response = request!( - Route::GuildsIdWebhooks(guild_id), - get, - "/guilds/{}/webhooks", - guild_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response) - .map_err(From::from) -} + if let Some(around) = map.remove(&"around") { + ftry!(write!(query, "&around={}", around)); + } -/// Gets a paginated list of the current user's guilds. -/// -/// The `limit` has a maximum value of 100. -/// -/// [Discord's documentation][docs] -/// -/// # Examples -/// -/// Get the first 10 guilds after a certain guild's Id: -/// -/// ```rust,no_run -/// use serenity::http::{GuildPagination, get_guilds}; -/// use serenity::model::id::GuildId; -/// -/// let guild_id = GuildId(81384788765712384); -/// -/// let guilds = get_guilds(&GuildPagination::After(guild_id), 10).unwrap(); -/// ``` -/// -/// [docs]: https://discordapp.com/developers/docs/resources/user#get-current-user-guilds -pub fn get_guilds(target: &GuildPagination, limit: u64) -> Result<Vec<GuildInfo>> { - let mut uri = format!("/users/@me/guilds?limit={}", limit); + if let Some(before) = map.remove(&"before") { + ftry!(write!(query, "&before={}", before)); + } - match *target { - GuildPagination::After(id) => { - write!(uri, "&after={}", id)?; - }, - GuildPagination::Before(id) => { - write!(uri, "&before={}", id)?; - }, + self.get(Route::GetMessages { + channel_id, + query, + }) } - let response = request!(Route::UsersMeGuilds, get, "{}", uri); - - serde_json::from_reader::<HyperResponse, Vec<GuildInfo>>(response) - .map_err(From::from) -} + /// Gets all pins of a channel. + pub fn get_pins(&self, channel_id: u64) -> impl Future<Item = Vec<Message>, Error = Error> { + self.get(Route::GetPins { channel_id }) + } -/// Gets information about a specific invite. -#[allow(unused_mut)] -pub fn get_invite(code: &str, stats: bool) -> Result<Invite> { - let mut invite = code; + /// Gets user Ids based on their reaction to a message. + pub fn get_reaction_users( + &self, + channel_id: u64, + message_id: u64, + reaction_type: &ReactionType, + limit: Option<u8>, + after: Option<u64> + ) -> impl Future<Item = Vec<User>, Error = Error> { + let reaction = utils::reaction_type_data(reaction_type); + self.get(Route::GetReactionUsers { + limit: limit.unwrap_or(50), + after, + channel_id, + message_id, + reaction, + }) + } - #[cfg(feature = "utils")] - { - invite = ::utils::parse_invite(invite); + // /// Gets the current unresolved incidents from Discord's Status API. + // /// + // /// Does not require authentication. + // pub fn get_unresolved_incidents(&self) -> impl Future<Item = Vec<Incident>, Error = Error> { + // let client = request_client!(); + + // let response = retry(|| client.get(status!("/incidents/unresolved.json")))?; + + // let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + + // match map.remove("incidents") { + // Some(v) => serde_json::from_value::<Vec<Incident>>(v) + // .map_err(From::from), + // None => Ok(vec![]), + // } + // } + + // /// Gets the upcoming (planned) maintenances from Discord's Status API. + // /// + // /// Does not require authentication. + // pub fn get_upcoming_maintenances(&self) -> impl Future<Item = Vec<Maintenance>, Error = Error> { + // let client = request_client!(); + + // let response = retry(|| { + // client.get(status!("/scheduled-maintenances/upcoming.json")) + // })?; + + // let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + + // match map.remove("scheduled_maintenances") { + // Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) + // .map_err(From::from), + // None => Ok(vec![]), + // } + // } + + /// Gets a user by Id. + pub fn get_user(&self, user_id: u64) -> impl Future<Item = User, Error = Error> { + self.get(Route::GetUser { user_id }) } - let mut uri = format!("/invites/{}", invite); + /// Gets our DM channels. + pub fn get_user_dm_channels(&self) + -> impl Future<Item = Vec<PrivateChannel>, Error = Error> { + self.get(Route::GetUserDmChannels) + } - if stats { - uri.push_str("?with_counts=true"); + /// Gets all voice regions. + pub fn get_voice_regions(&self) -> impl Future<Item = Vec<VoiceRegion>, Error = Error> { + self.get(Route::GetVoiceRegions) } - let response = request!(Route::InvitesCode, get, "{}", uri); + /// Retrieves a webhook given its Id. + /// + /// This method requires authentication, whereas [`get_webhook_with_token`] does + /// not. + /// + /// # Examples + /// + /// Retrieve a webhook by Id: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let webhook = http::get_webhook(id).expect("Error getting webhook"); + /// ``` + /// + /// [`get_webhook_with_token`]: fn.get_webhook_with_token.html + pub fn get_webhook(&self, webhook_id: u64) -> impl Future<Item = Webhook, Error = Error> { + self.get(Route::GetWebhook { webhook_id }) + } - serde_json::from_reader::<HyperResponse, Invite>(response) - .map_err(From::from) -} + /// Retrieves a webhook given its Id and unique token. + /// + /// This method does _not_ require authentication. + /// + /// # Examples + /// + /// Retrieve a webhook by Id and its unique token: + /// + /// ```rust,no_run + /// use serenity::http; + /// + /// let id = 245037420704169985; + /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; + /// + /// let webhook = http::get_webhook_with_token(id, token) + /// .expect("Error getting webhook"); + /// ``` + pub fn get_webhook_with_token<'a>( + &self, + webhook_id: u64, + token: &'a str, + ) -> Box<Future<Item = Webhook, Error = Error> + 'a> { + self.get(Route::GetWebhookWithToken { token, webhook_id }) + } -/// Gets member of a guild. -pub fn get_member(guild_id: u64, user_id: u64) -> Result<Member> { - let response = request!( - Route::GuildsIdMembersId(guild_id), - get, - "/guilds/{}/members/{}", - guild_id, - user_id - ); + /// Kicks a member from a guild. + pub fn kick_member(&self, guild_id: u64, user_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::KickMember { guild_id, user_id }, None) + } - let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?; + /// Leaves a group DM. + pub fn leave_group(&self, group_id: u64) -> impl Future<Item = (), Error = Error> { + self.verify(Route::DeleteChannel { + channel_id: group_id, + }, None) + } - if let Some(map) = v.as_object_mut() { - map.insert("guild_id".to_string(), Value::Number(Number::from(guild_id))); + /// Leaves a guild. + pub fn leave_guild(&self, guild_id: u64) -> impl Future<Item = (), Error = Error> { + self.verify(Route::LeaveGuild { guild_id }, None) } - serde_json::from_value::<Member>(v).map_err(From::from) -} + /// Deletes a user from group DM. + pub fn remove_group_recipient(&self, group_id: u64, user_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::RemoveGroupRecipient { group_id, user_id }, None) + } -/// Gets a message by an Id, bots only. -pub fn get_message(channel_id: u64, message_id: u64) -> Result<Message> { - let response = request!( - Route::ChannelsIdMessagesId(LightMethod::Any, channel_id), - get, - "/channels/{}/messages/{}", - channel_id, - message_id - ); - - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + // /// Sends file(s) to a channel. + // /// + // /// # Errors + // /// + // /// Returns an + // /// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] + // /// if the file is too large to send. + // /// + // /// [`HttpError::InvalidRequest`]: enum.HttpError.html#variant.InvalidRequest + pub fn send_files<F, T, It>( + &self, + channel_id: u64, + files: It, + f: F, + ) -> Box<Future<Item = Message, Error = Error>> + where F: FnOnce(CreateMessage) -> CreateMessage, + T: Into<AttachmentType>, + It: IntoIterator<Item = T> { + let msg = f(CreateMessage::default()); + let map = serenity_utils::vecmap_to_json_map(msg.0); + + let uri = try_uri!(Path::channel_messages(channel_id).as_ref()); + let mut form = Form::default(); + let mut file_num = "0".to_string(); + + for file in files { + match file.into() { + AttachmentType::Bytes((mut bytes, filename)) => { + form.add_reader_file( + file_num.to_owned(), + Cursor::new(bytes), + filename, + ); + }, + AttachmentType::File((mut f, filename)) => { + form.add_reader_file( + file_num.to_owned(), + f, + filename, + ); + }, + } -/// Gets X messages from a channel. -pub fn get_messages(channel_id: u64, query: &str) -> Result<Vec<Message>> { - let url = format!(api!("/channels/{}/messages{}"), channel_id, query); - let client = request_client!(); + unsafe { + let vec = file_num.as_mut_vec(); + vec[0] += 1; + } + } - let response = request(Route::ChannelsIdMessages(channel_id), || client.get(&url))?; + for (k, v) in map.into_iter() { + match v { + Value::Bool(false) => form.add_text(k, "false"), + Value::Bool(true) => form.add_text(k, "true"), + Value::Number(inner) => form.add_text(k, inner.to_string()), + Value::String(inner) => form.add_text(k, inner), + Value::Object(inner) => form.add_text(k, ftry!(serde_json::to_string(&inner))), + _ => continue, + } + } - serde_json::from_reader::<HyperResponse, Vec<Message>>(response) - .map_err(From::from) -} + let mut request = Request::new(Method::Get, uri); + form.set_body(&mut request); -/// Gets all pins of a channel. -pub fn get_pins(channel_id: u64) -> Result<Vec<Message>> { - let response = request!( - Route::ChannelsIdPins(channel_id), - get, - "/channels/{}/pins", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Vec<Message>>(response) - .map_err(From::from) -} + let client = Rc::clone(&self.multiparter); -/// Gets user Ids based on their reaction to a message. This endpoint is dumb. -pub fn get_reaction_users(channel_id: u64, - message_id: u64, - reaction_type: &ReactionType, - limit: u8, - after: Option<u64>) - -> Result<Vec<User>> { - let mut uri = format!( - "/channels/{}/messages/{}/reactions/{}?limit={}", - channel_id, - message_id, - reaction_type.as_data(), - limit - ); - - if let Some(user_id) = after { - write!(uri, "&after={}", user_id)?; - } - - let response = request!( - Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - get, - "{}", - uri - ); - - serde_json::from_reader::<HyperResponse, Vec<User>>(response) - .map_err(From::from) -} + let done = client.request(request) + .from_err() + .and_then(verify_status) + .and_then(|res| res.body().concat2().map_err(From::from)) + .and_then(|body| serde_json::from_slice(&body).map_err(From::from)); -/// Gets the current unresolved incidents from Discord's Status API. -/// -/// Does not require authentication. -pub fn get_unresolved_incidents() -> Result<Vec<Incident>> { - let client = request_client!(); + Box::new(done) + } - let response = retry(|| client.get(status!("/incidents/unresolved.json")))?; + /// Sends a message to a channel. + pub fn send_message<F>(&self, channel_id: u64, f: F) -> impl Future<Item = Message, Error = Error> + where F: FnOnce(CreateMessage) -> CreateMessage { + let msg = f(CreateMessage::default()); + let map = Value::Object(serenity_utils::vecmap_to_json_map(msg.0)); - let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + self.post(Route::CreateMessage { channel_id }, Some(&map)) + } - match map.remove("incidents") { - Some(v) => serde_json::from_value::<Vec<Incident>>(v) - .map_err(From::from), - None => Ok(vec![]), + /// Pins a message in a channel. + pub fn pin_message(&self, channel_id: u64, message_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::PinMessage { channel_id, message_id }, None) } -} -/// Gets the upcoming (planned) maintenances from Discord's Status API. -/// -/// Does not require authentication. -pub fn get_upcoming_maintenances() -> Result<Vec<Maintenance>> { - let client = request_client!(); + /// Unbans a user from a guild. + pub fn remove_ban(&self, guild_id: u64, user_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::RemoveBan { guild_id, user_id }, None) + } - let response = retry(|| { - client.get(status!("/scheduled-maintenances/upcoming.json")) - })?; + /// Deletes a single [`Role`] from a [`Member`] in a [`Guild`]. + /// + /// **Note**: Requires the [Manage Roles] permission and respect of role + /// hierarchy. + /// + /// [`Guild`]: ../model/guild/struct.Guild.html + /// [`Member`]: ../model/guild/struct.Member.html + /// [`Role`]: ../model/guild/struct.Role.html + /// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html + pub fn remove_member_role( + &self, + guild_id: u64, + user_id: u64, + role_id: u64, + ) -> impl Future<Item = (), Error = Error> { + self.verify( + Route::RemoveMemberRole { guild_id, role_id, user_id }, + None, + ) + } - let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?; + /// Starts removing some members from a guild based on the last time they've been online. + pub fn start_guild_prune(&self, guild_id: u64, days: u16) + -> impl Future<Item = GuildPrune, Error = Error> { + self.post(Route::StartGuildPrune { + days: days as u64, + guild_id, + }, None) + } - match map.remove("scheduled_maintenances") { - Some(v) => serde_json::from_value::<Vec<Maintenance>>(v) - .map_err(From::from), - None => Ok(vec![]), + /// Starts syncing an integration with a guild. + pub fn start_integration_sync(&self, guild_id: u64, integration_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify( + Route::StartIntegrationSync { guild_id, integration_id }, + None, + ) } -} -/// Gets a user by Id. -pub fn get_user(user_id: u64) -> Result<User> { - let response = request!(Route::UsersId, get, "/users/{}", user_id); + /// Unpins a message from a channel. + pub fn unpin_message(&self, channel_id: u64, message_id: u64) + -> impl Future<Item = (), Error = Error> { + self.verify(Route::UnpinMessage { channel_id, message_id }, None) + } - serde_json::from_reader::<HyperResponse, User>(response) - .map_err(From::from) -} + fn delete<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + self.request(route, map) + } -/// Gets our DM channels. -pub fn get_user_dm_channels() -> Result<Vec<PrivateChannel>> { - let response = request!(Route::UsersMeChannels, get, "/users/@me/channels"); + fn get<'a, T: DeserializeOwned + 'static>(&self, route: Route<'a>) + -> Box<Future<Item = T, Error = Error> + 'a> { + self.request(route, None) + } - serde_json::from_reader::<HyperResponse, Vec<PrivateChannel>>(response) - .map_err(From::from) -} + fn patch<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + self.request(route, map) + } -/// Gets all voice regions. -pub fn get_voice_regions() -> Result<Vec<VoiceRegion>> { - let response = request!(Route::VoiceRegions, get, "/voice/regions"); + fn post<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + self.request(route, map) + } - serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response) - .map_err(From::from) -} + fn request<'a, T: DeserializeOwned + 'static>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = T, Error = Error>> { + let (method, path, url) = route.deconstruct(); -/// Retrieves a webhook given its Id. -/// -/// This method requires authentication, whereas [`get_webhook_with_token`] does -/// not. -/// -/// # Examples -/// -/// Retrieve a webhook by Id: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let webhook = http::get_webhook(id).expect("Error getting webhook"); -/// ``` -/// -/// [`get_webhook_with_token`]: fn.get_webhook_with_token.html -pub fn get_webhook(webhook_id: u64) -> Result<Webhook> { - let response = request!( - Route::WebhooksId(webhook_id), - get, - "/webhooks/{}", - webhook_id, - ); - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + let built_uri = try_uri!(url.as_ref()); + let mut request = Request::new(method.hyper_method(), built_uri); -/// Retrieves a webhook given its Id and unique token. -/// -/// This method does _not_ require authentication. -/// -/// # Examples -/// -/// Retrieve a webhook by Id and its unique token: -/// -/// ```rust,no_run -/// use serenity::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// -/// let webhook = http::get_webhook_with_token(id, token) -/// .expect("Error getting webhook"); -/// ``` -pub fn get_webhook_with_token(webhook_id: u64, token: &str) -> Result<Webhook> { - let client = request_client!(); - - let response = retry(|| { - client - .get(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - }).map_err(Error::Hyper)?; - - serde_json::from_reader::<HyperResponse, Webhook>(response) - .map_err(From::from) -} + if let Some(value) = map { + request.set_body(ftry!(serde_json::to_string(value))); + } -/// Kicks a member from a guild. -pub fn kick_member(guild_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdMembersId(guild_id), - delete, - "/guilds/{}/members/{}", - guild_id, - user_id - ), - ) -} + { + let headers = request.headers_mut(); + headers.set(Authorization(self.token())); + headers.set(ContentType::json()); + } -/// Leaves a group DM. -pub fn leave_group(guild_id: u64) -> Result<Group> { - let response = request!(Route::None, delete, "/channels/{}", guild_id); + let client = Rc::clone(&self.client); - serde_json::from_reader::<HyperResponse, Group>(response) - .map_err(From::from) -} + Box::new(ftry!(self.ratelimiter.try_borrow_mut()).take(&path) + .and_then(move |_| client.request(request).map_err(From::from)) + .from_err() + .and_then(verify_status) + .and_then(|res| res.body().concat2().map_err(From::from)) + .and_then(|body| serde_json::from_slice(&body).map_err(From::from))) + } -/// Leaves a guild. -pub fn leave_guild(guild_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::UsersMeGuildsId, - delete, - "/users/@me/guilds/{}", - guild_id - ), - ) -} + fn verify<'a>( + &self, + route: Route<'a>, + map: Option<&Value>, + ) -> Box<Future<Item = (), Error = Error>> { + let (method, path, url) = route.deconstruct(); -/// Deletes a user from group DM. -pub fn remove_group_recipient(group_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::None, - delete, - "/channels/{}/recipients/{}", - group_id, - user_id - ), - ) -} + let mut request = Request::new( + method.hyper_method(), + try_uri!(url.as_ref()), + ); -/// Sends file(s) to a channel. -/// -/// # Errors -/// -/// Returns an -/// [`HttpError::InvalidRequest(PayloadTooLarge)`][`HttpError::InvalidRequest`] -/// if the file is too large to send. -/// -/// [`HttpError::InvalidRequest`]: enum.HttpError.html#variant.InvalidRequest -pub fn send_files<'a, T, It: IntoIterator<Item=T>>(channel_id: u64, files: It, map: JsonMap) -> Result<Message> - where T: Into<AttachmentType<'a>> { - let uri = format!(api!("/channels/{}/messages"), channel_id); - let url = match Url::parse(&uri) { - Ok(url) => url, - Err(_) => return Err(Error::Url(uri)), - }; - - let tc = NativeTlsClient::new()?; - let connector = HttpsConnector::new(tc); - let mut request = Request::with_connector(Method::Post, url, &connector)?; - request - .headers_mut() - .set(header::Authorization(TOKEN.lock().clone())); - request - .headers_mut() - .set(header::UserAgent(constants::USER_AGENT.to_string())); - - let mut request = Multipart::from_request(request)?; - let mut file_num = "0".to_string(); - - for file in files { - match file.into() { - AttachmentType::Bytes((mut bytes, filename)) => { - request - .write_stream(&file_num, &mut bytes, Some(filename), None)?; - }, - AttachmentType::File((mut f, filename)) => { - request - .write_stream(&file_num, &mut f, Some(filename), None)?; - }, - AttachmentType::Path(p) => { - request.write_file(&file_num, &p)?; - }, + if let Some(value) = map { + request.set_body(ftry!(serde_json::to_string(value))); } - unsafe { - let vec = file_num.as_mut_vec(); - vec[0] += 1; + { + let headers = request.headers_mut(); + headers.set(Authorization(self.token())); + headers.set(ContentType::json()); } - } - for (k, v) in map { - match v { - Value::Bool(false) => request.write_text(&k, "false")?, - Value::Bool(true) => request.write_text(&k, "true")?, - Value::Number(inner) => request.write_text(&k, inner.to_string())?, - Value::String(inner) => request.write_text(&k, inner)?, - Value::Object(inner) => request.write_text(&k, serde_json::to_string(&inner)?)?, - _ => continue, - }; - } - - let response = request.send()?; + let client = Rc::clone(&self.client); - if response.status.class() != StatusClass::Success { - return Err(Error::Http(HttpError::UnsuccessfulRequest(response))); + Box::new(ftry!(self.ratelimiter.try_borrow_mut()).take(&path) + .and_then(move |_| client.request(request).map_err(From::from)) + .map_err(From::from) + .and_then(verify_status) + .map(|_| ())) } - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + fn token(&self) -> String { + let pointer = Rc::into_raw(Rc::clone(&self.token)); + let token = unsafe { + (*pointer).clone() + }; -/// Sends a message to a channel. -pub fn send_message(channel_id: u64, map: &Value) -> Result<Message> { - let body = map.to_string(); - let response = request!( - Route::ChannelsIdMessages(channel_id), - post(body), - "/channels/{}/messages", - channel_id - ); - - serde_json::from_reader::<HyperResponse, Message>(response) - .map_err(From::from) -} + unsafe { + drop(Rc::from_raw(pointer)); + } -/// Pins a message in a channel. -pub fn pin_message(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdPinsMessageId(channel_id), - put, - "/channels/{}/pins/{}", - channel_id, - message_id - ), - ) + token + } } -/// Unbans a user from a guild. -pub fn remove_ban(guild_id: u64, user_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdBansUserId(guild_id), - delete, - "/guilds/{}/bans/{}", - guild_id, - user_id - ), - ) -} -/// Deletes a single [`Role`] from a [`Member`] in a [`Guild`]. +/// Verifies the status of the response according to the method used to create +/// the accompanying request. /// -/// **Note**: Requires the [Manage Roles] permission and respect of role -/// hierarchy. +/// If the status code is correct (a 200 is expected and the response status is +/// also 200), then the future resolves. Otherwise, a leaf future is returned +/// with an error as the `Error` type. /// -/// [`Guild`]: ../model/guild/struct.Guild.html -/// [`Member`]: ../model/guild/struct.Member.html -/// [`Role`]: ../model/guild/struct.Role.html -/// [Manage Roles]: ../model/permissions/constant.MANAGE_ROLES.html -pub fn remove_member_role(guild_id: u64, user_id: u64, role_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdMembersIdRolesId(guild_id), - delete, - "/guilds/{}/members/{}/roles/{}", - guild_id, - user_id, - role_id - ), - ) -} - -/// Starts removing some members from a guild based on the last time they've been online. -pub fn start_guild_prune(guild_id: u64, map: &Value) -> Result<GuildPrune> { - let body = map.to_string(); - let response = request!( - Route::GuildsIdPrune(guild_id), - post(body), - "/guilds/{}/prune", - guild_id - ); - - serde_json::from_reader::<HyperResponse, GuildPrune>(response) - .map_err(From::from) -} - -/// Starts syncing an integration with a guild. -pub fn start_integration_sync(guild_id: u64, integration_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::GuildsIdIntegrationsIdSync(guild_id), - post, - "/guilds/{}/integrations/{}/sync", - guild_id, - integration_id - ), - ) -} - -/// Unpins a message from a channel. -pub fn unpin_message(channel_id: u64, message_id: u64) -> Result<()> { - verify( - 204, - request!( - Route::ChannelsIdPinsMessageId(channel_id), - delete, - "/channels/{}/pins/{}", - channel_id, - message_id - ), - ) -} - -fn request<'a, F>(route: Route, f: F) -> Result<HyperResponse> - where F: Fn() -> RequestBuilder<'a> { - let response = ratelimiting::perform(route, || { - f().header(header::Authorization(TOKEN.lock().clone())) - .header(header::ContentType::json()) - })?; - - if response.status.class() == StatusClass::Success { - Ok(response) +/// # Errors +/// +/// Returns [`Error::InvalidRequest`] if the response status code is unexpected. +/// +/// [`Error::InvalidRequest`]: enum.Error.html#variant.InvalidRequest +fn verify_status(response: Response) -> + Box<Future<Item = Response, Error = Error>> { + if response.status().is_success() { + Box::new(future::ok(response)) } else { - Err(Error::Http(HttpError::UnsuccessfulRequest(response))) - } -} - -pub(crate) fn retry<'a, F>(f: F) -> HyperResult<HyperResponse> - where F: Fn() -> RequestBuilder<'a> { - let req = || { - f().header(header::UserAgent(constants::USER_AGENT.to_string())) - .send() - }; - - match req() { - Err(HyperError::Io(ref io)) if io.kind() == IoErrorKind::ConnectionAborted => req(), - other => other, + Box::new(future::err(Error::Http(HttpError::InvalidRequest(response)))) } } -fn verify(expected: u16, response: HyperResponse) -> Result<()> { - if response.status.to_u16() == expected { - return Ok(()); - } - - debug!("Expected {}, got {}", expected, response.status); - trace!("Unsuccessful response: {:?}", response); - - Err(Error::Http(HttpError::UnsuccessfulRequest(response))) -} - -/// Enum that allows a user to pass a `Path` or a `File` type to `send_files` -#[derive(Clone, Debug)] -pub enum AttachmentType<'a> { +/// Enum that allows a user to pass a `Path` or a `File` type to `send_files`. +#[derive(Debug)] +pub enum AttachmentType { /// Indicates that the `AttachmentType` is a byte slice with a filename. - Bytes((&'a [u8], &'a str)), + Bytes((Vec<u8>, String)), /// Indicates that the `AttachmentType` is a `File` - File((&'a File, &'a str)), - /// Indicates that the `AttachmentType` is a `Path` - Path(&'a Path), + File((File, String)), } -impl<'a> From<(&'a [u8], &'a str)> for AttachmentType<'a> { - fn from(params: (&'a [u8], &'a str)) -> AttachmentType { AttachmentType::Bytes(params) } -} - -impl<'a> From<&'a str> for AttachmentType<'a> { - fn from(s: &'a str) -> AttachmentType { AttachmentType::Path(Path::new(s)) } -} - -impl<'a> From<&'a Path> for AttachmentType<'a> { - fn from(path: &'a Path) -> AttachmentType { - AttachmentType::Path(path) +impl From<(Vec<u8>, String)> for AttachmentType { + fn from(params: (Vec<u8>, String)) -> AttachmentType { + AttachmentType::Bytes(params) } } -impl<'a> From<&'a PathBuf> for AttachmentType<'a> { - fn from(pathbuf: &'a PathBuf) -> AttachmentType { AttachmentType::Path(pathbuf.as_path()) } -} - -impl<'a> From<(&'a File, &'a str)> for AttachmentType<'a> { - fn from(f: (&'a File, &'a str)) -> AttachmentType<'a> { AttachmentType::File((f.0, f.1)) } +impl From<(File, String)> for AttachmentType { + fn from(f: (File, String)) -> AttachmentType { + AttachmentType::File((f.0, f.1)) + } } /// Representation of the method of a query to send for the [`get_guilds`] diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs index 64bdf68..9f41f24 100644 --- a/src/http/ratelimiting.rs +++ b/src/http/ratelimiting.rs @@ -38,41 +38,156 @@ //! differentiating between different ratelimits. //! //! [Taken from]: https://discordapp.com/developers/docs/topics/rate-limits#rate-limits -#![allow(zero_ptr)] +#![cfg_attr(feature = "cargo-clippy", allow(zero_ptr))] use chrono::{DateTime, Utc}; -use hyper::client::{RequestBuilder, Response}; +use futures::sync::oneshot::{self, Receiver, Sender}; +use futures::{Future, future}; +use hyper::client::{Response}; use hyper::header::Headers; -use hyper::status::StatusCode; -use internal::prelude::*; -use parking_lot::Mutex; -use std::{ - collections::HashMap, - sync::Arc, - time::Duration, - str, - thread, - i64 -}; -use super::{HttpError, LightMethod}; - -/// The calculated offset of the time difference between Discord and the client -/// in seconds. +use hyper::StatusCode; +use std::cell::RefCell; +use std::collections::{HashMap, VecDeque}; +use std::error::Error as StdError; +use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::rc::Rc; +use std::time::Duration; +use std::{i64, str, u8}; +use super::{Error, Path, Result}; +use tokio_core::reactor::Handle; +use tokio_timer::Timer; + +#[derive(Debug)] +pub enum RateLimitError { + /// When the decoding of a header could not be properly decoded as UTF-8. + DecodingUtf8, + /// When the decoding of a header could not be properly decoded as an `i64`. + DecodingInteger, +} + +impl Display for RateLimitError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_str(self.description()) + } +} + +impl StdError for RateLimitError { + fn description(&self) -> &str { + use self::RateLimitError::*; + + match *self { + DecodingInteger => "Error decoding a header into an i64", + DecodingUtf8 => "Error decoding a header from UTF-8", + } + } +} + +#[derive(Clone, Debug)] +pub enum RateLimit { + Global(i64), + NotReached(RateLimitHeaders), + Reached(i64), +} + +impl RateLimit { + fn from_headers(headers: &Headers) -> Result<Self> { + if headers.get_raw("x-ratelimit-global").is_some() { + if let Some(retry_after) = parse_header(headers, "retry-after")? { + debug!("Global ratelimited for {}ms", retry_after); + + return Ok(RateLimit::Global(retry_after)); + } + + warn!("Globally ratelimited with no retry-after? Skipping..."); + } + + if let Some(retry_after) = parse_header(headers, "retry-after")? { + return Ok(RateLimit::Reached(retry_after)); + } + + let limit = parse_header(headers, "x-ratelimit-limit")?.map(|x| x as u8); + let remaining = parse_header(headers, "x-ratelimit-remaining")?.map(|x| x as u8); + let reset = parse_header(headers, "x-ratelimit-remaining")?; + + Ok(RateLimit::NotReached(RateLimitHeaders { + limit, + remaining, + reset, + })) + } +} + +#[derive(Clone, Debug)] +pub struct RateLimitHeaders { + pub limit: Option<u8>, + pub remaining: Option<u8>, + pub reset: Option<i64>, +} + +/// A set of data containing information about the ratelimits for a particular +/// [`Route`], which is stored in the [`ROUTES`] mutex. /// -/// This does not have millisecond precision as calculating that isn't -/// realistic. +/// See the [Discord docs] on ratelimits for more information. /// -/// This is used in ratelimiting to help determine how long to wait for -/// pre-emptive ratelimits. For example, if the client is 2 seconds ahead, then -/// the client would think the ratelimit is over 2 seconds before it actually is -/// and would then send off queued requests. Using an offset, we can know that -/// there's actually still 2 seconds left (+/- some milliseconds). +/// **Note**: You should _not_ mutate any of the fields, as this can cause 429s. /// -/// This isn't a definitive solution to fix all problems, but it can help with -/// some precision gains. -static mut OFFSET: Option<i64> = None; +/// [`ROUTES`]: struct.ROUTES.html +/// [`Route`]: enum.Route.html +/// [Discord docs]: https://discordapp.com/developers/docs/topics/rate-limits +// todo: impl Debug +#[derive(Debug)] +pub struct Bucket { + /// The total number of requests that can be made in a period of time. + pub limit: i64, + /// A queue of requests that were held back due to a pre-emptive ratelimit. + pub queue: VecDeque<Sender<()>>, + /// The number of requests remaining in the period of time. + pub remaining: i64, + /// When the interval resets and the [`limit`] resets to the value of + /// [`remaining`]. + /// + /// [`limit`]: #structfield.limit + /// [`remaining`]: #structfield.remaining + pub reset: i64, + /// Whether the bucket has a timeout in the background to release (part of) + /// the queue. + pub timeout: bool, +} + +impl Default for Bucket { + fn default() -> Self { + Self { + limit: i64::MAX, + queue: VecDeque::new(), + remaining: i64::MAX, + reset: i64::MAX, + timeout: false, + } + } +} + +impl Bucket { + fn take(&mut self) -> Option<Receiver<()>> { + if self.reset == 0 { + let (tx, rx) = oneshot::channel(); + + self.queue.push_back(tx); + + Some(rx) + } else { + None + } + } +} + +#[derive(Clone, Debug, Default)] +pub struct Global { + pub blocked: bool, + pub queue: Rc<RefCell<VecDeque<Receiver<()>>>>, +} -lazy_static! { +#[derive(Clone, Debug)] +pub struct RateLimiter { /// The global mutex is a mutex unlocked and then immediately re-locked /// prior to every request, to abide by Discord's global ratelimit. /// @@ -87,7 +202,24 @@ lazy_static! { /// The only reason that you would need to use the global mutex is to /// block requests yourself. This has the side-effect of potentially /// blocking many of your event handlers or framework commands. - pub static ref GLOBAL: Arc<Mutex<()>> = Arc::new(Mutex::new(())); + pub global: Rc<RefCell<Global>>, + /// A handle to the core that this is running on. + pub handle: Handle, + /// The calculated offset of the time difference between Discord and the client + /// in seconds. + /// + /// This does not have millisecond precision as calculating that isn't + /// realistic. + /// + /// This is used in ratelimiting to help determine how long to wait for + /// pre-emptive ratelimits. For example, if the client is 2 seconds ahead, then + /// the client would think the ratelimit is over 2 seconds before it actually is + /// and would then send off queued requests. Using an offset, we can know that + /// there's actually still 2 seconds left (+/- some milliseconds). + /// + /// This isn't a definitive solution to fix all problems, but it can help with + /// some precision gains. + offset: Option<i64>, /// The routes mutex is a HashMap of each [`Route`] and their respective /// ratelimit information. /// @@ -108,269 +240,122 @@ lazy_static! { /// /// [`RateLimit`]: struct.RateLimit.html /// [`Route`]: enum.Route.html - pub static ref ROUTES: Arc<Mutex<HashMap<Route, Arc<Mutex<RateLimit>>>>> = { - Arc::new(Mutex::new(HashMap::default())) - }; + pub routes: Rc<RefCell<HashMap<Path, Bucket>>>, } -/// A representation of all routes registered within the library. These are safe -/// and memory-efficient representations of each path that functions exist for -/// in the [`http`] module. -/// -/// [`http`]: ../index.html -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Route { - /// Route for the `/channels/:channel_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsId(u64), - /// Route for the `/channels/:channel_id/invites` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdInvites(u64), - /// Route for the `/channels/:channel_id/messages` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessages(u64), - /// Route for the `/channels/:channel_id/messages/bulk-delete` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesBulkDelete(u64), - /// Route for the `/channels/:channel_id/messages/:message_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - // This route is a unique case. The ratelimit for message _deletions_ is - // different than the overall route ratelimit. - // - // Refer to the docs on [Rate Limits] in the yellow warning section. - // - // Additionally, this needs to be a `LightMethod` from the parent module - // and _not_ a `hyper` `Method` due to `hyper`'s not deriving `Copy`. - // - // [Rate Limits]: https://discordapp.com/developers/docs/topics/rate-limits - ChannelsIdMessagesId(LightMethod, u64), - /// Route for the `/channels/:channel_id/messages/:message_id/ack` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesIdAck(u64), - /// Route for the `/channels/:channel_id/messages/:message_id/reactions` - /// path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesIdReactions(u64), - /// Route for the - /// `/channels/:channel_id/messages/:message_id/reactions/:reaction/@me` - /// path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdMessagesIdReactionsUserIdType(u64), - /// Route for the `/channels/:channel_id/permissions/:target_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdPermissionsOverwriteId(u64), - /// Route for the `/channels/:channel_id/pins` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdPins(u64), - /// Route for the `/channels/:channel_id/pins/:message_id` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdPinsMessageId(u64), - /// Route for the `/channels/:channel_id/typing` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdTyping(u64), - /// Route for the `/channels/:channel_id/webhooks` path. - /// - /// The data is the relevant [`ChannelId`]. - /// - /// [`ChannelId`]: ../../model/id/struct.ChannelId.html - ChannelsIdWebhooks(u64), - /// Route for the `/gateway` path. - Gateway, - /// Route for the `/gateway/bot` path. - GatewayBot, - /// Route for the `/guilds` path. - Guilds, - /// Route for the `/guilds/:guild_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsId(u64), - /// Route for the `/guilds/:guild_id/bans` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdBans(u64), - /// Route for the `/guilds/:guild_id/audit-logs` path. - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdAuditLogs(u64), - /// Route for the `/guilds/:guild_id/bans/:user_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdBansUserId(u64), - /// Route for the `/guilds/:guild_id/channels/:channel_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdChannels(u64), - /// Route for the `/guilds/:guild_id/embed` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdEmbed(u64), - /// Route for the `/guilds/:guild_id/emojis` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdEmojis(u64), - /// Route for the `/guilds/:guild_id/emojis/:emoji_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdEmojisId(u64), - /// Route for the `/guilds/:guild_id/integrations` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdIntegrations(u64), - /// Route for the `/guilds/:guild_id/integrations/:integration_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdIntegrationsId(u64), - /// Route for the `/guilds/:guild_id/integrations/:integration_id/sync` - /// path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdIntegrationsIdSync(u64), - /// Route for the `/guilds/:guild_id/invites` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdInvites(u64), - /// Route for the `/guilds/:guild_id/members` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembers(u64), - /// Route for the `/guilds/:guild_id/members/:user_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembersId(u64), - /// Route for the `/guilds/:guild_id/members/:user_id/roles/:role_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembersIdRolesId(u64), - /// Route for the `/guilds/:guild_id/members/@me/nick` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdMembersMeNick(u64), - /// Route for the `/guilds/:guild_id/prune` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdPrune(u64), - /// Route for the `/guilds/:guild_id/regions` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdRegions(u64), - /// Route for the `/guilds/:guild_id/roles` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdRoles(u64), - /// Route for the `/guilds/:guild_id/roles/:role_id` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdRolesId(u64), - /// Route for the `/guilds/:guild_id/vanity-url` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdVanityUrl(u64), - /// Route for the `/guilds/:guild_id/webhooks` path. - /// - /// The data is the relevant [`GuildId`]. - /// - /// [`GuildId`]: struct.GuildId.html - GuildsIdWebhooks(u64), - /// Route for the `/invites/:code` path. - InvitesCode, - /// Route for the `/users/:user_id` path. - UsersId, - /// Route for the `/users/@me` path. - UsersMe, - /// Route for the `/users/@me/channels` path. - UsersMeChannels, - /// Route for the `/users/@me/guilds` path. - UsersMeGuilds, - /// Route for the `/users/@me/guilds/:guild_id` path. - UsersMeGuildsId, - /// Route for the `/voice/regions` path. - VoiceRegions, - /// Route for the `/webhooks/:webhook_id` path. - WebhooksId(u64), - /// Route where no ratelimit headers are in place. - /// - /// This is a special case, in that if the route is `None` then pre- and - /// post-hooks are not executed. - None, +impl RateLimiter { + pub fn new(handle: Handle) -> Self { + Self { + global: Rc::new(RefCell::new(Global::default())), + offset: None, + routes: Rc::new(RefCell::new(HashMap::new())), + handle, + } + } + + pub fn take(&mut self, route: &Path) + -> Box<Future<Item = (), Error = Error>> { + // TODO: handle global + let mut routes = self.routes.borrow_mut(); + let bucket = routes.entry(*route).or_insert_with(Default::default); + let take = bucket.take(); + + match take { + Some(rx) => { + if !bucket.timeout { + let reset_ms = (bucket.reset * 1000) as u64; + let now = Utc::now(); + let now_millis = now.timestamp_subsec_millis() as i64; + let now_ms = (now.timestamp() * 1000) + now_millis; + let wait_ms = reset_ms.saturating_sub(now_ms as u64); + let duration = Duration::from_millis(wait_ms as u64); + + let done = Timer::default() + .sleep(duration) + .map(|_| { + () + }).map_err(|why| { + warn!("Err with pre-ratelimit sleep: {:?}", why); + + () + }); + + self.handle.spawn(done); + } + + Box::new(rx.from_err()) + }, + None => Box::new(future::ok(())), + } + } + + pub fn handle<'a>(&'a mut self, route: &'a Path, response: &'a Response) + -> Result<Option<Box<Future<Item = (), Error = ()>>>> { + let mut routes = self.routes.borrow_mut(); + let bucket = routes.entry(*route).or_insert_with(Default::default); + + if response.status() != StatusCode::TooManyRequests { + return Ok(None); + } + + Ok(match RateLimit::from_headers(&response.headers())? { + RateLimit::Global(millis) => { + debug!("Globally ratelimited for {:?}ms", millis); + + self.global.borrow_mut().blocked = true; + let global = Rc::clone(&self.global); + + let done = Timer::default() + .sleep(Duration::from_millis(millis as u64)) + .map(move |_| { + let mut global = global.borrow_mut(); + global.blocked = false; + }) + .map_err(|why| { + warn!("Err with global ratelimit timer: {:?}", why); + + () + }); + + Some(Box::new(done)) + }, + RateLimit::NotReached(headers) => { + let RateLimitHeaders { limit, remaining, reset } = headers; + + if let Some(reset) = reset { + if reset != bucket.reset { + bucket.reset = reset; + + if let Some(limit) = limit { + bucket.limit = limit as i64; + } + + if let Some(remaining) = remaining { + bucket.remaining = remaining as i64; + } + } + } + + None + }, + RateLimit::Reached(millis) => { + debug!("Ratelimited on route {:?} for {:?}ms", route, millis); + + let done = Timer::default() + .sleep(Duration::from_millis(millis as u64)) + .map_err(|why| { + warn!("Err with ratelimited timer: {:?}", why); + + () + }); + + Some(Box::new(done)) + }, + }) + } } +/* pub(crate) fn perform<'a, F>(route: Route, f: F) -> Result<Response> where F: Fn() -> RequestBuilder<'a> { loop { @@ -538,8 +523,11 @@ impl RateLimit { }) } } +*/ -fn calculate_offset(header: Option<&[Vec<u8>]>) { +#[allow(dead_code)] +// todo +fn calculate_offset(header: Option<&[Vec<u8>]>) -> Option<i64> { // Get the current time as soon as possible. let now = Utc::now().timestamp(); @@ -560,23 +548,32 @@ fn calculate_offset(header: Option<&[Vec<u8>]>) { let diff = offset - now; - unsafe { - OFFSET = Some(diff); + debug!("[ratelimiting] Set the ratelimit offset to {}", diff); - debug!("[ratelimiting] Set the ratelimit offset to {}", diff); - } + return Some(diff); } } + + None } -fn parse_header(headers: &Headers, header: &str) -> Result<Option<i64>> { - headers.get_raw(header).map_or(Ok(None), |header| { +fn parse_header(headers: &Headers, header_raw: &str) -> Result<Option<i64>> { + headers.get_raw(header_raw).map_or(Ok(None), |header| { str::from_utf8(&header[0]) - .map_err(|_| Error::Http(HttpError::RateLimitUtf8)) + .map_err(|why| { + warn!("Error parsing {} as utf8: {:?}", header_raw, why); + + RateLimitError::DecodingUtf8 + }) .and_then(|v| { v.parse::<i64>() .map(Some) - .map_err(|_| Error::Http(HttpError::RateLimitI64)) + .map_err(|why| { + warn!("Error parsing {}: {:?} to i64", header_raw, why); + + RateLimitError::DecodingInteger + }) }) + .map_err(From::from) }) } diff --git a/src/http/routing.rs b/src/http/routing.rs new file mode 100644 index 0000000..53b7875 --- /dev/null +++ b/src/http/routing.rs @@ -0,0 +1,1430 @@ +use std::borrow::Cow; +use std::fmt::{Display, Formatter, Result as FmtResult, Write}; +use super::LightMethod; + +/// A representation of all path registered within the library. These are safe +/// and memory-efficient representations of each path that request functions +/// exist for. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Path { + /// Route for the `/channels/:channel_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsId(u64), + /// Route for the `/channels/:channel_id/invites` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdInvites(u64), + /// Route for the `/channels/:channel_id/messages` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessages(u64), + /// Route for the `/channels/:channel_id/messages/bulk-delete` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesBulkDelete(u64), + /// Route for the `/channels/:channel_id/messages/:message_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + // This route is a unique case. The ratelimit for message _deletions_ is + // different than the overall route ratelimit. + // + // Refer to the docs on [Rate Limits] in the yellow warning section. + // + // Additionally, this needs to be a `LightMethod` from the parent module + // and _not_ a `hyper` `Method` due to `hyper`'s not deriving `Copy`. + // + // [Rate Limits]: https://discordapp.com/developers/docs/topics/rate-limits + ChannelsIdMessagesId(LightMethod, u64), + /// Route for the `/channels/:channel_id/messages/:message_id/ack` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesIdAck(u64), + /// Route for the `/channels/:channel_id/messages/:message_id/reactions` + /// path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesIdReactions(u64), + /// Route for the + /// `/channels/:channel_id/messages/:message_id/reactions/:reaction/@me` + /// path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdMessagesIdReactionsUserIdType(u64), + /// Route for the `/channels/:channel_id/permissions/:target_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdPermissionsOverwriteId(u64), + /// Route for the `/channels/:channel_id/pins` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdPins(u64), + /// Route for the `/channels/:channel_id/pins/:message_id` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdPinsMessageId(u64), + /// Route for the `/channels/:channel_id/recipients/:user_id` path. + /// + /// The data is the relevant `ChannelId`. + ChannelsIdRecipientsId(u64), + /// Route for the `/channels/:channel_id/typing` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdTyping(u64), + /// Route for the `/channels/:channel_id/webhooks` path. + /// + /// The data is the relevant [`ChannelId`]. + /// + /// [`ChannelId`]: ../../model/id/struct.ChannelId.html + ChannelsIdWebhooks(u64), + /// Route for the `/gateway` path. + Gateway, + /// Route for the `/gateway/bot` path. + GatewayBot, + /// Route for the `/guilds` path. + Guilds, + /// Route for the `/guilds/:guild_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsId(u64), + /// Route for the `/guilds/:guild_id/bans` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdBans(u64), + /// Route for the `/guilds/:guild_id/audit-logs` path. + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdAuditLogs(u64), + /// Route for the `/guilds/:guild_id/bans/:user_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdBansUserId(u64), + /// Route for the `/guilds/:guild_id/channels/:channel_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdChannels(u64), + /// Route for the `/guilds/:guild_id/embed` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdEmbed(u64), + /// Route for the `/guilds/:guild_id/emojis` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdEmojis(u64), + /// Route for the `/guilds/:guild_id/emojis/:emoji_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdEmojisId(u64), + /// Route for the `/guilds/:guild_id/integrations` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdIntegrations(u64), + /// Route for the `/guilds/:guild_id/integrations/:integration_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdIntegrationsId(u64), + /// Route for the `/guilds/:guild_id/integrations/:integration_id/sync` + /// path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdIntegrationsIdSync(u64), + /// Route for the `/guilds/:guild_id/invites` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdInvites(u64), + /// Route for the `/guilds/:guild_id/members` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembers(u64), + /// Route for the `/guilds/:guild_id/members/:user_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembersId(u64), + /// Route for the `/guilds/:guild_id/members/:user_id/roles/:role_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembersIdRolesId(u64), + /// Route for the `/guilds/:guild_id/members/@me/nick` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdMembersMeNick(u64), + /// Route for the `/guilds/:guild_id/prune` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdPrune(u64), + /// Route for the `/guilds/:guild_id/regions` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdRegions(u64), + /// Route for the `/guilds/:guild_id/roles` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdRoles(u64), + /// Route for the `/guilds/:guild_id/roles/:role_id` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdRolesId(u64), + /// Route for the `/guilds/:guild_id/vanity-url` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdVanityUrl(u64), + /// Route for the `/guilds/:guild_id/webhooks` path. + /// + /// The data is the relevant [`GuildId`]. + /// + /// [`GuildId`]: struct.GuildId.html + GuildsIdWebhooks(u64), + /// Route for the `/invites/:code` path. + InvitesCode, + StatusMaintenancesActive, + /// Route for the `/users/:user_id` path. + UsersId, + /// Route for the `/users/@me` path. + UsersMe, + /// Route for the `/users/@me/channels` path. + UsersMeChannels, + /// Route for the `/users/@me/guilds` path. + UsersMeGuilds, + /// Route for the `/users/@me/guilds/:guild_id` path. + UsersMeGuildsId, + /// Route for the `/voice/regions` path. + VoiceRegions, + /// Route for the `/webhooks/:webhook_id` path. + WebhooksId(u64), +} + +impl Path { + pub fn channel(channel_id: u64) -> String { + format!(api!("/channels/{}"), channel_id) + } + + pub fn channel_invites(channel_id: u64) -> String { + format!(api!("/channels/{}/invites"), channel_id) + } + + pub fn channel_message(channel_id: u64, message_id: u64) -> String { + format!(api!("/channels/{}/messages/{}"), channel_id, message_id) + } + + pub fn channel_message_reaction<D, T>( + channel_id: u64, + message_id: u64, + user_id: D, + reaction_type: T + ) -> String where D: Display, T: Display { + format!( + api!("/channels/{}/messages/{}/reactions/{}/{}"), + channel_id, + message_id, + reaction_type, + user_id, + ) + } + + pub fn channel_message_reactions( + channel_id: u64, + message_id: u64, + reaction: &str, + limit: u8, + after: Option<u64>, + ) -> String { + let mut uri = format!( + api!("/channels/{}/messages/{}/reactions/{}?limit={}"), + channel_id, + message_id, + reaction, + limit, + ); + + if let Some(after) = after { + let _ = write!(uri, "&after={}", after); + } + + uri + } + + pub fn channel_messages(channel_id: u64) -> String { + format!(api!("/channels/{}/messages"), channel_id) + } + + pub fn channel_messages_bulk_delete(channel_id: u64) -> String { + format!(api!("/channels/{}/messages/bulk-delete"), channel_id) + } + + pub fn channel_permission(channel_id: u64, target_id: u64) -> String { + format!(api!("/channels/{}/permissions/{}"), channel_id, target_id) + } + + pub fn channel_pin(channel_id: u64, message_id: u64) -> String { + format!(api!("/channels/{}/pins/{}"), channel_id, message_id) + } + + pub fn channel_pins(channel_id: u64) -> String { + format!(api!("/channels/{}/pins"), channel_id) + } + + pub fn channel_typing(channel_id: u64) -> String { + format!(api!("/channels/{}/typing"), channel_id) + } + + pub fn channel_webhooks(channel_id: u64) -> String { + format!(api!("/channels/{}/webhooks"), channel_id) + } + + pub fn gateway() -> &'static str { + api!("/gateway") + } + + pub fn gateway_bot() -> &'static str { + api!("/gateway/bot") + } + + pub fn group_recipient(group_id: u64, user_id: u64) -> String { + format!(api!("/channels/{}/recipients/{}"), group_id, user_id) + } + + pub fn guild(guild_id: u64) -> String { + format!(api!("/guilds/{}"), guild_id) + } + + pub fn guild_audit_logs( + guild_id: u64, + action_type: Option<u8>, + user_id: Option<u64>, + before: Option<u64>, + limit: Option<u8>, + ) -> String { + let mut s = format!( + api!("/guilds/{}/audit-logs?"), + guild_id, + ); + + if let Some(action_type) = action_type { + let _ = write!(s, "&action_type={}", action_type); + } + + if let Some(before) = before { + let _ = write!(s, "&before={}", before); + } + + if let Some(limit) = limit { + let _ = write!(s, "&limit={}", limit); + } + + if let Some(user_id) = user_id { + let _ = write!(s, "&user_id={}", user_id); + } + + s + } + + pub fn guild_ban(guild_id: u64, user_id: u64) -> String { + format!(api!("/guilds/{}/bans/{}"), guild_id, user_id) + } + + pub fn guild_ban_optioned( + guild_id: u64, + user_id: u64, + delete_message_days: u8, + reason: &str, + ) -> String { + format!( + api!("/guilds/{}/bans/{}?delete_message_days={}&reason={}"), + guild_id, + user_id, + delete_message_days, + reason, + ) + } + + pub fn guild_bans(guild_id: u64) -> String { + format!(api!("/guilds/{}/bans"), guild_id) + } + + pub fn guild_channels(guild_id: u64) -> String { + format!(api!("/guilds/{}/channels"), guild_id) + } + + pub fn guild_embed(guild_id: u64) -> String { + format!(api!("/guilds/{}/embed"), guild_id) + } + + pub fn guild_emojis(guild_id: u64) -> String { + format!(api!("/guilds/{}/emojis"), guild_id) + } + + pub fn guild_emoji(guild_id: u64, emoji_id: u64) -> String { + format!(api!("/guilds/{}/emojis/{}"), guild_id, emoji_id) + } + + pub fn guild_integration( + guild_id: u64, + integration_id: u64, + ) -> String { + format!(api!("/guilds/{}/integrations/{}"), guild_id, integration_id) + } + + pub fn guild_integration_sync( + guild_id: u64, + integration_id: u64, + ) -> String { + format!( + api!("/guilds/{}/integrations/{}/sync"), + guild_id, + integration_id, + ) + } + + pub fn guild_integrations(guild_id: u64) -> String { + format!(api!("/guilds/{}/integrations"), guild_id) + } + + pub fn guild_invites(guild_id: u64) -> String { + format!(api!("/guilds/{}/invites"), guild_id) + } + + pub fn guild_member(guild_id: u64, user_id: u64) -> String { + format!(api!("/guilds/{}/members/{}"), guild_id, user_id) + } + + pub fn guild_member_role( + guild_id: u64, + user_id: u64, + role_id: u64, + ) -> String { + format!( + api!("/guilds/{}/members/{}/roles/{}"), + guild_id, + user_id, + role_id, + ) + } + + pub fn guild_members(guild_id: u64) -> String { + format!(api!("/guilds/{}/members"), guild_id) + } + + pub fn guild_members_optioned( + guild_id: u64, + after: Option<u64>, + limit: Option<u64>, + ) -> String { + let mut s = format!(api!("/guilds/{}/members?"), guild_id); + + if let Some(after) = after { + let _ = write!(s, "&after={}", after); + } + + if let Some(limit) = limit { + let _ = write!(s, "&limit={}", limit); + } + + s + } + + pub fn guild_nickname(guild_id: u64) -> String { + format!(api!("/guilds/{}/members/@me/nick"), guild_id) + } + + pub fn guild_prune(guild_id: u64, days: u64) -> String { + format!(api!("/guilds/{}/prune?days={}"), guild_id, days) + } + + pub fn guild_regions(guild_id: u64) -> String { + format!(api!("/guilds/{}/regions"), guild_id) + } + + pub fn guild_role(guild_id: u64, role_id: u64) -> String { + format!(api!("/guilds/{}/roles/{}"), guild_id, role_id) + } + + pub fn guild_roles(guild_id: u64) -> String { + format!(api!("/guilds/{}/roles"), guild_id) + } + + pub fn guild_vanity_url(guild_id: u64) -> String { + format!(api!("/guilds/{}/vanity-url"), guild_id) + } + + pub fn guild_webhooks(guild_id: u64) -> String { + format!(api!("/guilds/{}/webhooks"), guild_id) + } + + pub fn guilds() -> &'static str { + api!("/guilds") + } + + pub fn invite(code: &str) -> String { + format!(api!("/invites/{}"), code) + } + + pub fn invite_optioned(code: &str, stats: bool) -> String { + format!(api!("/invites/{}?with_counts={}"), code, stats) + } + + pub fn oauth2_application_current() -> &'static str { + api!("/oauth2/applications/@me") + } + + pub fn private_channel() -> &'static str { + api!("/users/@me/channels") + } + + pub fn status_incidents_unresolved() -> &'static str { + status!("/incidents/unresolved.json") + } + + pub fn status_maintenances_active() -> &'static str { + status!("/scheduled-maintenances/active.json") + } + + pub fn status_maintenances_upcoming() -> &'static str { + status!("/scheduled-maintenances/upcoming.json") + } + + pub fn user<D: Display>(target: D) -> String { + format!(api!("/users/{}"), target) + } + + pub fn user_dm_channels<D: Display>(target: D) -> String { + format!(api!("/users/{}/channels"), target) + } + + pub fn user_guild<D: Display>(target: D, guild_id: u64) -> String { + format!(api!("/users/{}/guilds/{}"), target, guild_id) + } + + pub fn user_guilds<D: Display>(target: D) -> String { + format!(api!("/users/{}/guilds"), target) + } + + pub fn user_guilds_optioned<D: Display>( + target: D, + after: Option<u64>, + before: Option<u64>, + limit: u64, + ) -> String { + let mut s = format!(api!("/users/{}/guilds?limit={}&"), target, limit); + + if let Some(after) = after { + let _ = write!(s, "&after={}", after); + } + + if let Some(before) = before { + let _ = write!(s, "&before={}", before); + } + + s + } + + pub fn voice_regions() -> &'static str { + api!("/voice/regions") + } + + pub fn webhook(webhook_id: u64) -> String { + format!(api!("/webhooks/{}"), webhook_id) + } + + pub fn webhook_with_token<D>(webhook_id: u64, token: D) -> String + where D: Display { + format!(api!("/webhooks/{}/{}"), webhook_id, token) + } + + pub fn webhook_with_token_optioned<D>(webhook_id: u64, token: D, wait: bool) + -> String where D: Display { + format!(api!("/webhooks/{}/{}?wait={}"), webhook_id, token, wait) + } + + /// Returns the path's base string without replacements. + pub fn base(&self) -> &str { + use self::Path::*; + + match *self { + ChannelsId(_) => "/channels/{}", + ChannelsIdInvites(_) => "/channels/{}/invites", + ChannelsIdMessages(_) => "/channels/{}/messages", + ChannelsIdMessagesBulkDelete(_) => "/channels/{}/messages/bulk-delete", + ChannelsIdMessagesId(_, _) => "/channels/{}/messages/{}", + ChannelsIdMessagesIdAck(_) => "/channels/{}/messages/ack", + ChannelsIdMessagesIdReactions(_) => "/channels/{}/messages/{}/reactions", + ChannelsIdMessagesIdReactionsUserIdType(_) => "/channels/{}/messages/{}/reactions/{}/@me", + ChannelsIdPermissionsOverwriteId(_) => "/channels/{}/permissions/{}", + ChannelsIdPins(_) => "/channels/{}/pins", + ChannelsIdPinsMessageId(_) => "/channels/{}/pins/{}", + ChannelsIdRecipientsId(_) => "/channels/{}/recipients/{}", + ChannelsIdTyping(_) => "/channels/{}/typing", + ChannelsIdWebhooks(_) => "/channels/{}/webhooks", + Gateway => "/gateway", + GatewayBot => "/gateway/bot", + Guilds => "/guilds", + GuildsId(_) => "/guilds/{}", + GuildsIdBans(_) => "/guilds/{}/bans", + GuildsIdAuditLogs(_) => "/guilds/{}/audit-logs", + GuildsIdBansUserId(_) => "/guilds/{}/bans/{}", + GuildsIdChannels(_) => "/guilds/{}/channels", + GuildsIdEmbed(_) => "/guilds/{}/embed", + GuildsIdEmojis(_) => "/guilds/{}/emojis", + GuildsIdEmojisId(_) => "/guilds/{}/emojis/{}", + GuildsIdIntegrations(_) => "/guilds/{}/integrations", + GuildsIdIntegrationsId(_) => "/guilds/{}/integrations/{}", + GuildsIdIntegrationsIdSync(_) => "/guilds/{}/integrations/{}/sync", + GuildsIdInvites(_) => "/guilds/{}/invites", + GuildsIdMembers(_) => "/guilds/{}/membes", + GuildsIdMembersId(_) => "/guilds/{}/members/{}", + GuildsIdMembersIdRolesId(_) => "/guilds/{}/members/{}/roles/{}", + GuildsIdMembersMeNick(_) => "/guilds/{}/members/@me/nick", + GuildsIdPrune(_) => "/guilds/{}/prune", + GuildsIdRegions(_) => "/guilds/{}/regions", + GuildsIdRoles(_) => "/guilds/{}/roles", + GuildsIdRolesId(_) => "/guilds/{}/roles/{}", + GuildsIdVanityUrl(_) => "/guilds/{}/vanity-url", + GuildsIdWebhooks(_) => "/guilds/{}/webhooks", + InvitesCode => "/invites/{}", + StatusMaintenancesActive => "/scheduled-maintenances/active.json", + UsersId => "/users/{}", + UsersMe => "/users/@me", + UsersMeChannels => "/users/@me/channels", + UsersMeGuilds => "/users/@me/guilds", + UsersMeGuildsId => "/users/@me/guilds/{}", + VoiceRegions => "/voice/regions", + WebhooksId(_) => "/webhooks/{}", + } + } +} + +impl Display for Path { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + f.write_str(self.base()) + } +} + +#[derive(Clone)] +pub enum Route<'a> { + AddGroupRecipient { + group_id: u64, + user_id: u64, + }, + AddMemberRole { + guild_id: u64, + role_id: u64, + user_id: u64, + }, + GuildBanUser { + guild_id: u64, + user_id: u64, + delete_message_days: Option<u8>, + reason: Option<&'a str>, + }, + BroadcastTyping { + channel_id: u64, + }, + CreateChannel { + guild_id: u64, + }, + CreateEmoji { + guild_id: u64, + }, + CreateGuild, + CreateGuildIntegration { + guild_id: u64, + integration_id: u64, + }, + CreateInvite { + channel_id: u64, + }, + CreateMessage { + channel_id: u64, + }, + CreatePermission { + channel_id: u64, + target_id: u64, + }, + CreatePrivateChannel, + CreateReaction { + channel_id: u64, + message_id: u64, + reaction: &'a str, + }, + CreateRole { + guild_id: u64, + }, + CreateWebhook { + channel_id: u64, + }, + DeleteChannel { + channel_id: u64, + }, + DeleteEmoji { + guild_id: u64, + emoji_id: u64, + }, + DeleteGuild { + guild_id: u64, + }, + DeleteGuildIntegration { + guild_id: u64, + integration_id: u64, + }, + DeleteInvite { + code: &'a str, + }, + DeleteMessage { + channel_id: u64, + message_id: u64, + }, + DeleteMessages { + channel_id: u64, + }, + DeleteMessageReactions { + channel_id: u64, + message_id: u64, + }, + DeletePermission { + channel_id: u64, + target_id: u64, + }, + DeleteReaction { + channel_id: u64, + message_id: u64, + user: &'a str, + reaction: &'a str, + }, + DeleteRole { + guild_id: u64, + role_id: u64, + }, + DeleteWebhook { + webhook_id: u64, + }, + DeleteWebhookWithToken { + token: &'a str, + webhook_id: u64, + }, + EditChannel { + channel_id: u64, + }, + EditEmoji { + guild_id: u64, + emoji_id: u64, + }, + EditGuild { + guild_id: u64, + }, + EditGuildChannels { + guild_id: u64, + }, + EditGuildEmbed { + guild_id: u64, + }, + EditMember { + guild_id: u64, + user_id: u64, + }, + EditMessage { + channel_id: u64, + message_id: u64, + }, + EditNickname { + guild_id: u64, + }, + EditProfile, + EditRole { + guild_id: u64, + role_id: u64, + }, + EditWebhook { + webhook_id: u64, + }, + EditWebhookWithToken { + token: &'a str, + webhook_id: u64, + }, + ExecuteWebhook { + token: &'a str, + wait: bool, + webhook_id: u64, + }, + GetActiveMaintenance, + GetAuditLogs { + action_type: Option<u8>, + before: Option<u64>, + guild_id: u64, + limit: Option<u8>, + user_id: Option<u64>, + }, + GetBans { + guild_id: u64, + }, + GetBotGateway, + GetChannel { + channel_id: u64, + }, + GetChannelInvites { + channel_id: u64, + }, + GetChannelWebhooks { + channel_id: u64, + }, + GetChannels { + guild_id: u64, + }, + GetCurrentApplicationInfo, + GetCurrentUser, + GetGateway, + GetGuild { + guild_id: u64, + }, + GetGuildEmbed { + guild_id: u64, + }, + GetGuildIntegrations { + guild_id: u64, + }, + GetGuildInvites { + guild_id: u64, + }, + GetGuildMembers { + after: Option<u64>, + limit: Option<u64>, + guild_id: u64, + }, + GetGuildPruneCount { + days: u64, + guild_id: u64, + }, + GetGuildRegions { + guild_id: u64, + }, + GetGuildRoles { + guild_id: u64, + }, + GetGuildVanityUrl { + guild_id: u64, + }, + GetGuildWebhooks { + guild_id: u64, + }, + GetGuilds { + after: Option<u64>, + before: Option<u64>, + limit: u64, + }, + GetInvite { + code: &'a str, + stats: bool, + }, + GetMember { + guild_id: u64, + user_id: u64, + }, + GetMessage { + channel_id: u64, + message_id: u64, + }, + GetMessages { + channel_id: u64, + query: String, + }, + GetPins { + channel_id: u64, + }, + GetReactionUsers { + after: Option<u64>, + channel_id: u64, + limit: u8, + message_id: u64, + reaction: String, + }, + GetUnresolvedIncidents, + GetUpcomingMaintenances, + GetUser { + user_id: u64, + }, + GetUserDmChannels, + GetVoiceRegions, + GetWebhook { + webhook_id: u64, + }, + GetWebhookWithToken { + token: &'a str, + webhook_id: u64, + }, + KickMember { + guild_id: u64, + user_id: u64, + }, + LeaveGroup { + group_id: u64, + }, + LeaveGuild { + guild_id: u64, + }, + RemoveGroupRecipient { + group_id: u64, + user_id: u64, + }, + PinMessage { + channel_id: u64, + message_id: u64, + }, + RemoveBan { + guild_id: u64, + user_id: u64, + }, + RemoveMemberRole { + guild_id: u64, + role_id: u64, + user_id: u64, + }, + StartGuildPrune { + days: u64, + guild_id: u64, + }, + StartIntegrationSync { + guild_id: u64, + integration_id: u64, + }, + UnpinMessage { + channel_id: u64, + message_id: u64, + }, +} + +impl<'a> Route<'a> { + pub fn deconstruct(&self) -> (LightMethod, Path, Cow<str>) { + match *self { + Route::AddGroupRecipient { group_id, user_id } => ( + LightMethod::Post, + Path::ChannelsIdRecipientsId(group_id), + Cow::from(Path::group_recipient(group_id, user_id)), + ), + Route::AddMemberRole { guild_id, role_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdMembersIdRolesId(guild_id), + Cow::from(Path::guild_member_role(guild_id, user_id, role_id)), + ), + Route::GuildBanUser { + guild_id, + delete_message_days, + reason, + user_id, + } => ( + // TODO + LightMethod::Delete, + Path::GuildsIdBansUserId(guild_id), + Cow::from(Path::guild_ban_optioned( + guild_id, + user_id, + delete_message_days.unwrap_or(0), + reason.unwrap_or(""), + )), + ), + Route::BroadcastTyping { channel_id } => ( + LightMethod::Post, + Path::ChannelsIdTyping(channel_id), + Cow::from(Path::channel_typing(channel_id)), + ), + Route::CreateChannel { guild_id } => ( + LightMethod::Post, + Path::GuildsIdChannels(guild_id), + Cow::from(Path::guild_channels(guild_id)), + ), + Route::CreateEmoji { guild_id } => ( + LightMethod::Post, + Path::GuildsIdEmojis(guild_id), + Cow::from(Path::guild_emojis(guild_id)), + ), + Route::CreateGuild => ( + LightMethod::Post, + Path::Guilds, + Cow::from(Path::guilds()), + ), + Route::CreateGuildIntegration { guild_id, integration_id } => ( + LightMethod::Post, + Path::GuildsIdIntegrationsId(guild_id), + Cow::from(Path::guild_integration(guild_id, integration_id)), + ), + Route::CreateInvite { channel_id } => ( + LightMethod::Post, + Path::ChannelsIdInvites(channel_id), + Cow::from(Path::channel_invites(channel_id)), + ), + Route::CreateMessage { channel_id } => ( + LightMethod::Post, + Path::ChannelsIdMessages(channel_id), + Cow::from(Path::channel_messages(channel_id)), + ), + Route::CreatePermission { channel_id, target_id } => ( + LightMethod::Post, + Path::ChannelsIdPermissionsOverwriteId(channel_id), + Cow::from(Path::channel_permission(channel_id, target_id)), + ), + Route::CreatePrivateChannel => ( + LightMethod::Post, + Path::UsersMeChannels, + Cow::from(Path::user_dm_channels("@me")), + ), + Route::CreateReaction { channel_id, message_id, reaction } => ( + LightMethod::Put, + Path::ChannelsIdMessagesIdReactionsUserIdType(channel_id), + Cow::from(Path::channel_message_reaction( + channel_id, + message_id, + "@me", + reaction, + )), + ), + Route::CreateRole { guild_id } => ( + LightMethod::Delete, + Path::GuildsIdRoles(guild_id), + Cow::from(Path::guild_roles(guild_id)), + ), + Route::CreateWebhook { channel_id } => ( + LightMethod::Delete, + Path::ChannelsIdWebhooks(channel_id), + Cow::from(Path::channel_webhooks(channel_id)), + ), + Route::DeleteChannel { channel_id } => ( + LightMethod::Delete, + Path::ChannelsId(channel_id), + Cow::from(Path::channel(channel_id)), + ), + Route::DeleteEmoji { emoji_id, guild_id } => ( + LightMethod::Delete, + Path::GuildsIdEmojisId(guild_id), + Cow::from(Path::guild_emoji(guild_id, emoji_id)), + ), + Route::DeleteGuild { guild_id } => ( + LightMethod::Delete, + Path::GuildsId(guild_id), + Cow::from(Path::guild(guild_id)), + ), + Route::DeleteGuildIntegration { guild_id, integration_id } => ( + LightMethod::Delete, + Path::GuildsIdIntegrationsId(guild_id), + Cow::from(Path::guild_integration(guild_id, integration_id)), + ), + Route::DeleteInvite { code } => ( + LightMethod::Delete, + Path::InvitesCode, + Cow::from(Path::invite(code)), + ), + Route::DeleteMessage { channel_id, message_id } => ( + LightMethod::Delete, + Path::ChannelsIdMessagesId(LightMethod::Delete, message_id), + Cow::from(Path::channel_message(channel_id, message_id)), + ), + Route::DeleteMessages { channel_id } => ( + LightMethod::Delete, + Path::ChannelsIdMessagesBulkDelete(channel_id), + Cow::from(Path::channel_messages_bulk_delete(channel_id)), + ), + Route::DeletePermission { channel_id, target_id } => ( + LightMethod::Delete, + Path::ChannelsIdPermissionsOverwriteId(channel_id), + Cow::from(Path::channel_permission(channel_id, target_id)), + ), + Route::DeleteReaction { + channel_id, + message_id, + reaction, + user, + } => ( + LightMethod::Delete, + Path::ChannelsIdMessagesIdReactionsUserIdType(channel_id), + Cow::from(Path::channel_message_reaction( + channel_id, + message_id, + user, + reaction, + )) + ), + Route::DeleteRole { guild_id, role_id } => ( + LightMethod::Delete, + Path::GuildsIdRolesId(guild_id), + Cow::from(Path::guild_role(guild_id, role_id)), + ), + Route::DeleteWebhook { webhook_id } => ( + LightMethod::Delete, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook(webhook_id)), + ), + Route::DeleteWebhookWithToken { token, webhook_id } => ( + LightMethod::Delete, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token(webhook_id, token)), + ), + Route::EditChannel { channel_id } => ( + LightMethod::Patch, + Path::ChannelsId(channel_id), + Cow::from(Path::channel(channel_id)), + ), + Route::EditEmoji { emoji_id, guild_id } => ( + LightMethod::Patch, + Path::GuildsIdEmojisId(guild_id), + Cow::from(Path::guild_emoji(guild_id, emoji_id)), + ), + Route::EditGuild { guild_id } => ( + LightMethod::Patch, + Path::GuildsId(guild_id), + Cow::from(Path::guild(guild_id)), + ), + Route::EditGuildChannels { guild_id } => ( + LightMethod::Patch, + Path::GuildsIdChannels(guild_id), + Cow::from(Path::guild_channels(guild_id)), + ), + Route::EditGuildEmbed { guild_id } => ( + LightMethod::Patch, + Path::GuildsIdEmbed(guild_id), + Cow::from(Path::guild_embed(guild_id)), + ), + Route::EditMember { guild_id, user_id } => ( + LightMethod::Patch, + Path::GuildsIdMembersId(guild_id), + Cow::from(Path::guild_member(guild_id, user_id)), + ), + Route::EditMessage { channel_id, message_id } => ( + LightMethod::Patch, + Path::ChannelsIdMessagesId(LightMethod::Patch, channel_id), + Cow::from(Path::channel_message(channel_id, message_id)), + ), + Route::EditNickname { guild_id } => ( + LightMethod::Patch, + Path::GuildsIdMembersMeNick(guild_id), + Cow::from(Path::guild_nickname(guild_id)), + ), + Route::EditProfile => ( + LightMethod::Patch, + Path::UsersMe, + Cow::from(Path::user("@me")), + ), + Route::EditRole { guild_id, role_id } => ( + LightMethod::Patch, + Path::GuildsIdRolesId(guild_id), + Cow::from(Path::guild_role(guild_id, role_id)), + ), + Route::EditWebhook { webhook_id } => ( + LightMethod::Patch, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook(webhook_id)), + ), + Route::EditWebhookWithToken { token, webhook_id } => ( + LightMethod::Patch, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token(webhook_id, token)), + ), + Route::ExecuteWebhook { token, wait, webhook_id } => ( + LightMethod::Post, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token_optioned( + webhook_id, + token, + wait, + )), + ), + Route::GetActiveMaintenance => ( + LightMethod::Get, + Path::StatusMaintenancesActive, + Cow::from(Path::status_maintenances_active()), + ), + Route::GetAuditLogs { + action_type, + before, + guild_id, + limit, + user_id, + } => ( + LightMethod::Get, + Path::GuildsIdAuditLogs(guild_id), + Cow::from(Path::guild_audit_logs( + guild_id, + action_type, + user_id, + before, + limit, + )), + ), + Route::GetBans { guild_id } => ( + LightMethod::Get, + Path::GuildsIdBans(guild_id), + Cow::from(Path::guild_bans(guild_id)), + ), + Route::GetBotGateway => ( + LightMethod::Get, + Path::GatewayBot, + Cow::from(Path::gateway_bot()), + ), + Route::GetChannel { channel_id } => ( + LightMethod::Get, + Path::ChannelsId(channel_id), + Cow::from(Path::channel(channel_id)), + ), + Route::GetChannelInvites { channel_id } => ( + LightMethod::Get, + Path::ChannelsIdInvites(channel_id), + Cow::from(Path::channel_invites(channel_id)), + ), + Route::GetChannelWebhooks { channel_id } => ( + LightMethod::Get, + Path::ChannelsIdWebhooks(channel_id), + Cow::from(Path::channel_webhooks(channel_id)), + ), + Route::GetChannels { guild_id } => ( + LightMethod::Get, + Path::GuildsIdChannels(guild_id), + Cow::from(Path::guild_channels(guild_id)), + ), + Route::GetCurrentUser => ( + LightMethod::Get, + Path::UsersMe, + Cow::from(Path::user("@me")), + ), + Route::GetGateway => ( + LightMethod::Get, + Path::Gateway, + Cow::from(Path::gateway()), + ), + Route::GetGuild { guild_id } => ( + LightMethod::Get, + Path::GuildsId(guild_id), + Cow::from(Path::guild(guild_id)), + ), + Route::GetGuildEmbed { guild_id } => ( + LightMethod::Get, + Path::GuildsIdEmbed(guild_id), + Cow::from(Path::guild_embed(guild_id)), + ), + Route::GetGuildIntegrations { guild_id } => ( + LightMethod::Get, + Path::GuildsIdIntegrations(guild_id), + Cow::from(Path::guild_integrations(guild_id)), + ), + Route::GetGuildInvites { guild_id } => ( + LightMethod::Get, + Path::GuildsIdInvites(guild_id), + Cow::from(Path::guild_invites(guild_id)), + ), + Route::GetGuildMembers { after, guild_id, limit } => ( + LightMethod::Get, + Path::GuildsIdMembers(guild_id), + Cow::from(Path::guild_members_optioned(guild_id, after, limit)), + ), + Route::GetGuildPruneCount { days, guild_id } => ( + LightMethod::Get, + Path::GuildsIdPrune(guild_id), + Cow::from(Path::guild_prune(guild_id, days)), + ), + Route::GetGuildRegions { guild_id } => ( + LightMethod::Get, + Path::GuildsIdRegions(guild_id), + Cow::from(Path::guild_regions(guild_id)), + ), + Route::GetGuildRoles { guild_id } => ( + LightMethod::Get, + Path::GuildsIdRoles(guild_id), + Cow::from(Path::guild_roles(guild_id)), + ), + Route::GetGuildVanityUrl { guild_id } => ( + LightMethod::Get, + Path::GuildsIdVanityUrl(guild_id), + Cow::from(Path::guild_vanity_url(guild_id)), + ), + Route::GetGuildWebhooks { guild_id } => ( + LightMethod::Get, + Path::GuildsIdWebhooks(guild_id), + Cow::from(Path::guild_webhooks(guild_id)), + ), + Route::GetGuilds { after, before, limit } => ( + LightMethod::Get, + Path::UsersMeGuilds, + Cow::from(Path::user_guilds_optioned( + "@me", + after, + before, + limit, + )), + ), + Route::GetInvite { code, stats } => ( + LightMethod::Get, + Path::InvitesCode, + Cow::from(Path::invite_optioned(code, stats)), + ), + Route::GetMember { guild_id, user_id } => ( + LightMethod::Get, + Path::GuildsIdMembersId(guild_id), + Cow::from(Path::guild_member(guild_id, user_id)), + ), + Route::GetMessage { channel_id, message_id } => ( + LightMethod::Get, + Path::ChannelsIdMessagesId(LightMethod::Get, channel_id), + Cow::from(Path::channel_message(channel_id, message_id)), + ), + Route::GetPins { channel_id } => ( + LightMethod::Get, + Path::ChannelsIdPins(channel_id), + Cow::from(Path::channel_pins(channel_id)), + ), + Route::GetReactionUsers { + after, + channel_id, + limit, + message_id, + ref reaction, + } => ( + LightMethod::Get, + Path::ChannelsIdMessagesIdReactions(channel_id), + Cow::from(Path::channel_message_reactions( + channel_id, + message_id, + reaction, + limit, + after,)), + ), + Route::GetUser { user_id } => ( + LightMethod::Get, + Path::UsersId, + Cow::from(Path::user(user_id)), + ), + Route::GetUserDmChannels => ( + LightMethod::Get, + Path::UsersMeChannels, + Cow::from(Path::user_dm_channels("@me")), + ), + Route::GetVoiceRegions => ( + LightMethod::Get, + Path::VoiceRegions, + Cow::from(Path::voice_regions()), + ), + Route::GetWebhook { webhook_id } => ( + LightMethod::Get, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook(webhook_id)), + ), + Route::GetWebhookWithToken { token, webhook_id } => ( + LightMethod::Get, + Path::WebhooksId(webhook_id), + Cow::from(Path::webhook_with_token(webhook_id, token)), + ), + Route::KickMember { guild_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdMembersId(guild_id), + Cow::from(Path::guild_member(guild_id, user_id)), + ), + Route::LeaveGroup { group_id } => ( + LightMethod::Delete, + Path::ChannelsId(group_id), + Cow::from(Path::channel(group_id)), + ), + Route::LeaveGuild { guild_id } => ( + LightMethod::Delete, + Path::UsersMeGuildsId, + Cow::from(Path::user_guild("@me", guild_id)), + ), + Route::RemoveGroupRecipient { group_id, user_id } => ( + LightMethod::Delete, + Path::ChannelsIdRecipientsId(group_id), + Cow::from(Path::group_recipient(group_id, user_id)), + ), + Route::PinMessage { channel_id, message_id } => ( + LightMethod::Put, + Path::ChannelsIdPins(channel_id), + Cow::from(Path::channel_pin(channel_id, message_id)), + ), + Route::RemoveBan { guild_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdBansUserId(guild_id), + Cow::from(Path::guild_ban(guild_id, user_id)), + ), + Route::RemoveMemberRole { guild_id, role_id, user_id } => ( + LightMethod::Delete, + Path::GuildsIdMembersIdRolesId(guild_id), + Cow::from(Path::guild_member_role(guild_id, user_id, role_id)), + ), + Route::StartGuildPrune { days, guild_id } => ( + LightMethod::Post, + Path::GuildsIdPrune(guild_id), + Cow::from(Path::guild_prune(guild_id, days)), + ), + Route::StartIntegrationSync { guild_id, integration_id } => ( + LightMethod::Post, + Path::GuildsIdIntegrationsId(guild_id), + Cow::from(Path::guild_integration_sync( + guild_id, + integration_id, + )), + ), + Route::UnpinMessage { channel_id, message_id } => ( + LightMethod::Delete, + Path::ChannelsIdPinsMessageId(channel_id), + Cow::from(Path::channel_pin(channel_id, message_id)), + ), + _ => unreachable!(), // TODO: finish 5 unconvered variants + } + } +} diff --git a/src/http/utils.rs b/src/http/utils.rs new file mode 100644 index 0000000..017fe0e --- /dev/null +++ b/src/http/utils.rs @@ -0,0 +1,12 @@ +use model::channel::ReactionType; + +pub fn reaction_type_data(reaction_type: &ReactionType) -> String { + match *reaction_type { + ReactionType::Custom { + id, + ref name, + .. + } => format!("{}:{}", name.as_ref().map_or("", |s| s.as_str()), id), + ReactionType::Unicode(ref unicode) => unicode.clone(), + } +} |