aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZeyla Hellyer <[email protected]>2017-12-03 13:48:37 -0800
committerZeyla Hellyer <[email protected]>2017-12-03 13:48:37 -0800
commitbc1870baa0ae1cf0f133c3f7009b11dab2c4f7b9 (patch)
tree2fdbdef751b2c67be6c7f6181a78d118b716cc43
downloadoauth-bc1870baa0ae1cf0f133c3f7009b11dab2c4f7b9.tar.xz
oauth-bc1870baa0ae1cf0f133c3f7009b11dab2c4f7b9.zip
Initial commitHEADmaster
-rw-r--r--.editorconfig18
-rw-r--r--.gitignore5
-rw-r--r--.travis.yml16
-rw-r--r--Cargo.toml26
-rw-r--r--LICENSE.md15
-rw-r--r--README.md43
-rw-r--r--examples/rocket.rs94
-rw-r--r--src/bridge/hyper.rs145
-rw-r--r--src/bridge/mod.rs6
-rw-r--r--src/constants.rs9
-rw-r--r--src/error.rs54
-rw-r--r--src/lib.rs45
-rw-r--r--src/model.rs196
-rw-r--r--src/scope.rs77
-rw-r--r--src/utils.rs130
15 files changed, 879 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..edd529e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.rs]
+indent_size = 4
+
+[*.md]
+indent_size = 4
+trim_trailing_whitespace = false
+
+[*.yml]
+indent_size = 2
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8d59474
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# editor
+.idea/
+.vscode/
+
+target/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..99112c2
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,16 @@
+language: rust
+rust:
+ - stable
+ - beta
+ - nightly
+
+sudo: false
+os:
+ - linux
+ - osx
+
+cache:
+ cargo: true
+
+script:
+ - cargo test
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..7bd2ebf
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+authors = ["Zeyla Hellyer <[email protected]>"]
+description = "Serenity ecosystem oauth support for the Discord API."
+documentation = "https://docs.rs/serenity-oauth"
+homepage = "https://github.com/serenity-rs/oauth"
+keywords = ["discord", "oauth", "serenity"]
+license = "ISC"
+name = "serenity-oauth"
+readme = "README.md"
+repository = "https://github.com/serenity-rs/oauth.git"
+version = "0.1.0"
+
+[dependencies]
+hyper = "~0.10"
+percent-encoding = "^1.0"
+serde = "^1.0"
+serde_derive = "^1.0"
+serde_json = "^1.0"
+serde_urlencoded = "~0.5"
+serenity-model = { git = "https://github.com/serenity-rs/model" }
+
+[dev-dependencies]
+hyper = "~0.10"
+hyper-native-tls = "~0.2"
+rocket = "~0.3"
+rocket_codegen = "~0.3"
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..563ba1f
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,15 @@
+ISC License (ISC)
+
+Copyright (c) 2017, Zeyla Hellyer <[email protected]>
+
+Permission to use, copy, modify, and/or distribute this software for any purpose
+with or without fee is hereby granted, provided that the above copyright notice
+and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c051429
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# serenity-oauth
+
+`serenity-oauth` is a collection of HTTP library support bridges for
+interacting with the OAuth2 API that Discord uses.
+
+It includes support for sending code exchange requests and refresh token
+requests.
+
+Included are models in the `model` directory that represent request bodies
+and response bodies. The `Scope` enum represents possible OAuth2 scopes
+that can be granted.
+
+In the `utils` module, functions to produce authorization URLs are
+available. For example, `utils::bot_authorization_url` can be used to
+produce a URL that can be used to redirect users to authorize an application
+with the `Scope::Bot` scope.
+
+### Installation
+
+Add the following to your `Cargo.toml`:
+
+```toml
+[dependencies]
+serenity-oauth = { git = "https://github.com/serenity-rs/oauth" }
+```
+
+And then the following to your `main.rs` or `lib.rs`:
+
+```rust
+extern crate serenity_oauth;
+```
+
+### Examples
+
+For an example of how to use this in a real-world program, see the [`examples`]
+directory.
+
+### License
+
+This project is licensed under [ISC][license].
+
+[license]: https://github.com/serenity-rs/oauth/blob/master/LICENSE.md
+[`examples`]: https://github.com/serenity-rs/oauth/tree/master/examples
diff --git a/examples/rocket.rs b/examples/rocket.rs
new file mode 100644
index 0000000..d661e2f
--- /dev/null
+++ b/examples/rocket.rs
@@ -0,0 +1,94 @@
+//! This is a sample program for running a Rocket.rs server. This redirects
+//! users to Discord's authorization page, requesting the `identity` scope.
+//!
+//! Once they have authorized, it will take the `code` given and then exchange
+//! it for an access token, which can be used to access the user's identity.
+//!
+//! This example requires the following environment variables, both available
+//! from your Discord application's settings:
+//!
+//! - `DISCORD_CLIENT_ID`
+//! - `DISCORD_CLIENT_SECRET`
+//!
+//! You will also need to register a redirect URI. Running this locally would
+//! cause the redirect URI to be `http://localhost:8000` for example. This can
+//! be registered in your application's settings.
+//!
+//! Example of how to run this:
+//!
+//! `$ git clone https://github.com/serenity-rs/oauth`
+//! `$ cd oauth`
+//! `$ DISCORD_CLIENT_SECRET=my_secret DISCORD_CLIENT_ID=my_client_id cargo run --example rocket`
+
+#![feature(custom_derive, plugin)]
+#![plugin(rocket_codegen)]
+
+extern crate hyper;
+extern crate hyper_native_tls;
+extern crate serenity_oauth;
+extern crate rocket;
+
+use hyper::net::HttpsConnector;
+use hyper::Client as HyperClient;
+use hyper_native_tls::NativeTlsClient;
+use rocket::response::Redirect;
+use serenity_oauth::model::AccessTokenExchangeRequest;
+use serenity_oauth::{DiscordOAuthHyperRequester, Scope};
+use std::env;
+use std::error::Error;
+
+#[derive(Debug, FromForm)]
+struct Params {
+ code: String,
+}
+
+fn get_client_id() -> u64 {
+ env::var("DISCORD_CLIENT_ID")
+ .expect("No DISCORD_CLIENT_ID present")
+ .parse::<u64>()
+ .expect("Error parsing DISCORD_CLIENT_ID into u64")
+}
+
+fn get_client_secret() -> String {
+ env::var("DISCORD_CLIENT_SECRET")
+ .expect("No DISCORD_CLIENT_SECRET present")
+}
+
+#[get("/callback?<params>")]
+fn get_callback(params: Params) -> Result<String, Box<Error>> {
+ // Exchange the code for an access token.
+ let ssl = NativeTlsClient::new()?;
+ let connector = HttpsConnector::new(ssl);
+ let client = HyperClient::with_connector(connector);
+
+ let response = client.exchange_code(&AccessTokenExchangeRequest::new(
+ get_client_id(),
+ get_client_secret(),
+ params.code,
+ "http://localhost:8000/callback",
+ ))?;
+
+ Ok(format!("The user's access token is: {}", response.access_token))
+}
+
+#[get("/")]
+fn get_redirect() -> Redirect {
+ // Although this example does not use a state, you _should always_ use one
+ // in production for security purposes.
+ let url = serenity_oauth::utils::authorization_code_grant_url(
+ get_client_id(),
+ &[Scope::Identify],
+ None,
+ "http://localhost:8000/callback",
+ );
+
+ Redirect::to(&url)
+}
+
+fn main() {
+ rocket::ignite()
+ .mount("/", routes![
+ get_callback,
+ get_redirect,
+ ]).launch();
+}
diff --git a/src/bridge/hyper.rs b/src/bridge/hyper.rs
new file mode 100644
index 0000000..4a7eba2
--- /dev/null
+++ b/src/bridge/hyper.rs
@@ -0,0 +1,145 @@
+//! Bridged support for the `hyper` HTTP client.
+
+use hyper::client::{Body, Client as HyperClient};
+use hyper::header::ContentType;
+use serde_json;
+use serde_urlencoded;
+use ::constants::BASE_TOKEN_URI;
+use ::model::{
+ AccessTokenExchangeRequest,
+ AccessTokenResponse,
+ RefreshTokenRequest,
+};
+use ::Result;
+
+/// A trait used that implements methods for interacting with Discord's OAuth2
+/// API on Hyper's client.
+///
+/// # Examples
+///
+/// Bringing in the trait and creating a client. Since the trait is in scope,
+/// the instance of hyper's Client will have those methods available:
+///
+/// ```rust,no_run
+/// extern crate hyper;
+/// extern crate serenity_oauth;
+///
+/// # fn main() {
+/// use hyper::Client;
+///
+/// let client = Client::new();
+///
+/// // At this point, the methods defined by the trait are not in scope. By
+/// // using the trait, they will be.
+/// use serenity_oauth::DiscordOAuthHyperRequester;
+///
+/// // The methods defined by `DiscordOAuthHyperRequester` are now in scope and
+/// // implemented on the instance of hyper's `Client`.
+/// # }
+/// ```
+///
+/// For examples of how to use the trait with the Client, refer to the trait's
+/// methods.
+pub trait DiscordOAuthHyperRequester {
+ /// Exchanges a code for the user's access token.
+ ///
+ /// # Examples
+ ///
+ /// Exchange a code for an access token:
+ ///
+ /// ```rust,no_run
+ /// extern crate hyper;
+ /// extern crate serenity_oauth;
+ ///
+ /// # use std::error::Error;
+ /// #
+ /// # fn try_main() -> Result<(), Box<Error>> {
+ /// use hyper::Client;
+ /// use serenity_oauth::model::AccessTokenExchangeRequest;
+ /// use serenity_oauth::DiscordOAuthHyperRequester;
+ ///
+ /// let request_data = AccessTokenExchangeRequest::new(
+ /// 249608697955745802,
+ /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4",
+ /// "user code here",
+ /// "https://myapplication.website",
+ /// );
+ ///
+ /// let client = Client::new();
+ /// let response = client.exchange_code(&request_data)?;
+ ///
+ /// println!("Access token: {}", response.access_token);
+ /// # Ok(())
+ /// # }
+ /// #
+ /// # fn main() {
+ /// # try_main().unwrap();
+ /// # }
+ /// ```
+ fn exchange_code(&self, request: &AccessTokenExchangeRequest)
+ -> Result<AccessTokenResponse>;
+
+ /// Exchanges a refresh token, returning a new refresh token and fresh
+ /// access token.
+ ///
+ /// # Examples
+ ///
+ /// Exchange a refresh token:
+ ///
+ /// ```rust,no_run
+ /// extern crate hyper;
+ /// extern crate serenity_oauth;
+ ///
+ /// # use std::error::Error;
+ /// #
+ /// # fn try_main() -> Result<(), Box<Error>> {
+ /// use hyper::Client;
+ /// use serenity_oauth::model::RefreshTokenRequest;
+ /// use serenity_oauth::DiscordOAuthHyperRequester;
+ ///
+ /// let request_data = RefreshTokenRequest::new(
+ /// 249608697955745802,
+ /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4",
+ /// "user code here",
+ /// "https://myapplication.website",
+ /// );
+ ///
+ /// let client = Client::new();
+ /// let response = client.exchange_refresh_token(&request_data)?;
+ ///
+ /// println!("Fresh access token: {}", response.access_token);
+ /// # Ok(())
+ /// # }
+ /// #
+ /// # fn main() {
+ /// # try_main().unwrap();
+ /// # }
+ /// ```
+ fn exchange_refresh_token(&self, request: &RefreshTokenRequest)
+ -> Result<AccessTokenResponse>;
+}
+
+impl DiscordOAuthHyperRequester for HyperClient {
+ fn exchange_code(&self, request: &AccessTokenExchangeRequest)
+ -> Result<AccessTokenResponse> {
+ let body = serde_urlencoded::to_string(request)?;
+
+ let response = self.post(BASE_TOKEN_URI)
+ .header(ContentType::form_url_encoded())
+ .body(Body::BufBody(body.as_bytes(), body.len()))
+ .send()?;
+
+ serde_json::from_reader(response).map_err(From::from)
+ }
+
+ fn exchange_refresh_token(&self, request: &RefreshTokenRequest)
+ -> Result<AccessTokenResponse> {
+ let body = serde_json::to_string(request)?;
+
+ let response = self.post(BASE_TOKEN_URI)
+ .body(Body::BufBody(body.as_bytes(), body.len()))
+ .send()?;
+
+ serde_json::from_reader(response).map_err(From::from)
+ }
+}
diff --git a/src/bridge/mod.rs b/src/bridge/mod.rs
new file mode 100644
index 0000000..92be3da
--- /dev/null
+++ b/src/bridge/mod.rs
@@ -0,0 +1,6 @@
+//! A module containing bridges to HTTP clients.
+//!
+//! This contains traits implemented on HTTP clients, as well as oneshot
+//! functions that create one-off clients for ease of use.
+
+pub mod hyper;
diff --git a/src/constants.rs b/src/constants.rs
new file mode 100644
index 0000000..76e4e0c
--- /dev/null
+++ b/src/constants.rs
@@ -0,0 +1,9 @@
+//! A set of constants around the OAuth2 API.
+
+/// The base authorization URI, used for authorizing an application.
+pub const BASE_AUTHORIZE_URI: &str = "https://discordapp.com/api/oauth2/authorize";
+/// The revocation URL, used to revoke an access token.
+pub const BASE_REVOKE_URI: &str = "https://discordapp.com/api/oauth2/revoke";
+/// The token URI, used for exchanging a refresh token for a fresh access token
+/// and new refresh token.
+pub const BASE_TOKEN_URI: &str = "https://discordapp.com/api/oauth2/token";
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..3e6a66e
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,54 @@
+use hyper::Error as HyperError;
+use serde_json::Error as JsonError;
+use serde_urlencoded::ser::Error as UrlEncodeError;
+use std::error::Error as StdError;
+use std::fmt::{Display, Formatter, Result as FmtResult};
+use std::result::Result as StdResult;
+
+/// Result type used throughout the library's public result functions.
+pub type Result<T> = StdResult<T, Error>;
+
+/// Standard error enum used to wrap different potential error types.
+#[derive(Debug)]
+pub enum Error {
+ /// An error from the `hyper` crate.
+ Hyper(HyperError),
+ /// An error from the `serde_json` crate.
+ Json(JsonError),
+ /// An error from the `serde_urlencoded` crate.
+ UrlEncode(UrlEncodeError),
+}
+
+impl From<HyperError> for Error {
+ fn from(err: HyperError) -> Self {
+ Error::Hyper(err)
+ }
+}
+
+impl From<JsonError> for Error {
+ fn from(err: JsonError) -> Self {
+ Error::Json(err)
+ }
+}
+
+impl From<UrlEncodeError> for Error {
+ fn from(err: UrlEncodeError) -> Self {
+ Error::UrlEncode(err)
+ }
+}
+
+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::Hyper(ref inner) => inner.description(),
+ Error::Json(ref inner) => inner.description(),
+ Error::UrlEncode(ref inner) => inner.description(),
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..9fd0556
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,45 @@
+//! # serenity-oauth
+//!
+//! `serenity-oauth` is a collection of HTTP library support bridges for
+//! interacting with the OAuth2 API that Discord uses.
+//!
+//! It includes support for sending code exchange requests and refresh token
+//! requests.
+//!
+//! Included are models in the [`model`] directory that represent request bodies
+//! and response bodies. The [`Scope`] enum represents possible OAuth2 scopes
+//! that can be granted.
+//!
+//! In the [`utils`] module, functions to produce authorization URLs are
+//! available. For example, [`utils::bot_authorization_url`] can be used to
+//! produce a URL that can be used to redirect users to authorize an application
+//! with the [`Scope::Bot`] scope.
+//!
+//! [`Scope`]: enum.Scope.html
+//! [`Scope::Bot`]: enum.Scope.html#variant.Bot
+//! [`model`]: model/
+//! [`utils`]: utils/
+//! [`utils::bot_authorization_url`]: utils/fn.bot_authorization_url.html
+
+#![deny(missing_docs)]
+
+#[macro_use] extern crate serde_derive;
+
+extern crate hyper;
+extern crate percent_encoding;
+extern crate serde;
+extern crate serde_json;
+extern crate serde_urlencoded;
+extern crate serenity_model;
+
+pub mod bridge;
+pub mod constants;
+pub mod model;
+pub mod utils;
+
+mod error;
+mod scope;
+
+pub use bridge::hyper::DiscordOAuthHyperRequester;
+pub use error::{Error, Result};
+pub use scope::Scope;
diff --git a/src/model.rs b/src/model.rs
new file mode 100644
index 0000000..86e3005
--- /dev/null
+++ b/src/model.rs
@@ -0,0 +1,196 @@
+//! A collection of models that can be deserialized from response bodies and
+//! serialized into request bodies.
+
+use serenity_model::{PartialGuild, Webhook};
+
+/// Structure of data used as the body of a request to exchange the [`code`] for
+/// an access token.
+///
+/// [`code`]: #structfield.code
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AccessTokenExchangeRequest {
+ /// Your application's client ID.
+ pub client_id: u64,
+ /// Your application's client secret.
+ pub client_secret: String,
+ /// The code in the query parameters to your redirect URI.
+ pub code: String,
+ /// The type of grant.
+ ///
+ /// Must be set to `authorization_code`.
+ ///
+ /// If using [`AccessTokenExchangeRequest::new`], this will automatically be
+ /// set for you.
+ pub grant_type: String,
+ /// Your redirect URI.
+ pub redirect_uri: String,
+}
+
+impl AccessTokenExchangeRequest {
+ /// Creates a new request body for exchanging a code for an access token.
+ ///
+ /// # Examples
+ ///
+ /// Create a new request and assert that the grant type is correct:
+ ///
+ /// ```rust
+ /// use serenity_oauth::model::AccessTokenExchangeRequest;
+ ///
+ /// let request = AccessTokenExchangeRequest::new(
+ /// 249608697955745802,
+ /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4",
+ /// "user code here",
+ /// "https://myapplication.website",
+ /// );
+ ///
+ /// assert_eq!(request.grant_type, "authorization_code");
+ /// ```
+ pub fn new<S, T, U>(
+ client_id: u64,
+ client_secret: S,
+ code: T,
+ redirect_uri: U,
+ ) -> Self where S: Into<String>, T: Into<String>, U: Into<String> {
+ Self {
+ client_secret: client_secret.into(),
+ code: code.into(),
+ grant_type: "authorization_code".to_owned(),
+ redirect_uri: redirect_uri.into(),
+ client_id,
+ }
+ }
+}
+
+/// Response data containing a new access token and refresh token.
+///
+/// This can be received when either:
+///
+/// 1. exchanging a code for an access token;
+/// 2. exchanging a refresh token for a fresh access token.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AccessTokenResponse {
+ /// The user's access token.
+ pub access_token: String,
+ /// The number of seconds until the access token expires.
+ pub expires_in: u64,
+ /// The refresh token to use when the access token expires.
+ pub refresh_token: String,
+ /// The scope that is granted.
+ pub scope: String,
+ /// The type of token received.
+ pub token_type: String,
+}
+
+/// Response data containing an access token, but without a refresh token.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct ClientCredentialsAccessTokenResponse {
+ /// The user's access token.
+ pub access_token: String,
+ /// The number of seconds until the access token expires.
+ pub expires_in: u64,
+ /// The scope that is granted.
+ pub scope: String,
+ /// The type of token received.
+ pub token_type: String,
+}
+
+/// An extended [`Scope::Bot`] authorization flow.
+///
+/// This will authorize the application as a bot into a user's selected guild,
+/// as well as granting additional scopes.
+///
+/// [`Scope::Bot`]: ../enum.Scope.html#variant.Bot
+#[derive(Clone, Debug, Deserialize)]
+pub struct ExtendedBotAuthorizationResponse {
+ /// The user's access token.
+ pub access_token: String,
+ /// The number of seconds until the access token expires.
+ pub expires_in: u64,
+ /// Partial guild data that the application was authorized into.
+ pub guild: PartialGuild,
+ /// The refresh token to use when the access token expires.
+ pub refresh_token: String,
+ /// The scope that is granted.
+ pub scope: String,
+ /// The type of token received.
+ pub token_type: String,
+}
+
+/// Request for exchanging a refresh token for a new access token.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct RefreshTokenRequest {
+ /// Your application's client ID.
+ pub client_id: u64,
+ /// Your application's client secret.
+ pub client_secret: String,
+ /// The type of grant.
+ ///
+ /// Must be set to `refresh_token`.
+ ///
+ /// If using [`RefreshTokenRequest::new`], this will automatically be
+ /// set for you.
+ pub grant_type: String,
+ /// Your redirect URI.
+ pub redirect_uri: String,
+ /// The user's refresh token.
+ pub refresh_token: String,
+}
+
+impl RefreshTokenRequest {
+ /// Creates a new request body for refreshing an access token using a
+ /// refresh token.
+ ///
+ /// # Examples
+ ///
+ /// Create a new request and assert that the grant type is correct:
+ ///
+ /// ```rust
+ /// use serenity_oauth::model::RefreshTokenRequest;
+ ///
+ /// let request = RefreshTokenRequest::new(
+ /// 249608697955745802,
+ /// "dd99opUAgs7SQEtk2kdRrTMU5zagR2a4",
+ /// "user code here",
+ /// "https://myapplication.website",
+ /// );
+ ///
+ /// assert_eq!(request.grant_type, "refresh_token");
+ /// ```
+ pub fn new<S, T, U>(
+ client_id: u64,
+ client_secret: S,
+ redirect_uri: T,
+ refresh_token: U,
+ ) -> Self where S: Into<String>, T: Into<String>, U: Into<String> {
+ Self {
+ client_secret: client_secret.into(),
+ grant_type: "refresh_token".to_owned(),
+ redirect_uri: redirect_uri.into(),
+ refresh_token: refresh_token.into(),
+ client_id,
+ }
+ }
+}
+
+/// The response data from a successful trading of a code for an access token
+/// after authorization of [`Scope::WebhookIncoming`].
+///
+/// You should store [`webhook`]'s `id` and `token` structfields.
+///
+/// [`Scope::WebhookIncoming`]: ../enum.Scope.html#variant.WebhookIncoming
+/// [`webhook`]: #structfield.webhook
+#[derive(Clone, Debug, Deserialize)]
+pub struct WebhookTokenResponse {
+ /// The user's access token.
+ pub access_token: String,
+ /// The number of seconds until the access token expires.
+ pub expires_in: u64,
+ /// The refresh token to use when the access token expires.
+ pub refresh_token: String,
+ /// The scope that is granted.
+ pub scope: String,
+ /// The type of token received.
+ pub token_type: String,
+ /// Information about the webhook created.
+ pub webhook: Webhook,
+}
diff --git a/src/scope.rs b/src/scope.rs
new file mode 100644
index 0000000..8ff55fc
--- /dev/null
+++ b/src/scope.rs
@@ -0,0 +1,77 @@
+use std::fmt::{Display, Formatter, Result as FmtResult};
+
+/// A Discord OAuth2 scope that can be granted.
+///
+/// If you require a scope that is not registered here, use [`Scope::Other`] and
+/// notify the library developers about the missing scope.
+///
+/// **Note**: The [`Scope::Bot`] and [`Scope::GuildsJoin`] scopes require you to
+/// have a bot account linked to your application. Also, in order to add a user
+/// to a guild, your bot has to already belong in that guild.
+///
+/// [`Scope::Bot`]: #variant.Bot
+/// [`Scope::GuildsJoin`]: #variant.GuildsJoin
+/// [`Scope::Other`]: #variant.Other
+#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
+pub enum Scope {
+ /// For OAuth2 bots, this puts the bot in the user's selected guild by
+ /// default.
+ Bot,
+ /// Allows the `/users/@me/connections` API endpoint to return linked
+ /// third-party accounts.
+ Connections,
+ /// Enables the `/users/@me` API endpoint to return an `email` field.
+ Email,
+ /// Allows the `/users/@me` API endpoint, without the `email` field.
+ Identify,
+ /// Allows the `/users/@me/guilds` API endpoint to return basic information
+ /// about all of a user's guilds.
+ Guilds,
+ /// Allows the `/invites/{code}` API endpoint to be used for joining users
+ /// to a guild.
+ GuildsJoin,
+ /// Allows your application to join users to a group DM.
+ GdmJoin,
+ /// For local RPC server API access, this allows you to read messages from
+ /// all cliuent channels.
+ ///
+ /// This is otherwise restricted to channels/guilds your application
+ /// creates.
+ MessagesRead,
+ /// For local RPC server access, this allows you to control a user's local
+ /// Discord client.
+ Rpc,
+ /// For local RPC server API access, this allows you to access the API as
+ /// the local user.
+ RpcApi,
+ /// For local RPC server API access, this allows you to receive
+ /// notifications pushed out to the user.
+ RpcNotificationsRead,
+ /// This generates a webhook that is returned in the OAuth token response
+ /// for authorization code grants.
+ WebhookIncoming,
+ /// A scope that does not have a matching enum variant.
+ Other(String),
+}
+
+impl Display for Scope {
+ fn fmt(&self, f: &mut Formatter) -> FmtResult {
+ use self::Scope::*;
+
+ f.write_str(match *self {
+ Bot => "bot",
+ Connections => "connections",
+ Email => "email",
+ Identify => "identify",
+ Guilds => "guilds",
+ GuildsJoin => "guilds.join",
+ GdmJoin => "gdm.join",
+ MessagesRead => "messages.read",
+ Rpc => "rpc",
+ RpcApi => "rpc.api",
+ RpcNotificationsRead => "rpc.notifications.read",
+ WebhookIncoming => "webhook.incoming",
+ Other(ref inner) => inner,
+ })
+ }
+}
diff --git a/src/utils.rs b/src/utils.rs
new file mode 100644
index 0000000..e11a313
--- /dev/null
+++ b/src/utils.rs
@@ -0,0 +1,130 @@
+//! A collection of functions for use with the OAuth2 API.
+//!
+//! This includes functions for easily generating URLs to redirect users to for
+//! authorization.
+
+pub use serenity_model::Permissions;
+
+use constants::BASE_AUTHORIZE_URI;
+use percent_encoding;
+use super::Scope;
+use std::fmt::Write;
+
+/// Creates a URL for a simple bot authorization flow.
+///
+/// This is a special,
+/// server-less and callback-less OAuth2 flow that makes it
+/// easy for users to add bots to guilds.
+///
+/// # Examples
+///
+/// Create an authorization URL for a bot requiring the "Add Reactions" and
+/// "Send Messages" permissions:
+///
+/// ```rust
+/// extern crate serenity_model;
+/// extern crate serenity_oauth;
+///
+/// # fn main() {
+/// use serenity_model::Permissions;
+///
+/// let client_id = 249608697955745802;
+/// let required = Permissions::ADD_REACTIONS | Permissions::SEND_MESSAGES;
+/// let url = serenity_oauth::utils::bot_authorization_url(client_id, required);
+///
+/// // Assert that the expected URL is correct.
+/// let expected = "https://discordapp.com/api/oauth2/authorize?client_id=249608697955745802&scope=bot&permissions=2112";
+/// assert_eq!(url, expected);
+/// # }
+/// ```
+pub fn bot_authorization_url(client_id: u64, permissions: Permissions)
+ -> String {
+ format!(
+ "{}?client_id={}&scope=bot&permissions={}",
+ BASE_AUTHORIZE_URI,
+ client_id,
+ permissions.bits(),
+ )
+}
+
+/// Creates a URL for an authorization code grant.
+///
+/// This will create a URL to redirect the user to, requesting the given scopes
+/// for your client ID.
+///
+/// The given `redirect_uri` will automatically be URL encoded.
+///
+/// A state _should_ be passed, as recommended by RFC 6749. It is a unique
+/// identifier for the user's request. When Discord redirects the user to the
+/// given redirect URI, it will append a `state` parameter to the query. It will
+/// match the state that you have recorded for that user. If it does not, there
+/// was likely a request interception.
+///
+/// As well as the callback URL having the same `state` appended in the query
+/// parameters, this will also append a `code`.
+///
+/// # Examples
+///
+/// Produce an authorization code grant URL for your client, requiring the
+/// [`Scope::Identify`] and [`Scope::GuildsJoin`] scopes, and an example of a
+/// state:
+///
+/// **Note**: Please randomly generate this using a crate like `rand`.
+///
+/// ```rust
+/// use serenity_oauth::Scope;
+///
+/// let client_id = 249608697955745802;
+/// let scopes = [Scope::GuildsJoin, Scope::Identify];
+/// let state = "15773059ghq9183habn";
+/// let redirect_uri = "https://myapplication.website";
+///
+/// let url = serenity_oauth::utils::authorization_code_grant_url(
+/// client_id,
+/// &scopes,
+/// Some(state),
+/// redirect_uri,
+/// );
+///
+/// // Assert that the URL is correct.
+/// let expected = "https://discordapp.com/api/oauth2/authorize?response_type=code&client_id=249608697955745802&redirect_uri=https%3A%2F%2Fmyapplication.website&scope=guilds.join%20identify&state=15773059ghq9183habn";
+/// assert_eq!(url, expected);
+/// ```
+///
+/// [`Scope::GuildsJoin`]: enum.Scope.html#variant.GuildsJoin
+/// [`Scope::Identify`]: enum.Scope.html#variant.Identify
+pub fn authorization_code_grant_url(
+ client_id: u64,
+ scopes: &[Scope],
+ state: Option<&str>,
+ redirect_uri: &str,
+) -> String {
+ let mut base = String::from(BASE_AUTHORIZE_URI);
+ let uri = percent_encoding::percent_encode(
+ redirect_uri.as_bytes(),
+ percent_encoding::USERINFO_ENCODE_SET,
+ );
+
+ let _ = write!(
+ base,
+ "?response_type=code&client_id={}&redirect_uri={}&scope=",
+ client_id,
+ uri,
+ );
+
+ let scope_count = scopes.len();
+
+ for (i, scope) in scopes.iter().enumerate() {
+ let _ = write!(base, "{}", scope);
+
+ if i + 1 < scope_count {
+ base.push_str("%20");
+ }
+ }
+
+ if let Some(state) = state {
+ let _ = write!(base, "&state={}", state);
+ }
+
+ base
+}