From db62348fc543c6c3dc18d0e5adefc0f138dd2fae Mon Sep 17 00:00:00 2001 From: Austin Hellyer Date: Sun, 6 Nov 2016 08:45:09 -0800 Subject: Move HTTP/ratelimiting into a separate module --- src/client/http.rs | 749 --------------------------------------- src/client/http/mod.rs | 751 ++++++++++++++++++++++++++++++++++++++++ src/client/http/ratelimiting.rs | 226 ++++++++++++ src/client/mod.rs | 1 - src/client/ratelimiting.rs | 227 ------------ 5 files changed, 977 insertions(+), 977 deletions(-) delete mode 100644 src/client/http.rs create mode 100644 src/client/http/mod.rs create mode 100644 src/client/http/ratelimiting.rs delete mode 100644 src/client/ratelimiting.rs (limited to 'src') diff --git a/src/client/http.rs b/src/client/http.rs deleted file mode 100644 index fca8865..0000000 --- a/src/client/http.rs +++ /dev/null @@ -1,749 +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. -//! -//! [`Client`]: ../struct.Client.html - -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 serde_json; -use std::default::Default; -use std::io::{ErrorKind as IoErrorKind, Read}; -use std::sync::{Arc, Mutex}; -use super::ratelimiting::{self, Route}; -use ::constants; -use ::model::*; -use ::prelude_internal::*; -use ::utils::decode_array; - -lazy_static! { - static ref TOKEN: Arc> = Arc::new(Mutex::new(String::default())); -} - -#[doc(hidden)] -pub fn set_token(token: &str) { - TOKEN.lock().unwrap().clone_from(&token.to_owned()); -} - -pub fn accept_invite(code: &str) -> Result { - let response = request!(Route::InvitesCode, post, "/invite/{}", code); - - Invite::decode(try!(serde_json::from_reader(response))) -} - -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)) -} - -pub fn add_group_recipient(group_id: u64, user_id: u64) - -> Result<()> { - verify(204, request!(Route::None, - put, - "/channels/{}/recipients/{}", - group_id, - user_id)) -} - -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)) -} - -pub fn broadcast_typing(channel_id: u64) -> Result<()> { - verify(204, request!(Route::ChannelsIdTyping(channel_id), - post, - "/channels/{}/typing", - channel_id)) -} - -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))) -} - -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))) -} - -pub fn create_guild(map: Value) -> Result { - let body = try!(serde_json::to_string(&map)); - let response = request!(Route::Guilds, post(body), "/guilds"); - - Guild::decode(try!(serde_json::from_reader(response))) -} - -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)) -} - -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))) -} - -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); - - Guild::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)) -} - -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)) -} - -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); - - Guild::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))) -} - -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))) -} - -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) -} - -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); - - Guild::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_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_voice_regions() -> Result> { - let response = request!(Route::VoiceRegions, get, "/voice/regions"); - - decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode) -} - -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::GuildsId(guild_id), - delete, - "/guilds/{}", - guild_id); - - Guild::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)) -} - -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::GuildsIdIntegrationsId(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 { - 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/mod.rs b/src/client/http/mod.rs new file mode 100644 index 0000000..9ef9bac --- /dev/null +++ b/src/client/http/mod.rs @@ -0,0 +1,751 @@ +//! 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. +//! +//! [`Client`]: ../struct.Client.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; +use std::default::Default; +use std::io::{ErrorKind as IoErrorKind, Read}; +use std::sync::{Arc, Mutex}; +use ::constants; +use ::model::*; +use ::prelude_internal::*; +use ::utils::decode_array; + +lazy_static! { + static ref TOKEN: Arc> = Arc::new(Mutex::new(String::default())); +} + +#[doc(hidden)] +pub fn set_token(token: &str) { + TOKEN.lock().unwrap().clone_from(&token.to_owned()); +} + +pub fn accept_invite(code: &str) -> Result { + let response = request!(Route::InvitesCode, post, "/invites/{}", code); + + Invite::decode(try!(serde_json::from_reader(response))) +} + +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)) +} + +pub fn add_group_recipient(group_id: u64, user_id: u64) + -> Result<()> { + verify(204, request!(Route::None, + put, + "/channels/{}/recipients/{}", + group_id, + user_id)) +} + +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)) +} + +pub fn broadcast_typing(channel_id: u64) -> Result<()> { + verify(204, request!(Route::ChannelsIdTyping(channel_id), + post, + "/channels/{}/typing", + channel_id)) +} + +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))) +} + +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))) +} + +pub fn create_guild(map: Value) -> Result { + let body = try!(serde_json::to_string(&map)); + let response = request!(Route::Guilds, post(body), "/guilds"); + + Guild::decode(try!(serde_json::from_reader(response))) +} + +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)) +} + +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))) +} + +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); + + Guild::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)) +} + +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)) +} + +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); + + Guild::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))) +} + +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))) +} + +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) +} + +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); + + Guild::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_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_voice_regions() -> Result> { + let response = request!(Route::VoiceRegions, get, "/voice/regions"); + + decode_array(try!(serde_json::from_reader(response)), VoiceRegion::decode) +} + +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::GuildsId(guild_id), + delete, + "/guilds/{}", + guild_id); + + Guild::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)) +} + +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::GuildsIdIntegrationsId(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 { + 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 new file mode 100644 index 0000000..01e031a --- /dev/null +++ b/src/client/http/ratelimiting.rs @@ -0,0 +1,226 @@ +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 ::prelude_internal::*; + +lazy_static! { + static ref GLOBAL: Arc> = Arc::new(Mutex::new(RateLimit::default())); + static ref ROUTES: Arc>> = Arc::new(Mutex::new(HashMap::default())); +} + +/// 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. +/// +/// # 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. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Route { + ChannelsId(u64), + ChannelsIdInvites(u64), + ChannelsIdMessages(u64), + ChannelsIdMessagesBulkDelete(u64), + ChannelsIdMessagesId(u64), + ChannelsIdMessagesIdAck(u64), + ChannelsIdMessagesIdReactionsUserIdType(u64), + ChannelsIdPermissionsOverwriteId(u64), + ChannelsIdPins(u64), + ChannelsIdPinsMessageId(u64), + ChannelsIdTyping(u64), + Gateway, + GatewayBot, + Global, + Guilds, + GuildsId(u64), + GuildsIdBans(u64), + GuildsIdBansUserId(u64), + GuildsIdChannels(u64), + GuildsIdEmbed(u64), + GuildsIdEmojis(u64), + GuildsIdEmojisId(u64), + GuildsIdIntegrations(u64), + GuildsIdIntegrationsId(u64), + GuildsIdIntegrationsIdSync(u64), + GuildsIdInvites(u64), + GuildsIdMembers(u64), + GuildsIdMembersId(u64), + GuildsIdPrune(u64), + GuildsIdRegions(u64), + GuildsIdRoles(u64), + GuildsIdRolesId(u64), + InvitesCode, + Users, + UsersId, + UsersMe, + UsersMeChannels, + USersMeConnections, + UsersMeGuilds, + UsersMeGuildsId, + VoiceRegions, + None, +} + +pub fn perform<'a, F>(route: Route, f: F) -> Result + where F: Fn() -> RequestBuilder<'a> { + // Keeping the global lock poisoned here for the duration of the function + // will ensure that requests are synchronous, which will further ensure + // that 429s are _never_ hit. + // + // This would otherwise cause the potential for 429s to be hit while + // requests are open. + let mut global = GLOBAL.lock().expect("global route lock poisoned"); + + 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 + global.pre_hook(); + + 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' + + let redo = if response.headers.get_raw("x-ratelimit-global").is_some() { + 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 diff = (self.reset - time::get_time().sec) 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 7870e2e..c235177 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -40,7 +40,6 @@ mod context; mod dispatch; mod event_store; mod login_type; -mod ratelimiting; pub use self::connection::{Connection, ConnectionError}; pub use self::context::Context; diff --git a/src/client/ratelimiting.rs b/src/client/ratelimiting.rs deleted file mode 100644 index 5690dbe..0000000 --- a/src/client/ratelimiting.rs +++ /dev/null @@ -1,227 +0,0 @@ -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 super::http; -use time; -use ::prelude_internal::*; - -lazy_static! { - static ref GLOBAL: Arc> = Arc::new(Mutex::new(RateLimit::default())); - static ref ROUTES: Arc>> = Arc::new(Mutex::new(HashMap::default())); -} - -/// 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. -/// -/// # 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. -#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub enum Route { - ChannelsId(u64), - ChannelsIdInvites(u64), - ChannelsIdMessages(u64), - ChannelsIdMessagesBulkDelete(u64), - ChannelsIdMessagesId(u64), - ChannelsIdMessagesIdAck(u64), - ChannelsIdMessagesIdReactionsUserIdType(u64), - ChannelsIdPermissionsOverwriteId(u64), - ChannelsIdPins(u64), - ChannelsIdPinsMessageId(u64), - ChannelsIdTyping(u64), - Gateway, - GatewayBot, - Global, - Guilds, - GuildsId(u64), - GuildsIdBans(u64), - GuildsIdBansUserId(u64), - GuildsIdChannels(u64), - GuildsIdEmbed(u64), - GuildsIdEmojis(u64), - GuildsIdEmojisId(u64), - GuildsIdIntegrations(u64), - GuildsIdIntegrationsId(u64), - GuildsIdIntegrationsIdSync(u64), - GuildsIdInvites(u64), - GuildsIdMembers(u64), - GuildsIdMembersId(u64), - GuildsIdPrune(u64), - GuildsIdRegions(u64), - GuildsIdRoles(u64), - GuildsIdRolesId(u64), - InvitesCode, - Users, - UsersId, - UsersMe, - UsersMeChannels, - USersMeConnections, - UsersMeGuilds, - UsersMeGuildsId, - VoiceRegions, - None, -} - -pub fn perform<'a, F>(route: Route, f: F) -> Result - where F: Fn() -> RequestBuilder<'a> { - // Keeping the global lock poisoned here for the duration of the function - // will ensure that requests are synchronous, which will further ensure - // that 429s are _never_ hit. - // - // This would otherwise cause the potential for 429s to be hit while - // requests are open. - let mut global = GLOBAL.lock().expect("global route lock poisoned"); - - 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 - global.pre_hook(); - - if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) { - route.pre_hook(); - } - - let response = try!(http::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' - - let redo = if response.headers.get_raw("x-ratelimit-global").is_some() { - 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 diff = (self.reset - time::get_time().sec) 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), - } -} -- cgit v1.2.3