aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorZeyla Hellyer <[email protected]>2017-05-22 17:02:00 -0700
committerZeyla Hellyer <[email protected]>2017-05-22 17:02:00 -0700
commit9969be60cf320797c37b317da24d9a08fd5eafa5 (patch)
treef27bf7a57af95bbc11990b1edcea9cca99276964 /src/client
parentReasonably derive Debug on items (diff)
downloadserenity-9969be60cf320797c37b317da24d9a08fd5eafa5.tar.xz
serenity-9969be60cf320797c37b317da24d9a08fd5eafa5.zip
Restructure modules
Modules are now separated into a fashion where the library can be used for most use cases, without needing to compile the rest. The core of serenity, with no features enabled, contains only the struct (model) definitions, constants, and prelude. Models do not have most functions compiled in, as that is separated into the `model` feature. The `client` module has been split into 3 modules: `client`, `gateway`, and `http`. `http` contains functions to interact with the REST API. `gateway` contains the Shard to interact with the gateway, requiring `http` for retrieving the gateway URL. `client` requires both of the other features and acts as an abstracted interface over both the gateway and REST APIs, handling the event loop. The `builder` module has been separated from `utils`, and can now be optionally compiled in. It and the `http` feature are required by the `model` feature due to a large number of methods requiring access to them. `utils` now contains a number of utilities, such as the Colour struct, the `MessageBuilder`, and mention parsing functions. Each of the original `ext` modules are still featured, with `cache` not requiring any feature to be enabled, `framework` requiring the `client`, `model`, and `utils`, and `voice` requiring `gateway`. In total the features and their requirements are: - `builder`: none - `cache`: none - `client`: `gateway`, `http` - `framework`: `client`, `model`, `utils` - `gateway`: `http` - `http`: none - `model`: `builder`, `http` - `utils`: none - `voice`: `gateway` The default features are `builder`, `cache`, `client`, `framework`, `gateway`, `model`, `http`, and `utils`. To help with forwards compatibility, modules have been re-exported from their original locations.
Diffstat (limited to 'src/client')
-rw-r--r--src/client/context.rs18
-rw-r--r--src/client/dispatch.rs2
-rw-r--r--src/client/error.rs149
-rw-r--r--src/client/gateway/error.rs33
-rw-r--r--src/client/gateway/mod.rs59
-rw-r--r--src/client/gateway/prep.rs162
-rw-r--r--src/client/gateway/shard.rs683
-rw-r--r--src/client/gateway/status.rs11
-rw-r--r--src/client/mod.rs89
-rw-r--r--src/client/rest/mod.rs1535
-rw-r--r--src/client/rest/ratelimiting.rs504
11 files changed, 55 insertions, 3190 deletions
diff --git a/src/client/context.rs b/src/client/context.rs
index 27e5ab9..22216b1 100644
--- a/src/client/context.rs
+++ b/src/client/context.rs
@@ -1,18 +1,19 @@
use std::sync::{Arc, Mutex};
-use super::gateway::Shard;
-use super::rest;
use typemap::ShareMap;
-use ::utils::builder::EditProfile;
+use ::gateway::Shard;
+use ::http;
use ::internal::prelude::*;
use ::model::*;
#[cfg(feature="cache")]
use super::CACHE;
+#[cfg(feature="builder")]
+use ::builder::EditProfile;
/// The context is a general utility struct provided on event dispatches, which
/// helps with dealing with the current "context" of the event dispatch.
/// The context also acts as a general high-level interface over the associated
-/// [`Shard`] which received the event, or the low-level [`rest`] module.
+/// [`Shard`] which received the event, or the low-level [`http`] module.
///
/// The context contains "shortcuts", like for interacting with the shard.
/// Methods like [`set_game`] will unlock the shard and perform an update for
@@ -21,8 +22,8 @@ use super::CACHE;
/// A context will only live for the event it was dispatched for. After the
/// event handler finished, it is destroyed and will not be re-used.
///
-/// [`Shard`]: gateway/struct.Shard.html
-/// [`rest`]: rest/index.html
+/// [`Shard`]: ../gateway/struct.Shard.html
+/// [`http`]: ../http/index.html
/// [`set_game`]: #method.set_game
#[derive(Clone)]
pub struct Context {
@@ -76,6 +77,7 @@ impl Context {
/// ```rust,ignore
/// context.edit_profile(|p| p.username("Hakase"));
/// ```
+ #[cfg(feature="builder")]
pub fn edit_profile<F: FnOnce(EditProfile) -> EditProfile>(&self, f: F) -> Result<CurrentUser> {
let mut map = Map::new();
@@ -88,7 +90,7 @@ impl Context {
map.insert("email".to_owned(), Value::String(email.clone()));
}
} else {
- let user = rest::get_current_user()?;
+ let user = http::get_current_user()?;
map.insert("username".to_owned(), Value::String(user.name.clone()));
@@ -99,7 +101,7 @@ impl Context {
let edited = f(EditProfile(map)).0;
- rest::edit_profile(&edited)
+ http::edit_profile(&edited)
}
/// Sets the current user as being [`Online`]. This maintains the current
diff --git a/src/client/dispatch.rs b/src/client/dispatch.rs
index 56b5f6e..648cea7 100644
--- a/src/client/dispatch.rs
+++ b/src/client/dispatch.rs
@@ -2,8 +2,8 @@ use std::sync::{Arc, Mutex, RwLock};
use std::thread;
use super::event_store::EventStore;
use super::Context;
-use super::gateway::Shard;
use typemap::ShareMap;
+use ::gateway::Shard;
use ::model::event::Event;
use ::model::{ChannelId, Message};
diff --git a/src/client/error.rs b/src/client/error.rs
index a3df57c..a08d15b 100644
--- a/src/client/error.rs
+++ b/src/client/error.rs
@@ -1,152 +1,39 @@
-use hyper::status::StatusCode;
-use ::constants::ErrorCode;
-use ::model::{ChannelType, Permissions};
+use std::error::Error as StdError;
+use std::fmt::{Display, Formatter, Result as FmtResult};
-/// An error returned from the [`Client`] or the [`Context`], or model instance.
+/// An error returned from the [`Client`].
///
/// This is always wrapped within the library's generic [`Error::Client`]
/// variant.
///
-/// # Examples
-///
-/// Matching an [`Error`] with this variant would look something like the
-/// following for the [`GuildId::ban`] method, which in this example is used to
-/// re-ban all members with an odd discriminator:
-///
-/// ```rust,no_run
-/// use serenity::client::{Client, ClientError};
-/// use serenity::Error;
-/// use std::env;
-///
-/// let token = env::var("DISCORD_BOT_TOKEN").unwrap();
-/// let mut client = Client::login(&token);
-///
-/// client.on_member_unban(|context, guild_id, user| {
-/// // If the user has an even discriminator, don't re-ban them.
-/// if user.discriminator % 2 == 0 {
-/// return;
-/// }
-///
-/// match guild_id.ban(user, 8) {
-/// Ok(()) => {
-/// // Ban successful.
-/// },
-/// Err(Error::Client(ClientError::DeleteMessageDaysAmount(amount))) => {
-/// println!("Failed deleting {} days' worth of messages", amount);
-/// },
-/// Err(why) => {
-/// println!("Unexpected error: {:?}", why);
-/// },
-/// }
-/// });
-/// ```
-///
/// [`Client`]: struct.Client.html
-/// [`Context`]: struct.Context.html
/// [`Error`]: ../enum.Error.html
/// [`Error::Client`]: ../enum.Error.html#variant.Client
/// [`GuildId::ban`]: ../model/struct.GuildId.html#method.ban
#[allow(enum_variant_names)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum Error {
- /// When attempting to delete below or above the minimum and maximum allowed
- /// number of messages.
- BulkDeleteAmount,
- /// When attempting to delete a number of days' worth of messages that is
- /// not allowed.
- DeleteMessageDaysAmount(u8),
- /// Indicates that the textual content of an embed exceeds the maximum
- /// length.
- EmbedTooLarge(u64),
- /// When there is an error from Discord for a specific action, such as
- /// [`ErrorCode::EditByOtherAuthor`]. This is a friendlier representation of
- /// the numerical error codes Discord provides.
- ///
- /// [`ErrorCode::EditByOtherAuthor`]: rest/enum.ErrorCode.html#variant.EditByOtherAuthor
- ErrorCode(ErrorCode),
- /// When there was an error retrieving the gateway URI from the REST API.
- Gateway,
- /// An indication that a [guild][`Guild`] could not be found by
- /// [Id][`GuildId`] in the [`Cache`].
- ///
- /// [`Guild`]: ../model/struct.Guild.html
- /// [`GuildId`]: ../model/struct.GuildId.html
- /// [`Cache`]: ../ext/cache/struct.Cache.html
- GuildNotFound,
- /// An indicator that an unknown opcode was received from the gateway.
- InvalidOpCode,
- /// Indicates that you do not have the required permissions to perform an
- /// operation.
- ///
- /// The provided [`Permission`]s is the set of required permissions
- /// required.
- ///
- /// [`Permission`]: ../model/permissions/struct.Permissions.html
- InvalidPermissions(Permissions),
- /// An indicator that the shard data received from the gateway is invalid.
- InvalidShards,
/// When the token provided is invalid. This is returned when validating a
/// token through the [`validate_token`] function.
///
/// [`validate_token`]: fn.validate_token.html
InvalidToken,
- /// An indicator that the [current user] can not perform an action.
- ///
- /// [current user]: ../model/struct.CurrentUser.html
- InvalidUser,
- /// An indicator that an item is missing from the [`Cache`], and the action
- /// can not be continued.
- ///
- /// [`Cache`]: ../ext/cache/struct.Cache.html
- ItemMissing,
- /// Indicates that a [`Message`]s content was too long and will not
- /// successfully send, as the length is over 2000 codepoints, or 4000 bytes.
- ///
- /// The number of bytes larger than the limit is provided.
- ///
- /// [`Message`]: ../model/struct.Message.html
- MessageTooLong(u64),
- /// When attempting to use a [`Context`] helper method which requires a
- /// contextual [`ChannelId`], but the current context is not appropriate for
- /// the action.
- ///
- /// [`ChannelId`]: ../model/struct.ChannelId.html
- /// [`Context`]: struct.Context.html
- NoChannelId,
- /// When the decoding of a ratelimit header could not be properly decoded
- /// into an `i64`.
- RateLimitI64,
- /// When the decoding of a ratelimit header could not be properly decoded
- /// from UTF-8.
- RateLimitUtf8,
- /// When attempting to find a required record from the Cache could not be
- /// found. This is required in methods such as [`Context::edit_role`].
- ///
- /// [`Context::edit_role`]: struct.Context.html#method.edit_role
- RecordNotFound,
/// When a shard has completely failed to reboot after resume and/or
/// reconnect attempts.
ShardBootFailure,
- /// When the shard being retrieved from within the Client could not be
- /// found after being inserted into the Client's internal vector of
- /// [`Shard`]s.
- ///
- /// This can be returned from one of the options for starting one or
- /// multiple shards.
- ///
- /// **This should never be received.**
- ///
- /// [`Shard`]: gateway/struct.Shard.html
- ShardUnknown,
- /// When a function such as [`Context::edit_channel`] did not expect the
- /// received [`ChannelType`].
- ///
- /// [`ChannelType`]: ../model/enum.ChannelType.html
- /// [`Context::edit_channel`]: struct.Context.html#method.edit_channel
- UnexpectedChannelType(ChannelType),
- /// When a status code was unexpectedly received for a request's status.
- InvalidRequest(StatusCode),
- /// When a status is received, but the verification to ensure the response
- /// is valid does not recognize the status.
- UnknownStatus(u16),
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ f.write_str(self.description())
+ }
+}
+
+impl StdError for Error {
+ fn description(&self) -> &str {
+ match *self {
+ Error::InvalidToken => "The provided token was invalid",
+ Error::ShardBootFailure => "Failed to (re-)boot a shard",
+ }
+ }
}
diff --git a/src/client/gateway/error.rs b/src/client/gateway/error.rs
deleted file mode 100644
index 121de71..0000000
--- a/src/client/gateway/error.rs
+++ /dev/null
@@ -1,33 +0,0 @@
-use std::fmt::{self, Display};
-
-/// An error that occurred while attempting to deal with the gateway.
-///
-/// Note that - from a user standpoint - there should be no situation in which
-/// you manually handle these.
-#[derive(Clone, Debug)]
-pub enum Error {
- /// The connection closed, potentially uncleanly.
- Closed(Option<u16>, String),
- /// Expected a Hello during a handshake
- ExpectedHello,
- /// Expected a Ready or an InvalidateSession
- InvalidHandshake,
- /// When a session Id was expected (for resuming), but was not present.
- NoSessionId,
- /// Failed to reconnect after a number of attempts.
- ReconnectFailure,
-}
-
-impl Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match *self {
- Error::Closed(s, ref v) => {
- f.write_str(&format!("Connection closed {:?}: {:?}", s, v))
- },
- Error::ExpectedHello => f.write_str("Expected Hello during handshake"),
- Error::InvalidHandshake => f.write_str("Expected Ready or InvalidateSession"),
- Error::NoSessionId => f.write_str("No Session Id present"),
- Error::ReconnectFailure => f.write_str("Failed to Reconnect"),
- }
- }
-}
diff --git a/src/client/gateway/mod.rs b/src/client/gateway/mod.rs
deleted file mode 100644
index 38cdaa1..0000000
--- a/src/client/gateway/mod.rs
+++ /dev/null
@@ -1,59 +0,0 @@
-//! The gateway module contains the pieces - primarily the `Shard` -
-//! responsible for maintaing a WebSocket connection with Discord.
-//!
-//! A shard is an interface for the lower-level receiver and sender. It provides
-//! what can otherwise be thought of as "sugar methods". A shard represents a
-//! single connection to Discord. If acting as a [`Bot`] user, you can make
-//! use of a method named "sharding" to have multiple shards, potentially
-//! offloading some server load to another server(s).
-//!
-//! # Sharding
-//!
-//! Sharding is a method to split portions of bots into separate processes. This
-//! is an enforced strategy by Discord once a bot reaches a certain number of
-//! guilds (2500). Once this number is reached, a bot must be sharded in a way
-//! that only 2500 guilds maximum may be allocated per shard.
-//!
-//! The "recommended" number of guilds per shard is _around_ 1000. Sharding can
-//! be useful for splitting processes across separate servers. Often you may
-//! want some or all shards to be in the same process, allowing for a shared
-//! State. This is possible through this library.
-//!
-//! See [Discord's documentation][docs] for more information.
-//!
-//! If you are not using a bot account or do not require sharding - such as for
-//! a small bot - then use [`Client::start`].
-//!
-//! There are a few methods of sharding available:
-//!
-//! - [`Client::start_autosharded`]: retrieves the number of shards Discord
-//! recommends using from the API, and then automatically starts that number of
-//! shards.
-//! - [`Client::start_shard`]: starts a single shard for use in the instance,
-//! handled by the instance of the Client. Use this if you only want 1 shard
-//! handled by this instance.
-//! - [`Client::start_shards`]: starts all shards in this instance. This is best
-//! for when you want a completely shared State.
-//! - [`Client::start_shard_range`]: start a range of shards within this
-//! instance. This should be used when you, for example, want to split 10 shards
-//! across 3 instances.
-//!
-//! **Note**: User accounts can not shard. Use [`Client::start`].
-//!
-//! [`Bot`]: ../enum.LoginType.html#variant.Bot
-//! [`Client`]: ../struct.Client.html
-//! [`Client::start`]: ../struct.Client.html#method.start
-//! [`Client::start_autosharded`]: ../struct.Client.html#method.start_autosharded
-//! [`Client::start_shard`]: ../struct.Client.html#method.start_shard
-//! [`Client::start_shard_range`]: ../struct.Client.html#method.start_shard_range
-//! [`Client::start_shards`]: ../struct.Client.html#method.start_shards
-//! [docs]: https://discordapp.com/developers/docs/topics/gateway#sharding
-
-mod error;
-mod prep;
-mod shard;
-mod status;
-
-pub use self::error::Error as GatewayError;
-pub use self::shard::Shard;
-pub use self::status::Status as GatewayStatus;
diff --git a/src/client/gateway/prep.rs b/src/client/gateway/prep.rs
deleted file mode 100644
index 5609749..0000000
--- a/src/client/gateway/prep.rs
+++ /dev/null
@@ -1,162 +0,0 @@
-use serde_json::Value;
-use std::sync::mpsc::{
- Receiver as MpscReceiver,
- Sender as MpscSender,
- TryRecvError,
-};
-use std::sync::{Arc, Mutex};
-use std::time::{Duration as StdDuration, Instant};
-use std::{env, thread};
-use super::super::ClientError;
-use super::{GatewayError, GatewayStatus};
-use time::{self, Duration};
-use websocket::client::request::Url as RequestUrl;
-use websocket::client::{Receiver, Sender};
-use websocket::result::WebSocketError as WsError;
-use websocket::stream::WebSocketStream;
-use ::constants::{self, LARGE_THRESHOLD, OpCode};
-use ::error::{Error, Result};
-use ::internal::ws_impl::{ReceiverExt, SenderExt};
-use ::model::event::{Event, GatewayEvent, ReadyEvent};
-
-#[inline]
-pub fn parse_ready(event: GatewayEvent,
- tx: &MpscSender<GatewayStatus>,
- receiver: &mut Receiver<WebSocketStream>,
- identification: Value)
- -> Result<(ReadyEvent, u64)> {
- match event {
- GatewayEvent::Dispatch(seq, Event::Ready(event)) => {
- Ok((event, seq))
- },
- GatewayEvent::InvalidateSession => {
- debug!("Session invalidation");
-
- let _ = tx.send(GatewayStatus::SendMessage(identification));
-
- match receiver.recv_json(GatewayEvent::decode)? {
- GatewayEvent::Dispatch(seq, Event::Ready(event)) => {
- Ok((event, seq))
- },
- other => {
- debug!("Unexpected event: {:?}", other);
-
- Err(Error::Gateway(GatewayError::InvalidHandshake))
- },
- }
- },
- other => {
- debug!("Unexpected event: {:?}", other);
-
- Err(Error::Gateway(GatewayError::InvalidHandshake))
- },
- }
-}
-
-pub fn identify(token: &str, shard_info: Option<[u64; 2]>) -> Value {
- json!({
- "op": OpCode::Identify.num(),
- "d": {
- "compression": !cfg!(feature="debug"),
- "large_threshold": LARGE_THRESHOLD,
- "shard": shard_info.unwrap_or([0, 1]),
- "token": token,
- "v": constants::GATEWAY_VERSION,
- "properties": {
- "$browser": "serenity",
- "$device": "serenity",
- "$os": env::consts::OS,
- },
- },
- })
-}
-
-pub fn build_gateway_url(base: &str) -> Result<RequestUrl> {
- RequestUrl::parse(&format!("{}?v={}", base, constants::GATEWAY_VERSION))
- .map_err(|_| Error::Client(ClientError::Gateway))
-}
-
-pub fn keepalive(interval: u64,
- heartbeat_sent: Arc<Mutex<Instant>>,
- mut sender: Sender<WebSocketStream>,
- channel: &MpscReceiver<GatewayStatus>) {
- let mut base_interval = Duration::milliseconds(interval as i64);
- let mut next_tick = time::get_time() + base_interval;
-
- let mut last_sequence = 0;
- let mut last_successful = false;
-
- 'outer: loop {
- thread::sleep(StdDuration::from_millis(100));
-
- loop {
- match channel.try_recv() {
- Ok(GatewayStatus::Interval(interval)) => {
- base_interval = Duration::milliseconds(interval as i64);
- },
- Ok(GatewayStatus::Sender(new_sender)) => {
- sender = new_sender;
- },
- Ok(GatewayStatus::SendMessage(val)) => {
- if let Err(why) = sender.send_json(&val) {
- warn!("Error sending message: {:?}", why);
- }
- },
- Ok(GatewayStatus::Sequence(seq)) => {
- last_sequence = seq;
- },
- Err(TryRecvError::Empty) => break,
- Err(TryRecvError::Disconnected) => break 'outer,
- }
- }
-
- if time::get_time() >= next_tick {
- next_tick = next_tick + base_interval;
-
- let map = json!({
- "d": last_sequence,
- "op": OpCode::Heartbeat.num(),
- });
-
- trace!("Sending heartbeat d: {}", last_sequence);
-
- match sender.send_json(&map) {
- Ok(_) => {
- let now = Instant::now();
-
- *heartbeat_sent.lock().unwrap() = now;
- },
- Err(why) => {
- match why {
- Error::WebSocket(WsError::IoError(err)) => {
- if err.raw_os_error() != Some(32) {
- debug!("Err w/ keepalive: {:?}", err);
- }
- },
- other => warn!("Other err w/ keepalive: {:?}", other),
- }
-
- if last_successful {
- debug!("If next keepalive fails, closing");
- } else {
- break;
- }
-
- last_successful = false;
- },
- }
- }
- }
-
- debug!("Closing keepalive");
-
- match sender.shutdown_all() {
- Ok(_) => debug!("Successfully shutdown sender/receiver"),
- Err(why) => {
- // This can fail if the receiver already shutdown.
- if why.raw_os_error() != Some(107) {
- warn!("Failed to shutdown sender/receiver: {:?}", why);
- }
- },
- }
-}
diff --git a/src/client/gateway/shard.rs b/src/client/gateway/shard.rs
deleted file mode 100644
index a64ff2a..0000000
--- a/src/client/gateway/shard.rs
+++ /dev/null
@@ -1,683 +0,0 @@
-use std::io::Write;
-use std::net::Shutdown;
-use std::sync::mpsc::{self, Sender as MpscSender};
-use std::sync::{Arc, Mutex};
-use std::thread::{self, Builder as ThreadBuilder};
-use std::time::{Duration as StdDuration, Instant};
-use std::mem;
-use super::super::rest;
-use super::{GatewayError, GatewayStatus, prep};
-use time;
-use websocket::client::{Client as WsClient, Sender, Receiver};
-use websocket::message::Message as WsMessage;
-use websocket::result::WebSocketError;
-use websocket::stream::WebSocketStream;
-use websocket::ws::sender::Sender as WsSender;
-use ::constants::OpCode;
-use ::internal::prelude::*;
-use ::internal::ws_impl::{ReceiverExt, SenderExt};
-use ::model::event::{Event, GatewayEvent, ReadyEvent};
-use ::model::{Game, GuildId, OnlineStatus};
-
-#[cfg(feature="cache")]
-use ::client::CACHE;
-#[cfg(feature="voice")]
-use ::ext::voice::Manager as VoiceManager;
-#[cfg(feature="cache")]
-use ::utils;
-
-type CurrentPresence = (Option<Game>, OnlineStatus, bool);
-
-/// A Shard is a higher-level handler for a websocket connection to Discord's
-/// gateway. The shard allows for sending and receiving messages over the
-/// websocket, such as setting the active game, reconnecting, syncing guilds,
-/// and more.
-///
-/// Refer to the [module-level documentation][module docs] for information on
-/// effectively using multiple shards, if you need to.
-///
-/// Note that there are additional methods available if you are manually
-/// managing a shard yourself, although they are hidden from the documentation
-/// since there are few use cases for doing such.
-///
-/// # Stand-alone shards
-///
-/// You may instantiate a shard yourself - decoupled from the [`Client`] - if
-/// you need to. For most use cases, you will not need to do this, and you can
-/// leave the client to do it.
-///
-/// This can be done by passing in the required parameters to [`new`]. You can
-/// then manually handle the shard yourself and receive events via
-/// [`receive`].
-///
-/// **Note**: You _really_ do not need to do this. Just call one of the
-/// appropriate methods on the [`Client`].
-///
-/// # Examples
-///
-/// See the documentation for [`new`] on how to use this.
-///
-/// [`Client`]: ../struct.Client.html
-/// [`new`]: #method.new
-/// [`receive`]: #method.receive
-/// [docs]: https://discordapp.com/developers/docs/topics/gateway#sharding
-/// [module docs]: index.html#sharding
-#[derive(Clone, Debug)]
-pub struct Shard {
- current_presence: CurrentPresence,
- /// A tuple of the last instant that a heartbeat was sent, and the last that
- /// an acknowledgement was received.
- ///
- /// This can be used to calculate [`latency`].
- ///
- /// [`latency`]: fn.latency.html
- heartbeat_instants: (Arc<Mutex<Instant>>, Option<Instant>),
- keepalive_channel: MpscSender<GatewayStatus>,
- seq: u64,
- session_id: Option<String>,
- shard_info: Option<[u64; 2]>,
- token: String,
- ws_url: String,
- /// The voice connections that this Shard is responsible for. The Shard will
- /// update the voice connections' states.
- #[cfg(feature="voice")]
- pub manager: VoiceManager,
-}
-
-impl Shard {
- /// Instantiates a new instance of a Shard, bypassing the client.
- ///
- /// **Note**: You should likely never need to do this yourself.
- ///
- /// # Examples
- ///
- /// Instantiating a new Shard manually for a bot with no shards, and
- /// then listening for events:
- ///
- /// ```rust,ignore
- /// use serenity::client::gateway::Shard;
- /// use serenity::client::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 = rest::get_gateway().expect("Valid gateway response").url;
- /// let shard = Shard::new(&gateway, &token, None)
- /// .expect("Working shard");
- ///
- /// // at this point, you can create a `loop`, and receive events and match
- /// // their variants
- /// ```
- pub fn new(base_url: &str,
- token: &str,
- shard_info: Option<[u64; 2]>)
- -> Result<(Shard, ReadyEvent, Receiver<WebSocketStream>)> {
- let url = prep::build_gateway_url(base_url)?;
-
- let response = WsClient::connect(url)?.send()?;
- response.validate()?;
-
- let (mut sender, mut receiver) = response.begin().split();
-
- let identification = prep::identify(token, shard_info);
- sender.send_json(&identification)?;
-
- let heartbeat_interval = match receiver.recv_json(GatewayEvent::decode)? {
- GatewayEvent::Hello(interval) => interval,
- other => {
- debug!("Unexpected event during shard start: {:?}", other);
-
- return Err(Error::Gateway(GatewayError::ExpectedHello));
- },
- };
-
- let (tx, rx) = mpsc::channel();
- let thread_name = match shard_info {
- Some(info) => format!("serenity keepalive [shard {}/{}]",
- info[0],
- info[1] - 1),
- None => "serenity keepalive [unsharded]".to_owned(),
- };
-
- let heartbeat_sent = Arc::new(Mutex::new(Instant::now()));
- let heartbeat_clone = heartbeat_sent.clone();
-
- ThreadBuilder::new()
- .name(thread_name)
- .spawn(move || {
- prep::keepalive(heartbeat_interval, heartbeat_clone, sender, &rx)
- })?;
-
- // Parse READY
- let event = receiver.recv_json(GatewayEvent::decode)?;
- let (ready, sequence) = prep::parse_ready(event,
- &tx,
- &mut receiver,
- identification)?;
-
- Ok((feature_voice! {{
- Shard {
- current_presence: (None, OnlineStatus::Online, false),
- heartbeat_instants: (heartbeat_sent, None),
- keepalive_channel: tx.clone(),
- seq: sequence,
- token: token.to_owned(),
- session_id: Some(ready.ready.session_id.clone()),
- shard_info: shard_info,
- ws_url: base_url.to_owned(),
- manager: VoiceManager::new(tx, ready.ready.user.id),
- }
- } else {
- Shard {
- current_presence: (None, OnlineStatus::Online, false),
- heartbeat_instants: (heartbeat_sent, None),
- keepalive_channel: tx.clone(),
- seq: sequence,
- token: token.to_owned(),
- session_id: Some(ready.ready.session_id.clone()),
- shard_info: shard_info,
- ws_url: base_url.to_owned(),
- }
- }}, ready, receiver))
- }
-
- /// Retrieves a copy of the current shard information.
- ///
- /// The first element is the _current_ shard - 0-indexed - while the second
- /// element is the _total number_ of shards -- 1-indexed.
- ///
- /// For example, if using 3 shards in total, and if this is shard 1, then it
- /// can be read as "the second of three shards".
- pub fn shard_info(&self) -> Option<[u64; 2]> {
- self.shard_info
- }
-
- /// Sets whether the current user is afk. This helps Discord determine where
- /// to send notifications.
- ///
- /// Other presence settings are maintained.
- pub fn set_afk(&mut self, afk: bool) {
- self.current_presence.2 = afk;
-
- self.update_presence();
- }
-
- /// Sets the user's current game, if any.
- ///
- /// Other presence settings are maintained.
- pub fn set_game(&mut self, game: Option<Game>) {
- self.current_presence.0 = game;
-
- self.update_presence();
- }
-
- /// Sets the user's current online status.
- ///
- /// Note that [`Offline`] is not a valid presence, so it is automatically
- /// converted to [`Invisible`].
- ///
- /// Other presence settings are maintained.
- ///
- /// [`Invisible`]: ../../model/enum.OnlineStatus.html#variant.Invisible
- /// [`Offline`]: ../../model/enum.OnlineStatus.html#variant.Offline
- pub fn set_status(&mut self, online_status: OnlineStatus) {
- self.current_presence.1 = match online_status {
- OnlineStatus::Offline => OnlineStatus::Invisible,
- other => other,
- };
-
- self.update_presence();
- }
-
- /// Sets the user's full presence information.
- ///
- /// Consider using the individual setters if you only need to modify one of
- /// these.
- ///
- /// # Examples
- ///
- /// Set the current user as playing `"Heroes of the Storm"`, being online,
- /// and not being afk:
- ///
- /// ```rust,ignore
- /// use serenity::model::{Game, OnlineStatus};
- ///
- /// // assuming you are in a context
- ///
- /// context.shard.lock()
- /// .unwrap()
- /// .set_presence(Some(Game::playing("Heroes of the Storm")),
- /// OnlineStatus::Online,
- /// false);
- /// ```
- pub fn set_presence(&mut self,
- game: Option<Game>,
- mut status: OnlineStatus,
- afk: bool) {
- if status == OnlineStatus::Offline {
- status = OnlineStatus::Invisible;
- }
-
- self.current_presence = (game, status, afk);
-
- self.update_presence();
- }
-
- /// Handles an event from the gateway over the receiver, requiring the
- /// receiver to be passed if a reconnect needs to occur.
- ///
- /// The best case scenario is that one of two values is returned:
- ///
- /// - `Ok(None)`: a heartbeat, late hello, or session invalidation was
- /// received;
- /// - `Ok(Some((event, None)))`: an op0 dispatch was received, and the
- /// shard's voice state will be updated, _if_ the `voice` feature is
- /// enabled.
- #[allow(cyclomatic_complexity)]
- #[doc(hidden)]
- pub fn handle_event(&mut self,
- event: Result<GatewayEvent>,
- mut receiver: &mut Receiver<WebSocketStream>)
- -> Result<Option<(Event, Option<Receiver<WebSocketStream>>)>> {
- match event {
- Ok(GatewayEvent::Dispatch(seq, event)) => {
- let status = GatewayStatus::Sequence(seq);
- let _ = self.keepalive_channel.send(status);
-
- self.seq = seq;
-
- self.handle_dispatch(&event);
-
- Ok(Some((event, None)))
- },
- Ok(GatewayEvent::Heartbeat(s)) => {
- info!("Received shard heartbeat");
-
- // Received seq is off -- attempt to resume.
- if s > self.seq + 1 {
- info!("Received off sequence (them: {}; us: {}); resuming",
- s,
- self.seq);
-
- return if self.session_id.is_some() {
- self.resume(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- } else {
- self.reconnect(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- };
- }
-
- let map = json!({
- "d": Value::Null,
- "op": OpCode::Heartbeat.num(),
- });
- let status = GatewayStatus::SendMessage(map);
- let _ = self.keepalive_channel.send(status);
-
- Ok(None)
- },
- Ok(GatewayEvent::HeartbeatAck) => {
- self.heartbeat_instants.1 = Some(Instant::now());
-
- Ok(None)
- },
- Ok(GatewayEvent::Hello(interval)) => {
- if interval > 0 {
- let status = GatewayStatus::Interval(interval);
- let _ = self.keepalive_channel.send(status);
- }
-
- if self.session_id.is_some() {
- self.resume(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- } else {
- self.reconnect(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- }
- },
- Ok(GatewayEvent::InvalidateSession) => {
- info!("Received session invalidation; re-identifying");
- self.seq = 0;
- self.session_id = None;
-
- let identification = prep::identify(&self.token, self.shard_info);
- let status = GatewayStatus::SendMessage(identification);
- let _ = self.keepalive_channel.send(status);
-
- Ok(None)
- },
- Ok(GatewayEvent::Reconnect) => {
- self.reconnect(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- },
- Err(Error::Gateway(GatewayError::Closed(num, message))) => {
- let clean = num == Some(1000);
-
- {
- let kind = if clean { "Cleanly" } else { "Uncleanly" };
-
- info!("{} closing with {:?}: {}", kind, num, message);
- }
-
- match num {
- Some(4001) => warn!("Sent invalid opcode"),
- Some(4002) => warn!("Sent invalid message"),
- Some(4003) => warn!("Sent no authentication"),
- Some(4004) => warn!("Sent invalid authentication"),
- Some(4005) => warn!("Already authenticated"),
- Some(4007) => {
- warn!("Sent invalid seq: {}", self.seq);
-
- self.seq = 0;
- },
- Some(4008) => warn!("Gateway ratelimited"),
- Some(4010) => warn!("Sent invalid shard"),
- Some(4011) => error!("Bot requires more shards"),
- Some(4006) | Some(4009) => {
- info!("Invalid session");
-
- self.session_id = None;
- },
- Some(other) if !clean => {
- warn!("Unknown unclean close {}: {:?}", other, message);
- },
- _ => {},
- }
-
- let resume = num.map(|x| x != 1000 && x != 4004 && self.session_id.is_some())
- .unwrap_or(false);
-
- if resume {
- info!("Attempting to resume");
-
- if self.session_id.is_some() {
- match self.resume(receiver) {
- Ok((ev, rec)) => {
- info!("Resumed");
-
- return Ok(Some((ev, Some(rec))));
- },
- Err(why) => {
- warn!("Error resuming: {:?}", why);
- info!("Falling back to reconnecting");
- },
- }
- }
- }
-
- info!("Reconnecting");
-
- self.reconnect(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- },
- Err(Error::WebSocket(why)) => {
- if let WebSocketError::NoDataAvailable = why {
- if self.heartbeat_instants.1.is_none() {
- return Ok(None);
- }
- }
-
- warn!("Websocket error: {:?}", why);
- info!("Will attempt to reconnect or resume");
-
- // Attempt to resume if the following was not received:
- //
- // - InvalidateSession.
- //
- // Otherwise, fallback to reconnecting.
- if self.session_id.is_some() {
- info!("Attempting to resume");
-
- match self.resume(&mut receiver) {
- Ok((ev, rec)) => {
- info!("Resumed");
-
- return Ok(Some((ev, Some(rec))));
- },
- Err(why) => {
- warn!("Error resuming: {:?}", why);
- info!("Falling back to reconnecting");
- },
- }
- }
-
- info!("Reconnecting");
-
- self.reconnect(receiver).map(|(ev, rec)| Some((ev, Some(rec))))
- },
- Err(error) => Err(error),
- }
- }
-
- /// Calculates the heartbeat latency (in nanoseconds) between the shard and
- /// Discord.
- // Shamelessly stolen from brayzure's commit in eris:
- // <https://github.com/abalabahaha/eris/commit/0ce296ae9a542bcec0edf1c999ee2d9986bed5a6>
- pub fn latency(&self) -> Option<StdDuration> {
- self.heartbeat_instants.1.map(|send| send - *self.heartbeat_instants.0.lock().unwrap())
- }
-
- /// Shuts down the receiver by attempting to cleanly close the
- /// connection.
- #[doc(hidden)]
- pub fn shutdown_clean(receiver: &mut Receiver<WebSocketStream>)
- -> Result<()> {
- let r = receiver.get_mut().get_mut();
-
- {
- let mut sender = Sender::new(r.by_ref(), true);
- let message = WsMessage::close_because(1000, "");
-
- sender.send_message(&message)?;
- }
-
- r.flush()?;
- r.shutdown(Shutdown::Both)?;
-
- debug!("Cleanly shutdown shard");
-
- Ok(())
- }
-
- /// Uncleanly shuts down the receiver by not sending a close code.
- #[doc(hidden)]
- pub fn shutdown(receiver: &mut Receiver<WebSocketStream>) -> Result<()> {
- let r = receiver.get_mut().get_mut();
-
- r.flush()?;
- r.shutdown(Shutdown::Both)?;
-
- Ok(())
- }
-
- /// Requests that one or multiple [`Guild`]s be chunked.
- ///
- /// This will ask Discord to start sending member chunks for large guilds
- /// (250 members+). If a guild is over 250 members, then a full member list
- /// will not be downloaded, and must instead be requested to be sent in
- /// "chunks" containing members.
- ///
- /// Member chunks are sent as the [`Event::GuildMembersChunk`] event. Each
- /// chunk only contains a partial amount of the total members.
- ///
- /// If the `cache` feature is enabled, the cache will automatically be
- /// updated with member chunks.
- pub fn chunk_guilds(&self, guild_ids: &[GuildId], limit: Option<u16>, query: Option<&str>) {
- let msg = json!({
- "op": OpCode::GetGuildMembers.num(),
- "d": {
- "guild_id": guild_ids.iter().map(|x| x.0).collect::<Vec<u64>>(),
- "limit": limit.unwrap_or(0),
- "query": query.unwrap_or(""),
- },
- });
-
- let _ = self.keepalive_channel.send(GatewayStatus::SendMessage(msg));
- }
-
- /// Calculates the number of guilds that the shard is responsible for.
- ///
- /// If sharding is not being used (i.e. 1 shard), then the total number of
- /// guilds in the [`Cache`] will be used.
- ///
- /// **Note**: Requires the `cache` feature be enabled.
- ///
- /// [`Cache`]: ../../ext/cache/struct.Cache.html
- #[cfg(feature="cache")]
- pub fn guilds_handled(&self) -> u16 {
- let cache = CACHE.read().unwrap();
-
- if let Some((shard_id, shard_count)) = self.shard_info.map(|s| (s[0], s[1])) {
- cache.guilds
- .keys()
- .filter(|guild_id| utils::shard_id(guild_id.0, shard_count) == shard_id)
- .count() as u16
- } else {
- cache.guilds.len() as u16
- }
- }
-
- #[allow(unused_variables)]
- fn handle_dispatch(&mut self, event: &Event) {
- #[cfg(feature="voice")]
- {
- if let Event::VoiceStateUpdate(ref update) = *event {
- if let Some(guild_id) = update.guild_id {
- if let Some(handler) = self.manager.get(guild_id) {
- handler.update_state(&update.voice_state);
- }
- }
- }
-
- if let Event::VoiceServerUpdate(ref update) = *event {
- if let Some(guild_id) = update.guild_id {
- if let Some(handler) = self.manager.get(guild_id) {
- handler.update_server(&update.endpoint, &update.token);
- }
- }
- }
- }
- }
-
- fn reconnect(&mut self, mut receiver: &mut Receiver<WebSocketStream>)
- -> Result<(Event, Receiver<WebSocketStream>)> {
- info!("Attempting to reconnect");
-
- // Take a few attempts at reconnecting.
- for i in 1u64..11u64 {
- let gateway_url = rest::get_gateway()?.url;
-
- let shard = Shard::new(&gateway_url,
- &self.token,
- self.shard_info);
-
- if let Ok((shard, ready, receiver_new)) = shard {
- let _ = Shard::shutdown(&mut receiver);
-
- mem::replace(self, shard);
- self.session_id = Some(ready.ready.session_id.clone());
-
- return Ok((Event::Ready(ready), receiver_new));
- }
-
- let seconds = i.pow(2);
-
- debug!("Exponentially backing off for {} seconds", seconds);
-
- // Exponentially back off.
- thread::sleep(StdDuration::from_secs(seconds));
- }
-
- // Reconnecting failed; just return an error instead.
- Err(Error::Gateway(GatewayError::ReconnectFailure))
- }
-
- #[doc(hidden)]
- pub fn resume(&mut self, receiver: &mut Receiver<WebSocketStream>)
- -> Result<(Event, Receiver<WebSocketStream>)> {
- let session_id = match self.session_id.clone() {
- Some(session_id) => session_id,
- None => return Err(Error::Gateway(GatewayError::NoSessionId)),
- };
-
- let _ = receiver.shutdown_all();
- let url = prep::build_gateway_url(&self.ws_url)?;
-
- let response = WsClient::connect(url)?.send()?;
- response.validate()?;
-
- let (mut sender, mut receiver) = response.begin().split();
-
- sender.send_json(&json!({
- "op": OpCode::Resume.num(),
- "d": {
- "session_id": session_id,
- "seq": self.seq,
- "token": self.token,
- },
- }))?;
-
- // Note to self when this gets accepted in a decade:
- // https://github.com/rust-lang/rfcs/issues/961
- let ev;
-
- loop {
- match receiver.recv_json(GatewayEvent::decode)? {
- GatewayEvent::Dispatch(seq, event) => {
- match event {
- Event::Ready(ref ready) => {
- self.session_id = Some(ready.ready.session_id.clone());
- },
- Event::Resumed(_) => info!("Resumed"),
- ref other => warn!("Unknown resume event: {:?}", other),
- }
-
- self.seq = seq;
- ev = event;
-
- break;
- },
- GatewayEvent::Hello(i) => {
- let _ = self.keepalive_channel.send(GatewayStatus::Interval(i));
- }
- GatewayEvent::InvalidateSession => {
- sender.send_json(&prep::identify(&self.token, self.shard_info))?;
- },
- other => {
- debug!("Unexpected event: {:?}", other);
-
- return Err(Error::Gateway(GatewayError::InvalidHandshake));
- },
- }
- }
-
- let _ = self.keepalive_channel.send(GatewayStatus::Sender(sender));
-
- Ok((ev, receiver))
- }
-
- fn update_presence(&self) {
- let (ref game, status, afk) = self.current_presence;
- let now = time::get_time().sec as u64;
-
- let msg = json!({
- "op": OpCode::StatusUpdate.num(),
- "d": {
- "afk": afk,
- "since": now,
- "status": status.name(),
- "game": game.as_ref().map(|x| json!({
- "name": x.name,
- })),
- },
- });
-
- let _ = self.keepalive_channel.send(GatewayStatus::SendMessage(msg));
-
- #[cfg(feature="cache")]
- {
- let mut cache = CACHE.write().unwrap();
- let current_user_id = cache.user.id;
-
- cache.presences.get_mut(&current_user_id).map(|presence| {
- presence.game = game.clone();
- presence.last_modified = Some(now);
- });
- }
- }
-}
diff --git a/src/client/gateway/status.rs b/src/client/gateway/status.rs
deleted file mode 100644
index f6e5ec2..0000000
--- a/src/client/gateway/status.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use serde_json::Value;
-use websocket::client::Sender;
-use websocket::stream::WebSocketStream;
-
-#[doc(hidden)]
-pub enum Status {
- Interval(u64),
- Sender(Sender<WebSocketStream>),
- SendMessage(Value),
- Sequence(u64),
-}
diff --git a/src/client/mod.rs b/src/client/mod.rs
index d40792b..1ba555a 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 `rest` module and `Cache` are also automatically handled by the
+//! the `http` module and `Cache` are also automatically handled by the
//! Client module for you.
//!
//! A [`Context`] is provided for every handler. The context is a method of
//! accessing the lower-level HTTP functions relevant to the contextual channel.
//!
-//! The `rest` module is the lower-level method of interacting with the Discord
+//! The `http` 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 `rest`
+//! as the Context will do this for you. A possible use case of using the `http`
//! module is if you do not have a Cache, for purposes such as low memory
//! requirements.
//!
@@ -20,9 +20,6 @@
//! [Client examples]: struct.Client.html#examples
#![allow(zero_ptr)]
-pub mod gateway;
-pub mod rest;
-
mod context;
mod dispatch;
mod error;
@@ -31,66 +28,32 @@ mod event_store;
pub use self::context::Context;
pub use self::error::Error as ClientError;
+// Note: the following re-exports are here for backwards compatibility
+pub use ::gateway;
+pub use ::http as rest;
+
+#[cfg(feature="cache")]
+pub use ::CACHE;
+
use self::dispatch::dispatch;
use self::event_store::EventStore;
-use self::gateway::Shard;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
use std::{mem, thread};
+use super::gateway::Shard;
use typemap::ShareMap;
use websocket::client::Receiver;
use websocket::result::WebSocketError;
use websocket::stream::WebSocketStream;
+use ::http;
use ::internal::prelude::*;
use ::internal::ws_impl::ReceiverExt;
use ::model::event::*;
use ::model::*;
#[cfg(feature="framework")]
-use ::ext::framework::Framework;
-
-#[cfg(feature="cache")]
-use ::ext::cache::Cache;
-
-#[cfg(feature="cache")]
-lazy_static! {
- /// A mutable and lazily-initialized static binding. It can be accessed
- /// across any function and in any context.
- ///
- /// This [`Cache`] instance is updated for every event received, so you do
- /// not need to maintain your own cache.
- ///
- /// See the [cache module documentation] for more details.
- ///
- /// The Cache itself is wrapped within an `RwLock`, which allows for
- /// multiple readers or at most one writer at a time across threads. This
- /// means that you may have multiple commands reading from the Cache
- /// concurrently.
- ///
- /// # Examples
- ///
- /// Retrieve the [current user][`CurrentUser`]'s Id, by opening a Read
- /// guard:
- ///
- /// ```rust,ignore
- /// use serenity::client::CACHE;
- ///
- /// println!("{}", CACHE.read().unwrap().user.id);
- /// ```
- ///
- /// By `unwrap()`ing, the thread managing an event dispatch will be blocked
- /// until the guard can be opened.
- ///
- /// If you do not want to block the current thread, you may instead use
- /// `RwLock::try_read`. Refer to `RwLock`'s documentation in the stdlib for
- /// more information.
- ///
- /// [`CurrentUser`]: ../model/struct.CurrentUser.html
- /// [`Cache`]: ../ext/cache/struct.Cache.html
- /// [cache module documentation]: ../ext/cache/index.html
- pub static ref CACHE: RwLock<Cache> = RwLock::new(Cache::default());
-}
+use ::framework::Framework;
/// The Client is the way to "login" and be able to start sending authenticated
/// requests over the REST API, as well as initializing a WebSocket connection
@@ -192,7 +155,7 @@ impl Client {
/// information on usage.
///
/// [`on_message`]: #method.on_message
- /// [framework docs]: ../ext/framework/index.html
+ /// [framework docs]: ../framework/index.html
#[cfg(feature="framework")]
pub fn with_framework<F>(&mut self, f: F)
where F: FnOnce(Framework) -> Framework + Send + Sync + 'static {
@@ -213,7 +176,7 @@ impl Client {
///
/// [gateway docs]: gateway/index.html#sharding
pub fn start(&mut self) -> Result<()> {
- self.start_connection(None, rest::get_gateway()?.url)
+ self.start_connection(None, http::get_gateway()?.url)
}
/// Establish the connection(s) and start listening for events.
@@ -230,7 +193,7 @@ impl Client {
///
/// [gateway docs]: gateway/index.html#sharding
pub fn start_autosharded(&mut self) -> Result<()> {
- let mut res = rest::get_bot_gateway()?;
+ let mut res = http::get_bot_gateway()?;
let x = res.shards as u64 - 1;
let y = res.shards as u64;
@@ -255,7 +218,7 @@ impl Client {
///
/// [gateway docs]: gateway/index.html#sharding
pub fn start_shard(&mut self, shard: u64, shards: u64) -> Result<()> {
- self.start_connection(Some([shard, shard, shards]), rest::get_gateway()?.url)
+ self.start_connection(Some([shard, shard, shards]), http::get_gateway()?.url)
}
/// Establish sharded connections and start listening for events.
@@ -274,7 +237,7 @@ impl Client {
/// [`start_shard_range`]: #method.start_shards
/// [Gateway docs]: gateway/index.html#sharding
pub fn start_shards(&mut self, total_shards: u64) -> Result<()> {
- self.start_connection(Some([0, total_shards - 1, total_shards]), rest::get_gateway()?.url)
+ self.start_connection(Some([0, total_shards - 1, total_shards]), http::get_gateway()?.url)
}
/// Establish a range of sharded connections and start listening for events.
@@ -308,7 +271,7 @@ impl Client {
/// [`start_shards`]: #method.start_shards
/// [Gateway docs]: gateway/index.html#sharding
pub fn start_shard_range(&mut self, range: [u64; 2], total_shards: u64) -> Result<()> {
- self.start_connection(Some([range[0], range[1], total_shards]), rest::get_gateway()?.url)
+ self.start_connection(Some([range[0], range[1], total_shards]), http::get_gateway()?.url)
}
/// Attaches a handler for when a [`ChannelCreate`] is received.
@@ -665,7 +628,7 @@ impl Client {
// This also acts as a form of check to ensure the token is correct.
#[cfg(feature="framework")]
{
- let user = rest::get_current_user()?;
+ let user = http::get_current_user()?;
self.framework.lock()
.unwrap()
@@ -778,7 +741,7 @@ impl Client {
///
/// [`GuildDelete`]: ../model/event/enum.Event.html#variant.GuildDelete
/// [`Role`]: ../model/struct.Role.html
- /// [`Cache`]: ../ext/cache/struct.Cache.html
+ /// [`Cache`]: ../cache/struct.Cache.html
pub fn on_guild_delete<F>(&mut self, handler: F)
where F: Fn(Context, PartialGuild, Option<Arc<RwLock<Guild>>>) + Send + Sync + 'static {
self.event_store.write()
@@ -825,7 +788,7 @@ impl Client {
/// it did not exist in the [`Cache`] before the update.
///
/// [`GuildRoleUpdate`]: ../model/event/enum.Event.html#variant.GuildRoleUpdate
- /// [`Cache`]: ../ext/cache/struct.Cache.html
+ /// [`Cache`]: ../cache/struct.Cache.html
pub fn on_guild_role_update<F>(&mut self, handler: F)
where F: Fn(Context, GuildId, Option<Role>, Role) + Send + Sync + 'static {
self.event_store.write()
@@ -872,7 +835,7 @@ impl Client {
///
/// [`GuildDelete`]: ../model/event/enum.Event.html#variant.GuildDelete
/// [`Role`]: ../model/struct.Role.html
- /// [`Cache`]: ../ext/cache/struct.Cache.html
+ /// [`Cache`]: ../cache/struct.Cache.html
pub fn on_guild_delete<F>(&mut self, handler: F)
where F: Fn(Context, PartialGuild) + Send + Sync + 'static {
self.event_store.write()
@@ -916,7 +879,7 @@ impl Client {
/// Attaches a handler for when a [`GuildRoleUpdate`] is received.
///
/// [`GuildRoleUpdate`]: ../model/event/enum.Event.html#variant.GuildRoleUpdate
- /// [`Cache`]: ../ext/cache/struct.Cache.html
+ /// [`Cache`]: ../cache/struct.Cache.html
pub fn on_guild_role_update<F>(&mut self, handler: F)
where F: Fn(Context, GuildId, Role) + Send + Sync + 'static {
self.event_store.write()
@@ -985,7 +948,7 @@ fn boot_shard(info: &BootInfo) -> Result<(Shard, ReadyEvent, Receiver<WebSocketS
//
// If doing so fails, count this as a boot attempt.
if attempt_number > 3 {
- match rest::get_gateway() {
+ match http::get_gateway() {
Ok(g) => *info.gateway_url.lock().unwrap() = g.url,
Err(why) => {
warn!("Failed to retrieve gateway URL: {:?}", why);
@@ -1137,7 +1100,7 @@ fn handle_shard(info: &mut MonitorInfo) {
}
fn login(token: String) -> Client {
- rest::set_token(&token);
+ http::set_token(&token);
feature_framework! {{
Client {
diff --git a/src/client/rest/mod.rs b/src/client/rest/mod.rs
deleted file mode 100644
index 3890a46..0000000
--- a/src/client/rest/mod.rs
+++ /dev/null
@@ -1,1535 +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
-
-pub mod ratelimiting;
-
-pub use hyper::status::{StatusClass, StatusCode};
-
-use hyper::client::{
- Client as HyperClient,
- RequestBuilder,
- Response as HyperResponse,
- Request,
-};
-use hyper::method::Method;
-use hyper::{Error as HyperError, Result as HyperResult, Url, header};
-use multipart::client::Multipart;
-use self::ratelimiting::Route;
-use serde_json;
-use std::collections::BTreeMap;
-use std::default::Default;
-use std::fmt::Write as FmtWrite;
-use std::io::{ErrorKind as IoErrorKind, Read};
-use std::sync::{Arc, Mutex};
-use ::constants;
-use ::internal::prelude::*;
-use ::model::*;
-
-/// An method used for ratelimiting special routes.
-///
-/// This is needed because `hyper`'s `Method` enum does not derive Copy.
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
-pub enum LightMethod {
- /// Indicates that a route is for "any" method.
- Any,
- /// Indicates that a route is for the `DELETE` method only.
- Delete,
- /// Indicates that a route is for the `GET` method only.
- Get,
- /// Indicates that a route is for the `PATCH` method only.
- Patch,
- /// Indicates that a route is for the `POST` method only.
- Post,
- /// Indicates that a route is for the `PUT` method only.
- Put,
-}
-
-lazy_static! {
- static ref TOKEN: Arc<Mutex<String>> = Arc::new(Mutex::new(String::default()));
-}
-
-/// Sets the token to be used across all requests which require authentication.
-///
-/// This is really only for internal use, and if you are reading this as a user,
-/// you should _not_ use this yourself.
-#[doc(hidden)]
-pub fn set_token(token: &str) {
- TOKEN.lock().unwrap().clone_from(&token.to_owned());
-}
-
-/// Adds a [`User`] as a recipient to a [`Group`].
-///
-/// **Note**: Groups have a limit of 10 recipients, including the current user.
-///
-/// [`Group`]: ../../model/struct.Group.html
-/// [`Group::add_recipient`]: ../../model/struct.Group.html#method.add_recipient
-/// [`User`]: ../../model/struct.User.html
-pub fn add_group_recipient(group_id: u64, user_id: u64) -> Result<()> {
- verify(204, request!(Route::None,
- put,
- "/channels/{}/recipients/{}",
- group_id,
- user_id))
-}
-
-/// Adds a single [`Role`] to a [`Member`] in a [`Guild`].
-///
-/// **Note**: Requires the [Manage Roles] permission and respect of role
-/// hierarchy.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-/// [`Member`]: ../../model/struct.Member.html
-/// [`Role`]: ../../model/struct.Role.html
-/// [Manage Roles]: ../../model/permissions/constant.MANAGE_ROLES.html
-pub fn add_member_role(guild_id: u64, user_id: u64, role_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdMembersIdRolesId(guild_id),
- put,
- "/guilds/{}/members/{}/roles/{}",
- guild_id,
- user_id,
- role_id))
-}
-
-/// Bans a [`User`] from a [`Guild`], removing their messages sent in the last
-/// X number of days.
-///
-/// Passing a `delete_message_days` of `0` is equivalent to not removing any
-/// messages. Up to `7` days' worth of messages may be deleted.
-///
-/// **Note**: Requires that you have the [Ban Members] permission.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-/// [`User`]: ../../model/struct.User.html
-/// [Ban Members]: ../../model/permissions/constant.BAN_MEMBERS.html
-pub fn ban_user(guild_id: u64, user_id: u64, delete_message_days: u8) -> Result<()> {
- verify(204, request!(Route::GuildsIdBansUserId(guild_id),
- put,
- "/guilds/{}/bans/{}?delete_message_days={}",
- guild_id,
- user_id,
- delete_message_days))
-}
-
-/// Broadcasts that the current user is typing in the given [`Channel`].
-///
-/// This lasts for about 10 seconds, and will then need to be renewed to
-/// indicate that the current user is still typing.
-///
-/// This should rarely be used for bots, although it is a good indicator that a
-/// long-running command is still being processed.
-///
-/// [`Channel`]: ../../model/enum.Channel.html
-pub fn broadcast_typing(channel_id: u64) -> Result<()> {
- verify(204, request!(Route::ChannelsIdTyping(channel_id),
- post,
- "/channels/{}/typing",
- channel_id))
-}
-
-/// Creates a [`GuildChannel`] in the [`Guild`] given its Id.
-///
-/// Refer to the Discord's [docs] for information on what fields this requires.
-///
-/// **Note**: Requires the [Manage Channels] permission.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-/// [`GuildChannel`]: ../../model/struct.GuildChannel.html
-/// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-channel
-/// [Manage Channels]: ../../model/permissions/constant.MANAGE_CHANNELS.html
-pub fn create_channel(guild_id: u64, map: &Value) -> Result<GuildChannel> {
- let body = map.to_string();
- let response = request!(Route::GuildsIdChannels(guild_id),
- post(body),
- "/guilds/{}/channels",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, GuildChannel>(response).map_err(From::from)
-}
-
-/// Creates an emoji in the given [`Guild`] with the given data.
-///
-/// View the source code for [`Context::create_emoji`] to see what fields this
-/// requires.
-///
-/// **Note**: Requires the [Manage Emojis] permission.
-///
-/// [`Context::create_emoji`]: ../struct.Context.html#method.create_emoji
-/// [`Guild`]: ../../model/struct.Guild.html
-/// [Manage Emojis]: ../../model/permissions/constant.MANAGE_EMOJIS.html
-pub fn create_emoji(guild_id: u64, map: &Value) -> Result<Emoji> {
- let body = map.to_string();
- let response = request!(Route::GuildsIdEmojis(guild_id),
- post(body),
- "/guilds/{}/emojis",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Emoji>(response).map_err(From::from)
-}
-
-/// Creates a guild with the data provided.
-///
-/// Only a [`PartialGuild`] will be immediately returned, and a full [`Guild`]
-/// will be received over a [`Shard`], if at least one is running.
-///
-/// **Note**: This endpoint is currently limited to 10 active guilds. The
-/// limits are raised for whitelisted [GameBridge] applications. See the
-/// [documentation on this endpoint] for more info.
-///
-/// # Examples
-///
-/// Create a guild called `"test"` in the [US West region]:
-///
-/// ```rust,ignore
-/// extern crate serde_json;
-///
-/// use serde_json::builder::ObjectBuilder;
-/// use serde_json::Value;
-/// use serenity::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
-/// [GameBridge]: https://discordapp.com/developers/docs/topics/gamebridge
-/// [US West Region]: ../../model/enum.Region.html#variant.UsWest
-/// [documentation on this endpoint]: https://discordapp.com/developers/docs/resources/guild#create-guild
-/// [whitelist]: https://discordapp.com/developers/docs/resources/guild#create-guild
-pub fn create_guild(map: &Value) -> Result<PartialGuild> {
- let body = map.to_string();
- let response = request!(Route::Guilds, post(body), "/guilds");
-
- serde_json::from_reader::<HyperResponse, PartialGuild>(response).map_err(From::from)
-}
-
-/// Creates an [`Integration`] for a [`Guild`].
-///
-/// Refer to Discord's [docs] for field information.
-///
-/// **Note**: Requires the [Manage Guild] permission.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-/// [`Integration`]: ../../model/struct.Integration.html
-/// [Manage Guild]: ../../model/permissions/constant.MANAGE_GUILD.html
-/// [docs]: https://discordapp.com/developers/docs/resources/guild#create-guild-integration
-pub fn create_guild_integration(guild_id: u64, integration_id: u64, map: &Value) -> Result<()> {
- let body = map.to_string();
-
- verify(204, request!(Route::GuildsIdIntegrations(guild_id),
- post(body),
- "/guilds/{}/integrations/{}",
- guild_id,
- integration_id))
-}
-
-/// Creates a [`RichInvite`] for the given [channel][`GuildChannel`].
-///
-/// Refer to Discord's [docs] for field information.
-///
-/// All fields are optional.
-///
-/// **Note**: Requires the [Create Invite] permission.
-///
-/// [`GuildChannel`]: ../../model/struct.GuildChannel.html
-/// [`RichInvite`]: ../../model/struct.RichInvite.html
-/// [Create Invite]: ../../model/permissions/constant.CREATE_INVITE.html
-/// [docs]: https://discordapp.com/developers/docs/resources/channel#create-channel-invite
-pub fn create_invite(channel_id: u64, map: &JsonMap) -> Result<RichInvite> {
- let body = serde_json::to_string(map)?;
- let response = request!(Route::ChannelsIdInvites(channel_id),
- post(body),
- "/channels/{}/invites",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, RichInvite>(response).map_err(From::from)
-}
-
-/// Creates a permission override for a member or a role in a channel.
-pub fn create_permission(channel_id: u64, target_id: u64, map: &Value) -> Result<()> {
- let body = map.to_string();
-
- verify(204, request!(Route::ChannelsIdPermissionsOverwriteId(channel_id),
- put(body),
- "/channels/{}/permissions/{}",
- channel_id,
- target_id))
-}
-
-/// Creates a private channel with a user.
-pub fn create_private_channel(map: &Value) -> Result<PrivateChannel> {
- let body = map.to_string();
- let response = request!(Route::UsersMeChannels,
- post(body),
- "/users/@me/channels");
-
- serde_json::from_reader::<HyperResponse, PrivateChannel>(response).map_err(From::from)
-}
-
-/// Reacts to a message.
-pub fn create_reaction(channel_id: u64,
- message_id: u64,
- reaction_type: &ReactionType)
- -> Result<()> {
- verify(204, request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id),
- put,
- "/channels/{}/messages/{}/reactions/{}/@me",
- channel_id,
- message_id,
- reaction_type.as_data()))
-}
-
-/// Creates a role.
-pub fn create_role(guild_id: u64, map: &JsonMap) -> Result<Role> {
- let body = serde_json::to_string(map)?;
- let response = request!(Route::GuildsIdRoles(guild_id),
- post(body),
- "/guilds/{}/roles",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Role>(response).map_err(From::from)
-}
-
-/// Creates a webhook for the given [channel][`GuildChannel`]'s Id, passing in
-/// the given data.
-///
-/// This method requires authentication.
-///
-/// The Value is a map with the values of:
-///
-/// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar
-/// (_optional_);
-/// - **name**: the name of the webhook, limited to between 2 and 100 characters
-/// long.
-///
-/// # Examples
-///
-/// Creating a webhook named `test`:
-///
-/// ```rust,ignore
-/// extern crate serde_json;
-/// extern crate serenity;
-///
-/// use serde_json::builder::ObjectBuilder;
-/// use serenity::client::rest;
-///
-/// let channel_id = 81384788765712384;
-/// let map = ObjectBuilder::new().insert("name", "test").build();
-///
-/// let webhook = rest::create_webhook(channel_id, map).expect("Error creating");
-/// ```
-///
-/// [`GuildChannel`]: ../../model/struct.GuildChannel.html
-pub fn create_webhook(channel_id: u64, map: &Value) -> Result<Webhook> {
- let body = map.to_string();
- let response = request!(Route::ChannelsIdWebhooks(channel_id),
- post(body),
- "/channels/{}/webhooks",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Webhook>(response).map_err(From::from)
-}
-
-/// Deletes a private channel or a channel in a guild.
-pub fn delete_channel(channel_id: u64) -> Result<Channel> {
- let response = request!(Route::ChannelsId(channel_id),
- delete,
- "/channels/{}",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Channel>(response).map_err(From::from)
-}
-
-/// Deletes an emoji from a server.
-pub fn delete_emoji(guild_id: u64, emoji_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdEmojisId(guild_id),
- delete,
- "/guilds/{}/emojis/{}",
- guild_id,
- emoji_id))
-}
-
-/// Deletes a guild, only if connected account owns it.
-pub fn delete_guild(guild_id: u64) -> Result<PartialGuild> {
- let response = request!(Route::GuildsId(guild_id),
- delete,
- "/guilds/{}",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, PartialGuild>(response).map_err(From::from)
-}
-
-/// Remvoes an integration from a guild.
-pub fn delete_guild_integration(guild_id: u64, integration_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdIntegrationsId(guild_id),
- delete,
- "/guilds/{}/integrations/{}",
- guild_id,
- integration_id))
-}
-
-/// Deletes an invite by code.
-pub fn delete_invite(code: &str) -> Result<Invite> {
- let response = request!(Route::InvitesCode, delete, "/invites/{}", code);
-
- serde_json::from_reader::<HyperResponse, Invite>(response).map_err(From::from)
-}
-
-/// Deletes a message if created by us or we have
-/// specific permissions.
-pub fn delete_message(channel_id: u64, message_id: u64) -> Result<()> {
- verify(204, request!(Route::ChannelsIdMessagesId(LightMethod::Delete, channel_id),
- delete,
- "/channels/{}/messages/{}",
- channel_id,
- message_id))
-}
-
-/// Deletes a bunch of messages, only works for bots.
-pub fn delete_messages(channel_id: u64, map: &Value) -> Result<()> {
- let body = map.to_string();
-
- verify(204, request!(Route::ChannelsIdMessagesBulkDelete(channel_id),
- post(body),
- "/channels/{}/messages/bulk_delete",
- channel_id))
-}
-
-/// Deletes all of the [`Reaction`]s associated with a [`Message`].
-///
-/// # Examples
-///
-/// ```rust,no_run
-/// use serenity::client::rest;
-/// use serenity::model::{ChannelId, MessageId};
-///
-/// let channel_id = ChannelId(7);
-/// let message_id = MessageId(8);
-///
-/// let _ = rest::delete_message_reactions(channel_id.0, message_id.0)
-/// .expect("Error deleting reactions");
-/// ```
-///
-/// [`Message`]: ../../model/struct.Message.html
-/// [`Reaction`]: ../../model/struct.Reaction.html
-pub fn delete_message_reactions(channel_id: u64, message_id: u64) -> Result<()> {
- verify(204, request!(Route::ChannelsIdMessagesIdReactions(channel_id),
- delete,
- "/channels/{}/messages/{}/reactions",
- channel_id,
- message_id))
-}
-
-/// Deletes a permission override from a role or a member in a channel.
-pub fn delete_permission(channel_id: u64, target_id: u64) -> Result<()> {
- verify(204, request!(Route::ChannelsIdPermissionsOverwriteId(channel_id),
- delete,
- "/channels/{}/permissions/{}",
- channel_id,
- target_id))
-}
-
-/// Deletes a reaction from a message if owned by us or
-/// we have specific permissions.
-pub fn delete_reaction(channel_id: u64,
- message_id: u64,
- user_id: Option<u64>,
- reaction_type: &ReactionType)
- -> Result<()> {
- let user = user_id.map(|uid| uid.to_string()).unwrap_or_else(|| "@me".to_string());
-
- verify(204, request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id),
- delete,
- "/channels/{}/messages/{}/reactions/{}/{}",
- channel_id,
- message_id,
- reaction_type.as_data(),
- user))
-}
-
-/// Deletes a role from a server. Can't remove the default everyone role.
-pub fn delete_role(guild_id: u64, role_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdRolesId(guild_id),
- delete,
- "/guilds/{}/roles/{}",
- guild_id,
- role_id))
-}
-
-/// Deletes a [`Webhook`] given its Id.
-///
-/// This method requires authentication, whereas [`delete_webhook_with_token`]
-/// does not.
-///
-/// # Examples
-///
-/// Deletes a webhook given its Id:
-///
-/// ```rust,no_run
-/// use serenity::client::{Client, rest};
-/// use std::env;
-///
-/// // Due to the `delete_webhook` function requiring you to authenticate, you
-/// // must have set the token first.
-/// rest::set_token(&env::var("DISCORD_TOKEN").unwrap());
-///
-/// rest::delete_webhook(245037420704169985).expect("Error deleting webhook");
-/// ```
-///
-/// [`Webhook`]: ../../model/struct.Webhook.html
-/// [`delete_webhook_with_token`]: fn.delete_webhook_with_token.html
-pub fn delete_webhook(webhook_id: u64) -> Result<()> {
- verify(204, request!(Route::WebhooksId, delete, "/webhooks/{}", webhook_id))
-}
-
-/// Deletes a [`Webhook`] given its Id and unique token.
-///
-/// This method does _not_ require authentication.
-///
-/// # Examples
-///
-/// Deletes a webhook given its Id and unique token:
-///
-/// ```rust,no_run
-/// use serenity::client::rest;
-///
-/// let id = 245037420704169985;
-/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
-///
-/// rest::delete_webhook_with_token(id, token).expect("Error deleting webhook");
-/// ```
-///
-/// [`Webhook`]: ../../model/struct.Webhook.html
-pub fn delete_webhook_with_token(webhook_id: u64, token: &str) -> Result<()> {
- let client = HyperClient::new();
- verify(204, retry(|| client
- .delete(&format!(api!("/webhooks/{}/{}"), webhook_id, token)))
- .map_err(Error::Hyper)?)
-}
-
-/// Changes channel information.
-pub fn edit_channel(channel_id: u64, map: &JsonMap) -> Result<GuildChannel> {
- let body = serde_json::to_string(map)?;
- let response = request!(Route::ChannelsId(channel_id),
- patch(body),
- "/channels/{}",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, GuildChannel>(response).map_err(From::from)
-}
-
-/// Changes emoji information.
-pub fn edit_emoji(guild_id: u64, emoji_id: u64, map: &Value) -> Result<Emoji> {
- let body = map.to_string();
- let response = request!(Route::GuildsIdEmojisId(guild_id),
- patch(body),
- "/guilds/{}/emojis/{}",
- guild_id,
- emoji_id);
-
- serde_json::from_reader::<HyperResponse, Emoji>(response).map_err(From::from)
-}
-
-/// Changes guild information.
-pub fn edit_guild(guild_id: u64, map: &JsonMap) -> Result<PartialGuild> {
- let body = serde_json::to_string(map)?;
- let response = request!(Route::GuildsId(guild_id),
- patch(body),
- "/guilds/{}",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, PartialGuild>(response).map_err(From::from)
-}
-
-/// Edits a [`Guild`]'s embed setting.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-pub fn edit_guild_embed(guild_id: u64, map: &Value) -> Result<GuildEmbed> {
- let body = map.to_string();
- let response = request!(Route::GuildsIdEmbed(guild_id),
- patch(body),
- "/guilds/{}/embed",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, GuildEmbed>(response).map_err(From::from)
-}
-
-/// Does specific actions to a member.
-pub fn edit_member(guild_id: u64, user_id: u64, map: &JsonMap) -> Result<()> {
- let body = serde_json::to_string(map)?;
-
- verify(204, request!(Route::GuildsIdMembersId(guild_id),
- patch(body),
- "/guilds/{}/members/{}",
- guild_id,
- user_id))
-}
-
-/// Edits a message by Id.
-///
-/// **Note**: Only the author of a message can modify it.
-pub fn edit_message(channel_id: u64, message_id: u64, map: &Value) -> Result<Message> {
- let body = map.to_string();
- let response = request!(Route::ChannelsIdMessagesId(LightMethod::Any, channel_id),
- patch(body),
- "/channels/{}/messages/{}",
- channel_id,
- message_id);
-
- serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
-}
-
-/// Edits the current user's nickname for the provided [`Guild`] via its Id.
-///
-/// Pass `None` to reset the nickname.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-pub fn edit_nickname(guild_id: u64, new_nickname: Option<&str>) -> Result<()> {
- let map = json!({
- "nick": new_nickname
- });
- let body = map.to_string();
- let response = request!(Route::GuildsIdMembersMeNick(guild_id),
- patch(body),
- "/guilds/{}/members/@me/nick",
- guild_id);
-
- verify(200, response)
-}
-
-/// Edits the current user's profile settings.
-///
-/// For bot users, the password is optional.
-///
-/// # User Accounts
-///
-/// If a new token is received due to a password change, then the stored token
-/// internally will be updated.
-///
-/// **Note**: this token change may cause requests made between the actual token
-/// change and when the token is internally changed to be invalid requests, as
-/// the token may be outdated.
-pub fn edit_profile(map: &JsonMap) -> Result<CurrentUser> {
- let body = serde_json::to_string(map)?;
- let response = request!(Route::UsersMe, patch(body), "/users/@me");
-
- let mut value = serde_json::from_reader::<HyperResponse, Value>(response)?;
-
- if let Some(map) = value.as_object_mut() {
- if !TOKEN.lock().unwrap().starts_with("Bot ") {
- if let Some(Value::String(token)) = map.remove("token") {
- set_token(&token);
- }
- }
- }
-
- serde_json::from_value::<CurrentUser>(value).map_err(From::from)
-}
-
-/// Changes a role in a guild.
-pub fn edit_role(guild_id: u64, role_id: u64, map: &JsonMap) -> Result<Role> {
- let body = serde_json::to_string(map)?;
- let response = request!(Route::GuildsIdRolesId(guild_id),
- patch(body),
- "/guilds/{}/roles/{}",
- guild_id,
- role_id);
-
- serde_json::from_reader::<HyperResponse, Role>(response).map_err(From::from)
-}
-
-/// Edits a the webhook with the given data.
-///
-/// The Value is a map with optional values of:
-///
-/// - **avatar**: base64-encoded 128x128 image for the webhook's default avatar
-/// (_optional_);
-/// - **name**: the name of the webhook, limited to between 2 and 100 characters
-/// long.
-///
-/// Note that, unlike with [`create_webhook`], _all_ values are optional.
-///
-/// This method requires authentication, whereas [`edit_webhook_with_token`]
-/// does not.
-///
-/// # Examples
-///
-/// Edit the image of a webhook given its Id and unique token:
-///
-/// ```rust,ignore
-/// extern crate serde_json;
-/// extern crate serenity;
-///
-/// use serde_json::builder::ObjectBuilder;
-/// use serenity::client::rest;
-///
-/// let id = 245037420704169985;
-/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
-/// let image = serenity::utils::read_image("./webhook_img.png")
-/// .expect("Error reading image");
-/// let map = ObjectBuilder::new().insert("avatar", image).build();
-///
-/// let edited = rest::edit_webhook_with_token(id, token, map)
-/// .expect("Error editing webhook");
-/// ```
-///
-/// [`create_webhook`]: fn.create_webhook.html
-/// [`edit_webhook_with_token`]: fn.edit_webhook_with_token.html
-// The tests are ignored, rather than no_run'd, due to rustdoc tests with
-// external crates being incredibly messy and misleading in the end user's view.
-pub fn edit_webhook(webhook_id: u64, map: &Value) -> Result<Webhook> {
- let body = map.to_string();
- let response = request!(Route::WebhooksId,
- patch(body),
- "/webhooks/{}",
- webhook_id);
-
- serde_json::from_reader::<HyperResponse, Webhook>(response).map_err(From::from)
-}
-
-/// Edits the webhook with the given data.
-///
-/// Refer to the documentation for [`edit_webhook`] for more information.
-///
-/// This method does _not_ require authentication.
-///
-/// # Examples
-///
-/// Edit the name of a webhook given its Id and unique token:
-///
-/// ```rust,ignore
-/// extern crate serde_json;
-/// extern crate serenity;
-///
-/// use serde_json::builder::ObjectBuilder;
-/// use serenity::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("Error editing webhook");
-/// ```
-///
-/// [`edit_webhook`]: fn.edit_webhook.html
-pub fn edit_webhook_with_token(webhook_id: u64, token: &str, map: &JsonMap) -> Result<Webhook> {
- let body = serde_json::to_string(map)?;
- let client = HyperClient::new();
- let response = retry(|| client
- .patch(&format!(api!("/webhooks/{}/{}"), webhook_id, token))
- .body(&body))
- .map_err(Error::Hyper)?;
-
- serde_json::from_reader::<HyperResponse, Webhook>(response).map_err(From::from)
-}
-
-/// Executes a webhook, posting a [`Message`] in the webhook's associated
-/// [`Channel`].
-///
-/// This method does _not_ require authentication.
-///
-/// Pass `true` to `wait` to wait for server confirmation of the message sending
-/// before receiving a response. From the [Discord docs]:
-///
-/// > waits for server confirmation of message send before response, and returns
-/// > the created message body (defaults to false; when false a message that is
-/// > not saved does not return an error)
-///
-/// The map can _optionally_ contain the following data:
-///
-/// - `avatar_url`: Override the default avatar of the webhook with a URL.
-/// - `tts`: Whether this is a text-to-speech message (defaults to `false`).
-/// - `username`: Override the default username of the webhook.
-///
-/// Additionally, _at least one_ of the following must be given:
-///
-/// - `content`: The content of the message.
-/// - `embeds`: An array of rich embeds.
-///
-/// **Note**: For embed objects, all fields are registered by Discord except for
-/// `height`, `provider`, `proxy_url`, `type` (it will always be `rich`),
-/// `video`, and `width`. The rest will be determined by Discord.
-///
-/// # Examples
-///
-/// Sending a webhook with message content of `test`:
-///
-/// ```rust,ignore
-/// extern crate serde_json;
-/// extern crate serenity;
-///
-/// use serde_json::builder::ObjectBuilder;
-/// use serenity::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;
-/// },
-/// };
-///
-/// [`Channel`]: ../../model/enum.Channel.html
-/// [`Message`]: ../../model/struct.Message.html
-/// [Discord docs]: https://discordapp.com/developers/docs/resources/webhook#querystring-params
-pub fn execute_webhook(webhook_id: u64, token: &str, map: &JsonMap) -> Result<Message> {
- let body = serde_json::to_string(map)?;
- let client = HyperClient::new();
- let response = retry(|| client
- .post(&format!(api!("/webhooks/{}/{}"), webhook_id, token))
- .body(&body))
- .map_err(Error::Hyper)?;
-
- serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
-}
-
-/// Gets the active maintenances from Discord's Status API.
-///
-/// Does not require authentication.
-pub fn get_active_maintenances() -> Result<Vec<Maintenance>> {
- let client = HyperClient::new();
- let response = retry(|| client.get(
- status!("/scheduled-maintenances/active.json")))?;
-
- let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?;
-
- match map.remove("scheduled_maintenances") {
- Some(v) => serde_json::from_value::<Vec<Maintenance>>(v).map_err(From::from),
- None => Ok(vec![]),
- }
-}
-
-/// Gets information about an oauth2 application that the current user owns.
-///
-/// **Note**: Only user accounts may use this endpoint.
-pub fn get_application_info(id: u64) -> Result<ApplicationInfo> {
- let response = request!(Route::None, get, "/oauth2/applications/{}", id);
-
- serde_json::from_reader::<HyperResponse, ApplicationInfo>(response).map_err(From::from)
-}
-
-/// Gets all oauth2 applications we've made.
-///
-/// **Note**: Only user accounts may use this endpoint.
-pub fn get_applications() -> Result<Vec<ApplicationInfo>> {
- let response = request!(Route::None, get, "/oauth2/applications");
-
- serde_json::from_reader::<HyperResponse, Vec<ApplicationInfo>>(response).map_err(From::from)
-}
-
-/// Gets all the users that are banned in specific guild.
-pub fn get_bans(guild_id: u64) -> Result<Vec<Ban>> {
- let response = request!(Route::GuildsIdBans(guild_id),
- get,
- "/guilds/{}/bans",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Ban>>(response).map_err(From::from)
-}
-
-/// Gets current bot gateway.
-pub fn get_bot_gateway() -> Result<BotGateway> {
- let response = request!(Route::GatewayBot, get, "/gateway/bot");
-
- serde_json::from_reader::<HyperResponse, BotGateway>(response).map_err(From::from)
-}
-
-/// Gets all invites for a channel.
-pub fn get_channel_invites(channel_id: u64) -> Result<Vec<RichInvite>> {
- let response = request!(Route::ChannelsIdInvites(channel_id),
- get,
- "/channels/{}/invites",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response).map_err(From::from)
-}
-
-/// Retrieves the webhooks for the given [channel][`GuildChannel`]'s Id.
-///
-/// This method requires authentication.
-///
-/// # Examples
-///
-/// Retrieve all of the webhooks owned by a channel:
-///
-/// ```rust,no_run
-/// use serenity::client::rest;
-///
-/// let channel_id = 81384788765712384;
-///
-/// let webhooks = rest::get_channel_webhooks(channel_id)
-/// .expect("Error getting channel webhooks");
-/// ```
-///
-/// [`GuildChannel`]: ../../model/struct.GuildChannel.html
-pub fn get_channel_webhooks(channel_id: u64) -> Result<Vec<Webhook>> {
- let response = request!(Route::ChannelsIdWebhooks(channel_id),
- get,
- "/channels/{}/webhooks",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response).map_err(From::from)
-}
-
-/// Gets channel information.
-pub fn get_channel(channel_id: u64) -> Result<Channel> {
- let response = request!(Route::ChannelsId(channel_id),
- get,
- "/channels/{}",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Channel>(response).map_err(From::from)
-}
-
-/// Gets all channels in a guild.
-pub fn get_channels(guild_id: u64) -> Result<Vec<GuildChannel>> {
- let response = request!(Route::ChannelsId(guild_id),
- get,
- "/guilds/{}/channels",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<GuildChannel>>(response).map_err(From::from)
-}
-
-/// Gets information about the current application.
-///
-/// **Note**: Only applications may use this endpoint.
-pub fn get_current_application_info() -> Result<CurrentApplicationInfo> {
- let response = request!(Route::None, get, "/oauth2/applications/@me");
-
- serde_json::from_reader::<HyperResponse, CurrentApplicationInfo>(response).map_err(From::from)
-}
-
-/// Gets information about the user we're connected with.
-pub fn get_current_user() -> Result<CurrentUser> {
- let response = request!(Route::UsersMe, get, "/users/@me");
-
- serde_json::from_reader::<HyperResponse, CurrentUser>(response).map_err(From::from)
-}
-
-/// Gets current gateway.
-pub fn get_gateway() -> Result<Gateway> {
- let response = request!(Route::Gateway, get, "/gateway");
-
- serde_json::from_reader::<HyperResponse, Gateway>(response).map_err(From::from)
-}
-
-/// Gets information about an emoji.
-pub fn get_emoji(guild_id: u64, emoji_id: u64) -> Result<Emoji> {
- let response = request!(Route::GuildsIdEmojisId(guild_id),
- get,
- "/guilds/{}/emojis/{}",
- guild_id,
- emoji_id);
-
- serde_json::from_reader::<HyperResponse, Emoji>(response).map_err(From::from)
-}
-
-/// Gets all emojis in a guild.
-pub fn get_emojis(guild_id: u64) -> Result<Vec<Emoji>> {
- let response = request!(Route::GuildsIdEmojis(guild_id),
- get,
- "/guilds/{}/emojis",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Emoji>>(response).map_err(From::from)
-}
-
-/// Gets guild information.
-pub fn get_guild(guild_id: u64) -> Result<PartialGuild> {
- let response = request!(Route::GuildsId(guild_id),
- get,
- "/guilds/{}",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, PartialGuild>(response).map_err(From::from)
-}
-
-/// Gets a guild embed information.
-pub fn get_guild_embed(guild_id: u64) -> Result<GuildEmbed> {
- let response = request!(Route::GuildsIdEmbed(guild_id),
- get,
- "/guilds/{}/embeds",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, GuildEmbed>(response).map_err(From::from)
-}
-
-/// Gets integrations that a guild has.
-pub fn get_guild_integrations(guild_id: u64) -> Result<Vec<Integration>> {
- let response = request!(Route::GuildsIdIntegrations(guild_id),
- get,
- "/guilds/{}/integrations",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Integration>>(response).map_err(From::from)
-}
-
-/// Gets all invites to a guild.
-pub fn get_guild_invites(guild_id: u64) -> Result<Vec<RichInvite>> {
- let response = request!(Route::GuildsIdInvites(guild_id),
- get,
- "/guilds/{}/invites",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<RichInvite>>(response).map_err(From::from)
-}
-
-/// Gets the members of a guild. Optionally pass a `limit` and the Id of the
-/// user to offset the result by.
-pub fn get_guild_members(guild_id: u64, limit: Option<u64>, after: Option<u64>)
- -> Result<Vec<Member>> {
- let response = request!(Route::GuildsIdMembers(guild_id),
- get,
- "/guilds/{}/members?limit={}&after={}",
- guild_id,
- limit.unwrap_or(500),
- after.unwrap_or(0));
-
- let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?;
-
- if let Some(values) = v.as_array_mut() {
- let num = Value::Number(Number::from(guild_id));
-
- for value in values {
- if let Some(element) = value.as_object_mut() {
- element.insert("guild_id".to_owned(), num.clone());
- }
- }
- }
-
- serde_json::from_value::<Vec<Member>>(v).map_err(From::from)
-}
-
-/// Gets the amount of users that can be pruned.
-pub fn get_guild_prune_count(guild_id: u64, map: &Value) -> Result<GuildPrune> {
- let body = map.to_string();
- let response = request!(Route::GuildsIdPrune(guild_id),
- get(body),
- "/guilds/{}/prune",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, GuildPrune>(response).map_err(From::from)
-}
-
-/// Gets regions that a guild can use. If a guild has [`Feature::VipRegions`]
-/// enabled, then additional VIP-only regions are returned.
-///
-/// [`Feature::VipRegions`]: ../../model/enum.Feature.html#variant.VipRegions
-pub fn get_guild_regions(guild_id: u64) -> Result<Vec<VoiceRegion>> {
- let response = request!(Route::GuildsIdRegions(guild_id),
- get,
- "/guilds/{}/regions",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response).map_err(From::from)
-}
-
-/// Retrieves a list of roles in a [`Guild`].
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-pub fn get_guild_roles(guild_id: u64) -> Result<Vec<Role>> {
- let response = request!(Route::GuildsIdRoles(guild_id),
- get,
- "/guilds/{}/roles",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Role>>(response).map_err(From::from)
-}
-
-/// Retrieves the webhooks for the given [guild][`Guild`]'s Id.
-///
-/// This method requires authentication.
-///
-/// # Examples
-///
-/// Retrieve all of the webhooks owned by a guild:
-///
-/// ```rust,no_run
-/// use serenity::client::rest;
-///
-/// let guild_id = 81384788765712384;
-///
-/// let webhooks = rest::get_guild_webhooks(guild_id)
-/// .expect("Error getting guild webhooks");
-/// ```
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-pub fn get_guild_webhooks(guild_id: u64) -> Result<Vec<Webhook>> {
- let response = request!(Route::GuildsIdWebhooks(guild_id),
- get,
- "/guilds/{}/webhooks",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Webhook>>(response).map_err(From::from)
-}
-
-/// Gets a paginated list of the current user's guilds.
-///
-/// The `limit` has a maximum value of 100.
-///
-/// [Discord's documentation][docs]
-///
-/// # Examples
-///
-/// Get the first 10 guilds after a certain guild's Id:
-///
-/// ```rust,no_run
-/// use serenity::client::rest::{GuildPagination, get_guilds};
-/// use serenity::model::GuildId;
-///
-/// let guild_id = GuildId(81384788765712384);
-///
-/// let guilds = get_guilds(&GuildPagination::After(guild_id), 10).unwrap();
-/// ```
-///
-/// [docs]: https://discordapp.com/developers/docs/resources/user#get-current-user-guilds
-pub fn get_guilds(target: &GuildPagination, limit: u64) -> Result<Vec<GuildInfo>> {
- let mut uri = format!("/users/@me/guilds?limit={}", limit);
-
- match *target {
- GuildPagination::After(id) => {
- write!(uri, "&after={}", id)?;
- },
- GuildPagination::Before(id) => {
- write!(uri, "&before={}", id)?;
- },
- }
-
- let response = request!(Route::UsersMeGuilds, get, "{}", uri);
-
- serde_json::from_reader::<HyperResponse, Vec<GuildInfo>>(response).map_err(From::from)
-}
-
-/// Gets information about a specific invite.
-pub fn get_invite(code: &str, stats: bool) -> Result<Invite> {
- let invite = ::utils::parse_invite(code);
- let mut uri = format!("/invites/{}", invite);
-
- if stats {
- uri.push_str("?with_counts=true");
- }
-
- let response = request!(Route::InvitesCode, get, "{}", uri);
-
- serde_json::from_reader::<HyperResponse, Invite>(response).map_err(From::from)
-}
-
-/// Gets member of a guild.
-pub fn get_member(guild_id: u64, user_id: u64) -> Result<Member> {
- let response = request!(Route::GuildsIdMembersId(guild_id),
- get,
- "/guilds/{}/members/{}",
- guild_id,
- user_id);
-
- let mut v = serde_json::from_reader::<HyperResponse, Value>(response)?;
-
- if let Some(map) = v.as_object_mut() {
- map.insert("guild_id".to_owned(), Value::Number(Number::from(guild_id)));
- }
-
- serde_json::from_value::<Member>(v).map_err(From::from)
-}
-
-/// Gets a message by an Id, bots only.
-pub fn get_message(channel_id: u64, message_id: u64) -> Result<Message> {
- let response = request!(Route::ChannelsIdMessagesId(LightMethod::Any, channel_id),
- get,
- "/channels/{}/messages/{}",
- channel_id,
- message_id);
-
- serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
-}
-
-/// Gets X messages from a channel.
-pub fn get_messages(channel_id: u64, query: &str)
- -> Result<Vec<Message>> {
- let url = format!(api!("/channels/{}/messages{}"),
- channel_id,
- query);
- let client = HyperClient::new();
- let response = request(Route::ChannelsIdMessages(channel_id),
- || client.get(&url))?;
-
- serde_json::from_reader::<HyperResponse, Vec<Message>>(response).map_err(From::from)
-}
-
-/// Gets all pins of a channel.
-pub fn get_pins(channel_id: u64) -> Result<Vec<Message>> {
- let response = request!(Route::ChannelsIdPins(channel_id),
- get,
- "/channels/{}/pins",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Vec<Message>>(response).map_err(From::from)
-}
-
-/// Gets user Ids based on their reaction to a message. This endpoint is dumb.
-pub fn get_reaction_users(channel_id: u64,
- message_id: u64,
- reaction_type: &ReactionType,
- limit: u8,
- after: Option<u64>)
- -> Result<Vec<User>> {
- let mut uri = format!("/channels/{}/messages/{}/reactions/{}?limit={}",
- channel_id,
- message_id,
- reaction_type.as_data(),
- limit);
-
- if let Some(user_id) = after {
- write!(uri, "&after={}", user_id)?;
- }
-
- let response = request!(Route::ChannelsIdMessagesIdReactionsUserIdType(channel_id),
- get,
- "{}",
- uri);
-
- serde_json::from_reader::<HyperResponse, Vec<User>>(response).map_err(From::from)
-}
-
-/// Gets the current unresolved incidents from Discord's Status API.
-///
-/// Does not require authentication.
-pub fn get_unresolved_incidents() -> Result<Vec<Incident>> {
- let client = HyperClient::new();
- let response = retry(|| client.get(
- status!("/incidents/unresolved.json")))?;
-
- let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?;
-
- match map.remove("incidents") {
- Some(v) => serde_json::from_value::<Vec<Incident>>(v).map_err(From::from),
- None => Ok(vec![]),
- }
-}
-
-/// Gets the upcoming (planned) maintenances from Discord's Status API.
-///
-/// Does not require authentication.
-pub fn get_upcoming_maintenances() -> Result<Vec<Maintenance>> {
- let client = HyperClient::new();
- let response = retry(|| client.get(
- status!("/scheduled-maintenances/upcoming.json")))?;
-
- let mut map: BTreeMap<String, Value> = serde_json::from_reader(response)?;
-
- match map.remove("scheduled_maintenances") {
- Some(v) => serde_json::from_value::<Vec<Maintenance>>(v).map_err(From::from),
- None => Ok(vec![]),
- }
-}
-
-/// Gets a user by Id.
-pub fn get_user(user_id: u64) -> Result<User> {
- let response = request!(Route::UsersId, get, "/users/{}", user_id);
-
- serde_json::from_reader::<HyperResponse, User>(response).map_err(From::from)
-}
-
-/// Gets our DM channels.
-pub fn get_user_dm_channels() -> Result<Vec<PrivateChannel>> {
- let response = request!(Route::UsersMeChannels, get, "/users/@me/channels");
-
- serde_json::from_reader::<HyperResponse, Vec<PrivateChannel>>(response).map_err(From::from)
-}
-
-/// Gets all voice regions.
-pub fn get_voice_regions() -> Result<Vec<VoiceRegion>> {
- let response = request!(Route::VoiceRegions, get, "/voice/regions");
-
- serde_json::from_reader::<HyperResponse, Vec<VoiceRegion>>(response).map_err(From::from)
-}
-
-/// Retrieves a webhook given its Id.
-///
-/// This method requires authentication, whereas [`get_webhook_with_token`] does
-/// not.
-///
-/// # Examples
-///
-/// Retrieve a webhook by Id:
-///
-/// ```rust,no_run
-/// use serenity::client::rest;
-///
-/// let id = 245037420704169985;
-/// let webhook = rest::get_webhook(id).expect("Error getting webhook");
-/// ```
-///
-/// [`get_webhook_with_token`]: fn.get_webhook_with_token.html
-pub fn get_webhook(webhook_id: u64) -> Result<Webhook> {
- let response = request!(Route::WebhooksId, get, "/webhooks/{}", webhook_id);
-
- serde_json::from_reader::<HyperResponse, Webhook>(response).map_err(From::from)
-}
-
-/// Retrieves a webhook given its Id and unique token.
-///
-/// This method does _not_ require authentication.
-///
-/// # Examples
-///
-/// Retrieve a webhook by Id and its unique token:
-///
-/// ```rust,no_run
-/// use serenity::client::rest;
-///
-/// let id = 245037420704169985;
-/// let token = "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
-///
-/// let webhook = rest::get_webhook_with_token(id, token)
-/// .expect("Error getting webhook");
-/// ```
-pub fn get_webhook_with_token(webhook_id: u64, token: &str) -> Result<Webhook> {
- let client = HyperClient::new();
- let response = retry(|| client
- .get(&format!(api!("/webhooks/{}/{}"), webhook_id, token)))
- .map_err(Error::Hyper)?;
-
- serde_json::from_reader::<HyperResponse, Webhook>(response).map_err(From::from)
-}
-
-/// Kicks a member from a guild.
-pub fn kick_member(guild_id: u64, user_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdMembersId(guild_id),
- delete,
- "/guilds/{}/members/{}",
- guild_id,
- user_id))
-}
-
-/// Leaves a group DM.
-pub fn leave_group(guild_id: u64) -> Result<Group> {
- let response = request!(Route::None,
- delete,
- "/channels/{}",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, Group>(response).map_err(From::from)
-}
-
-/// Leaves a guild.
-pub fn leave_guild(guild_id: u64) -> Result<()> {
- verify(204, request!(Route::UsersMeGuildsId, delete, "/users/@me/guilds/{}", guild_id))
-}
-
-/// Deletes a user from group DM.
-pub fn remove_group_recipient(group_id: u64, user_id: u64) -> Result<()> {
- verify(204, request!(Route::None,
- delete,
- "/channels/{}/recipients/{}",
- group_id,
- user_id))
-}
-
-/// Sends a file to a channel.
-pub fn send_file<R: Read>(channel_id: u64, mut file: R, filename: &str, map: JsonMap)
- -> Result<Message> {
- let uri = format!(api!("/channels/{}/messages"), channel_id);
- let url = match Url::parse(&uri) {
- Ok(url) => url,
- Err(_) => return Err(Error::Url(uri)),
- };
-
- let mut request = Request::new(Method::Post, url)?;
- request.headers_mut()
- .set(header::Authorization(TOKEN.lock().unwrap().clone()));
- request.headers_mut()
- .set(header::UserAgent(constants::USER_AGENT.to_owned()));
-
- let mut request = Multipart::from_request(request)?;
-
- request.write_stream("file", &mut file, Some(filename), None)?;
-
- for (k, v) in map {
- let _ = match v {
- Value::Bool(false) => request.write_text(&k, "false")?,
- Value::Bool(true) => request.write_text(&k, "true")?,
- Value::Number(inner) => request.write_text(&k, inner.to_string())?,
- Value::String(inner) => request.write_text(&k, inner)?,
- _ => continue,
- };
- }
-
- let response = request.send()?;
-
- serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
-}
-
-/// Sends a message to a channel.
-pub fn send_message(channel_id: u64, map: &Value) -> Result<Message> {
- let body = map.to_string();
- let response = request!(Route::ChannelsIdMessages(channel_id),
- post(body),
- "/channels/{}/messages",
- channel_id);
-
- serde_json::from_reader::<HyperResponse, Message>(response).map_err(From::from)
-}
-
-/// Pins a message in a channel.
-pub fn pin_message(channel_id: u64, message_id: u64) -> Result<()> {
- verify(204, request!(Route::ChannelsIdPinsMessageId(channel_id),
- put,
- "/channels/{}/pins/{}",
- channel_id,
- message_id))
-}
-
-/// Unbans a user from a guild.
-pub fn remove_ban(guild_id: u64, user_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdBansUserId(guild_id),
- delete,
- "/guilds/{}/bans/{}",
- guild_id,
- user_id))
-}
-
-/// Deletes a single [`Role`] from a [`Member`] in a [`Guild`].
-///
-/// **Note**: Requires the [Manage Roles] permission and respect of role
-/// hierarchy.
-///
-/// [`Guild`]: ../../model/struct.Guild.html
-/// [`Member`]: ../../model/struct.Member.html
-/// [`Role`]: ../../model/struct.Role.html
-/// [Manage Roles]: ../../model/permissions/constant.MANAGE_ROLES.html
-pub fn remove_member_role(guild_id: u64, user_id: u64, role_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdMembersIdRolesId(guild_id),
- delete,
- "/guilds/{}/members/{}/roles/{}",
- guild_id,
- user_id,
- role_id))
-}
-
-/// Starts removing some members from a guild based on the last time they've been online.
-pub fn start_guild_prune(guild_id: u64, map: &Value) -> Result<GuildPrune> {
- let body = map.to_string();
- let response = request!(Route::GuildsIdPrune(guild_id),
- post(body),
- "/guilds/{}/prune",
- guild_id);
-
- serde_json::from_reader::<HyperResponse, GuildPrune>(response).map_err(From::from)
-}
-
-/// Starts syncing an integration with a guild.
-pub fn start_integration_sync(guild_id: u64, integration_id: u64) -> Result<()> {
- verify(204, request!(Route::GuildsIdIntegrationsIdSync(guild_id),
- post,
- "/guilds/{}/integrations/{}/sync",
- guild_id,
- integration_id))
-}
-
-/// Unpins a message from a channel.
-pub fn unpin_message(channel_id: u64, message_id: u64) -> Result<()> {
- verify(204, request!(Route::ChannelsIdPinsMessageId(channel_id),
- delete,
- "/channels/{}/pins/{}",
- channel_id,
- message_id))
-}
-
-fn request<'a, F>(route: Route, f: F) -> Result<HyperResponse>
- where F: Fn() -> RequestBuilder<'a> {
- let response = ratelimiting::perform(route, || f()
- .header(header::Authorization(TOKEN.lock().unwrap().clone()))
- .header(header::ContentType::json()))?;
-
- if response.status.class() == StatusClass::Success {
- Ok(response)
- } else {
- Err(Error::Client(ClientError::InvalidRequest(response.status)))
- }
-}
-
-#[doc(hidden)]
-pub fn retry<'a, F>(f: F) -> HyperResult<HyperResponse>
- where F: Fn() -> RequestBuilder<'a> {
- let req = || f()
- .header(header::UserAgent(constants::USER_AGENT.to_owned()))
- .send();
-
- match req() {
- Err(HyperError::Io(ref io))
- if io.kind() == IoErrorKind::ConnectionAborted => req(),
- other => other,
- }
-}
-
-fn verify(expected_status_code: u16, mut response: HyperResponse) -> Result<()> {
- let expected_status = match expected_status_code {
- 200 => StatusCode::Ok,
- 204 => StatusCode::NoContent,
- 401 => StatusCode::Unauthorized,
- _ => {
- let client_error = 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();
- response.read_to_string(&mut s)?;
-
- debug!("Content: {}", s);
-
- Err(Error::Client(ClientError::InvalidRequest(response.status)))
-}
-
-/// Representation of the method of a query to send for the [`get_guilds`]
-/// function.
-///
-/// [`get_guilds`]: fn.get_guilds.html
-pub enum GuildPagination {
- /// The Id to get the guilds after.
- After(GuildId),
- /// The Id to get the guilds before.
- Before(GuildId),
-}
diff --git a/src/client/rest/ratelimiting.rs b/src/client/rest/ratelimiting.rs
deleted file mode 100644
index cb88476..0000000
--- a/src/client/rest/ratelimiting.rs
+++ /dev/null
@@ -1,504 +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 equivalent in both URIs' format.
-//!
-//! # Examples
-//!
-//! First: taking the first two URIs - `GET /channels/4/messages/7` and
-//! `GET /channels/5/messages/8` - and assuming both buckets have a `limit` of
-//! `10`, requesting the first URI will result in the response containing a
-//! `remaining` of `9`. Immediately after - prior to buckets resetting -
-//! performing a request to the _second_ URI will also contain a `remaining` of
-//! `9` in the response, as the major parameter - `channel_id` - is different
-//! in the two requests (`4` and `5`).
-//!
-//! Second: take for example the last two URIs. Assuming the bucket's `limit` is
-//! `10`, requesting the first URI will return a `remaining` of `9` in the
-//! response. Immediately after - prior to buckets resetting - performing a
-//! request to the _second_ URI will return a `remaining` of `8` in the
-//! response, as the major parameter - `channel_id` - is equivalent for the two
-//! requests (`10`).
-//!
-//! Major parameters are why some variants (i.e. all of the channel/guild
-//! variants) have an associated u64 as data. This is the Id of the parameter,
-//! differentiating between different ratelimits.
-//!
-//! [Taken from]: https://discordapp.com/developers/docs/topics/rate-limits#rate-limits
-#![allow(zero_ptr)]
-
-use hyper::client::{RequestBuilder, Response};
-use hyper::header::Headers;
-use hyper::status::StatusCode;
-use std::collections::HashMap;
-use std::sync::{Arc, Mutex};
-use std::time::Duration;
-use std::{str, thread};
-use super::LightMethod;
-use time;
-use ::internal::prelude::*;
-
-lazy_static! {
- /// The global mutex is a mutex unlocked and then immediately re-locked
- /// prior to every request, to abide by Discord's global ratelimit.
- ///
- /// The global ratelimit is the total number of requests that may be made
- /// across the entirity of the API within an amount of time. If this is
- /// reached, then the global mutex is unlocked for the amount of time
- /// present in the "Retry-After" header.
- ///
- /// While locked, all requests are blocked until each request can acquire
- /// the lock.
- ///
- /// The only reason that you would need to use the global mutex is to
- /// block requests yourself. This has the side-effect of potentially
- /// blocking many of your event handlers or framework commands.
- pub static ref GLOBAL: Arc<Mutex<()>> = Arc::new(Mutex::new(()));
- /// The routes mutex is a HashMap of each [`Route`] and their respective
- /// ratelimit information.
- ///
- /// See the documentation for [`RateLimit`] for more infomation on how the
- /// library handles ratelimiting.
- ///
- /// # Examples
- ///
- /// View the `reset` time of the route for `ChannelsId(7)`:
- ///
- /// ```rust,no_run
- /// use serenity::client::rest::ratelimiting::{ROUTES, Route};
- ///
- /// let routes = ROUTES.lock().unwrap();
- ///
- /// if let Some(route) = routes.get(&Route::ChannelsId(7)) {
- /// println!("Reset time at: {}", route.reset);
- /// }
- /// ```
- ///
- /// [`RateLimit`]: struct.RateLimit.html
- /// [`Route`]: enum.Route.html
- pub static ref ROUTES: Arc<Mutex<HashMap<Route, RateLimit>>> = Arc::new(Mutex::new(HashMap::default()));
-}
-
-/// A representation of all routes registered within the library. These are safe
-/// and memory-efficient representations of each path that functions exist for
-/// in the [`rest`] module.
-///
-/// [`rest`]: ../index.html
-#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
-pub enum Route {
- /// Route for the `/channels/:channel_id` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsId(u64),
- /// Route for the `/channels/:channel_id/invites` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdInvites(u64),
- /// Route for the `/channels/:channel_id/messages` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdMessages(u64),
- /// Route for the `/channels/:channel_id/messages/bulk-delete` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdMessagesBulkDelete(u64),
- /// Route for the `/channels/:channel_id/messages/:message_id` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- // This route is a unique case. The ratelimit for message _deletions_ is
- // different than the overall route ratelimit.
- //
- // Refer to the docs on [Rate Limits] in the yellow warning section.
- //
- // Additionally, this needs to be a `LightMethod` from the parent module
- // and _not_ a `hyper` `Method` due to `hyper`'s not deriving `Copy`.
- //
- // [Rate Limits]: https://discordapp.com/developers/docs/topics/rate-limits
- ChannelsIdMessagesId(LightMethod, u64),
- /// Route for the `/channels/:channel_id/messages/:message_id/ack` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdMessagesIdAck(u64),
- /// Route for the `/channels/:channel_id/messages/:message_id/reactions`
- /// path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdMessagesIdReactions(u64),
- /// Route for the
- /// `/channels/:channel_id/messages/:message_id/reactions/:reaction/@me`
- /// path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdMessagesIdReactionsUserIdType(u64),
- /// Route for the `/channels/:channel_id/permissions/:target_id` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdPermissionsOverwriteId(u64),
- /// Route for the `/channels/:channel_id/pins` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdPins(u64),
- /// Route for the `/channels/:channel_id/pins/:message_id` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdPinsMessageId(u64),
- /// Route for the `/channels/:channel_id/typing` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdTyping(u64),
- /// Route for the `/channels/:channel_id/webhooks` path.
- ///
- /// The data is the relevant [`ChannelId`].
- ///
- /// [`ChannelId`]: ../../model/struct.ChannelId.html
- ChannelsIdWebhooks(u64),
- /// Route for the `/gateway` path.
- Gateway,
- /// Route for the `/gateway/bot` path.
- GatewayBot,
- /// Route for the `/guilds` path.
- Guilds,
- /// Route for the `/guilds/:guild_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsId(u64),
- /// Route for the `/guilds/:guild_id/bans` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdBans(u64),
- /// Route for the `/guilds/:guild_id/bans/:user_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdBansUserId(u64),
- /// Route for the `/guilds/:guild_id/channels/:channel_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdChannels(u64),
- /// Route for the `/guilds/:guild_id/embed` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdEmbed(u64),
- /// Route for the `/guilds/:guild_id/emojis` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdEmojis(u64),
- /// Route for the `/guilds/:guild_id/emojis/:emoji_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdEmojisId(u64),
- /// Route for the `/guilds/:guild_id/integrations` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdIntegrations(u64),
- /// Route for the `/guilds/:guild_id/integrations/:integration_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdIntegrationsId(u64),
- /// Route for the `/guilds/:guild_id/integrations/:integration_id/sync`
- /// path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdIntegrationsIdSync(u64),
- /// Route for the `/guilds/:guild_id/invites` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdInvites(u64),
- /// Route for the `/guilds/:guild_id/members` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdMembers(u64),
- /// Route for the `/guilds/:guild_id/members/:user_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdMembersId(u64),
- /// Route for the `/guilds/:guild_id/members/:user_id/roles/:role_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdMembersIdRolesId(u64),
- /// Route for the `/guilds/:guild_id/members/@me/nick` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdMembersMeNick(u64),
- /// Route for the `/guilds/:guild_id/prune` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdPrune(u64),
- /// Route for the `/guilds/:guild_id/regions` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdRegions(u64),
- /// Route for the `/guilds/:guild_id/roles` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdRoles(u64),
- /// Route for the `/guilds/:guild_id/roles/:role_id` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdRolesId(u64),
- /// Route for the `/guilds/:guild_id/webhooks` path.
- ///
- /// The data is the relevant [`GuildId`].
- ///
- /// [`GuildId`]: struct.GuildId.html
- GuildsIdWebhooks(u64),
- /// Route for the `/invites/:code` path.
- InvitesCode,
- /// Route for the `/users/:user_id` path.
- UsersId,
- /// Route for the `/users/@me` path.
- UsersMe,
- /// Route for the `/users/@me/channels` path.
- UsersMeChannels,
- /// Route for the `/users/@me/guilds` path.
- UsersMeGuilds,
- /// Route for the `/users/@me/guilds/:guild_id` path.
- UsersMeGuildsId,
- /// Route for the `/voice/regions` path.
- VoiceRegions,
- /// Route for the `/webhooks/:webhook_id` path.
- WebhooksId,
- /// Route where no ratelimit headers are in place (i.e. user account-only
- /// routes).
- ///
- /// This is a special case, in that if the route is `None` then pre- and
- /// post-hooks are not executed.
- None,
-}
-
-#[doc(hidden)]
-pub fn perform<'a, F>(route: Route, f: F) -> Result<Response>
- where F: Fn() -> RequestBuilder<'a> {
- loop {
- {
- // This will block if another thread already has the global
- // unlocked already (due to receiving an x-ratelimit-global).
- let mut _global = GLOBAL.lock().expect("global route lock poisoned");
- }
-
- // Perform pre-checking here:
- //
- // - get the route's relevant rate
- // - sleep if that route's already rate-limited until the end of the
- // 'reset' time;
- // - get the global rate;
- // - sleep if there is 0 remaining
- // - then, perform the request
- if route != Route::None {
- if let Some(route) = ROUTES.lock().expect("routes poisoned").get_mut(&route) {
- route.pre_hook();
- }
- }
-
- let response = super::retry(&f)?;
-
- // Check if the request got ratelimited by checking for status 429,
- // and if so, sleep for the value of the header 'retry-after' -
- // which is in milliseconds - and then `continue` to try again
- //
- // If it didn't ratelimit, subtract one from the RateLimit's
- // 'remaining'
- //
- // Update the 'reset' with the value of the 'x-ratelimit-reset'
- // header
- //
- // It _may_ be possible for the limit to be raised at any time,
- // so check if it did from the value of the 'x-ratelimit-limit'
- // header. If the limit was 5 and is now 7, add 2 to the 'remaining'
- if route != Route::None {
- let redo = if response.headers.get_raw("x-ratelimit-global").is_some() {
- let _ = GLOBAL.lock().expect("global route lock poisoned");
-
- Ok(if let Some(retry_after) = parse_header(&response.headers, "retry-after")? {
- debug!("Ratelimited: {:?}ms", retry_after);
- thread::sleep(Duration::from_millis(retry_after as u64));
-
- true
- } else {
- false
- })
- } else {
- ROUTES.lock()
- .expect("routes poisoned")
- .entry(route)
- .or_insert_with(RateLimit::default)
- .post_hook(&response)
- };
-
- if !redo.unwrap_or(true) {
- return Ok(response);
- }
- } else {
- return Ok(response);
- }
- }
-}
-
-/// A set of data containing information about the ratelimits for a particular
-/// [`Route`], which is stored in the [`ROUTES`] mutex.
-///
-/// See the [Discord docs] on ratelimits for more information.
-///
-/// **Note**: You should _not_ mutate any of the fields, as this can help cause
-/// 429s.
-///
-/// [`ROUTES`]: struct.ROUTES.html
-/// [`Route`]: enum.Route.html
-/// [Discord docs]: https://discordapp.com/developers/docs/topics/rate-limits
-#[derive(Clone, Debug, Default)]
-pub struct RateLimit {
- /// The total number of requests that can be made in a period of time.
- pub limit: i64,
- /// The number of requests remaining in the period of time.
- pub remaining: i64,
- /// When the interval resets and the the [`limit`] resets to the value of
- /// [`remaining`].
- ///
- /// [`limit`]: #structfield.limit
- /// [`remaining`]: #structfield.remaining
- pub reset: i64,
-}
-
-impl RateLimit {
- #[doc(hidden)]
- pub fn pre_hook(&mut self) {
- if self.limit == 0 {
- return;
- }
-
- let current_time = time::get_time().sec;
-
- // The reset was in the past, so we're probably good.
- if current_time > self.reset {
- self.remaining = self.limit;
-
- return;
- }
-
- let diff = (self.reset - current_time) as u64;
-
- if self.remaining == 0 {
- let delay = (diff * 1000) + 500;
-
- debug!("Pre-emptive ratelimit for {:?}ms", delay);
- thread::sleep(Duration::from_millis(delay));
-
- return;
- }
-
- self.remaining -= 1;
- }
-
- #[doc(hidden)]
- pub fn post_hook(&mut self, response: &Response) -> Result<bool> {
- if let Some(limit) = parse_header(&response.headers, "x-ratelimit-limit")? {
- self.limit = limit;
- }
-
- if let Some(remaining) = parse_header(&response.headers, "x-ratelimit-remaining")? {
- self.remaining = remaining;
- }
-
- if let Some(reset) = parse_header(&response.headers, "x-ratelimit-reset")? {
- self.reset = reset;
- }
-
- Ok(if response.status != StatusCode::TooManyRequests {
- false
- } else if let Some(retry_after) = parse_header(&response.headers, "retry-after")? {
- debug!("Ratelimited: {:?}ms", retry_after);
- thread::sleep(Duration::from_millis(retry_after as u64));
-
- true
- } else {
- false
- })
- }
-}
-
-fn parse_header(headers: &Headers, header: &str) -> Result<Option<i64>> {
- match headers.get_raw(header) {
- Some(header) => match str::from_utf8(&header[0]) {
- Ok(v) => match v.parse::<i64>() {
- Ok(v) => Ok(Some(v)),
- Err(_) => Err(Error::Client(ClientError::RateLimitI64)),
- },
- Err(_) => Err(Error::Client(ClientError::RateLimitUtf8)),
- },
- None => Ok(None),
- }
-}