aboutsummaryrefslogtreecommitdiff
path: root/src/http
diff options
context:
space:
mode:
Diffstat (limited to 'src/http')
-rw-r--r--src/http/error.rs35
-rw-r--r--src/http/mod.rs1544
-rw-r--r--src/http/ratelimiting.rs504
3 files changed, 2083 insertions, 0 deletions
diff --git a/src/http/error.rs b/src/http/error.rs
new file mode 100644
index 0000000..8e68c44
--- /dev/null
+++ b/src/http/error.rs
@@ -0,0 +1,35 @@
+use hyper::status::StatusCode;
+use std::error::Error as StdError;
+use std::fmt::{Display, Formatter, Result as FmtResult};
+
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+pub enum Error {
+ /// When a status code was unexpectedly received for a request's status.
+ InvalidRequest(StatusCode),
+ /// 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,
+ /// When a status is received, but the verification to ensure the response
+ /// is valid does not recognize the status.
+ UnknownStatus(u16),
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ f.write_str(self.description())
+ }
+}
+
+impl StdError for Error {
+ fn description(&self) -> &str {
+ match *self {
+ Error::InvalidRequest(_) => "Received an unexpected status code",
+ Error::RateLimitI64 => "Error decoding a header into an i64",
+ Error::RateLimitUtf8 => "Error decoding a header from UTF-8",
+ Error::UnknownStatus(_) => "Verification does not understand status",
+ }
+ }
+}
diff --git a/src/http/mod.rs b/src/http/mod.rs
new file mode 100644
index 0000000..8c7f4c4
--- /dev/null
+++ b/src/http/mod.rs
@@ -0,0 +1,1544 @@
+//! The HTTP module which provides functions for performing requests to
+//! endpoints in Discord's API.
+//!
+//! An important function of the REST API is ratelimiting. Requests to endpoints
+//! are ratelimited to prevent spam, and once ratelimited Discord will stop
+//! performing requests. The library implements protection to pre-emptively
+//! ratelimit, to ensure that no wasted requests are made.
+//!
+//! The HTTP module comprises of two types of requests:
+//!
+//! - REST API requests, which require an authorization token;
+//! - Other requests, which do not require an authorization token.
+//!
+//! The former require a [`Client`] to have logged in, while the latter may be
+//! made regardless of any other usage of the library.
+//!
+//! If a request spuriously fails, it will be retried once.
+//!
+//! Note that you may want to perform requests through a [`Context`] or through
+//! [model]s' instance methods where possible, as they each offer different
+//! levels of a high-level interface to the HTTP module.
+//!
+//! [`Client`]: ../struct.Client.html
+//! [`Context`]: ../struct.Context.html
+//! [model]: ../../model/index.html
+
+pub mod ratelimiting;
+
+mod error;
+
+pub use self::error::Error as HttpError;
+pub use hyper::status::{StatusClass, StatusCode};
+
+use hyper::client::{
+ Client as HyperClient,
+ RequestBuilder,
+ Response as HyperResponse,
+ Request,
+};
+use hyper::method::Method;
+use hyper::{Error as HyperError, Result as HyperResult, Url, header};
+use multipart::client::Multipart;
+use self::ratelimiting::Route;
+use serde_json;
+use std::collections::BTreeMap;
+use std::default::Default;
+use std::fmt::Write as FmtWrite;
+use std::io::{ErrorKind as IoErrorKind, Read};
+use std::sync::{Arc, Mutex};
+use ::constants;
+use ::internal::prelude::*;
+use ::model::*;
+
+/// 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.
+ Get,
+ /// Indicates that a route is for the `PATCH` method only.
+ Patch,
+ /// Indicates that a route is for the `POST` method only.
+ Post,
+ /// Indicates that a route is for the `PUT` method only.
+ Put,
+}
+
+lazy_static! {
+ static ref TOKEN: Arc<Mutex<String>> = Arc::new(Mutex::new(String::default()));
+}
+
+/// Sets the token to be used across all requests which require authentication.
+///
+/// This is really only for internal use, and if you are reading this as a user,
+/// you should _not_ use this yourself.
+#[doc(hidden)]
+pub fn set_token(token: &str) {
+ TOKEN.lock().unwrap().clone_from(&token.to_owned());
+}
+
+/// Adds a [`User`] as a recipient to a [`Group`].
+///
+/// **Note**: Groups have a limit of 10 recipients, including the current user.
+///
+/// [`Group`]: ../../model/struct.Group.html
+/// [`Group::add_recipient`]: ../../model/struct.Group.html#method.add_recipient
+/// [`User`]: ../../model/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))
+}
+
+/// Adds a single [`Role`] to a [`Member`] in a [`Guild`].
+///
+/// **Note**: Requires the [Manage Roles] permission and respect of role
+/// hierarchy.
+///
+/// [`Guild`]: ../../model/struct.Guild.html
+/// [`Member`]: ../../model/struct.Member.html
+/// [`Role`]: ../../model/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))
+}
+
+/// 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/struct.Guild.html
+/// [`User`]: ../../model/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) -> Result<()> {
+ verify(204, request!(Route::GuildsIdBansUserId(guild_id),
+ put,
+ "/guilds/{}/bans/{}?delete_message_days={}",
+ guild_id,
+ user_id,
+ delete_message_days))
+}
+
+/// 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/enum.Channel.html
+pub fn broadcast_typing(channel_id: u64) -> Result<()> {
+ verify(204, request!(Route::ChannelsIdTyping(channel_id),
+ post,
+ "/channels/{}/typing",
+ channel_id))
+}
+
+/// 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/struct.Guild.html
+/// [`GuildChannel`]: ../../model/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 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/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 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/struct.Guild.html
+/// [`PartialGuild`]: ../../model/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(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 an [`Integration`] for a [`Guild`].
+///
+/// Refer to Discord's [docs] for field information.
+///
+/// **Note**: Requires the [Manage Guild] permission.
+///
+/// [`Guild`]: ../../model/struct.Guild.html
+/// [`Integration`]: ../../model/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/{}",
+ 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/struct.GuildChannel.html
+/// [`RichInvite`]: ../../model/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)
+}
+
+/// 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();
+
+ verify(204, request!(Route::ChannelsIdPermissionsOverwriteId(channel_id),
+ put(body),
+ "/channels/{}/permissions/{}",
+ channel_id,
+ target_id))
+}
+
+/// 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");
+
+ serde_json::from_reader::<HyperResponse, PrivateChannel>(response).map_err(From::from)
+}
+
+/// 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",
+ channel_id,
+ message_id,
+ reaction_type.as_data()))
+}
+
+/// 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 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/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)
+}
+
+/// 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)
+}
+
+/// 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 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);
+
+ serde_json::from_reader::<HyperResponse, PartialGuild>(response).map_err(From::from)
+}
+
+/// 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/{}",
+ guild_id,
+ integration_id))
+}
+
+/// Deletes an invite by code.
+pub fn delete_invite(code: &str) -> Result<Invite> {
+ let response = request!(Route::InvitesCode, delete, "/invites/{}", code);
+
+ 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(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(channel_id: u64, map: &Value) -> Result<()> {
+ let body = map.to_string();
+
+ verify(204, request!(Route::ChannelsIdMessagesBulkDelete(channel_id),
+ post(body),
+ "/channels/{}/messages/bulk_delete",
+ channel_id))
+}
+
+/// Deletes 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/struct.Message.html
+/// [`Reaction`]: ../../model/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))
+}
+
+/// Deletes a permission override from a role or a member in a channel.
+pub fn delete_permission(channel_id: u64, target_id: u64) -> Result<()> {
+ verify(204, request!(Route::ChannelsIdPermissionsOverwriteId(channel_id),
+ delete,
+ "/channels/{}/permissions/{}",
+ channel_id,
+ target_id))
+}
+
+/// 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/{}/{}",
+ channel_id,
+ message_id,
+ reaction_type.as_data(),
+ user))
+}
+
+/// Deletes a role from a server. Can't remove the default everyone role.
+pub fn delete_role(guild_id: u64, role_id: u64) -> Result<()> {
+ verify(204, request!(Route::GuildsIdRolesId(guild_id),
+ delete,
+ "/guilds/{}/roles/{}",
+ guild_id,
+ role_id))
+}
+
+/// Deletes a [`Webhook`] given its Id.
+///
+/// This method requires authentication, whereas [`delete_webhook_with_token`]
+/// does not.
+///
+/// # Examples
+///
+/// Deletes a webhook given its Id:
+///
+/// ```rust,no_run
+/// use serenity::{Client, http};
+/// use std::env;
+///
+/// // Due to the `delete_webhook` function requiring you to authenticate, you
+/// // must have set the token first.
+/// http::set_token(&env::var("DISCORD_TOKEN").unwrap());
+///
+/// http::delete_webhook(245037420704169985).expect("Error deleting webhook");
+/// ```
+///
+/// [`Webhook`]: ../../model/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, delete, "/webhooks/{}", webhook_id))
+}
+
+/// Deletes a [`Webhook`] given its Id and unique token.
+///
+/// This method does _not_ require authentication.
+///
+/// # Examples
+///
+/// Deletes a webhook given its Id and unique token:
+///
+/// ```rust,no_run
+/// use serenity::http;
+///
+/// let id = 245037420704169985;
+/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
+///
+/// http::delete_webhook_with_token(id, token).expect("Error deleting webhook");
+/// ```
+///
+/// [`Webhook`]: ../../model/struct.Webhook.html
+pub fn delete_webhook_with_token(webhook_id: u64, token: &str) -> Result<()> {
+ let client = HyperClient::new();
+ verify(204, retry(|| client
+ .delete(&format!(api!("/webhooks/{}/{}"), webhook_id, token)))
+ .map_err(Error::Hyper)?)
+}
+
+/// 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 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)
+}
+
+/// 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)
+}
+
+/// Edits a [`Guild`]'s embed setting.
+///
+/// [`Guild`]: ../../model/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)
+}
+
+/// 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)?;
+
+ verify(204, request!(Route::GuildsIdMembersId(guild_id),
+ patch(body),
+ "/guilds/{}/members/{}",
+ guild_id,
+ user_id))
+}
+
+/// 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)
+}
+
+/// Edits the current user's nickname for the provided [`Guild`] via its Id.
+///
+/// Pass `None` to reset the nickname.
+///
+/// [`Guild`]: ../../model/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)
+}
+
+/// Edits the current user's profile settings.
+///
+/// For bot users, the password is optional.
+///
+/// # User Accounts
+///
+/// If a new token is received due to a password change, then the stored token
+/// internally will be updated.
+///
+/// **Note**: this token change may cause requests made between the actual token
+/// change and when the token is internally changed to be invalid requests, as
+/// the token may be outdated.
+pub fn edit_profile(map: &JsonMap) -> Result<CurrentUser> {
+ let body = serde_json::to_string(map)?;
+ let response = request!(Route::UsersMe, patch(body), "/users/@me");
+
+ let mut value = serde_json::from_reader::<HyperResponse, Value>(response)?;
+
+ if let Some(map) = value.as_object_mut() {
+ if !TOKEN.lock().unwrap().starts_with("Bot ") {
+ if let Some(Value::String(token)) = map.remove("token") {
+ set_token(&token);
+ }
+ }
+ }
+
+ serde_json::from_value::<CurrentUser>(value).map_err(From::from)
+}
+
+/// 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 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,
+ patch(body),
+ "/webhooks/{}",
+ webhook_id);
+
+ serde_json::from_reader::<HyperResponse, Webhook>(response).map_err(From::from)
+}
+
+/// Edits the webhook with the given data.
+///
+/// Refer to the documentation for [`edit_webhook`] for more information.
+///
+/// This method does _not_ require authentication.
+///
+/// # Examples
+///
+/// Edit the name of a webhook given its Id and unique token:
+///
+/// ```rust,ignore
+/// extern crate serde_json;
+/// extern crate serenity;
+///
+/// use serde_json::builder::ObjectBuilder;
+/// use serenity::http;
+///
+/// let id = 245037420704169985;
+/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
+/// let map = ObjectBuilder::new().insert("name", "new name").build();
+///
+/// let edited = http::edit_webhook_with_token(id, token, map)
+/// .expect("Error editing webhook");
+/// ```
+///
+/// [`edit_webhook`]: fn.edit_webhook.html
+pub fn edit_webhook_with_token(webhook_id: u64, token: &str, map: &JsonMap) -> Result<Webhook> {
+ let body = serde_json::to_string(map)?;
+ let client = HyperClient::new();
+ 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)
+}
+
+/// 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, map) {
+/// Ok(message) => message,
+/// Err(why) => {
+/// println!("Error executing webhook: {:?}", why);
+///
+/// return;
+/// },
+/// };
+///
+/// [`Channel`]: ../../model/enum.Channel.html
+/// [`Message`]: ../../model/struct.Message.html
+/// [Discord docs]: https://discordapp.com/developers/docs/resources/webhook#querystring-params
+pub fn execute_webhook(webhook_id: u64, token: &str, map: &JsonMap) -> Result<Message> {
+ let body = serde_json::to_string(map)?;
+ let client = HyperClient::new();
+ let response = retry(|| client
+ .post(&format!(api!("/webhooks/{}/{}"), webhook_id, token))
+ .body(&body))
+ .map_err(Error::Hyper)?;
+
+ serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
+}
+
+/// Gets the active maintenances from Discord's Status API.
+///
+/// Does not require authentication.
+pub fn get_active_maintenances() -> Result<Vec<Maintenance>> {
+ let client = HyperClient::new();
+ let response = retry(|| client.get(
+ status!("/scheduled-maintenances/active.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 information about an oauth2 application that the current user owns.
+///
+/// **Note**: Only user accounts may use this endpoint.
+pub fn get_application_info(id: u64) -> Result<ApplicationInfo> {
+ let response = request!(Route::None, get, "/oauth2/applications/{}", id);
+
+ serde_json::from_reader::<HyperResponse, ApplicationInfo>(response).map_err(From::from)
+}
+
+/// Gets all oauth2 applications we've made.
+///
+/// **Note**: Only user accounts may use this endpoint.
+pub fn get_applications() -> Result<Vec<ApplicationInfo>> {
+ let response = request!(Route::None, get, "/oauth2/applications");
+
+ serde_json::from_reader::<HyperResponse, Vec<ApplicationInfo>>(response).map_err(From::from)
+}
+
+/// 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 current bot gateway.
+pub fn get_bot_gateway() -> Result<BotGateway> {
+ let response = request!(Route::GatewayBot, get, "/gateway/bot");
+
+ serde_json::from_reader::<HyperResponse, BotGateway>(response).map_err(From::from)
+}
+
+/// 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)
+}
+
+/// 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/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 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 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 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");
+
+ serde_json::from_reader::<HyperResponse, CurrentApplicationInfo>(response).map_err(From::from)
+}
+
+/// Gets information about the user we're connected with.
+pub fn get_current_user() -> Result<CurrentUser> {
+ let response = request!(Route::UsersMe, get, "/users/@me");
+
+ serde_json::from_reader::<HyperResponse, CurrentUser>(response).map_err(From::from)
+}
+
+/// Gets current gateway.
+pub fn get_gateway() -> Result<Gateway> {
+ let response = request!(Route::Gateway, get, "/gateway");
+
+ serde_json::from_reader::<HyperResponse, Gateway>(response).map_err(From::from)
+}
+
+/// Gets information about an emoji.
+pub fn get_emoji(guild_id: u64, emoji_id: u64) -> Result<Emoji> {
+ let response = request!(Route::GuildsIdEmojisId(guild_id),
+ get,
+ "/guilds/{}/emojis/{}",
+ guild_id,
+ emoji_id);
+
+ serde_json::from_reader::<HyperResponse, Emoji>(response).map_err(From::from)
+}
+
+/// Gets all emojis in a guild.
+pub fn get_emojis(guild_id: u64) -> Result<Vec<Emoji>> {
+ let response = request!(Route::GuildsIdEmojis(guild_id),
+ get,
+ "/guilds/{}/emojis",
+ guild_id);
+
+ serde_json::from_reader::<HyperResponse, Vec<Emoji>>(response).map_err(From::from)
+}
+
+/// Gets guild information.
+pub fn get_guild(guild_id: u64) -> Result<PartialGuild> {
+ let response = request!(Route::GuildsId(guild_id),
+ get,
+ "/guilds/{}",
+ guild_id);
+
+ serde_json::from_reader::<HyperResponse, PartialGuild>(response).map_err(From::from)
+}
+
+/// 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)
+}
+
+/// 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 all invites to a guild.
+pub fn get_guild_invites(guild_id: u64) -> Result<Vec<RichInvite>> {
+ let response = request!(Route::GuildsIdInvites(guild_id),
+ get,
+ "/guilds/{}/invites",
+ guild_id);
+
+ serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response).map_err(From::from)
+}
+
+/// Gets 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_owned(), num.clone());
+ }
+ }
+ }
+
+ serde_json::from_value::<Vec<Member>>(v).map_err(From::from)
+}
+
+/// 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 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(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)
+}
+
+/// Retrieves a list of roles in a [`Guild`].
+///
+/// [`Guild`]: ../../model/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)
+}
+
+/// 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/struct.Guild.html
+pub fn get_guild_webhooks(guild_id: u64) -> Result<Vec<Webhook>> {
+ let response = request!(Route::GuildsIdWebhooks(guild_id),
+ get,
+ "/guilds/{}/webhooks",
+ guild_id);
+
+ serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response).map_err(From::from)
+}
+
+/// Gets 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(target: &GuildPagination, limit: u64) -> Result<Vec<GuildInfo>> {
+ let mut uri = format!("/users/@me/guilds?limit={}", limit);
+
+ match *target {
+ GuildPagination::After(id) => {
+ write!(uri, "&after={}", id)?;
+ },
+ GuildPagination::Before(id) => {
+ write!(uri, "&before={}", id)?;
+ },
+ }
+
+ let response = request!(Route::UsersMeGuilds, get, "{}", uri);
+
+ serde_json::from_reader::<HyperResponse, Vec<GuildInfo>>(response).map_err(From::from)
+}
+
+/// Gets information about a specific invite.
+pub fn get_invite(code: &str, stats: bool) -> Result<Invite> {
+ let mut invite = code;
+
+ #[cfg(feature="utils")]
+ {
+ invite = ::utils::parse_invite(invite);
+ }
+
+ let mut uri = format!("/invites/{}", invite);
+
+ if stats {
+ uri.push_str("?with_counts=true");
+ }
+
+ let response = request!(Route::InvitesCode, get, "{}", uri);
+
+ serde_json::from_reader::<HyperResponse, Invite>(response).map_err(From::from)
+}
+
+/// 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);
+
+ let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?;
+
+ if let Some(map) = v.as_object_mut() {
+ map.insert("guild_id".to_owned(), Value::Number(Number::from(guild_id)));
+ }
+
+ serde_json::from_value::<Member>(v).map_err(From::from)
+}
+
+/// Gets a message by an Id, bots only.
+pub fn get_message(channel_id: u64, message_id: u64) -> Result<Message> {
+ let response = request!(Route::ChannelsIdMessagesId(LightMethod::Any, channel_id),
+ get,
+ "/channels/{}/messages/{}",
+ channel_id,
+ message_id);
+
+ serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
+}
+
+/// Gets 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 = HyperClient::new();
+ let response = request(Route::ChannelsIdMessages(channel_id),
+ || client.get(&url))?;
+
+ serde_json::from_reader::<HyperResponse, Vec<Message>>(response).map_err(From::from)
+}
+
+/// 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)
+}
+
+/// 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)
+}
+
+/// Gets the current unresolved incidents from Discord's Status API.
+///
+/// Does not require authentication.
+pub fn get_unresolved_incidents() -> Result<Vec<Incident>> {
+ let client = HyperClient::new();
+ 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() -> Result<Vec<Maintenance>> {
+ let client = HyperClient::new();
+ 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(user_id: u64) -> Result<User> {
+ let response = request!(Route::UsersId, get, "/users/{}", user_id);
+
+ serde_json::from_reader::<HyperResponse, User>(response).map_err(From::from)
+}
+
+/// Gets our DM channels.
+pub fn get_user_dm_channels() -> Result<Vec<PrivateChannel>> {
+ let response = request!(Route::UsersMeChannels, get, "/users/@me/channels");
+
+ serde_json::from_reader::<HyperResponse, Vec<PrivateChannel>>(response).map_err(From::from)
+}
+
+/// Gets all voice regions.
+pub fn get_voice_regions() -> Result<Vec<VoiceRegion>> {
+ let response = request!(Route::VoiceRegions, get, "/voice/regions");
+
+ serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response).map_err(From::from)
+}
+
+/// 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, get, "/webhooks/{}", webhook_id);
+
+ serde_json::from_reader::<HyperResponse, Webhook>(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(webhook_id: u64, token: &str) -> Result<Webhook> {
+ let client = HyperClient::new();
+ 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)
+}
+
+/// 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))
+}
+
+/// Leaves a group DM.
+pub fn leave_group(guild_id: u64) -> Result<Group> {
+ let response = request!(Route::None,
+ delete,
+ "/channels/{}",
+ guild_id);
+
+ serde_json::from_reader::<HyperResponse, Group>(response).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))
+}
+
+/// 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))
+}
+
+/// Sends a file to a channel.
+pub fn send_file<R: Read>(channel_id: u64, mut file: R, filename: &str, map: JsonMap)
+ -> Result<Message> {
+ let uri = format!(api!("/channels/{}/messages"), channel_id);
+ let url = match Url::parse(&uri) {
+ Ok(url) => url,
+ Err(_) => return Err(Error::Url(uri)),
+ };
+
+ let mut request = Request::new(Method::Post, url)?;
+ request.headers_mut()
+ .set(header::Authorization(TOKEN.lock().unwrap().clone()));
+ request.headers_mut()
+ .set(header::UserAgent(constants::USER_AGENT.to_owned()));
+
+ let mut request = Multipart::from_request(request)?;
+
+ request.write_stream("file", &mut file, Some(filename), None)?;
+
+ for (k, v) in map {
+ let _ = match v {
+ Value::Bool(false) => request.write_text(&k, "false")?,
+ Value::Bool(true) => request.write_text(&k, "true")?,
+ Value::Number(inner) => request.write_text(&k, inner.to_string())?,
+ Value::String(inner) => request.write_text(&k, inner)?,
+ _ => continue,
+ };
+ }
+
+ let response = request.send()?;
+
+ serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
+}
+
+/// 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)
+}
+
+/// 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))
+}
+
+/// 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`].
+///
+/// **Note**: Requires the [Manage Roles] permission and respect of role
+/// hierarchy.
+///
+/// [`Guild`]: ../../model/struct.Guild.html
+/// [`Member`]: ../../model/struct.Member.html
+/// [`Role`]: ../../model/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().unwrap().clone()))
+ .header(header::ContentType::json()))?;
+
+ if response.status.class() == StatusClass::Success {
+ Ok(response)
+ } else {
+ Err(Error::Http(HttpError::InvalidRequest(response.status)))
+ }
+}
+
+#[doc(hidden)]
+pub fn retry<'a, F>(f: F) -> HyperResult<HyperResponse>
+ where F: Fn() -> RequestBuilder<'a> {
+ let req = || f()
+ .header(header::UserAgent(constants::USER_AGENT.to_owned()))
+ .send();
+
+ match req() {
+ Err(HyperError::Io(ref io))
+ if io.kind() == IoErrorKind::ConnectionAborted => req(),
+ other => other,
+ }
+}
+
+fn verify(expected_status_code: u16, mut response: HyperResponse) -> Result<()> {
+ let expected_status = match expected_status_code {
+ 200 => StatusCode::Ok,
+ 204 => StatusCode::NoContent,
+ 401 => StatusCode::Unauthorized,
+ _ => {
+ let client_error = HttpError::UnknownStatus(expected_status_code);
+
+ return Err(Error::Http(client_error));
+ },
+ };
+
+ if response.status == expected_status {
+ return Ok(());
+ }
+
+ debug!("Expected {}, got {}", expected_status_code, response.status);
+
+ let mut s = String::default();
+ response.read_to_string(&mut s)?;
+
+ debug!("Content: {}", s);
+
+ Err(Error::Http(HttpError::InvalidRequest(response.status)))
+}
+
+/// Representation of the method of a query to send for the [`get_guilds`]
+/// function.
+///
+/// [`get_guilds`]: fn.get_guilds.html
+pub enum GuildPagination {
+ /// The Id to get the guilds after.
+ After(GuildId),
+ /// The Id to get the guilds before.
+ Before(GuildId),
+}
diff --git a/src/http/ratelimiting.rs b/src/http/ratelimiting.rs
new file mode 100644
index 0000000..2b467b5
--- /dev/null
+++ b/src/http/ratelimiting.rs
@@ -0,0 +1,504 @@
+//! Routes are used for ratelimiting. These are to differentiate between the
+//! different _types_ of routes - such as getting the current user's channels -
+//! for the most part, with the exception being major parameters.
+//!
+//! [Taken from] the Discord docs, major parameters are:
+//!
+//! > Additionally, rate limits take into account major parameters in the URL.
+//! > For example, `/channels/:channel_id` and
+//! > `/channels/:channel_id/messages/:message_id` both take `channel_id` into
+//! > account when generating rate limits since it's the major parameter. The
+//! only current major parameters are `channel_id` and `guild_id`.
+//!
+//! This results in the two URIs of `GET /channels/4/messages/7` and
+//! `GET /channels/5/messages/8` being rate limited _separately_. However, the
+//! two URIs of `GET /channels/10/messages/11` and
+//! `GET /channels/10/messages/12` will count towards the "same ratelimit", as
+//! the major parameter - `10` is equivalent in both URIs' format.
+//!
+//! # Examples
+//!
+//! First: taking the first two URIs - `GET /channels/4/messages/7` and
+//! `GET /channels/5/messages/8` - and assuming both buckets have a `limit` of
+//! `10`, requesting the first URI will result in the response containing a
+//! `remaining` of `9`. Immediately after - prior to buckets resetting -
+//! performing a request to the _second_ URI will also contain a `remaining` of
+//! `9` in the response, as the major parameter - `channel_id` - is different
+//! in the two requests (`4` and `5`).
+//!
+//! Second: take for example the last two URIs. Assuming the bucket's `limit` is
+//! `10`, requesting the first URI will return a `remaining` of `9` in the
+//! response. Immediately after - prior to buckets resetting - performing a
+//! request to the _second_ URI will return a `remaining` of `8` in the
+//! response, as the major parameter - `channel_id` - is equivalent for the two
+//! requests (`10`).
+//!
+//! Major parameters are why some variants (i.e. all of the channel/guild
+//! variants) have an associated u64 as data. This is the Id of the parameter,
+//! differentiating between different ratelimits.
+//!
+//! [Taken from]: https://discordapp.com/developers/docs/topics/rate-limits#rate-limits
+#![allow(zero_ptr)]
+
+use hyper::client::{RequestBuilder, Response};
+use hyper::header::Headers;
+use hyper::status::StatusCode;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+use std::{str, thread};
+use super::{HttpError, LightMethod};
+use time;
+use ::internal::prelude::*;
+
+lazy_static! {
+ /// The global mutex is a mutex unlocked and then immediately re-locked
+ /// prior to every request, to abide by Discord's global ratelimit.
+ ///
+ /// The global ratelimit is the total number of requests that may be made
+ /// across the entirity of the API within an amount of time. If this is
+ /// reached, then the global mutex is unlocked for the amount of time
+ /// present in the "Retry-After" header.
+ ///
+ /// While locked, all requests are blocked until each request can acquire
+ /// the lock.
+ ///
+ /// 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(()));
+ /// The routes mutex is a HashMap of each [`Route`] and their respective
+ /// ratelimit information.
+ ///
+ /// See the documentation for [`RateLimit`] for more infomation on how the
+ /// library handles ratelimiting.
+ ///
+ /// # Examples
+ ///
+ /// View the `reset` time of the route for `ChannelsId(7)`:
+ ///
+ /// ```rust,no_run
+ /// use serenity::http::ratelimiting::{ROUTES, Route};
+ ///
+ /// let routes = ROUTES.lock().unwrap();
+ ///
+ /// if let Some(route) = routes.get(&Route::ChannelsId(7)) {
+ /// println!("Reset time at: {}", route.reset);
+ /// }
+ /// ```
+ ///
+ /// [`RateLimit`]: struct.RateLimit.html
+ /// [`Route`]: enum.Route.html
+ pub static ref ROUTES: Arc<Mutex<HashMap<Route, RateLimit>>> = Arc::new(Mutex::new(HashMap::default()));
+}
+
+/// 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/struct.ChannelId.html
+ ChannelsId(u64),
+ /// Route for the `/channels/:channel_id/invites` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdInvites(u64),
+ /// Route for the `/channels/:channel_id/messages` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdMessages(u64),
+ /// Route for the `/channels/:channel_id/messages/bulk-delete` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdMessagesBulkDelete(u64),
+ /// Route for the `/channels/:channel_id/messages/:message_id` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/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/struct.ChannelId.html
+ ChannelsIdMessagesIdAck(u64),
+ /// Route for the `/channels/:channel_id/messages/:message_id/reactions`
+ /// path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/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/struct.ChannelId.html
+ ChannelsIdMessagesIdReactionsUserIdType(u64),
+ /// Route for the `/channels/:channel_id/permissions/:target_id` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdPermissionsOverwriteId(u64),
+ /// Route for the `/channels/:channel_id/pins` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdPins(u64),
+ /// Route for the `/channels/:channel_id/pins/:message_id` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdPinsMessageId(u64),
+ /// Route for the `/channels/:channel_id/typing` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/struct.ChannelId.html
+ ChannelsIdTyping(u64),
+ /// Route for the `/channels/:channel_id/webhooks` path.
+ ///
+ /// The data is the relevant [`ChannelId`].
+ ///
+ /// [`ChannelId`]: ../../model/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/bans/:user_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdBansUserId(u64),
+ /// Route for the `/guilds/:guild_id/channels/:channel_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdChannels(u64),
+ /// Route for the `/guilds/:guild_id/embed` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdEmbed(u64),
+ /// Route for the `/guilds/:guild_id/emojis` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdEmojis(u64),
+ /// Route for the `/guilds/:guild_id/emojis/:emoji_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdEmojisId(u64),
+ /// Route for the `/guilds/:guild_id/integrations` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdIntegrations(u64),
+ /// Route for the `/guilds/:guild_id/integrations/:integration_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdIntegrationsId(u64),
+ /// Route for the `/guilds/:guild_id/integrations/:integration_id/sync`
+ /// path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdIntegrationsIdSync(u64),
+ /// Route for the `/guilds/:guild_id/invites` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdInvites(u64),
+ /// Route for the `/guilds/:guild_id/members` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdMembers(u64),
+ /// Route for the `/guilds/:guild_id/members/:user_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdMembersId(u64),
+ /// Route for the `/guilds/:guild_id/members/:user_id/roles/:role_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdMembersIdRolesId(u64),
+ /// Route for the `/guilds/:guild_id/members/@me/nick` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdMembersMeNick(u64),
+ /// Route for the `/guilds/:guild_id/prune` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdPrune(u64),
+ /// Route for the `/guilds/:guild_id/regions` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdRegions(u64),
+ /// Route for the `/guilds/:guild_id/roles` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdRoles(u64),
+ /// Route for the `/guilds/:guild_id/roles/:role_id` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdRolesId(u64),
+ /// Route for the `/guilds/:guild_id/webhooks` path.
+ ///
+ /// The data is the relevant [`GuildId`].
+ ///
+ /// [`GuildId`]: struct.GuildId.html
+ GuildsIdWebhooks(u64),
+ /// Route for the `/invites/:code` path.
+ InvitesCode,
+ /// Route for the `/users/:user_id` path.
+ UsersId,
+ /// Route for the `/users/@me` path.
+ UsersMe,
+ /// Route for the `/users/@me/channels` path.
+ UsersMeChannels,
+ /// Route for the `/users/@me/guilds` path.
+ UsersMeGuilds,
+ /// Route for the `/users/@me/guilds/:guild_id` path.
+ UsersMeGuildsId,
+ /// Route for the `/voice/regions` path.
+ VoiceRegions,
+ /// Route for the `/webhooks/:webhook_id` path.
+ WebhooksId,
+ /// Route where no ratelimit headers are in place (i.e. user account-only
+ /// routes).
+ ///
+ /// This is a special case, in that if the route is `None` then pre- and
+ /// post-hooks are not executed.
+ None,
+}
+
+#[doc(hidden)]
+pub fn perform<'a, F>(route: Route, f: F) -> Result<Response>
+ where F: Fn() -> RequestBuilder<'a> {
+ loop {
+ {
+ // This will block if another thread already has the global
+ // unlocked already (due to receiving an x-ratelimit-global).
+ let mut _global = GLOBAL.lock().expect("global route lock poisoned");
+ }
+
+ // Perform pre-checking here:
+ //
+ // - get the route's relevant rate
+ // - sleep if that route's already rate-limited until the end of the
+ // 'reset' time;
+ // - get the global rate;
+ // - sleep if there is 0 remaining
+ // - then, perform the request
+ if route != Route::None {
+ if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) {
+ route.pre_hook();
+ }
+ }
+
+ let response = super::retry(&f)?;
+
+ // Check if the request got ratelimited by checking for status 429,
+ // and if so, sleep for the value of the header 'retry-after' -
+ // which is in milliseconds - and then `continue` to try again
+ //
+ // If it didn't ratelimit, subtract one from the RateLimit's
+ // 'remaining'
+ //
+ // Update the 'reset' with the value of the 'x-ratelimit-reset'
+ // header
+ //
+ // It _may_ be possible for the limit to be raised at any time,
+ // so check if it did from the value of the 'x-ratelimit-limit'
+ // header. If the limit was 5 and is now 7, add 2 to the 'remaining'
+ if route != Route::None {
+ let redo = if response.headers.get_raw("x-ratelimit-global").is_some() {
+ let _ = GLOBAL.lock().expect("global route lock poisoned");
+
+ Ok(if let Some(retry_after) = parse_header(&response.headers, "retry-after")? {
+ debug!("Ratelimited: {:?}ms", retry_after);
+ thread::sleep(Duration::from_millis(retry_after as u64));
+
+ true
+ } else {
+ false
+ })
+ } else {
+ ROUTES.lock()
+ .expect("routes poisoned")
+ .entry(route)
+ .or_insert_with(RateLimit::default)
+ .post_hook(&response)
+ };
+
+ if !redo.unwrap_or(true) {
+ return Ok(response);
+ }
+ } else {
+ return Ok(response);
+ }
+ }
+}
+
+/// A set of data containing information about the ratelimits for a particular
+/// [`Route`], which is stored in the [`ROUTES`] mutex.
+///
+/// See the [Discord docs] on ratelimits for more information.
+///
+/// **Note**: You should _not_ mutate any of the fields, as this can help cause
+/// 429s.
+///
+/// [`ROUTES`]: struct.ROUTES.html
+/// [`Route`]: enum.Route.html
+/// [Discord docs]: https://discordapp.com/developers/docs/topics/rate-limits
+#[derive(Clone, Debug, Default)]
+pub struct RateLimit {
+ /// The total number of requests that can be made in a period of time.
+ pub limit: i64,
+ /// The number of requests remaining in the period of time.
+ pub remaining: i64,
+ /// When the interval resets and the the [`limit`] resets to the value of
+ /// [`remaining`].
+ ///
+ /// [`limit`]: #structfield.limit
+ /// [`remaining`]: #structfield.remaining
+ pub reset: i64,
+}
+
+impl RateLimit {
+ #[doc(hidden)]
+ pub fn pre_hook(&mut self) {
+ if self.limit == 0 {
+ return;
+ }
+
+ let current_time = time::get_time().sec;
+
+ // The reset was in the past, so we're probably good.
+ if current_time > self.reset {
+ self.remaining = self.limit;
+
+ return;
+ }
+
+ let diff = (self.reset - current_time) as u64;
+
+ if self.remaining == 0 {
+ let delay = (diff * 1000) + 500;
+
+ debug!("Pre-emptive ratelimit for {:?}ms", delay);
+ thread::sleep(Duration::from_millis(delay));
+
+ return;
+ }
+
+ self.remaining -= 1;
+ }
+
+ #[doc(hidden)]
+ pub fn post_hook(&mut self, response: &Response) -> Result<bool> {
+ if let Some(limit) = parse_header(&response.headers, "x-ratelimit-limit")? {
+ self.limit = limit;
+ }
+
+ if let Some(remaining) = parse_header(&response.headers, "x-ratelimit-remaining")? {
+ self.remaining = remaining;
+ }
+
+ if let Some(reset) = parse_header(&response.headers, "x-ratelimit-reset")? {
+ self.reset = reset;
+ }
+
+ Ok(if response.status != StatusCode::TooManyRequests {
+ false
+ } else if let Some(retry_after) = parse_header(&response.headers, "retry-after")? {
+ debug!("Ratelimited: {:?}ms", retry_after);
+ thread::sleep(Duration::from_millis(retry_after as u64));
+
+ true
+ } else {
+ false
+ })
+ }
+}
+
+fn parse_header(headers: &Headers, header: &str) -> Result<Option<i64>> {
+ match headers.get_raw(header) {
+ Some(header) => match str::from_utf8(&header[0]) {
+ Ok(v) => match v.parse::<i64>() {
+ Ok(v) => Ok(Some(v)),
+ Err(_) => Err(Error::Http(HttpError::RateLimitI64)),
+ },
+ Err(_) => Err(Error::Http(HttpError::RateLimitUtf8)),
+ },
+ None => Ok(None),
+ }
+}