spectacles_rest/
lib.rs

1//! # Spectacles REST
2//! Spectacles REST offers a simple, easy-to-use client for making HTTP requests to the Discord API.
3//! All HTTP requests are made asynchronously and never block the thread, with the help of the Tokio runtime.
4//!
5//! ## Creating a Client
6//! A [`RestClient`], resoponsible for performing all requests, can be easily constructed with the `new` function.
7//! ```rust
8//! use spectacles_rest::RestClient;
9//! let token = std::env::var("DISCORD_TOKEN").expect("Failed to parse token");
10//! let rest = RestClient::new(token, true);
11//! ```
12//! The client accepts a boolean as a second parameter, which determines whether or not the internal rate limiter will be used on each request.
13//!
14//! ## Views
15//! The Client ships with three views for specific endpoints of the Discord API.
16//!
17//! The [`ChannelView`] provides a set of methods for interacting with a Discord channel.
18//!
19//! The [`GuildView`] provides a set of methods for interacting with a Discord guild, or "server".
20//!
21//! The [`WebhookView`] provides a set of methods for interacting with Discord webhooks.
22//!
23//! [`ChannelView`]: struct.ChannelView.html
24//! [`GuildView`]: struct.GuildView.html
25//! [`WebhookView`]: struct.WebhookView.html
26//! [`RestClient`]: struct.RestClient.html
27//!
28//! Here is a brief example of sending a message to a Discord channel using the ChannelView.
29//! ```rust, norun
30//! use tokio::prelude::*;
31//! use spectacles_rest::RestClient;
32//! use spectacles_model::Snowflake;
33//!
34//! fn main() {
35//!     // Initialize the Rest Client, with a token.
36//!     let rest = RestClient::new(token, true);
37//!     tokio::run(rest.channel(&Snowflake(CHANNEL_ID_HERE)).create_message("Hello World")
38//!         .map(|message| println!("Message Sent: {:?}", message))
39//!         .map_err(|err| {
40//!             eprintln!("Error whilst attempting to send message. {:?}", err);
41//!         })
42//!     );
43//! }
44//! ```
45//! ## Rate Limiting
46//! As mentioned earlier, the library includes an in-memory rate limiter bucket system for preemptively managing Discord Ratelimits.
47//! This is sufficient if you do not plan on accessing the Discord API from a single server.
48//! If you plan to make requests in a distributed fashion, you will need to make use of an external state for keeping track of rate limits.
49//!
50//! The library currently supports using a custom Discord proxy, featured in the Spectacles client, to be used as a central hub for handling requests.
51//! ```rust
52//! use spectacles_rest::RestClient;
53//! let proxy = std::env::var("PROXY").expect("Failed to parse proxy URL.");
54//! let rest = RestClient::new(token, false) // false tells the client to skip the default in memory rate limiter.
55//!     .set_proxy(proxy);
56//!
57//! ```
58//! More rate limiting strategies will be considered in future.
59//!
60//!
61//! ## Installation
62//! Simply add the package to your Cargo.toml file.
63//! ```toml
64//! [dependencies]
65//! spectacles-rest = "0.1.0"
66//!
67
68#[macro_use]
69extern crate log;
70#[macro_use]
71extern crate serde_derive;
72#[warn(rust_2018_idioms)]
73#[macro_use]
74extern crate serde_json;
75
76use std::sync::Arc;
77
78use futures::future::{Future, Loop};
79use http::header::HeaderValue;
80use parking_lot::Mutex;
81use reqwest::header::HeaderMap;
82use reqwest::Method;
83use reqwest::r#async::{
84    Client as ReqwestClient,
85    ClientBuilder,
86    multipart::Form,
87};
88use serde::de::DeserializeOwned;
89use serde::ser::Serialize;
90use serde_json::Value;
91
92pub(crate) use ratelimit::*;
93use spectacles_model::channel::Channel;
94use spectacles_model::guild::{CreateGuildOptions, Guild};
95use spectacles_model::invite::Invite;
96use spectacles_model::snowflake::Snowflake;
97use spectacles_model::User;
98use spectacles_model::voice::VoiceRegion;
99/// A collection of interfaces for endpoint-specific Discord objects.
100pub use views::*;
101
102pub use crate::errors::{Error, Result};
103
104mod errors;
105mod ratelimit;
106mod views;
107mod constants;
108
109/// The Main client which is used to interface with the various components of the Discord API.
110#[derive(Clone, Debug)]
111pub struct RestClient {
112    /// The bot token for this user.
113    pub token: String,
114    /// The base URL of the client. This may be changed to accomodate an external proxy system.
115    pub base_url: String,
116    pub http: ReqwestClient,
117    ratelimiter: Option<Arc<Mutex<Ratelimter>>>,
118}
119
120impl RestClient {
121    /// Creates a new REST client with the provided configuration.
122    /// The second argument denotes whether or not to use the built-in rate limiter to rate limit requests to the Discord API.
123    /// If you plan to use a distributed architecture, you will need an external ratelimiter to ensure ratelimis are kept across servers.
124    pub fn new(token: String, using_ratelimiter: bool) -> Self {
125        let token = if token.starts_with("Bot ") {
126            token
127        } else {
128            format!("Bot {}", token)
129        };
130        let mut headers = HeaderMap::new();
131        let value = HeaderValue::from_str(&token).unwrap();
132        let agent = HeaderValue::from_str(
133            "DiscordBot (https://github.com/spec-tacles/spectacles-rs, v1.0.0)"
134        ).unwrap();
135        headers.insert("Authorization", value);
136        headers.insert("User-Agent", agent);
137
138        let client = ClientBuilder::new().default_headers(headers).build()
139            .expect("Failed to build HTTP client");
140
141        let mut rest = RestClient {
142            token,
143            http: client.clone(),
144            base_url: constants::BASE_URL.to_string(),
145            ratelimiter: None,
146        };
147
148        if using_ratelimiter {
149            rest.ratelimiter = Some(Arc::new(Mutex::new(Ratelimter::new(client))));
150        };
151
152        rest
153    }
154
155    /// Enables support for routing all requests though an HTTP rate limiting proxy.
156    /// If you plan on making distributed REST requests, an HTTP proxy is recommended for handling rate limits in a distributed manner.
157    pub fn set_proxy(mut self, url: String) -> Self {
158        self.base_url = url;
159        self
160    }
161
162    /// Opens a ChannelView for the provided Channel snowflake.
163    pub fn channel(&self, id: &Snowflake) -> ChannelView {
164        ChannelView::new(id.0, self.clone())
165    }
166
167    /// Opens a GuildView for the provided Guild snowflake.
168    pub fn guild(&self, id: &Snowflake) -> GuildView {
169        GuildView::new(id.0, self.clone())
170    }
171
172    /// Opens a WebhookView for the provided Webhook snowflake.
173    pub fn webhook(&self, id: &Snowflake) -> WebhookView {
174        WebhookView::new(id.0, self.clone())
175    }
176
177    /// Gets a User object for the provided snowflake.
178    pub fn get_user(&self, id: &Snowflake) -> impl Future<Item=User, Error=Error> {
179        self.request(Endpoint::new(
180            Method::GET,
181            format!("/users/{}", id.0),
182        ))
183    }
184
185    /// Opens a new DM channel with the user at the provided user ID.
186    pub fn create_dm(&self, user: &Snowflake) -> impl Future<Item=Channel, Error=Error> {
187        let json = json!({
188            "recipient_id": user.0
189        });
190
191        self.request(Endpoint::new(
192            Method::POST,
193            String::from("/users/@me/channels"),
194        ).json(json))
195    }
196
197    /// Creates a new guild, setting the current client user as owner.
198    /// This endpoint may only be used for bots who are in less than 10 guilds.
199    pub fn create_guild(&self, opts: CreateGuildOptions) -> impl Future<Item=Guild, Error=Error> {
200        self.request(Endpoint::new(
201            Method::POST,
202            String::from("/guilds"),
203        ).json(opts))
204    }
205
206    /// Leaves the guild using the provided guild ID.
207    pub fn leave_guild(&self, id: &Snowflake) -> impl Future<Item=(), Error=Error> {
208        self.request_empty(Endpoint::new(
209            Method::DELETE,
210            format!("/users/@me/guilds/{}", id.0),
211        ))
212    }
213
214
215    /// Modifies properties for the current user.
216    /*pub fn modify_current_user(&self) -> impl Future<Item = User, Error = Error> {
217        self.client.request(Endpoint::new(
218            Method::PATCH,
219            String::from("/users/@me)",
220        ))
221    }*/
222
223    /// Obtains a list of Discord voice regions.
224    pub fn get_voice_regions(&self) -> impl Future<Item=Vec<VoiceRegion>, Error=Error> {
225        self.request(Endpoint::new(
226            Method::GET,
227            String::from("/voice/regions"),
228        ))
229    }
230
231    /// Obtains an invite object from Discord using the given code.
232    /// The second argument denotes whether the invite should contain approximate member counts
233    pub fn get_invite(&self, code: &str, member_counts: bool) -> impl Future<Item=Invite, Error=Error> {
234        self.request(Endpoint::new(
235            Method::GET,
236            format!("/invites/{}?with_counts={}", code, member_counts),
237        ))
238    }
239
240    /// Deletes this invite from the its parent channel.
241    /// This requires that the client have the `MANAGE_CHANNELS` permission.
242    pub fn delete_invite(&self, code: &str) -> impl Future<Item=Invite, Error=Error> {
243        self.request(Endpoint::new(
244            Method::GET,
245            format!("/invites/{}", code),
246        ))
247    }
248
249    /// Makes an HTTP request to the provided Discord API endpoint.
250    /// Depending on the ratelimiter status, the request may or may not be rate limited.
251    pub fn request<T>(&self, mut endpt: Endpoint) -> Box<Future<Item=T, Error=Error> + Send>
252        where T: DeserializeOwned + Send + 'static
253    {
254        if let Some(ref rl) = self.ratelimiter {
255            let http = self.http.clone();
256            let base = self.base_url.clone();
257            Box::new(futures::future::loop_fn(Arc::clone(rl), move |ratelimit| {
258                let req_url = format!("{}{}", base, &endpt.url);
259                let route = Bucket::make_route(endpt.method.clone(), req_url.clone());
260                let mut req = http.request(endpt.method.clone(), &req_url)
261                    .query(&endpt.query)
262                    .json(&endpt.json);
263                if endpt.multipart.is_some() {
264                    let form = endpt.multipart.take().unwrap();
265                    req = req.multipart(form);
266                };
267                let limiter = Arc::clone(&ratelimit);
268                let limiter_2 = Arc::clone(&limiter);
269                ratelimit.lock().enqueue(route.clone())
270                    .and_then(|_| req.send().from_err())
271                    .and_then(move |resp| limiter.lock().handle_resp(route, resp))
272                    .map(move |status| match status {
273                        ResponseStatus::Success(resp) => Loop::Break(resp),
274                        ResponseStatus::Ratelimited | ResponseStatus::ServerError => Loop::Continue(limiter_2)
275                    })
276            }).and_then(|mut resp| resp.json().from_err()))
277        } else {
278            let req_url = format!("{}{}", self.base_url, &endpt.url);
279            let req = self.http.request(endpt.method.clone(), &req_url)
280                .query(&endpt.query)
281                .json(&endpt.json);
282            Box::new(req.send().map_err(Error::from)
283                .and_then(|mut resp| resp.json().from_err())
284            )
285        }
286    }
287
288    /// Similar to the above method, but does not attempt to deserialize a JSON payload from the request.
289    /// Use this method if you are dealing with routes that return 204 (No content).
290    pub fn request_empty(&self, mut endpt: Endpoint) -> Box<Future<Item=(), Error=Error> + Send> {
291        if let Some(ref rl) = self.ratelimiter {
292            let http = self.http.clone();
293            let base = self.base_url.clone();
294            Box::new(futures::future::loop_fn(Arc::clone(rl), move |ratelimit| {
295                let req_url = format!("{}{}", base, &endpt.url);
296                let route = Bucket::make_route(endpt.method.clone(), req_url.clone());
297                let mut req = http.request(endpt.method.clone(), &req_url)
298                    .query(&endpt.query)
299                    .json(&endpt.json);
300                if endpt.multipart.is_some() {
301                    let form = endpt.multipart.take().unwrap();
302                    req = req.multipart(form);
303                };
304                let limiter = Arc::clone(&ratelimit);
305                let limiter_2 = Arc::clone(&limiter);
306                ratelimit.lock().enqueue(route.clone())
307                    .and_then(|_| req.send().from_err())
308                    .and_then(move |resp| limiter.lock().handle_resp(route, resp))
309                    .map(move |status| match status {
310                        ResponseStatus::Success(resp) => Loop::Break(resp),
311                        ResponseStatus::Ratelimited | ResponseStatus::ServerError => Loop::Continue(limiter_2)
312                    })
313            }).map(|_| ()))
314        } else {
315            let req_url = format!("{}{}", self.base_url, &endpt.url);
316            let req = self.http.request(endpt.method.clone(), &req_url)
317                .query(&endpt.query)
318                .json(&endpt.json);
319            Box::new(req.send().map_err(Error::from)
320                .map(|_| ())
321            )
322        }
323    }
324}
325
326/// A structure representing a Discord API endpoint, in the context of an HTTP request.
327#[derive(Debug)]
328pub struct Endpoint {
329    url: String,
330    method: Method,
331    json: Option<Value>,
332    query: Option<Value>,
333    multipart: Option<Form>,
334}
335
336impl Endpoint {
337    /// Creates a new endpoint from the following HTTP method and URL string.
338    pub fn new(method: Method, url: String) -> Self {
339        Self {
340            method,
341            url,
342            json: None,
343            query: None,
344            multipart: None,
345        }
346    }
347
348    /// Adds a json body to the request.
349    pub fn json<T: Serialize>(mut self, payload: T) -> Endpoint {
350        match serde_json::to_value(payload) {
351            Ok(val) => self.json = Some(val),
352            Err(_) => self.json = None
353        };
354
355        self
356    }
357
358    /// Adds a query parameter to the endpoint.
359    pub fn query<T: Serialize>(mut self, payload: T) -> Endpoint {
360        match serde_json::to_value(payload) {
361            Ok(val) => self.query = Some(val),
362            Err(_) => self.query = None
363        };
364
365        self
366    }
367
368    /// Adds a multipart form to the endpoint, which is useful for sending files to the Discord API.
369    pub fn multipart(mut self, payload: Form) -> Endpoint {
370        self.multipart = Some(payload);
371        self
372    }
373}