diff options
| author | Zeyla Hellyer <[email protected]> | 2017-05-22 17:02:00 -0700 |
|---|---|---|
| committer | Zeyla Hellyer <[email protected]> | 2017-05-22 17:02:00 -0700 |
| commit | 9969be60cf320797c37b317da24d9a08fd5eafa5 (patch) | |
| tree | f27bf7a57af95bbc11990b1edcea9cca99276964 /src/http | |
| parent | Reasonably derive Debug on items (diff) | |
| download | serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.tar.xz serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.zip | |
Restructure modules
Modules are now separated into a fashion where the library can be used
for most use cases, without needing to compile the rest.
The core of serenity, with no features enabled, contains only the
struct (model) definitions, constants, and prelude. Models do not have
most functions compiled in, as that is separated into the `model`
feature.
The `client` module has been split into 3 modules: `client`, `gateway`,
and `http`.
`http` contains functions to interact with the REST API. `gateway`
contains the Shard to interact with the gateway, requiring `http` for
retrieving the gateway URL. `client` requires both of the other features
and acts as an abstracted interface over both the gateway and REST APIs,
handling the event loop.
The `builder` module has been separated from `utils`, and can now be
optionally compiled in. It and the `http` feature are required by the
`model` feature due to a large number of methods requiring access to
them.
`utils` now contains a number of utilities, such as the Colour struct, the
`MessageBuilder`, and mention parsing functions.
Each of the original `ext` modules are still featured, with `cache` not
requiring any feature to be enabled, `framework` requiring the `client`,
`model`, and `utils`, and `voice` requiring `gateway`.
In total the features and their requirements are:
- `builder`: none
- `cache`: none
- `client`: `gateway`, `http`
- `framework`: `client`, `model`, `utils`
- `gateway`: `http`
- `http`: none
- `model`: `builder`, `http`
- `utils`: none
- `voice`: `gateway`
The default features are `builder`, `cache`, `client`, `framework`,
`gateway`, `model`, `http`, and `utils`.
To help with forwards compatibility, modules have been re-exported from
their original locations.
Diffstat (limited to 'src/http')
| -rw-r--r-- | src/http/error.rs | 35 | ||||
| -rw-r--r-- | src/http/mod.rs | 1544 | ||||
| -rw-r--r-- | src/http/ratelimiting.rs | 504 |
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), + } +} |