From e75f30b6dedf366d72d8d56b44fdbe191961fa72 Mon Sep 17 00:00:00 2001 From: Austin Hellyer Date: Fri, 25 Nov 2016 08:33:15 -0800 Subject: Rename the `http` module to `rest` --- src/client/context.rs | 144 ++-- src/client/gateway/shard.rs | 4 +- src/client/http/mod.rs | 1368 ---------------------------------- src/client/http/ratelimiting.rs | 240 ------ src/client/mod.rs | 22 +- src/client/rest/mod.rs | 1368 ++++++++++++++++++++++++++++++++++ src/client/rest/ratelimiting.rs | 240 ++++++ src/error.rs | 4 +- src/ext/cache/mod.rs | 6 +- src/ext/framework/configuration.rs | 4 +- src/model/channel.rs | 62 +- src/model/guild.rs | 52 +- src/model/id.rs | 12 +- src/model/invite.rs | 22 +- src/model/user.rs | 8 +- src/model/webhook.rs | 42 +- src/utils/builder/create_message.rs | 4 +- src/utils/builder/execute_webhook.rs | 6 +- 18 files changed, 1804 insertions(+), 1804 deletions(-) delete mode 100644 src/client/http/mod.rs delete mode 100644 src/client/http/ratelimiting.rs create mode 100644 src/client/rest/mod.rs create mode 100644 src/client/rest/ratelimiting.rs (limited to 'src') diff --git a/src/client/context.rs b/src/client/context.rs index 7a0dc74..a4d893e 100644 --- a/src/client/context.rs +++ b/src/client/context.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::io::Read; use std::sync::{Arc, Mutex}; use super::gateway::Shard; -use super::http; +use super::rest; use super::login_type::LoginType; use ::utils::builder::{ CreateEmbed, @@ -27,7 +27,7 @@ use super::CACHE; /// helps with dealing with the current "context" of the event dispatch, /// and providing helper methods where possible. The context also acts as a /// general high-level interface over the associated [`Shard`] which -/// received the event, or the low-level [`http`] module. +/// received the event, or the low-level [`rest`] module. /// /// For example, when the [`Client::on_message`] handler is dispatched to, the /// context will contain the Id of the [`Channel`] that the message was created @@ -71,7 +71,7 @@ use super::CACHE; /// [`Shard`]: gateway/struct.Shard.html /// [`Cache`]: ../ext/cache/struct.Cache.html /// [`get_channel`]: #method.get_channel -/// [`http`]: http/index.html +/// [`rest`]: rest/index.html /// [`say`]: #method.say /// [`set_game`]: #method.set_game #[derive(Clone)] @@ -110,7 +110,7 @@ impl Context { /// Accepts the given invite. /// - /// Refer to the documentation for [`http::accept_invite`] for restrictions + /// Refer to the documentation for [`rest::accept_invite`] for restrictions /// on accepting an invite. /// /// **Note**: Requires that the current user be a user account. @@ -129,12 +129,12 @@ impl Context { let code = utils::parse_invite(invite); - http::accept_invite(code) + rest::accept_invite(code) } /// Mark a [`Channel`] as being read up to a certain [`Message`]. /// - /// Refer to the documentation for [`http::ack_message`] for more + /// Refer to the documentation for [`rest::ack_message`] for more /// information. /// /// # Errors @@ -144,20 +144,20 @@ impl Context { /// [`Channel`]: ../../model/enum.Channel.html /// [`ClientError::InvalidOperationAsBot`]: ../enum.ClientError.html#variant.InvalidOperationAsUser /// [`Message`]: ../../model/struct.Message.html - /// [`http::ack_message`]: http/fn.ack_message.html + /// [`rest::ack_message`]: rest/fn.ack_message.html pub fn ack(&self, channel_id: C, message_id: M) -> Result<()> where C: Into, M: Into { if self.login_type == LoginType::User { return Err(Error::Client(ClientError::InvalidOperationAsUser)); } - http::ack_message(channel_id.into().0, message_id.into().0) + rest::ack_message(channel_id.into().0, message_id.into().0) } /// Ban a [`User`] from a [`Guild`], removing their messages sent in the /// last X number of days. /// - /// Refer to the documentation for [`http::ban_user`] for more information. + /// Refer to the documentation for [`rest::ban_user`] for more information. /// /// **Note**: Requires that you have the [Ban Members] permission. /// @@ -185,7 +185,7 @@ impl Context { return Err(Error::Client(ClientError::DeleteMessageDaysAmount(delete_message_days))); } - http::ban_user(guild_id.into().0, user_id.into().0, delete_message_days) + rest::ban_user(guild_id.into().0, user_id.into().0, delete_message_days) } /// Broadcast that you are typing to a channel for the next 5 seconds. @@ -204,12 +204,12 @@ impl Context { /// ``` pub fn broadcast_typing(&self, channel_id: C) -> Result<()> where C: Into { - http::broadcast_typing(channel_id.into().0) + rest::broadcast_typing(channel_id.into().0) } /// Creates a [`PublicChannel`] in the given [`Guild`]. /// - /// Refer to [`http::create_channel`] for more information. + /// Refer to [`rest::create_channel`] for more information. /// /// **Note**: Requires the [Manage Channels] permission. /// @@ -225,7 +225,7 @@ impl Context { /// /// [`Guild`]: ../model/struct.Guild.html /// [`PublicChannel`]: ../model/struct.PublicChannel.html - /// [`http::create_channel`]: http/fn.create_channel.html + /// [`rest::create_channel`]: rest/fn.create_channel.html /// [Manage Channels]: ../model/permissions/constant.MANAGE_CHANNELS.html pub fn create_channel(&self, guild_id: G, name: &str, kind: ChannelType) -> Result where G: Into { @@ -234,7 +234,7 @@ impl Context { .insert("type", kind.name()) .build(); - http::create_channel(guild_id.into().0, map) + rest::create_channel(guild_id.into().0, map) } /// Creates an emoji in the given guild with a name and base64-encoded @@ -260,7 +260,7 @@ impl Context { .insert("image", image) .build(); - http::create_emoji(guild_id.into().0, map) + rest::create_emoji(guild_id.into().0, map) } /// Creates a guild with the data provided. @@ -296,7 +296,7 @@ impl Context { .insert("region", region.name()) .build(); - http::create_guild(map) + rest::create_guild(map) } /// Creates an [`Integration`] for a [`Guild`]. @@ -318,7 +318,7 @@ impl Context { .insert("type", kind) .build(); - http::create_guild_integration(guild_id.into().0, integration_id.0, map) + rest::create_guild_integration(guild_id.into().0, integration_id.0, map) } /// Creates an invite for the channel, providing a builder so that fields @@ -335,7 +335,7 @@ impl Context { where C: Into, F: FnOnce(CreateInvite) -> CreateInvite { let map = f(CreateInvite::default()).0.build(); - http::create_invite(channel_id.into().0, map) + rest::create_invite(channel_id.into().0, map) } /// Creates a [permission overwrite][`PermissionOverwrite`] for either a @@ -420,7 +420,7 @@ impl Context { .insert("type", kind) .build(); - http::create_permission(channel_id.into().0, id, map) + rest::create_permission(channel_id.into().0, id, map) } /// Creates a direct message channel between the [current user] and another @@ -434,7 +434,7 @@ impl Context { .insert("recipient_id", user_id.into().0) .build(); - http::create_private_channel(map) + rest::create_private_channel(map) } /// React to a [`Message`] with a custom [`Emoji`] or unicode character. @@ -457,7 +457,7 @@ impl Context { where C: Into, M: Into, R: Into { - http::create_reaction(channel_id.into().0, + rest::create_reaction(channel_id.into().0, message_id.into().0, reaction_type.into()) } @@ -467,10 +467,10 @@ impl Context { let id = guild_id.into().0; // The API only allows creating an empty role. - let role = try!(http::create_role(id)); + let role = try!(rest::create_role(id)); let map = f(EditRole::default()).0.build(); - http::edit_role(id, role.id.0, map) + rest::edit_role(id, role.id.0, map) } /// Deletes a [`Channel`] based on the Id given. @@ -484,7 +484,7 @@ impl Context { /// [Manage Messages]: ../model/permissions/constant.MANAGE_CHANNELS.html pub fn delete_channel(&self, channel_id: C) -> Result where C: Into { - http::delete_channel(channel_id.into().0) + rest::delete_channel(channel_id.into().0) } /// Deletes an emoji in a [`Guild`] given its Id. @@ -495,7 +495,7 @@ impl Context { /// [Manage Emojis]: ../model/permissions/constant.MANAGE_EMOJIS.html pub fn delete_emoji(&self, guild_id: G, emoji_id: E) -> Result<()> where E: Into, G: Into { - http::delete_emoji(guild_id.into().0, emoji_id.into().0) + rest::delete_emoji(guild_id.into().0, emoji_id.into().0) } /// Deletes a guild. You must be the guild owner to be able to delete it. @@ -505,12 +505,12 @@ impl Context { /// [`PartialGuild`]: ../model/struct.PartialGuild.html pub fn delete_guild>(&self, guild_id: G) -> Result { - http::delete_guild(guild_id.into().0) + rest::delete_guild(guild_id.into().0) } pub fn delete_integration(&self, guild_id: G, integration_id: I) -> Result<()> where G: Into, I: Into { - http::delete_guild_integration(guild_id.into().0, + rest::delete_guild_integration(guild_id.into().0, integration_id.into().0) } @@ -530,7 +530,7 @@ impl Context { pub fn delete_invite(&self, invite: &str) -> Result { let code = utils::parse_invite(invite); - http::delete_invite(code) + rest::delete_invite(code) } /// Deletes a [`Message`] given its Id. @@ -554,7 +554,7 @@ impl Context { /// [`Message`]: ../model/struct.Message.html pub fn delete_message(&self, channel_id: C, message_id: M) -> Result<()> where C: Into, M: Into { - http::delete_message(channel_id.into().0, message_id.into().0) + rest::delete_message(channel_id.into().0, message_id.into().0) } pub fn delete_messages(&self, channel_id: C, message_ids: &[MessageId]) @@ -571,7 +571,7 @@ impl Context { .insert("messages", ids) .build(); - http::delete_messages(channel_id.into().0, map) + rest::delete_messages(channel_id.into().0, map) } pub fn delete_note>(&self, user_id: U) -> Result<()> { @@ -579,7 +579,7 @@ impl Context { .insert("note", "") .build(); - http::edit_note(user_id.into().0, map) + rest::edit_note(user_id.into().0, map) } pub fn delete_permission(&self, @@ -591,7 +591,7 @@ impl Context { PermissionOverwriteType::Role(id) => id.0, }; - http::delete_permission(channel_id.into().0, id) + rest::delete_permission(channel_id.into().0, id) } @@ -612,7 +612,7 @@ impl Context { where C: Into, M: Into, R: Into { - http::delete_reaction(channel_id.into().0, + rest::delete_reaction(channel_id.into().0, message_id.into().0, user_id.map(|uid| uid.0), reaction_type.into()) @@ -620,7 +620,7 @@ impl Context { pub fn delete_role(&self, guild_id: G, role_id: R) -> Result<()> where G: Into, R: Into { - http::delete_role(guild_id.into().0, role_id.into().0) + rest::delete_role(guild_id.into().0, role_id.into().0) } /// Sends a message to a user through a direct message channel. This is a @@ -702,7 +702,7 @@ impl Context { let edited = f(EditChannel(map)).0.build(); - http::edit_channel(channel_id.0, edited) + rest::edit_channel(channel_id.0, edited) } pub fn edit_emoji(&self, guild_id: G, emoji_id: E, name: &str) @@ -711,14 +711,14 @@ impl Context { .insert("name", name) .build(); - http::edit_emoji(guild_id.into().0, emoji_id.into().0, map) + rest::edit_emoji(guild_id.into().0, emoji_id.into().0, map) } pub fn edit_guild(&self, guild_id: G, f: F) -> Result where F: FnOnce(EditGuild) -> EditGuild, G: Into { let map = f(EditGuild::default()).0.build(); - http::edit_guild(guild_id.into().0, map) + rest::edit_guild(guild_id.into().0, map) } pub fn edit_member(&self, guild_id: G, user_id: U, f: F) @@ -727,7 +727,7 @@ impl Context { U: Into { let map = f(EditMember::default()).0.build(); - http::edit_member(guild_id.into().0, user_id.into().0, map) + rest::edit_member(guild_id.into().0, user_id.into().0, map) } /// Edits the current user's nickname for the provided [`Guild`] via its Id. @@ -741,12 +741,12 @@ impl Context { #[inline] pub fn edit_nickname(&self, guild_id: G, new_nickname: Option<&str>) -> Result<()> where G: Into { - http::edit_nickname(guild_id.into().0, new_nickname) + rest::edit_nickname(guild_id.into().0, new_nickname) } pub fn edit_profile EditProfile>(&mut self, f: F) -> Result { - let user = try!(http::get_current_user()); + let user = try!(rest::get_current_user()); let mut map = ObjectBuilder::new() .insert("avatar", user.avatar) @@ -758,7 +758,7 @@ impl Context { let edited = f(EditProfile(map)).0.build(); - http::edit_profile(edited) + rest::edit_profile(edited) } pub fn edit_role(&self, guild_id: G, role_id: R, f: F) @@ -781,11 +781,11 @@ impl Context { let map = f(EditRole::new(role)).0.build(); - http::edit_role(guild_id.0, role_id.0, map) + rest::edit_role(guild_id.0, role_id.0, map) } else { let map = f(EditRole::default()).0.build(); - http::edit_role(guild_id.0, role_id.0, map) + rest::edit_role(guild_id.0, role_id.0, map) }} } @@ -806,7 +806,7 @@ impl Context { map = map.insert("embed", Value::Object(embed)); } - http::edit_message(channel_id.into().0, message_id.into().0, map.build()) + rest::edit_message(channel_id.into().0, message_id.into().0, map.build()) } pub fn edit_note>(&self, user_id: U, note: &str) @@ -815,16 +815,16 @@ impl Context { .insert("note", note) .build(); - http::edit_note(user_id.into().0, map) + rest::edit_note(user_id.into().0, map) } pub fn get_bans>(&self, guild_id: G) -> Result> { - http::get_bans(guild_id.into().0) + rest::get_bans(guild_id.into().0) } pub fn get_channel_invites>(&self, channel_id: C) -> Result> { - http::get_channel_invites(channel_id.into().0) + rest::get_channel_invites(channel_id.into().0) } pub fn get_channel(&self, channel_id: C) -> Result @@ -837,7 +837,7 @@ impl Context { } }} - http::get_channel(channel_id.0) + rest::get_channel(channel_id.0) } pub fn get_channels(&self, guild_id: G) @@ -854,7 +854,7 @@ impl Context { let mut channels = HashMap::new(); - for channel in try!(http::get_channels(guild_id.0)) { + for channel in try!(rest::get_channels(guild_id.0)) { channels.insert(channel.id, channel); } @@ -863,22 +863,22 @@ impl Context { pub fn get_emoji(&self, guild_id: G, emoji_id: E) -> Result where E: Into, G: Into { - http::get_emoji(guild_id.into().0, emoji_id.into().0) + rest::get_emoji(guild_id.into().0, emoji_id.into().0) } pub fn get_emojis>(&self, guild_id: G) -> Result> { - http::get_emojis(guild_id.into().0) + rest::get_emojis(guild_id.into().0) } pub fn get_guild>(&self, guild_id: G) -> Result { - http::get_guild(guild_id.into().0) + rest::get_guild(guild_id.into().0) } pub fn get_guild_invites(&self, guild_id: G) -> Result> where G: Into { - http::get_guild_invites(guild_id.into().0) + rest::get_guild_invites(guild_id.into().0) } pub fn get_guild_prune_count(&self, guild_id: G, days: u16) @@ -887,22 +887,22 @@ impl Context { .insert("days", days) .build(); - http::get_guild_prune_count(guild_id.into().0, map) + rest::get_guild_prune_count(guild_id.into().0, map) } pub fn get_guilds(&self) -> Result> { - http::get_guilds() + rest::get_guilds() } pub fn get_integrations>(&self, guild_id: G) -> Result> { - http::get_guild_integrations(guild_id.into().0) + rest::get_guild_integrations(guild_id.into().0) } pub fn get_invite(&self, invite: &str) -> Result { let code = utils::parse_invite(invite); - http::get_invite(code) + rest::get_invite(code) } pub fn get_member(&self, guild_id: G, user_id: U) -> Result @@ -918,7 +918,7 @@ impl Context { } }} - http::get_member(guild_id.0, user_id.0) + rest::get_member(guild_id.0, user_id.0) } /// Retrieves a single [`Message`] from a [`Channel`]. @@ -939,7 +939,7 @@ impl Context { return Err(Error::Client(ClientError::InvalidOperationAsUser)) } - http::get_message(channel_id.into().0, message_id.into().0) + rest::get_message(channel_id.into().0, message_id.into().0) } pub fn get_messages(&self, channel_id: C, f: F) -> Result> @@ -967,7 +967,7 @@ impl Context { query }; - http::get_messages(channel_id.into().0, &query) + rest::get_messages(channel_id.into().0, &query) } /// Retrieves the list of [`User`]s who have reacted to a [`Message`] with a @@ -1005,7 +1005,7 @@ impl Context { U: Into { let limit = limit.map(|x| if x > 100 { 100 } else { x }).unwrap_or(50); - http::get_reaction_users(channel_id.into().0, + rest::get_reaction_users(channel_id.into().0, message_id.into().0, reaction_type.into(), limit, @@ -1021,12 +1021,12 @@ impl Context { /// [Kick Members]: ../model/permissions/constant.KICK_MEMBERS.html pub fn kick_member(&self, guild_id: G, user_id: U) -> Result<()> where G: Into, U: Into { - http::kick_member(guild_id.into().0, user_id.into().0) + rest::kick_member(guild_id.into().0, user_id.into().0) } pub fn leave_guild>(&self, guild_id: G) -> Result { - http::leave_guild(guild_id.into().0) + rest::leave_guild(guild_id.into().0) } pub fn move_member(&self, guild_id: G, user_id: U, channel_id: C) @@ -1037,7 +1037,7 @@ impl Context { .insert("channel_id", channel_id.into().0) .build(); - http::edit_member(guild_id.into().0, user_id.into().0, map) + rest::edit_member(guild_id.into().0, user_id.into().0, map) } /// Retrieves the list of [`Message`]s which are pinned to the specified @@ -1047,12 +1047,12 @@ impl Context { /// [`Message`]: ../model/struct.Message.html pub fn get_pins(&self, channel_id: C) -> Result> where C: Into { - http::get_pins(channel_id.into().0) + rest::get_pins(channel_id.into().0) } pub fn pin(&self, channel_id: C, message_id: M) -> Result<()> where C: Into, M: Into { - http::pin_message(channel_id.into().0, message_id.into().0) + rest::pin_message(channel_id.into().0, message_id.into().0) } /// Sends a message with just the given message content in the channel that @@ -1106,7 +1106,7 @@ impl Context { return Err(Error::Client(ClientError::MessageTooLong(length_over))); } - http::send_file(channel_id.into().0, content, file, filename) + rest::send_file(channel_id.into().0, content, file, filename) } /// Sends a message to a [`Channel`]. @@ -1228,7 +1228,7 @@ impl Context { } } - http::send_message(channel_id.into().0, Value::Object(map)) + rest::send_message(channel_id.into().0, Value::Object(map)) } /// Sets the current user as being [`Online`]. This maintains the current @@ -1369,12 +1369,12 @@ impl Context { .insert("days", days) .build(); - http::start_guild_prune(guild_id.into().0, map) + rest::start_guild_prune(guild_id.into().0, map) } pub fn start_integration_sync(&self, guild_id: G, integration_id: I) -> Result<()> where G: Into, I: Into { - http::start_integration_sync(guild_id.into().0, integration_id.into().0) + rest::start_integration_sync(guild_id.into().0, integration_id.into().0) } /// Unbans a [`User`] from a [`Guild`]. @@ -1386,11 +1386,11 @@ impl Context { /// [Ban Members]: ../model/permissions/constant.BAN_MEMBERS.html pub fn unban(&self, guild_id: G, user_id: U) -> Result<()> where G: Into, U: Into { - http::remove_ban(guild_id.into().0, user_id.into().0) + rest::remove_ban(guild_id.into().0, user_id.into().0) } pub fn unpin(&self, channel_id: C, message_id: M) -> Result<()> where C: Into, M: Into { - http::unpin_message(channel_id.into().0, message_id.into().0) + rest::unpin_message(channel_id.into().0, message_id.into().0) } } diff --git a/src/client/gateway/shard.rs b/src/client/gateway/shard.rs index 4ebe26f..61bcafc 100644 --- a/src/client/gateway/shard.rs +++ b/src/client/gateway/shard.rs @@ -86,12 +86,12 @@ impl Shard { /// /// ```rust,ignore /// use serenity::client::gateway::Shard; - /// use serenity::client::{LoginType, http}; + /// use serenity::client::{LoginType, rest}; /// use std::env; /// /// let token = env::var("DISCORD_BOT_TOKEN").expect("Token in environment"); /// // retrieve the gateway response, which contains the URL to connect to - /// let gateway = http::get_gateway().expect("Valid gateway response").url; + /// let gateway = rest::get_gateway().expect("Valid gateway response").url; /// let shard = Shard::new(&gateway, &token, None, LoginType::Bot) /// .expect("Working shard"); /// diff --git a/src/client/http/mod.rs b/src/client/http/mod.rs deleted file mode 100644 index 682b69c..0000000 --- a/src/client/http/mod.rs +++ /dev/null @@ -1,1368 +0,0 @@ -//! 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 - -mod ratelimiting; - -use hyper::client::{ - Client as HyperClient, - RequestBuilder, - Response as HyperResponse, - Request, -}; -use hyper::method::Method; -use hyper::status::StatusCode; -use hyper::{Error as HyperError, Result as HyperResult, Url, header}; -use multipart::client::Multipart; -use self::ratelimiting::Route; -use serde_json::builder::ObjectBuilder; -use serde_json; -use std::default::Default; -use std::io::{ErrorKind as IoErrorKind, Read}; -use std::sync::{Arc, Mutex}; -use ::constants; -use ::internal::prelude::*; -use ::model::*; -use ::utils::decode_array; - -lazy_static! { - static ref TOKEN: Arc> = 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()); -} - -/// Accepts the [`Invite`] given its code, placing the current user in the -/// [`Guild`] that the invite was for. -/// -/// Use [`utils::parse_invite`] to retrieve codes from URLs. -/// -/// Refer to the documentation for [`Context::accept_invite`] for restrictions on -/// accepting an invite. -/// -/// This will fire the [`Client::on_guild_create`] handler once the associated -/// event is received. -/// -/// **Note**: This will fail if you are already in the guild, or are banned. A -/// ban is equivilant to an IP ban. -/// -/// **Note**: Requires that the current user be a user account. Bots can not -/// accept invites. Instead, they must be accepted via OAuth2 authorization -/// links. These are in the format of: -/// -/// `https://discordapp.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot` -/// -/// # Examples -/// -/// Accept an invite given a code from a URL: -/// -/// ```rust,no_run -/// use serenity::client::http; -/// use serenity::utils; -/// -/// let url = "https://discord.gg/0cDvIgU2voY8RSYL"; -/// let code = utils::parse_invite(url); -/// -/// let _result = http::accept_invite(code); -/// ``` -/// -/// [`Context::accept_invite`]: ../struct.Context.html#method.accept_invite -/// [`Invite`]: ../../model/struct.Invite.html -/// [`utils::parse_invite`]: ../../utils/fn.parse_invite.html -pub fn accept_invite(code: &str) -> Result { - let response = request!(Route::InvitesCode, post, "/invites/{}", code); - - Invite::decode(try!(serde_json::from_reader(response))) -} - -/// Marks a [`Channel`] as being "read" up to a certain [`Message`]. Any -/// message past the given one will not be marked as read. -/// -/// Usually you should use this to mark the latest message as being read. -/// -/// **Note**: Bot users should not use this, as it has no bearing on them -/// whatsoever. -/// -/// [`Channel`]: ../../model/enum.Channel.html -/// [`Message`]: ../../model/struct.Message.html -pub fn ack_message(channel_id: u64, message_id: u64) -> Result<()> { - verify(204, request!(Route::ChannelsIdMessagesIdAck(channel_id), - post, - "/channels/{}/messages/{}/ack", - channel_id, - message_id)) -} - -/// 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 equivilant 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 [`PublicChannel`] 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 -/// [`PublicChannel`]: ../../model/struct.PublicChannel.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 { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdChannels(guild_id), - post(body), - "/guilds/{}/channels", - guild_id); - - Channel::decode(try!(serde_json::from_reader(response))) -} - -/// 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 { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdEmojis(guild_id), - post(body), - "/guilds/{}/emojis", - guild_id); - - Emoji::decode(try!(serde_json::from_reader(response))) -} - -/// 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 usually only available for user accounts. Refer -/// to Discord's documentation for the endpoint [here][whitelist] for more -/// information. If your bot requires this, re-think what you are doing and -/// whether it _really_ needs to be doing this. -/// -/// # 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::client::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 -/// [US West Region]: ../../model/enum.Region.html#variant.UsWest -/// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild -pub fn create_guild(map: Value) -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::Guilds, post(body), "/guilds"); - - PartialGuild::decode(try!(serde_json::from_reader(response))) -} - -/// 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 = try!(serde_json::to_string(&map)); - - verify(204, request!(Route::GuildsIdIntegrations(guild_id), - post(body), - "/guilds/{}/integrations/{}", - guild_id, - integration_id)) -} - -/// Creates a [`RichInvite`] for the given [channel][`PublicChannel`]. -/// -/// Refer to Discord's [docs] for field information. -/// -/// All fields are optional. -/// -/// **Note**: Requires the [Create Invite] permission. -/// -/// [`PublicChannel`]: ../../model/struct.PublicChannel.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: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::ChannelsIdInvites(channel_id), - post(body), - "/channels/{}/invites", - channel_id); - - RichInvite::decode(try!(serde_json::from_reader(response))) -} - -pub fn create_permission(channel_id: u64, target_id: u64, map: Value) - -> Result<()> { - let body = try!(serde_json::to_string(&map)); - - verify(204, request!(Route::ChannelsIdPermissionsOverwriteId(channel_id), - put(body), - "/channels/{}/permissions/{}", - channel_id, - target_id)) -} - -pub fn create_private_channel(map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::UsersMeChannels, - post(body), - "/users/@me/channels"); - - PrivateChannel::decode(try!(serde_json::from_reader(response))) -} - -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())) -} - -pub fn create_role(guild_id: u64) -> Result { - let body = String::from("{}"); - let response = request!(Route::GuildsIdRoles(guild_id), - post(body), - "/guilds/{}/roles", - guild_id); - - Role::decode(try!(serde_json::from_reader(response))) -} - -/// Creates a webhook for the given [channel][`PublicChannel`]'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::client::http; -/// -/// let channel_id = 81384788765712384; -/// let map = ObjectBuilder::new().insert("name", "test").build(); -/// -/// let webhook = http::create_webhook(channel_id, map).expect("err creating"); -/// ``` -/// -/// [`PublicChannel`]: ../../model/struct.PublicChannel.html -pub fn create_webhook(channel_id: u64, map: Value) -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::ChannelsIdWebhooks(channel_id), - post(body), - "/channels/{}/webhooks", - channel_id); - - Webhook::decode(try!(serde_json::from_reader(response))) -} - -pub fn delete_channel(channel_id: u64) -> Result { - let response = request!(Route::ChannelsId(channel_id), - delete, - "/channels/{}", - channel_id); - - Channel::decode(try!(serde_json::from_reader(response))) -} - -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)) -} - -pub fn delete_guild(guild_id: u64) -> Result { - let response = request!(Route::GuildsId(guild_id), - delete, - "/guilds/{}", - guild_id); - - PartialGuild::decode(try!(serde_json::from_reader(response))) -} - -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)) -} - -pub fn delete_invite(code: &str) -> Result { - let response = request!(Route::InvitesCode, delete, "/invite/{}", code); - - Invite::decode(try!(serde_json::from_reader(response))) -} - -pub fn delete_message(channel_id: u64, message_id: u64) - -> Result<()> { - verify(204, request!(Route::ChannelsIdMessagesId(channel_id), - delete, - "/channels/{}/messages/{}", - channel_id, - message_id)) -} - -pub fn delete_messages(channel_id: u64, map: Value) -> Result<()> { - let body = try!(serde_json::to_string(&map)); - - verify(204, request!(Route::ChannelsIdMessagesBulkDelete(channel_id), - post(body), - "/channels/{}/messages/bulk_delete", - channel_id)) -} - -/// Delete all of the [`Reaction`]s associated with a [`Message`]. -/// -/// # Examples -/// -/// ```rust,no_run -/// use serenity::client::http; -/// use serenity::model::{ChannelId, MessageId}; -/// -/// let channel_id = ChannelId(7); -/// let message_id = MessageId(8); -/// -/// match http::delete_message_reactions(channel_id.0, message_id.0) { -/// Ok(()) => println!("Reactions deleted"), -/// Err(why) => println!("Error deleting reactions: {:?}", why), -/// } -/// ``` -/// -/// [`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)) -} - -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)) -} - -pub fn delete_reaction(channel_id: u64, - message_id: u64, - user_id: Option, - reaction_type: ReactionType) - -> Result<()> { - let user = user_id.map(|uid| uid.to_string()).unwrap_or("@me".to_string()); - - verify(204, request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - delete, - "/channels/{}/messages/{}/reactions/{}/{}", - channel_id, - message_id, - reaction_type.as_data(), - user)) -} - -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 -/// -/// Delete a webhook given its Id: -/// -/// ```rust,no_run -/// use serenity::client::{Client, http}; -/// use std::env; -/// -/// // Due to the `delete_webhook` function requiring you to authenticate, you -/// // must have initialized a client first. -/// let client = Client::login_user(&env::var("DISCORD_TOKEN").unwrap()); -/// -/// http::delete_webhook(245037420704169985).expect("err 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 -/// -/// Delete a webhook given its Id and unique token: -/// -/// ```rust,no_run -/// use serenity::client::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// -/// http::delete_webhook_with_token(id, token).expect("err 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, try!(retry(|| client - .delete(&format!(api!("/webhooks/{}/{}"), webhook_id, token))) - .map_err(Error::Hyper))) -} - -pub fn edit_channel(channel_id: u64, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::ChannelsId(channel_id), - patch(body), - "/channels/{}", - channel_id); - - PublicChannel::decode(try!(serde_json::from_reader(response))) -} - -pub fn edit_emoji(guild_id: u64, emoji_id: u64, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdEmojisId(guild_id), - patch(body), - "/guilds/{}/emojis/{}", - guild_id, - emoji_id); - - Emoji::decode(try!(serde_json::from_reader(response))) -} - -pub fn edit_guild(guild_id: u64, map: Value) -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsId(guild_id), - patch(body), - "/guilds/{}", - guild_id); - - PartialGuild::decode(try!(serde_json::from_reader(response))) -} - -pub fn edit_member(guild_id: u64, user_id: u64, map: Value) - -> Result<()> { - let body = try!(serde_json::to_string(&map)); - - verify(204, request!(Route::GuildsIdMembersId(guild_id), - patch(body), - "/guilds/{}/members/{}", - guild_id, - user_id)) -} - -pub fn edit_message(channel_id: u64, - message_id: u64, - map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::ChannelsIdMessagesId(channel_id), - patch(body), - "/channels/{}/messages/{}", - channel_id, - message_id); - - Message::decode(try!(serde_json::from_reader(response))) -} - -/// 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 = ObjectBuilder::new().insert("nick", new_nickname).build(); - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdMembersMeNick(guild_id), - patch(body), - "/guilds/{}/members/@me/nick", - guild_id); - - verify(200, response) -} - -pub fn edit_note(user_id: u64, map: Value) -> Result<()> { - let body = try!(serde_json::to_string(&map)); - - verify(204, request!(Route::None, - put(body), - "/users/@me/notes/{}", - user_id)) -} - -pub fn edit_profile(map: Value) -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::UsersMe, patch(body), "/users/@me"); - - CurrentUser::decode(try!(serde_json::from_reader(response))) -} - -pub fn edit_role(guild_id: u64, role_id: u64, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdRolesId(guild_id), - patch(body), - "/guilds/{}/roles/{}", - guild_id, - role_id); - - Role::decode(try!(serde_json::from_reader(response))) -} - -/// 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::client::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// let image = serenity::utils::read_image("./webhook_img.png") -/// .expect("err reading image"); -/// let map = ObjectBuilder::new().insert("avatar", image).build(); -/// -/// let edited = http::edit_webhook_with_token(id, token, map) -/// .expect("err 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 { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::WebhooksId, - patch(body), - "/webhooks/{}", - webhook_id); - - Webhook::decode(try!(serde_json::from_reader(response))) -} - -/// 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::client::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("err editing webhook"); -/// ``` -/// -/// [`edit_webhook`]: fn.edit_webhook.html -pub fn edit_webhook_with_token(webhook_id: u64, token: &str, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let client = HyperClient::new(); - let response = try!(retry(|| client - .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - .body(&body)) - .map_err(Error::Hyper)); - - Webhook::decode(try!(serde_json::from_reader(response))) -} - -/// 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::client::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; -/// }, -/// }; -pub fn execute_webhook(webhook_id: u64, token: &str, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let client = HyperClient::new(); - let response = try!(retry(|| client - .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) - .body(&body)) - .map_err(Error::Hyper)); - - Message::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_application_info() -> Result { - let response = request!(Route::None, get, "/oauth2/applications/@me"); - - CurrentApplicationInfo::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_applications() -> Result> { - let response = request!(Route::None, get, "/oauth2/applications"); - let decoded = try!(serde_json::from_reader(response)); - - decode_array(decoded, ApplicationInfo::decode) -} - -pub fn get_bans(guild_id: u64) -> Result> { - let response = request!(Route::GuildsIdBans(guild_id), - get, - "/guilds/{}/bans", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), Ban::decode) -} - -pub fn get_bot_gateway() -> Result { - let response = request!(Route::GatewayBot, get, "/gateway/bot"); - - BotGateway::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_channel_invites(channel_id: u64) - -> Result> { - let response = request!(Route::ChannelsIdInvites(channel_id), - get, - "/channels/{}/invites", - channel_id); - - decode_array(try!(serde_json::from_reader(response)), - RichInvite::decode) -} - -/// Retrieves the webhooks for the given [channel][`PublicChannel`]'s Id. -/// -/// This method requires authentication. -/// -/// # Examples -/// -/// Retrieve all of the webhooks owned by a channel: -/// -/// ```rust,no_run -/// use serenity::client::http; -/// -/// let channel_id = 81384788765712384; -/// -/// let webhooks = http::get_channel_webhooks(channel_id) -/// .expect("err getting channel webhooks"); -/// ``` -/// -/// [`PublicChannel`]: ../../model/struct.PublicChannel.html -pub fn get_channel_webhooks(channel_id: u64) -> Result> { - let response = request!(Route::ChannelsIdWebhooks(channel_id), - get, - "/channels/{}/webhooks", - channel_id); - - decode_array(try!(serde_json::from_reader(response)), Webhook::decode) -} - -pub fn get_channel(channel_id: u64) -> Result { - let response = request!(Route::ChannelsId(channel_id), - get, - "/channels/{}", - channel_id); - - Channel::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_channels(guild_id: u64) -> Result> { - let response = request!(Route::ChannelsId(guild_id), - get, - "/guilds/{}/channels", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), - PublicChannel::decode) -} - -pub fn get_current_user() -> Result { - let response = request!(Route::UsersMe, get, "/users/@me"); - - CurrentUser::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_gateway() -> Result { - let response = request!(Route::Gateway, get, "/gateway"); - - Gateway::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_emoji(guild_id: u64, emoji_id: u64) -> Result { - let response = request!(Route::GuildsIdEmojisId(guild_id), - get, - "/guilds/{}/emojis/{}", - guild_id, - emoji_id); - - Emoji::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_emojis(guild_id: u64) -> Result> { - let response = request!(Route::GuildsIdEmojis(guild_id), - get, - "/guilds/{}/emojis", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), Emoji::decode) -} - -pub fn get_guild(guild_id: u64) -> Result { - let response = request!(Route::GuildsId(guild_id), - get, - "/guilds/{}", - guild_id); - - PartialGuild::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_guild_embed(guild_id: u64) -> Result { - let response = request!(Route::GuildsIdEmbed(guild_id), - get, - "/guilds/{}/embeds", - guild_id); - - GuildEmbed::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_guild_integrations(guild_id: u64) -> Result> { - let response = request!(Route::GuildsIdIntegrations(guild_id), - get, - "/guilds/{}/integrations", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), Integration::decode) -} - -pub fn get_guild_invites(guild_id: u64) -> Result> { - let response = request!(Route::GuildsIdInvites(guild_id), - get, - "/guilds/{}/invites", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), - RichInvite::decode) -} - -pub fn get_guild_prune_count(guild_id: u64, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdPrune(guild_id), - get(body), - "/guilds/{}/prune", - guild_id); - - GuildPrune::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_guild_regions(guild_id: u64) -> Result> { - let response = request!(Route::GuildsIdRegions(guild_id), - get, - "/guilds/{}/regions", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode) -} - -/// 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::client::http; -/// -/// let guild_id = 81384788765712384; -/// -/// let webhooks = http::get_guild_webhooks(guild_id) -/// .expect("err getting guild webhooks"); -/// ``` -/// -/// [`Guild`]: ../../model/struct.Guild.html -pub fn get_guild_webhooks(guild_id: u64) -> Result> { - let response = request!(Route::GuildsIdWebhooks(guild_id), - get, - "/guilds/{}/webhooks", - guild_id); - - decode_array(try!(serde_json::from_reader(response)), Webhook::decode) -} - -pub fn get_guilds() -> Result> { - let response = request!(Route::UsersMeGuilds, - get, - "/users/@me/guilds"); - - decode_array(try!(serde_json::from_reader(response)), GuildInfo::decode) -} - -pub fn get_invite(code: &str) -> Result { - let invite = ::utils::parse_invite(code); - let response = request!(Route::InvitesCode, get, "/invite/{}", invite); - - Invite::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_member(guild_id: u64, user_id: u64) -> Result { - let response = request!(Route::GuildsIdMembersId(guild_id), - get, - "/guilds/{}/members/{}", - guild_id, - user_id); - - Member::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_message(channel_id: u64, message_id: u64) - -> Result { - let response = request!(Route::ChannelsIdMessagesId(channel_id), - get, - "/channels/{}/messages/{}", - channel_id, - message_id); - - Message::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_messages(channel_id: u64, query: &str) - -> Result> { - let url = format!(api_concat!("/channels/{}/messages{}"), - channel_id, - query); - let client = HyperClient::new(); - let response = try!(request(Route::ChannelsIdMessages(channel_id), - || client.get(&url))); - - decode_array(try!(serde_json::from_reader(response)), Message::decode) -} - -pub fn get_pins(channel_id: u64) -> Result> { - let response = request!(Route::ChannelsIdPins(channel_id), - get, - "/channels/{}/pins", - channel_id); - - decode_array(try!(serde_json::from_reader(response)), Message::decode) -} - -pub fn get_reaction_users(channel_id: u64, - message_id: u64, - reaction_type: ReactionType, - limit: u8, - after: Option) - -> Result> { - let mut uri = format!("/channels/{}/messages/{}/reactions/{}?limit={}", - channel_id, - message_id, - reaction_type.as_data(), - limit); - - if let Some(user_id) = after { - uri.push_str("&after="); - uri.push_str(&user_id.to_string()); - } - - let response = request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), - get, - "{}", - uri); - - decode_array(try!(serde_json::from_reader(response)), User::decode) -} - -pub fn get_user(user_id: u64) -> Result { - let response = request!(Route::UsersId, get, "/users/{}", user_id); - - CurrentUser::decode(try!(serde_json::from_reader(response))) -} - -pub fn get_user_connections() -> Result> { - let response = request!(Route::UsersMeConnections, - get, - "/users/@me/connections"); - - decode_array(try!(serde_json::from_reader(response)), - UserConnection::decode) -} - -pub fn get_user_dm_channels() -> Result> { - let response = request!(Route::UsersMeChannels, get, "/users/@me/channels"); - - decode_array(try!(serde_json::from_reader(response)), - PrivateChannel::decode) -} - -pub fn get_voice_regions() -> Result> { - let response = request!(Route::VoiceRegions, get, "/voice/regions"); - - decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode) -} - -/// 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::client::http; -/// -/// let id = 245037420704169985; -/// let webhook = http::get_webhook(id).expect("err getting webhook"); -/// ``` -/// -/// [`get_webhook_with_token`]: fn.get_webhook_with_token.html -pub fn get_webhook(webhook_id: u64) -> Result { - let response = request!(Route::WebhooksId, get, "/webhooks/{}", webhook_id); - - Webhook::decode(try!(serde_json::from_reader(response))) -} - -/// 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::client::http; -/// -/// let id = 245037420704169985; -/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; -/// -/// let webhook = http::get_webhook_with_token(id, token) -/// .expect("err getting webhook"); -/// ``` -pub fn get_webhook_with_token(webhook_id: u64, token: &str) -> Result { - let client = HyperClient::new(); - let response = try!(retry(|| client - .get(&format!(api!("/webhooks/{}/{}"), webhook_id, token))) - .map_err(Error::Hyper)); - - Webhook::decode(try!(serde_json::from_reader(response))) -} - -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)) -} - -pub fn leave_group(guild_id: u64) -> Result { - let response = request!(Route::None, - delete, - "/channels/{}", - guild_id); - - Group::decode(try!(serde_json::from_reader(response))) -} - -pub fn leave_guild(guild_id: u64) -> Result { - let response = request!(Route::UsersMeGuildsId, - delete, - "/users/@me/guilds/{}", - guild_id); - - PartialGuild::decode(try!(serde_json::from_reader(response))) -} - -pub fn logout(map: Value) -> Result<()> { - let body = try!(serde_json::to_string(&map)); - - verify(204, request!(Route::None, post(body), "/auth/logout")) -} - -pub fn remove_group_recipient(group_id: u64, user_id: u64) - -> Result<()> { - verify(204, request!(Route::None, - delete, - "/channels/{}/recipients/{}", - group_id, - user_id)) -} - -pub fn send_file(channel_id: u64, - content: &str, - mut file: R, - filename: &str) - -> Result { - let uri = format!(api_concat!("/channels/{}/messages"), channel_id); - let url = match Url::parse(&uri) { - Ok(url) => url, - Err(_why) => return Err(Error::Url(uri)), - }; - - let mut request = try!(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 = try!(Multipart::from_request(request)); - try!(request.write_text("content", content)); - try!(request.write_stream("file", &mut file, Some(&filename), None)); - - Message::decode(try!(serde_json::from_reader(try!(request.send())))) -} - -pub fn send_message(channel_id: u64, map: Value) -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::ChannelsIdMessages(channel_id), - post(body), - "/channels/{}/messages", - channel_id); - - Message::decode(try!(serde_json::from_reader(response))) -} - -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)) -} - -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)) -} - -/// Removes 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)) -} - -pub fn start_guild_prune(guild_id: u64, map: Value) - -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::GuildsIdPrune(guild_id), - post(body), - "/guilds/{}/prune", - guild_id); - - GuildPrune::decode(try!(serde_json::from_reader(response))) -} - -pub fn start_integration_sync(guild_id: u64, integration_id: u64) - -> Result<()> { - verify(204, request!(Route::GuildsIdIntegrationsIdSync(guild_id), - post, - "/guilds/{}/integrations/{}", - guild_id, - integration_id)) -} - -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 - where F: Fn() -> RequestBuilder<'a> { - ratelimiting::perform(route, || f() - .header(header::Authorization(TOKEN.lock().unwrap().clone())) - .header(header::ContentType::json())) -} - -#[doc(hidden)] -pub fn retry<'a, F>(f: F) -> HyperResult - 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 = ClientError::UnknownStatus(expected_status_code); - - return Err(Error::Client(client_error)); - }, - }; - - if response.status == expected_status { - return Ok(()); - } - - debug!("Expected {}, got {}", expected_status_code, response.status); - - let mut s = String::default(); - try!(response.read_to_string(&mut s)); - - debug!("Content: {}", s); - - Err(Error::Client(ClientError::UnexpectedStatusCode(response.status))) -} diff --git a/src/client/http/ratelimiting.rs b/src/client/http/ratelimiting.rs deleted file mode 100644 index 65af1a3..0000000 --- a/src/client/http/ratelimiting.rs +++ /dev/null @@ -1,240 +0,0 @@ -//! 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 equivilant 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 equivilant for the two -//! requests (`10`). -//! -//! -//! With the examples out of the way: 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. - -use hyper::client::{RequestBuilder, Response}; -use hyper::header::Headers; -use hyper::status::StatusCode; -use std::collections::HashMap; -use std::str; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration; -use time; -use ::internal::prelude::*; - -lazy_static! { - static ref GLOBAL: Arc> = Arc::new(Mutex::new(RateLimit::default())); - static ref ROUTES: Arc>> = Arc::new(Mutex::new(HashMap::default())); -} - -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Route { - ChannelsId(u64), - ChannelsIdInvites(u64), - ChannelsIdMessages(u64), - ChannelsIdMessagesBulkDelete(u64), - ChannelsIdMessagesId(u64), - ChannelsIdMessagesIdAck(u64), - ChannelsIdMessagesIdReactions(u64), - ChannelsIdMessagesIdReactionsUserIdType(u64), - ChannelsIdPermissionsOverwriteId(u64), - ChannelsIdPins(u64), - ChannelsIdPinsMessageId(u64), - ChannelsIdTyping(u64), - ChannelsIdWebhooks(u64), - Gateway, - GatewayBot, - Guilds, - GuildsId(u64), - GuildsIdBans(u64), - GuildsIdBansUserId(u64), - GuildsIdChannels(u64), - GuildsIdEmbed(u64), - GuildsIdEmojis(u64), - GuildsIdEmojisId(u64), - GuildsIdIntegrations(u64), - GuildsIdIntegrationsId(u64), - GuildsIdIntegrationsIdSync(u64), - GuildsIdInvites(u64), - GuildsIdMembersId(u64), - GuildsIdMembersIdRolesId(u64), - GuildsIdMembersMeNick(u64), - GuildsIdPrune(u64), - GuildsIdRegions(u64), - GuildsIdRoles(u64), - GuildsIdRolesId(u64), - GuildsIdWebhooks(u64), - InvitesCode, - UsersId, - UsersMe, - UsersMeChannels, - UsersMeConnections, - UsersMeGuilds, - UsersMeGuildsId, - VoiceRegions, - WebhooksId, - None, -} - -pub fn perform<'a, F>(route: Route, f: F) -> Result - where F: Fn() -> RequestBuilder<'a> { - - loop { - // 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 - { - let mut global = GLOBAL.lock().expect("global route lock poisoned"); - global.pre_hook(); - } - - if route != Route::None { - if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) { - route.pre_hook(); - } - } - - let response = try!(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 mut global = GLOBAL.lock().expect("global route lock poisoned"); - global.post_hook(&response) - } else { - ROUTES.lock() - .expect("routes poisoned") - .entry(route) - .or_insert_with(RateLimit::default) - .post_hook(&response) - }; - - if redo.unwrap_or(false) { - continue; - } - } - - return Ok(response); - } -} - -#[derive(Clone, Debug, Default)] -pub struct RateLimit { - limit: i64, - remaining: i64, - reset: i64, -} - -impl RateLimit { - 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; - } - - pub fn post_hook(&mut self, response: &Response) -> Result { - if let Some(limit) = try!(get_header(&response.headers, "x-ratelimit-limit")) { - self.limit = limit; - } - - if let Some(remaining) = try!(get_header(&response.headers, "x-ratelimit-remaining")) { - self.remaining = remaining; - } - - if let Some(reset) = try!(get_header(&response.headers, "x-ratelimit-reset")) { - self.reset = reset; - } - - Ok(if response.status != StatusCode::TooManyRequests { - false - } else if let Some(retry_after) = try!(get_header(&response.headers, "retry-after")) { - debug!("Ratelimited: {:?}ms", retry_after); - thread::sleep(Duration::from_millis(retry_after as u64)); - - true - } else { - false - }) - } -} - -fn get_header(headers: &Headers, header: &str) -> Result> { - match headers.get_raw(header) { - Some(header) => match str::from_utf8(&header[0]) { - Ok(v) => match v.parse::() { - Ok(v) => Ok(Some(v)), - Err(_why) => Err(Error::Client(ClientError::RateLimitI64)), - }, - Err(_why) => Err(Error::Client(ClientError::RateLimitUtf8)), - }, - None => Ok(None), - } -} diff --git a/src/client/mod.rs b/src/client/mod.rs index cc7164f..2cefb19 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,15 +1,15 @@ //! The Client contains information about a single bot or user's token, as well //! as event handlers. Dispatching events to configured handlers and starting //! the shards' connections are handled directly via the client. In addition, -//! the [`http`] module and [`Cache`] are also automatically handled by the +//! the [`rest`] module and [`Cache`] are also automatically handled by the //! Client module for you. //! //! A [`Context`] is provided for every handler. The context is an ergonomic -//! method of accessing the lower-level http functions. +//! method of accessing the lower-level HTTP functions. //! -//! The `http` module is the lower-level method of interacting with the Discord +//! The `rest` module is the lower-level method of interacting with the Discord //! REST API. Realistically, there should be little reason to use this yourself, -//! as the Context will do this for you. A possible use case of using the `http` +//! as the Context will do this for you. A possible use case of using the `rest` //! module is if you do not have a Cache, for purposes such as low memory //! requirements. //! @@ -18,11 +18,11 @@ //! [`Client`]: struct.Client.html#examples //! [`Context`]: struct.Context.html //! [`Cache`]: ../ext/cache/index.html -//! [`http`]: http/index.html +//! [`rest`]: rest/index.html //! [Client examples]: struct.Client.html#examples -pub mod http; pub mod gateway; +pub mod rest; mod context; mod dispatch; @@ -180,7 +180,7 @@ impl Client { .insert("token", Value::Null) .build(); - http::logout(map) + rest::logout(map) } /// Sets a framework to be used with the client. All message events will be @@ -229,7 +229,7 @@ impl Client { /// /// [gateway docs]: gateway/index.html#sharding pub fn start_autosharded(&mut self) -> Result<()> { - let res = try!(http::get_bot_gateway()); + let res = try!(rest::get_bot_gateway()); self.start_connection(Some([0, res.shards as u8 - 1, res.shards as u8])) } @@ -718,7 +718,7 @@ impl Client { // // Not all shards need to be initialized in this process. fn start_connection(&mut self, shard_data: Option<[u8; 3]>) -> Result<()> { - let gateway_url = try!(http::get_gateway()).url; + let gateway_url = try!(rest::get_gateway()).url; for i in 0..shard_data.map_or(1, |x| x[1] + 1) { let shard = Shard::new(&gateway_url, @@ -795,7 +795,7 @@ impl Client { pub fn boot_shard(&mut self, shard_info: Option<[u8; 2]>) -> Result<(Shard, ReadyEvent, Receiver)> { - let gateway_url = try!(http::get_gateway()).url; + let gateway_url = try!(rest::get_gateway()).url; Shard::new(&gateway_url, &self.token, shard_info, self.login_type) } @@ -1164,7 +1164,7 @@ fn handle_shard(shard: Arc>, fn login(token: &str, login_type: LoginType) -> Client { let token = token.to_owned(); - http::set_token(&token); + rest::set_token(&token); feature_framework! {{ Client { diff --git a/src/client/rest/mod.rs b/src/client/rest/mod.rs new file mode 100644 index 0000000..0558df8 --- /dev/null +++ b/src/client/rest/mod.rs @@ -0,0 +1,1368 @@ +//! 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 + +mod ratelimiting; + +use hyper::client::{ + Client as HyperClient, + RequestBuilder, + Response as HyperResponse, + Request, +}; +use hyper::method::Method; +use hyper::status::StatusCode; +use hyper::{Error as HyperError, Result as HyperResult, Url, header}; +use multipart::client::Multipart; +use self::ratelimiting::Route; +use serde_json::builder::ObjectBuilder; +use serde_json; +use std::default::Default; +use std::io::{ErrorKind as IoErrorKind, Read}; +use std::sync::{Arc, Mutex}; +use ::constants; +use ::internal::prelude::*; +use ::model::*; +use ::utils::decode_array; + +lazy_static! { + static ref TOKEN: Arc> = 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()); +} + +/// Accepts the [`Invite`] given its code, placing the current user in the +/// [`Guild`] that the invite was for. +/// +/// Use [`utils::parse_invite`] to retrieve codes from URLs. +/// +/// Refer to the documentation for [`Context::accept_invite`] for restrictions on +/// accepting an invite. +/// +/// This will fire the [`Client::on_guild_create`] handler once the associated +/// event is received. +/// +/// **Note**: This will fail if you are already in the guild, or are banned. A +/// ban is equivilant to an IP ban. +/// +/// **Note**: Requires that the current user be a user account. Bots can not +/// accept invites. Instead, they must be accepted via OAuth2 authorization +/// links. These are in the format of: +/// +/// `https://discordapp.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot` +/// +/// # Examples +/// +/// Accept an invite given a code from a URL: +/// +/// ```rust,no_run +/// use serenity::client::rest; +/// use serenity::utils; +/// +/// let url = "https://discord.gg/0cDvIgU2voY8RSYL"; +/// let code = utils::parse_invite(url); +/// +/// let _result = rest::accept_invite(code); +/// ``` +/// +/// [`Context::accept_invite`]: ../struct.Context.html#method.accept_invite +/// [`Invite`]: ../../model/struct.Invite.html +/// [`utils::parse_invite`]: ../../utils/fn.parse_invite.html +pub fn accept_invite(code: &str) -> Result { + let response = request!(Route::InvitesCode, post, "/invites/{}", code); + + Invite::decode(try!(serde_json::from_reader(response))) +} + +/// Marks a [`Channel`] as being "read" up to a certain [`Message`]. Any +/// message past the given one will not be marked as read. +/// +/// Usually you should use this to mark the latest message as being read. +/// +/// **Note**: Bot users should not use this, as it has no bearing on them +/// whatsoever. +/// +/// [`Channel`]: ../../model/enum.Channel.html +/// [`Message`]: ../../model/struct.Message.html +pub fn ack_message(channel_id: u64, message_id: u64) -> Result<()> { + verify(204, request!(Route::ChannelsIdMessagesIdAck(channel_id), + post, + "/channels/{}/messages/{}/ack", + channel_id, + message_id)) +} + +/// 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 equivilant 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 [`PublicChannel`] 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 +/// [`PublicChannel`]: ../../model/struct.PublicChannel.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 { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdChannels(guild_id), + post(body), + "/guilds/{}/channels", + guild_id); + + Channel::decode(try!(serde_json::from_reader(response))) +} + +/// 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 { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdEmojis(guild_id), + post(body), + "/guilds/{}/emojis", + guild_id); + + Emoji::decode(try!(serde_json::from_reader(response))) +} + +/// 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 usually only available for user accounts. Refer +/// to Discord's documentation for the endpoint [here][whitelist] for more +/// information. If your bot requires this, re-think what you are doing and +/// whether it _really_ needs to be doing this. +/// +/// # 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::client::rest; +/// +/// let map = ObjectBuilder::new() +/// .insert("name", "test") +/// .insert("region", "us-west") +/// .build(); +/// +/// let _result = rest::create_guild(map); +/// ``` +/// +/// [`Guild`]: ../../model/struct.Guild.html +/// [`PartialGuild`]: ../../model/struct.PartialGuild.html +/// [`Shard`]: ../gateway/struct.Shard.html +/// [US West Region]: ../../model/enum.Region.html#variant.UsWest +/// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild +pub fn create_guild(map: Value) -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::Guilds, post(body), "/guilds"); + + PartialGuild::decode(try!(serde_json::from_reader(response))) +} + +/// 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 = try!(serde_json::to_string(&map)); + + verify(204, request!(Route::GuildsIdIntegrations(guild_id), + post(body), + "/guilds/{}/integrations/{}", + guild_id, + integration_id)) +} + +/// Creates a [`RichInvite`] for the given [channel][`PublicChannel`]. +/// +/// Refer to Discord's [docs] for field information. +/// +/// All fields are optional. +/// +/// **Note**: Requires the [Create Invite] permission. +/// +/// [`PublicChannel`]: ../../model/struct.PublicChannel.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: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::ChannelsIdInvites(channel_id), + post(body), + "/channels/{}/invites", + channel_id); + + RichInvite::decode(try!(serde_json::from_reader(response))) +} + +pub fn create_permission(channel_id: u64, target_id: u64, map: Value) + -> Result<()> { + let body = try!(serde_json::to_string(&map)); + + verify(204, request!(Route::ChannelsIdPermissionsOverwriteId(channel_id), + put(body), + "/channels/{}/permissions/{}", + channel_id, + target_id)) +} + +pub fn create_private_channel(map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::UsersMeChannels, + post(body), + "/users/@me/channels"); + + PrivateChannel::decode(try!(serde_json::from_reader(response))) +} + +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())) +} + +pub fn create_role(guild_id: u64) -> Result { + let body = String::from("{}"); + let response = request!(Route::GuildsIdRoles(guild_id), + post(body), + "/guilds/{}/roles", + guild_id); + + Role::decode(try!(serde_json::from_reader(response))) +} + +/// Creates a webhook for the given [channel][`PublicChannel`]'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::client::rest; +/// +/// let channel_id = 81384788765712384; +/// let map = ObjectBuilder::new().insert("name", "test").build(); +/// +/// let webhook = rest::create_webhook(channel_id, map).expect("err creating"); +/// ``` +/// +/// [`PublicChannel`]: ../../model/struct.PublicChannel.html +pub fn create_webhook(channel_id: u64, map: Value) -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::ChannelsIdWebhooks(channel_id), + post(body), + "/channels/{}/webhooks", + channel_id); + + Webhook::decode(try!(serde_json::from_reader(response))) +} + +pub fn delete_channel(channel_id: u64) -> Result { + let response = request!(Route::ChannelsId(channel_id), + delete, + "/channels/{}", + channel_id); + + Channel::decode(try!(serde_json::from_reader(response))) +} + +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)) +} + +pub fn delete_guild(guild_id: u64) -> Result { + let response = request!(Route::GuildsId(guild_id), + delete, + "/guilds/{}", + guild_id); + + PartialGuild::decode(try!(serde_json::from_reader(response))) +} + +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)) +} + +pub fn delete_invite(code: &str) -> Result { + let response = request!(Route::InvitesCode, delete, "/invite/{}", code); + + Invite::decode(try!(serde_json::from_reader(response))) +} + +pub fn delete_message(channel_id: u64, message_id: u64) + -> Result<()> { + verify(204, request!(Route::ChannelsIdMessagesId(channel_id), + delete, + "/channels/{}/messages/{}", + channel_id, + message_id)) +} + +pub fn delete_messages(channel_id: u64, map: Value) -> Result<()> { + let body = try!(serde_json::to_string(&map)); + + verify(204, request!(Route::ChannelsIdMessagesBulkDelete(channel_id), + post(body), + "/channels/{}/messages/bulk_delete", + channel_id)) +} + +/// Delete all of the [`Reaction`]s associated with a [`Message`]. +/// +/// # Examples +/// +/// ```rust,no_run +/// use serenity::client::rest; +/// use serenity::model::{ChannelId, MessageId}; +/// +/// let channel_id = ChannelId(7); +/// let message_id = MessageId(8); +/// +/// match rest::delete_message_reactions(channel_id.0, message_id.0) { +/// Ok(()) => println!("Reactions deleted"), +/// Err(why) => println!("Error deleting reactions: {:?}", why), +/// } +/// ``` +/// +/// [`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)) +} + +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)) +} + +pub fn delete_reaction(channel_id: u64, + message_id: u64, + user_id: Option, + reaction_type: ReactionType) + -> Result<()> { + let user = user_id.map(|uid| uid.to_string()).unwrap_or("@me".to_string()); + + verify(204, request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), + delete, + "/channels/{}/messages/{}/reactions/{}/{}", + channel_id, + message_id, + reaction_type.as_data(), + user)) +} + +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 +/// +/// Delete a webhook given its Id: +/// +/// ```rust,no_run +/// use serenity::client::{Client, rest}; +/// use std::env; +/// +/// // Due to the `delete_webhook` function requiring you to authenticate, you +/// // must have initialized a client first. +/// let client = Client::login_user(&env::var("DISCORD_TOKEN").unwrap()); +/// +/// rest::delete_webhook(245037420704169985).expect("err 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 +/// +/// Delete a webhook given its Id and unique token: +/// +/// ```rust,no_run +/// use serenity::client::rest; +/// +/// let id = 245037420704169985; +/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; +/// +/// rest::delete_webhook_with_token(id, token).expect("err 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, try!(retry(|| client + .delete(&format!(api!("/webhooks/{}/{}"), webhook_id, token))) + .map_err(Error::Hyper))) +} + +pub fn edit_channel(channel_id: u64, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::ChannelsId(channel_id), + patch(body), + "/channels/{}", + channel_id); + + PublicChannel::decode(try!(serde_json::from_reader(response))) +} + +pub fn edit_emoji(guild_id: u64, emoji_id: u64, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdEmojisId(guild_id), + patch(body), + "/guilds/{}/emojis/{}", + guild_id, + emoji_id); + + Emoji::decode(try!(serde_json::from_reader(response))) +} + +pub fn edit_guild(guild_id: u64, map: Value) -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsId(guild_id), + patch(body), + "/guilds/{}", + guild_id); + + PartialGuild::decode(try!(serde_json::from_reader(response))) +} + +pub fn edit_member(guild_id: u64, user_id: u64, map: Value) + -> Result<()> { + let body = try!(serde_json::to_string(&map)); + + verify(204, request!(Route::GuildsIdMembersId(guild_id), + patch(body), + "/guilds/{}/members/{}", + guild_id, + user_id)) +} + +pub fn edit_message(channel_id: u64, + message_id: u64, + map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::ChannelsIdMessagesId(channel_id), + patch(body), + "/channels/{}/messages/{}", + channel_id, + message_id); + + Message::decode(try!(serde_json::from_reader(response))) +} + +/// 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 = ObjectBuilder::new().insert("nick", new_nickname).build(); + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdMembersMeNick(guild_id), + patch(body), + "/guilds/{}/members/@me/nick", + guild_id); + + verify(200, response) +} + +pub fn edit_note(user_id: u64, map: Value) -> Result<()> { + let body = try!(serde_json::to_string(&map)); + + verify(204, request!(Route::None, + put(body), + "/users/@me/notes/{}", + user_id)) +} + +pub fn edit_profile(map: Value) -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::UsersMe, patch(body), "/users/@me"); + + CurrentUser::decode(try!(serde_json::from_reader(response))) +} + +pub fn edit_role(guild_id: u64, role_id: u64, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdRolesId(guild_id), + patch(body), + "/guilds/{}/roles/{}", + guild_id, + role_id); + + Role::decode(try!(serde_json::from_reader(response))) +} + +/// 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::client::rest; +/// +/// let id = 245037420704169985; +/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; +/// let image = serenity::utils::read_image("./webhook_img.png") +/// .expect("err reading image"); +/// let map = ObjectBuilder::new().insert("avatar", image).build(); +/// +/// let edited = rest::edit_webhook_with_token(id, token, map) +/// .expect("err 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 { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::WebhooksId, + patch(body), + "/webhooks/{}", + webhook_id); + + Webhook::decode(try!(serde_json::from_reader(response))) +} + +/// 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::client::rest; +/// +/// let id = 245037420704169985; +/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; +/// let map = ObjectBuilder::new().insert("name", "new name").build(); +/// +/// let edited = rest::edit_webhook_with_token(id, token, map) +/// .expect("err editing webhook"); +/// ``` +/// +/// [`edit_webhook`]: fn.edit_webhook.html +pub fn edit_webhook_with_token(webhook_id: u64, token: &str, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let client = HyperClient::new(); + let response = try!(retry(|| client + .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) + .body(&body)) + .map_err(Error::Hyper)); + + Webhook::decode(try!(serde_json::from_reader(response))) +} + +/// 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::client::rest; +/// +/// let id = 245037420704169985; +/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; +/// let map = ObjectBuilder::new().insert("content", "test").build(); +/// +/// let message = match rest::execute_webhook(id, token, map) { +/// Ok(message) => message, +/// Err(why) => { +/// println!("Error executing webhook: {:?}", why); +/// +/// return; +/// }, +/// }; +pub fn execute_webhook(webhook_id: u64, token: &str, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let client = HyperClient::new(); + let response = try!(retry(|| client + .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token)) + .body(&body)) + .map_err(Error::Hyper)); + + Message::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_application_info() -> Result { + let response = request!(Route::None, get, "/oauth2/applications/@me"); + + CurrentApplicationInfo::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_applications() -> Result> { + let response = request!(Route::None, get, "/oauth2/applications"); + let decoded = try!(serde_json::from_reader(response)); + + decode_array(decoded, ApplicationInfo::decode) +} + +pub fn get_bans(guild_id: u64) -> Result> { + let response = request!(Route::GuildsIdBans(guild_id), + get, + "/guilds/{}/bans", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), Ban::decode) +} + +pub fn get_bot_gateway() -> Result { + let response = request!(Route::GatewayBot, get, "/gateway/bot"); + + BotGateway::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_channel_invites(channel_id: u64) + -> Result> { + let response = request!(Route::ChannelsIdInvites(channel_id), + get, + "/channels/{}/invites", + channel_id); + + decode_array(try!(serde_json::from_reader(response)), + RichInvite::decode) +} + +/// Retrieves the webhooks for the given [channel][`PublicChannel`]'s Id. +/// +/// This method requires authentication. +/// +/// # Examples +/// +/// Retrieve all of the webhooks owned by a channel: +/// +/// ```rust,no_run +/// use serenity::client::rest; +/// +/// let channel_id = 81384788765712384; +/// +/// let webhooks = rest::get_channel_webhooks(channel_id) +/// .expect("err getting channel webhooks"); +/// ``` +/// +/// [`PublicChannel`]: ../../model/struct.PublicChannel.html +pub fn get_channel_webhooks(channel_id: u64) -> Result> { + let response = request!(Route::ChannelsIdWebhooks(channel_id), + get, + "/channels/{}/webhooks", + channel_id); + + decode_array(try!(serde_json::from_reader(response)), Webhook::decode) +} + +pub fn get_channel(channel_id: u64) -> Result { + let response = request!(Route::ChannelsId(channel_id), + get, + "/channels/{}", + channel_id); + + Channel::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_channels(guild_id: u64) -> Result> { + let response = request!(Route::ChannelsId(guild_id), + get, + "/guilds/{}/channels", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), + PublicChannel::decode) +} + +pub fn get_current_user() -> Result { + let response = request!(Route::UsersMe, get, "/users/@me"); + + CurrentUser::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_gateway() -> Result { + let response = request!(Route::Gateway, get, "/gateway"); + + Gateway::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_emoji(guild_id: u64, emoji_id: u64) -> Result { + let response = request!(Route::GuildsIdEmojisId(guild_id), + get, + "/guilds/{}/emojis/{}", + guild_id, + emoji_id); + + Emoji::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_emojis(guild_id: u64) -> Result> { + let response = request!(Route::GuildsIdEmojis(guild_id), + get, + "/guilds/{}/emojis", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), Emoji::decode) +} + +pub fn get_guild(guild_id: u64) -> Result { + let response = request!(Route::GuildsId(guild_id), + get, + "/guilds/{}", + guild_id); + + PartialGuild::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_guild_embed(guild_id: u64) -> Result { + let response = request!(Route::GuildsIdEmbed(guild_id), + get, + "/guilds/{}/embeds", + guild_id); + + GuildEmbed::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_guild_integrations(guild_id: u64) -> Result> { + let response = request!(Route::GuildsIdIntegrations(guild_id), + get, + "/guilds/{}/integrations", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), Integration::decode) +} + +pub fn get_guild_invites(guild_id: u64) -> Result> { + let response = request!(Route::GuildsIdInvites(guild_id), + get, + "/guilds/{}/invites", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), + RichInvite::decode) +} + +pub fn get_guild_prune_count(guild_id: u64, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdPrune(guild_id), + get(body), + "/guilds/{}/prune", + guild_id); + + GuildPrune::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_guild_regions(guild_id: u64) -> Result> { + let response = request!(Route::GuildsIdRegions(guild_id), + get, + "/guilds/{}/regions", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode) +} + +/// 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::client::rest; +/// +/// let guild_id = 81384788765712384; +/// +/// let webhooks = rest::get_guild_webhooks(guild_id) +/// .expect("err getting guild webhooks"); +/// ``` +/// +/// [`Guild`]: ../../model/struct.Guild.html +pub fn get_guild_webhooks(guild_id: u64) -> Result> { + let response = request!(Route::GuildsIdWebhooks(guild_id), + get, + "/guilds/{}/webhooks", + guild_id); + + decode_array(try!(serde_json::from_reader(response)), Webhook::decode) +} + +pub fn get_guilds() -> Result> { + let response = request!(Route::UsersMeGuilds, + get, + "/users/@me/guilds"); + + decode_array(try!(serde_json::from_reader(response)), GuildInfo::decode) +} + +pub fn get_invite(code: &str) -> Result { + let invite = ::utils::parse_invite(code); + let response = request!(Route::InvitesCode, get, "/invite/{}", invite); + + Invite::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_member(guild_id: u64, user_id: u64) -> Result { + let response = request!(Route::GuildsIdMembersId(guild_id), + get, + "/guilds/{}/members/{}", + guild_id, + user_id); + + Member::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_message(channel_id: u64, message_id: u64) + -> Result { + let response = request!(Route::ChannelsIdMessagesId(channel_id), + get, + "/channels/{}/messages/{}", + channel_id, + message_id); + + Message::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_messages(channel_id: u64, query: &str) + -> Result> { + let url = format!(api_concat!("/channels/{}/messages{}"), + channel_id, + query); + let client = HyperClient::new(); + let response = try!(request(Route::ChannelsIdMessages(channel_id), + || client.get(&url))); + + decode_array(try!(serde_json::from_reader(response)), Message::decode) +} + +pub fn get_pins(channel_id: u64) -> Result> { + let response = request!(Route::ChannelsIdPins(channel_id), + get, + "/channels/{}/pins", + channel_id); + + decode_array(try!(serde_json::from_reader(response)), Message::decode) +} + +pub fn get_reaction_users(channel_id: u64, + message_id: u64, + reaction_type: ReactionType, + limit: u8, + after: Option) + -> Result> { + let mut uri = format!("/channels/{}/messages/{}/reactions/{}?limit={}", + channel_id, + message_id, + reaction_type.as_data(), + limit); + + if let Some(user_id) = after { + uri.push_str("&after="); + uri.push_str(&user_id.to_string()); + } + + let response = request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id), + get, + "{}", + uri); + + decode_array(try!(serde_json::from_reader(response)), User::decode) +} + +pub fn get_user(user_id: u64) -> Result { + let response = request!(Route::UsersId, get, "/users/{}", user_id); + + CurrentUser::decode(try!(serde_json::from_reader(response))) +} + +pub fn get_user_connections() -> Result> { + let response = request!(Route::UsersMeConnections, + get, + "/users/@me/connections"); + + decode_array(try!(serde_json::from_reader(response)), + UserConnection::decode) +} + +pub fn get_user_dm_channels() -> Result> { + let response = request!(Route::UsersMeChannels, get, "/users/@me/channels"); + + decode_array(try!(serde_json::from_reader(response)), + PrivateChannel::decode) +} + +pub fn get_voice_regions() -> Result> { + let response = request!(Route::VoiceRegions, get, "/voice/regions"); + + decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode) +} + +/// 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::client::rest; +/// +/// let id = 245037420704169985; +/// let webhook = rest::get_webhook(id).expect("err getting webhook"); +/// ``` +/// +/// [`get_webhook_with_token`]: fn.get_webhook_with_token.html +pub fn get_webhook(webhook_id: u64) -> Result { + let response = request!(Route::WebhooksId, get, "/webhooks/{}", webhook_id); + + Webhook::decode(try!(serde_json::from_reader(response))) +} + +/// 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::client::rest; +/// +/// let id = 245037420704169985; +/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; +/// +/// let webhook = rest::get_webhook_with_token(id, token) +/// .expect("err getting webhook"); +/// ``` +pub fn get_webhook_with_token(webhook_id: u64, token: &str) -> Result { + let client = HyperClient::new(); + let response = try!(retry(|| client + .get(&format!(api!("/webhooks/{}/{}"), webhook_id, token))) + .map_err(Error::Hyper)); + + Webhook::decode(try!(serde_json::from_reader(response))) +} + +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)) +} + +pub fn leave_group(guild_id: u64) -> Result { + let response = request!(Route::None, + delete, + "/channels/{}", + guild_id); + + Group::decode(try!(serde_json::from_reader(response))) +} + +pub fn leave_guild(guild_id: u64) -> Result { + let response = request!(Route::UsersMeGuildsId, + delete, + "/users/@me/guilds/{}", + guild_id); + + PartialGuild::decode(try!(serde_json::from_reader(response))) +} + +pub fn logout(map: Value) -> Result<()> { + let body = try!(serde_json::to_string(&map)); + + verify(204, request!(Route::None, post(body), "/auth/logout")) +} + +pub fn remove_group_recipient(group_id: u64, user_id: u64) + -> Result<()> { + verify(204, request!(Route::None, + delete, + "/channels/{}/recipients/{}", + group_id, + user_id)) +} + +pub fn send_file(channel_id: u64, + content: &str, + mut file: R, + filename: &str) + -> Result { + let uri = format!(api_concat!("/channels/{}/messages"), channel_id); + let url = match Url::parse(&uri) { + Ok(url) => url, + Err(_why) => return Err(Error::Url(uri)), + }; + + let mut request = try!(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 = try!(Multipart::from_request(request)); + try!(request.write_text("content", content)); + try!(request.write_stream("file", &mut file, Some(&filename), None)); + + Message::decode(try!(serde_json::from_reader(try!(request.send())))) +} + +pub fn send_message(channel_id: u64, map: Value) -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::ChannelsIdMessages(channel_id), + post(body), + "/channels/{}/messages", + channel_id); + + Message::decode(try!(serde_json::from_reader(response))) +} + +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)) +} + +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)) +} + +/// Removes 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)) +} + +pub fn start_guild_prune(guild_id: u64, map: Value) + -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::GuildsIdPrune(guild_id), + post(body), + "/guilds/{}/prune", + guild_id); + + GuildPrune::decode(try!(serde_json::from_reader(response))) +} + +pub fn start_integration_sync(guild_id: u64, integration_id: u64) + -> Result<()> { + verify(204, request!(Route::GuildsIdIntegrationsIdSync(guild_id), + post, + "/guilds/{}/integrations/{}", + guild_id, + integration_id)) +} + +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 + where F: Fn() -> RequestBuilder<'a> { + ratelimiting::perform(route, || f() + .header(header::Authorization(TOKEN.lock().unwrap().clone())) + .header(header::ContentType::json())) +} + +#[doc(hidden)] +pub fn retry<'a, F>(f: F) -> HyperResult + 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 = ClientError::UnknownStatus(expected_status_code); + + return Err(Error::Client(client_error)); + }, + }; + + if response.status == expected_status { + return Ok(()); + } + + debug!("Expected {}, got {}", expected_status_code, response.status); + + let mut s = String::default(); + try!(response.read_to_string(&mut s)); + + debug!("Content: {}", s); + + Err(Error::Client(ClientError::UnexpectedStatusCode(response.status))) +} diff --git a/src/client/rest/ratelimiting.rs b/src/client/rest/ratelimiting.rs new file mode 100644 index 0000000..65af1a3 --- /dev/null +++ b/src/client/rest/ratelimiting.rs @@ -0,0 +1,240 @@ +//! 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 equivilant 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 equivilant for the two +//! requests (`10`). +//! +//! +//! With the examples out of the way: 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. + +use hyper::client::{RequestBuilder, Response}; +use hyper::header::Headers; +use hyper::status::StatusCode; +use std::collections::HashMap; +use std::str; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use time; +use ::internal::prelude::*; + +lazy_static! { + static ref GLOBAL: Arc> = Arc::new(Mutex::new(RateLimit::default())); + static ref ROUTES: Arc>> = Arc::new(Mutex::new(HashMap::default())); +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Route { + ChannelsId(u64), + ChannelsIdInvites(u64), + ChannelsIdMessages(u64), + ChannelsIdMessagesBulkDelete(u64), + ChannelsIdMessagesId(u64), + ChannelsIdMessagesIdAck(u64), + ChannelsIdMessagesIdReactions(u64), + ChannelsIdMessagesIdReactionsUserIdType(u64), + ChannelsIdPermissionsOverwriteId(u64), + ChannelsIdPins(u64), + ChannelsIdPinsMessageId(u64), + ChannelsIdTyping(u64), + ChannelsIdWebhooks(u64), + Gateway, + GatewayBot, + Guilds, + GuildsId(u64), + GuildsIdBans(u64), + GuildsIdBansUserId(u64), + GuildsIdChannels(u64), + GuildsIdEmbed(u64), + GuildsIdEmojis(u64), + GuildsIdEmojisId(u64), + GuildsIdIntegrations(u64), + GuildsIdIntegrationsId(u64), + GuildsIdIntegrationsIdSync(u64), + GuildsIdInvites(u64), + GuildsIdMembersId(u64), + GuildsIdMembersIdRolesId(u64), + GuildsIdMembersMeNick(u64), + GuildsIdPrune(u64), + GuildsIdRegions(u64), + GuildsIdRoles(u64), + GuildsIdRolesId(u64), + GuildsIdWebhooks(u64), + InvitesCode, + UsersId, + UsersMe, + UsersMeChannels, + UsersMeConnections, + UsersMeGuilds, + UsersMeGuildsId, + VoiceRegions, + WebhooksId, + None, +} + +pub fn perform<'a, F>(route: Route, f: F) -> Result + where F: Fn() -> RequestBuilder<'a> { + + loop { + // 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 + { + let mut global = GLOBAL.lock().expect("global route lock poisoned"); + global.pre_hook(); + } + + if route != Route::None { + if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) { + route.pre_hook(); + } + } + + let response = try!(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 mut global = GLOBAL.lock().expect("global route lock poisoned"); + global.post_hook(&response) + } else { + ROUTES.lock() + .expect("routes poisoned") + .entry(route) + .or_insert_with(RateLimit::default) + .post_hook(&response) + }; + + if redo.unwrap_or(false) { + continue; + } + } + + return Ok(response); + } +} + +#[derive(Clone, Debug, Default)] +pub struct RateLimit { + limit: i64, + remaining: i64, + reset: i64, +} + +impl RateLimit { + 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; + } + + pub fn post_hook(&mut self, response: &Response) -> Result { + if let Some(limit) = try!(get_header(&response.headers, "x-ratelimit-limit")) { + self.limit = limit; + } + + if let Some(remaining) = try!(get_header(&response.headers, "x-ratelimit-remaining")) { + self.remaining = remaining; + } + + if let Some(reset) = try!(get_header(&response.headers, "x-ratelimit-reset")) { + self.reset = reset; + } + + Ok(if response.status != StatusCode::TooManyRequests { + false + } else if let Some(retry_after) = try!(get_header(&response.headers, "retry-after")) { + debug!("Ratelimited: {:?}ms", retry_after); + thread::sleep(Duration::from_millis(retry_after as u64)); + + true + } else { + false + }) + } +} + +fn get_header(headers: &Headers, header: &str) -> Result> { + match headers.get_raw(header) { + Some(header) => match str::from_utf8(&header[0]) { + Ok(v) => match v.parse::() { + Ok(v) => Ok(Some(v)), + Err(_why) => Err(Error::Client(ClientError::RateLimitI64)), + }, + Err(_why) => Err(Error::Client(ClientError::RateLimitUtf8)), + }, + None => Ok(None), + } +} diff --git a/src/error.rs b/src/error.rs index 8c442ec..6c534fb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -34,10 +34,10 @@ pub type Result = ::std::result::Result; /// [`Result`]: type.Result.html #[derive(Debug)] pub enum Error { - /// An [http] or [client] error. + /// A [rest] or [client] error. /// /// [client]: client/index.html - /// [http]: client/http/index.html + /// [rest]: client/rest/index.html Client(ClientError), /// An error with the WebSocket [`Gateway`]. /// diff --git a/src/ext/cache/mod.rs b/src/ext/cache/mod.rs index 4e46188..e819eea 100644 --- a/src/ext/cache/mod.rs +++ b/src/ext/cache/mod.rs @@ -8,7 +8,7 @@ use ::model::*; /// some data from the event is possible. /// /// This acts as a cache, to avoid making requests over the REST API through the -/// [`http`] module where possible. All fields are public, and do not have +/// [`rest`] module where possible. All fields are public, and do not have /// getters, to allow you more flexibility with the stored data. However, this /// allows data to be "corrupted", and _may or may not_ cause misfunctions /// within the library. Mutate data at your own discretion. @@ -23,7 +23,7 @@ use ::model::*; /// /// This allows you to only need to perform the `Context::get_channel` call, /// and not need to first search through the cache - and if not found - _then_ -/// perform an HTTP request through the Context or `http` module. +/// perform an HTTP request through the Context or `rest` module. /// /// Additionally, note that some information received through events can _not_ /// be retrieved through the REST API. This is information such as [`Role`]s in @@ -34,7 +34,7 @@ use ::model::*; /// [`Context::get_channel`]: ../../client/struct.Context.html#method.get_channel /// [`Guild`]: ../../model/struct.Guild.html /// [`Role`]: ../../model/struct.Role.html -/// [`http`]: ../../client/http/index.html +/// [`rest`]: ../../client/rest/index.html #[derive(Debug, Clone)] pub struct Cache { /// A map of the currently active calls that the current user knows about, diff --git a/src/ext/framework/configuration.rs b/src/ext/framework/configuration.rs index 4b0fc48..6dd4fd2 100644 --- a/src/ext/framework/configuration.rs +++ b/src/ext/framework/configuration.rs @@ -1,5 +1,5 @@ use std::default::Default; -use ::client::http; +use ::client::rest; pub struct Configuration { #[doc(hidden)] @@ -53,7 +53,7 @@ impl Configuration { return self; } - if let Ok(current_user) = http::get_current_user() { + if let Ok(current_user) = rest::get_current_user() { self.on_mention = Some(vec![ format!("<@{}>", current_user.id), // Regular mention format!("<@!{}>", current_user.id), // Nickname mention diff --git a/src/model/channel.rs b/src/model/channel.rs index 3be8836..ffc8658 100644 --- a/src/model/channel.rs +++ b/src/model/channel.rs @@ -33,7 +33,7 @@ use ::utils::builder::{CreateEmbed, CreateInvite, EditChannel}; #[cfg(all(feature = "cache", feature = "methods"))] use ::client::CACHE; #[cfg(all(feature = "methods"))] -use ::client::http; +use ::client::rest; impl Attachment { /// If this attachment is an image, then a tuple of the width and height @@ -286,7 +286,7 @@ impl Group { /// Adds the given user to the group. If the user is already in the group, /// then nothing is done. /// - /// Refer to [`http::add_group_recipient`] for more information. + /// Refer to [`rest::add_group_recipient`] for more information. /// /// **Note**: Groups have a limit of 10 recipients, including the current /// user. @@ -299,13 +299,13 @@ impl Group { return Ok(()); } - http::add_group_recipient(self.channel_id.0, user.0) + rest::add_group_recipient(self.channel_id.0, user.0) } /// Broadcasts that the current user is typing in the group. #[cfg(feature = "methods")] pub fn broadcast_typing(&self) -> Result<()> { - http::broadcast_typing(self.channel_id.0) + rest::broadcast_typing(self.channel_id.0) } /// Deletes multiple messages in the group. @@ -337,7 +337,7 @@ impl Group { .insert("messages", ids) .build(); - http::delete_messages(self.channel_id.0, map) + rest::delete_messages(self.channel_id.0, map) } /// Returns the formatted URI of the group's icon if one exists. @@ -349,7 +349,7 @@ impl Group { /// Leaves the group. #[cfg(feature = "methods")] pub fn leave(&self) -> Result { - http::leave_group(self.channel_id.0) + rest::leave_group(self.channel_id.0) } /// Generates a name for the group. @@ -378,7 +378,7 @@ impl Group { /// Retrieves the list of messages that have been pinned in the group. #[cfg(feature = "methods")] pub fn pins(&self) -> Result> { - http::get_pins(self.channel_id.0) + rest::get_pins(self.channel_id.0) } /// Removes a recipient from the group. If the recipient is already not in @@ -394,7 +394,7 @@ impl Group { return Ok(()); } - http::remove_group_recipient(self.channel_id.0, user.0) + rest::remove_group_recipient(self.channel_id.0, user.0) } /// Sends a message to the group with the given content. @@ -412,7 +412,7 @@ impl Group { .insert("tts", false) .build(); - http::send_message(self.channel_id.0, map) + rest::send_message(self.channel_id.0, map) } } @@ -448,7 +448,7 @@ impl Message { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::delete_message(self.channel_id.0, self.id.0) + rest::delete_message(self.channel_id.0, self.id.0) } /// Deletes all of the [`Reaction`]s associated with the message. @@ -471,7 +471,7 @@ impl Message { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::delete_message_reactions(self.channel_id.0, self.id.0) + rest::delete_message_reactions(self.channel_id.0, self.id.0) } /// Edits this message, replacing the original content with new content. @@ -519,7 +519,7 @@ impl Message { map = map.insert("embed", Value::Object(embed)); } - match http::edit_message(self.channel_id.0, self.id.0, map.build()) { + match rest::edit_message(self.channel_id.0, self.id.0, map.build()) { Ok(edited) => { mem::replace(self, edited); @@ -568,7 +568,7 @@ impl Message { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::pin_message(self.channel_id.0, self.id.0) + rest::pin_message(self.channel_id.0, self.id.0) } /// React to the message with a custom [`Emoji`] or unicode character. @@ -592,7 +592,7 @@ impl Message { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::create_reaction(self.channel_id.0, + rest::create_reaction(self.channel_id.0, self.id.0, reaction_type.into()) } @@ -641,7 +641,7 @@ impl Message { .insert("tts", false) .build(); - http::send_message(self.channel_id.0, map) + rest::send_message(self.channel_id.0, map) } /// Unpins the message from its channel. @@ -664,7 +664,7 @@ impl Message { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::unpin_message(self.channel_id.0, self.id.0) + rest::unpin_message(self.channel_id.0, self.id.0) } } @@ -692,7 +692,7 @@ impl PrivateChannel { /// Broadcasts that the current user is typing to the recipient. #[cfg(feature = "methods")] pub fn broadcast_typing(&self) -> Result<()> { - http::broadcast_typing(self.id.0) + rest::broadcast_typing(self.id.0) } #[doc(hidden)] @@ -738,7 +738,7 @@ impl PrivateChannel { .insert("messages", ids) .build(); - http::delete_messages(self.id.0, map) + rest::delete_messages(self.id.0, map) } /// Deletes the channel. This does not delete the contents of the channel, @@ -746,14 +746,14 @@ impl PrivateChannel { /// be re-opened. #[cfg(feature = "methods")] pub fn delete(&self) -> Result { - http::delete_channel(self.id.0) + rest::delete_channel(self.id.0) } /// Retrieves the list of messages that have been pinned in the private /// channel. #[cfg(feature = "methods")] pub fn pins(&self) -> Result> { - http::get_pins(self.id.0) + rest::get_pins(self.id.0) } /// Sends a message to the channel with the given content. @@ -779,7 +779,7 @@ impl PrivateChannel { .insert("tts", false) .build(); - http::send_message(self.id.0, map) + rest::send_message(self.id.0, map) } } @@ -807,7 +807,7 @@ impl PublicChannel { /// [Send Messages]: permissions/constants.SEND_MESSAGES.html #[cfg(feature = "methods")] pub fn broadcast_typing(&self) -> Result<()> { - http::broadcast_typing(self.id.0) + rest::broadcast_typing(self.id.0) } #[cfg(feature = "methods")] @@ -821,7 +821,7 @@ impl PublicChannel { let map = f(CreateInvite::default()).0.build(); - http::create_invite(self.id.0, map) + rest::create_invite(self.id.0, map) } #[doc(hidden)] @@ -860,7 +860,7 @@ impl PublicChannel { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::delete_channel(self.id.0) + rest::delete_channel(self.id.0) } #[cfg(feature = "methods")] @@ -879,7 +879,7 @@ impl PublicChannel { let edited = f(EditChannel(map)).0.build(); - match http::edit_channel(self.id.0, edited) { + match rest::edit_channel(self.id.0, edited) { Ok(channel) => { mem::replace(self, channel); @@ -907,7 +907,7 @@ impl PublicChannel { #[cfg(feature = "methods")] pub fn pins(&self) -> Result> { - http::get_pins(self.id.0) + rest::get_pins(self.id.0) } /// Sends a message to the channel with the given content. @@ -946,7 +946,7 @@ impl PublicChannel { .insert("tts", false) .build(); - http::send_message(self.id.0, map) + rest::send_message(self.id.0, map) } /// Retrieves the channel's webhooks. @@ -956,7 +956,7 @@ impl PublicChannel { /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[cfg(feature = "methods")] pub fn webhooks(&self) -> Result> { - http::get_channel_webhooks(self.id.0) + rest::get_channel_webhooks(self.id.0) } } @@ -1006,12 +1006,12 @@ impl Reaction { } } - http::delete_reaction(self.channel_id.0, + rest::delete_reaction(self.channel_id.0, self.message_id.0, user, self.emoji.clone()) } else { - http::delete_reaction(self.channel_id.0, + rest::delete_reaction(self.channel_id.0, self.message_id.0, Some(self.user_id.0), self.emoji.clone()) @@ -1049,7 +1049,7 @@ impl Reaction { -> Result> where R: Into, U: Into { - http::get_reaction_users(self.channel_id.0, + rest::get_reaction_users(self.channel_id.0, self.message_id.0, reaction_type.into(), limit.unwrap_or(50), diff --git a/src/model/guild.rs b/src/model/guild.rs index 842333b..33adc80 100644 --- a/src/model/guild.rs +++ b/src/model/guild.rs @@ -23,7 +23,7 @@ use std::mem; #[cfg(feature = "methods")] use ::utils::builder::{EditGuild, EditMember, EditRole}; #[cfg(feature = "methods")] -use ::client::http; +use ::client::rest; #[cfg(feature = "cache")] use ::client::CACHE; @@ -70,7 +70,7 @@ impl Emoji { #[cfg(all(feature = "cache", feature = "methods"))] pub fn delete(&self) -> Result<()> { match self.find_guild_id() { - Some(guild_id) => http::delete_emoji(guild_id.0, self.id.0), + Some(guild_id) => rest::delete_emoji(guild_id.0, self.id.0), None => Err(Error::Client(ClientError::ItemMissing)), } } @@ -90,7 +90,7 @@ impl Emoji { .insert("name", name) .build(); - match http::edit_emoji(guild_id.0, self.id.0, map) { + match rest::edit_emoji(guild_id.0, self.id.0, map) { Ok(emoji) => { mem::replace(self, emoji); @@ -143,7 +143,7 @@ impl PartialGuild { #[cfg(feature = "methods")] #[inline] pub fn edit_nickname(&self, new_nickname: Option<&str>) -> Result<()> { - http::edit_nickname(self.id.0, new_nickname) + rest::edit_nickname(self.id.0, new_nickname) } /// Returns a formatted URL of the guild's icon, if the guild has an icon. @@ -160,7 +160,7 @@ impl PartialGuild { #[cfg(feature = "methods")] #[inline] pub fn webhooks(&self) -> Result> { - http::get_guild_webhooks(self.id.0) + rest::get_guild_webhooks(self.id.0) } } @@ -224,7 +224,7 @@ impl Guild { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::ban_user(self.id.0, user.into().0, delete_message_days) + rest::ban_user(self.id.0, user.into().0, delete_message_days) } /// Retrieves a list of [`Ban`]s for the guild. @@ -247,7 +247,7 @@ impl Guild { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::get_bans(self.id.0) + rest::get_bans(self.id.0) } /// Creates a new [`Channel`] in the guild. @@ -286,7 +286,7 @@ impl Guild { .insert("type", kind.name()) .build(); - http::create_channel(self.id.0, map) + rest::create_channel(self.id.0, map) } /// Creates a new [`Role`] in the guild with the data set, if any. @@ -314,11 +314,11 @@ impl Guild { } let role = { - try!(http::create_role(self.id.0)) + try!(rest::create_role(self.id.0)) }; let map = f(EditRole::default()).0.build(); - http::edit_role(self.id.0, role.id.0, map) + rest::edit_role(self.id.0, role.id.0, map) } #[doc(hidden)] @@ -387,7 +387,7 @@ impl Guild { } }} - http::delete_guild(self.id.0) + rest::delete_guild(self.id.0) } /// Edits the current guild with new data where specified. See the @@ -415,7 +415,7 @@ impl Guild { let map = f(EditGuild::default()).0.build(); - match http::edit_guild(self.id.0, map) { + match rest::edit_guild(self.id.0, map) { Ok(guild) => { self.afk_channel_id = guild.afk_channel_id; self.afk_timeout = guild.afk_timeout; @@ -458,7 +458,7 @@ impl Guild { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::edit_nickname(self.id.0, new_nickname) + rest::edit_nickname(self.id.0, new_nickname) } /// Attempts to retrieve a [`PublicChannel`] with the given Id. @@ -488,7 +488,7 @@ impl Guild { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::get_guild_invites(self.id.0) + rest::get_guild_invites(self.id.0) } /// Attempts to retrieve the given user's member instance in the guild. @@ -554,7 +554,7 @@ impl Guild { /// Leaves the guild. #[cfg(feature = "methods")] pub fn leave(&self) -> Result { - http::leave_guild(self.id.0) + rest::leave_guild(self.id.0) } /// Calculate a [`User`]'s permissions in a given channel in the guild. @@ -694,7 +694,7 @@ impl Guild { .insert("days", days) .build(); - http::get_guild_prune_count(self.id.0, map) + rest::get_guild_prune_count(self.id.0, map) } /// Starts a prune of [`Member`]s. @@ -724,7 +724,7 @@ impl Guild { .insert("days", days) .build(); - http::start_guild_prune(self.id.0, map) + rest::start_guild_prune(self.id.0, map) } /// Unbans the given [`User`] from the guild. @@ -747,7 +747,7 @@ impl Guild { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::remove_ban(self.id.0, user.into().0) + rest::remove_ban(self.id.0, user.into().0) } /// Retrieves the guild's webhooks. @@ -758,7 +758,7 @@ impl Guild { #[cfg(feature = "methods")] #[inline] pub fn webhooks(&self) -> Result> { - http::get_guild_webhooks(self.id.0) + rest::get_guild_webhooks(self.id.0) } } @@ -780,7 +780,7 @@ impl Member { let guild_id = try!(self.find_guild()); - match http::add_member_role(guild_id.0, self.user.id.0, role_id.0) { + match rest::add_member_role(guild_id.0, self.user.id.0, role_id.0) { Ok(()) => { self.roles.push(role_id); @@ -804,7 +804,7 @@ impl Member { let map = EditMember::default().roles(&self.roles).0.build(); - match http::edit_member(guild_id.0, self.user.id.0, map) { + match rest::edit_member(guild_id.0, self.user.id.0, map) { Ok(()) => Ok(()), Err(why) => { self.roles.retain(|r| !role_ids.contains(r)); @@ -824,7 +824,7 @@ impl Member { pub fn ban(&self, delete_message_days: u8) -> Result<()> { let guild_id = try!(self.find_guild()); - http::ban_user(guild_id.0, + rest::ban_user(guild_id.0, self.user.id.0, delete_message_days) } @@ -850,7 +850,7 @@ impl Member { let guild_id = try!(self.find_guild()); let map = f(EditMember::default()).0.build(); - http::edit_member(guild_id.0, self.user.id.0, map) + rest::edit_member(guild_id.0, self.user.id.0, map) } /// Finds the Id of the [`Guild`] that the member is in. @@ -893,7 +893,7 @@ impl Member { let guild_id = try!(self.find_guild()); - match http::remove_member_role(guild_id.0, self.user.id.0, role_id.0) { + match rest::remove_member_role(guild_id.0, self.user.id.0, role_id.0) { Ok(()) => { self.roles.retain(|r| r.0 != role_id.0); @@ -916,7 +916,7 @@ impl Member { let map = EditMember::default().roles(&self.roles).0.build(); - match http::edit_member(guild_id.0, self.user.id.0, map) { + match rest::edit_member(guild_id.0, self.user.id.0, map) { Ok(()) => Ok(()), Err(why) => { self.roles.extend_from_slice(role_ids); @@ -1017,7 +1017,7 @@ impl Role { pub fn delete(&self) -> Result<()> { let guild_id = try!(self.find_guild()); - http::delete_role(guild_id.0, self.id.0) + rest::delete_role(guild_id.0, self.id.0) } /// Searches the cache for the guild that owns the role. diff --git a/src/model/id.rs b/src/model/id.rs index d29fa2a..d7d131a 100644 --- a/src/model/id.rs +++ b/src/model/id.rs @@ -3,7 +3,7 @@ use super::*; #[cfg(all(feature = "cache", feature = "methods"))] use ::client::CACHE; #[cfg(feature = "methods")] -use ::client::http; +use ::client::rest; #[cfg(feature = "methods")] use ::internal::prelude::*; @@ -24,7 +24,7 @@ impl ChannelId { } }} - http::get_channel(self.0) + rest::get_channel(self.0) } /// Returns a [`Mention`] which will link to the [`Channel`]. @@ -45,7 +45,7 @@ impl ChannelId { /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[cfg(feature="methods")] pub fn webhooks(&self) -> Result> { - http::get_channel_webhooks(self.0) + rest::get_channel_webhooks(self.0) } } @@ -90,7 +90,7 @@ impl GuildId { /// all data with a guild retrieval. #[cfg(feature="methods")] pub fn get(&self) -> Result { - http::get_guild(self.0) + rest::get_guild(self.0) } /// Mentions the [`Guild`]'s default channel. @@ -117,7 +117,7 @@ impl GuildId { /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[cfg(feature="methods")] pub fn webhooks(&self) -> Result> { - http::get_guild_webhooks(self.0) + rest::get_guild_webhooks(self.0) } } @@ -229,6 +229,6 @@ impl WebhookId { /// [Manage Webhooks]: permissions/constant.MANAGE_WEBHOOKS.html #[cfg(feature="methods")] pub fn webhooks(&self) -> Result { - http::get_webhook(self.0) + rest::get_webhook(self.0) } } diff --git a/src/model/invite.rs b/src/model/invite.rs index 2f65fd1..8322ddb 100644 --- a/src/model/invite.rs +++ b/src/model/invite.rs @@ -1,5 +1,5 @@ use super::{Invite, RichInvite}; -use ::client::http; +use ::client::rest; use ::internal::prelude::*; use super::{permissions, utils}; @@ -10,7 +10,7 @@ impl Invite { /// Accepts the invite, placing the current user in the [`Guild`] that the /// invite was for. /// - /// Refer to [`http::accept_invite`] for more information. + /// Refer to [`rest::accept_invite`] for more information. /// /// **Note**: This will fail if you are already in the guild, or are banned. /// A ban is equivilant to an IP ban. @@ -25,7 +25,7 @@ impl Invite { /// /// [`ClientError::InvalidOperationAsBot`]: enum.ClientError.html#variant.InvalidOperationAsBot /// [`Guild`]: struct.Guild.html - /// [`http::accept_invite`]: ../client/http/fn.accept_invite.html + /// [`rest::accept_invite`]: ../client/rest/fn.accept_invite.html /// [permission]: permissions/index.html #[cfg(feature="methods")] pub fn accept(&self) -> Result { @@ -35,7 +35,7 @@ impl Invite { } }} - http::accept_invite(&self.code) + rest::accept_invite(&self.code) } /// Deletes the invite. @@ -58,7 +58,7 @@ impl Invite { return Err(Error::Client(ClientError::InvalidPermissions(req))); } - http::delete_invite(&self.code) + rest::delete_invite(&self.code) } } @@ -66,7 +66,7 @@ impl RichInvite { /// Accepts the invite, placing the current user in the [`Guild`] that the /// invite was for. /// - /// Refer to [`http::accept_invite`] for more information. + /// Refer to [`rest::accept_invite`] for more information. /// /// **Note**: This will fail if you are already in the guild, or are banned. /// A ban is equivilant to an IP ban. @@ -80,7 +80,7 @@ impl RichInvite { /// /// [`ClientError::InvalidOperationAsBot`]: enum.ClientError.html#variant.InvalidOperationAsBot /// [`Guild`]: struct.Guild.html - /// [`http::accept_invite`]: ../client/http/fn.accept_invite.html + /// [`rest::accept_invite`]: ../client/rest/fn.accept_invite.html #[cfg(feature="methods")] pub fn accept(&self) -> Result { feature_cache_enabled! {{ @@ -89,12 +89,12 @@ impl RichInvite { } }} - http::accept_invite(&self.code) + rest::accept_invite(&self.code) } /// Deletes the invite. /// - /// Refer to [`http::delete_invite`] for more information. + /// Refer to [`rest::delete_invite`] for more information. /// /// **Note**: Requires the [Manage Guild] permission. /// @@ -106,7 +106,7 @@ impl RichInvite { /// /// [`ClientError::InvalidPermissions`]: ../client/enum.ClientError.html#variant.InvalidPermissions /// [`Invite::delete`]: struct.Invite.html#method.delete - /// [`http::delete_invite`]: ../client/http/fn.delete_invite.html + /// [`rest::delete_invite`]: ../client/rest/fn.delete_invite.html /// [Manage Guild]: permissions/constant.MANAGE_GUILD.html /// [permission]: permissions/index.html #[cfg(feature = "methods")] @@ -119,6 +119,6 @@ impl RichInvite { } }} - http::delete_invite(&self.code) + rest::delete_invite(&self.code) } } diff --git a/src/model/user.rs b/src/model/user.rs index bedf696..4201bd2 100644 --- a/src/model/user.rs +++ b/src/model/user.rs @@ -20,7 +20,7 @@ use super::Message; #[cfg(feature = "methods")] use time::Timespec; #[cfg(feature = "methods")] -use ::client::http; +use ::client::rest; #[cfg(feature = "cache")] use ::client::CACHE; @@ -76,14 +76,14 @@ impl User { .insert("recipient_id", self.id.0) .build(); - try!(http::create_private_channel(map)).id + try!(rest::create_private_channel(map)).id } } else { let map = ObjectBuilder::new() .insert("recipient_id", self.id.0) .build(); - try!(http::create_private_channel(map)).id + try!(rest::create_private_channel(map)).id }}; let map = ObjectBuilder::new() @@ -92,7 +92,7 @@ impl User { .insert("tts", false) .build(); - http::send_message(private_channel_id.0, map) + rest::send_message(private_channel_id.0, map) } /// Check if a user has a [`Role`]. This will retrieve the diff --git a/src/model/webhook.rs b/src/model/webhook.rs index f0d7b16..4553f30 100644 --- a/src/model/webhook.rs +++ b/src/model/webhook.rs @@ -2,19 +2,19 @@ use serde_json::builder::ObjectBuilder; use std::mem; use super::{Message, Webhook}; use ::utils::builder::ExecuteWebhook; -use ::client::http; +use ::client::rest; use ::internal::prelude::*; impl Webhook { /// Deletes the webhook. /// - /// As this calls the [`http::delete_webhook_with_token`] function, + /// As this calls the [`rest::delete_webhook_with_token`] function, /// authentication is not required. /// - /// [`http::delete_webhook_with_token`]: ../client/http/fn.delete_webhook_with_token.html + /// [`rest::delete_webhook_with_token`]: ../client/rest/fn.delete_webhook_with_token.html #[cfg(feature="methods")] pub fn delete(&self) -> Result<()> { - http::delete_webhook_with_token(self.id.0, &self.token) + rest::delete_webhook_with_token(self.id.0, &self.token) } /// @@ -23,9 +23,9 @@ impl Webhook { /// To nullify the avatar, pass `Some("")`. Otherwise, passing `None` will /// not modify the avatar. /// - /// Refer to [`http::edit_webhook`] for restrictions on editing webhooks. + /// Refer to [`rest::edit_webhook`] for restrictions on editing webhooks. /// - /// As this calls the [`http::edit_webhook_with_token`] function, + /// As this calls the [`rest::edit_webhook_with_token`] function, /// authentication is not required. /// /// # Examples @@ -33,12 +33,12 @@ impl Webhook { /// Editing a webhook's name: /// /// ```rust,no_run - /// use serenity::client::http; + /// use serenity::client::rest; /// /// let id = 245037420704169985; /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// - /// let mut webhook = http::get_webhook_with_token(id, token) + /// let mut webhook = rest::get_webhook_with_token(id, token) /// .expect("valid webhook"); /// /// let _ = webhook.edit(Some("new name"), None).expect("err editing"); @@ -47,12 +47,12 @@ impl Webhook { /// Setting a webhook's avatar: /// /// ```rust,no_run - /// use serenity::client::http; + /// use serenity::client::rest; /// /// let id = 245037420704169985; /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// - /// let mut webhook = http::get_webhook_with_token(id, token) + /// let mut webhook = rest::get_webhook_with_token(id, token) /// .expect("valid webhook"); /// /// let image = serenity::utils::read_image("./webhook_img.png") @@ -61,8 +61,8 @@ impl Webhook { /// let _ = webhook.edit(None, Some(&image)).expect("err editing"); /// ``` /// - /// [`http::edit_webhook`]: ../client/http/fn.edit_webhook.html - /// [`http::edit_webhook_with_token`]: ../client/http/fn.edit_webhook_with_token.html + /// [`rest::edit_webhook`]: ../client/rest/fn.edit_webhook.html + /// [`rest::edit_webhook_with_token`]: ../client/rest/fn.edit_webhook_with_token.html #[cfg(feature="methods")] pub fn edit(&mut self, name: Option<&str>, avatar: Option<&str>) -> Result<()> { @@ -86,7 +86,7 @@ impl Webhook { let map = map.build(); - match http::edit_webhook_with_token(self.id.0, &self.token, map) { + match rest::edit_webhook_with_token(self.id.0, &self.token, map) { Ok(replacement) => { mem::replace(self, replacement); @@ -106,12 +106,12 @@ impl Webhook { /// Execute a webhook with message content of `test`: /// /// ```rust,no_run - /// use serenity::client::http; + /// use serenity::client::rest; /// /// let id = 245037420704169985; /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// - /// let mut webhook = http::get_webhook_with_token(id, token) + /// let mut webhook = rest::get_webhook_with_token(id, token) /// .expect("valid webhook"); /// /// let _ = webhook.execute(|w| w.content("test")).expect("err executing"); @@ -121,13 +121,13 @@ impl Webhook { /// username to `serenity`, and sending an embed: /// /// ```rust,no_run - /// use serenity::client::http; + /// use serenity::client::rest; /// use serenity::model::Embed; /// /// let id = 245037420704169985; /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// - /// let mut webhook = http::get_webhook_with_token(id, token) + /// let mut webhook = rest::get_webhook_with_token(id, token) /// .expect("valid webhook"); /// /// let embed = Embed::fake(|e| e @@ -148,19 +148,19 @@ impl Webhook { where F: FnOnce(ExecuteWebhook) -> ExecuteWebhook { let map = f(ExecuteWebhook::default()).0.build(); - http::execute_webhook(self.id.0, &self.token, map) + rest::execute_webhook(self.id.0, &self.token, map) } /// Retrieves the latest information about the webhook, editing the /// webhook in-place. /// - /// As this calls the [`http::get_webhook_with_token`] function, + /// As this calls the [`rest::get_webhook_with_token`] function, /// authentication is not required. /// - /// [`http::get_webhook_with_token`]: ../client/http/fn.get_webhook_with_token.html + /// [`rest::get_webhook_with_token`]: ../client/rest/fn.get_webhook_with_token.html #[cfg(feature="methods")] pub fn refresh(&mut self) -> Result<()> { - match http::get_webhook_with_token(self.id.0, &self.token) { + match rest::get_webhook_with_token(self.id.0, &self.token) { Ok(replacement) => { mem::replace(self, replacement); diff --git a/src/utils/builder/create_message.rs b/src/utils/builder/create_message.rs index 9bbc461..8522130 100644 --- a/src/utils/builder/create_message.rs +++ b/src/utils/builder/create_message.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use std::default::Default; use super::CreateEmbed; -/// A builder to specify the contents of an [`http::create_message`] request, +/// A builder to specify the contents of an [`rest::create_message`] request, /// primarily meant for use through [`Context::send_message`]. /// /// There are two situations where different field requirements are present: @@ -31,7 +31,7 @@ use super::CreateEmbed; /// [`Context::send_message`]: ../../client/struct.Context.html#method.send_message /// [`content`]: #method.content /// [`embed`]: #method.embed -/// [`http::create_message`]: ../../client/http/fn.create_message.html +/// [`rest::create_message`]: ../../client/rest/fn.create_message.html pub struct CreateMessage(pub BTreeMap); impl CreateMessage { diff --git a/src/utils/builder/execute_webhook.rs b/src/utils/builder/execute_webhook.rs index a434057..06bedf4 100644 --- a/src/utils/builder/execute_webhook.rs +++ b/src/utils/builder/execute_webhook.rs @@ -16,14 +16,14 @@ use std::default::Default; /// payload of [`Webhook::execute`]: /// /// ```rust,no_run -/// use serenity::client::http; +/// use serenity::client::rest; /// use serenity::model::Embed; /// use serenity::utils::Colour; /// /// let id = 245037420704169985; /// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"; /// -/// let webhook = http::get_webhook_with_token(id, token) +/// let webhook = rest::get_webhook_with_token(id, token) /// .expect("valid webhook"); /// /// let website = Embed::fake(|e| e @@ -51,7 +51,7 @@ use std::default::Default; /// /// [`Webhook`]: ../model/struct.Webhook.html /// [`Webhook::execute`]: ../../model/struct.Webhook.html#method.execute -/// [`execute_webhook`]: ../client/http/fn.execute_webhook.html +/// [`execute_webhook`]: ../client/rest/fn.execute_webhook.html pub struct ExecuteWebhook(pub ObjectBuilder); impl ExecuteWebhook { -- cgit v1.2.3