Skip to content

Commit

Permalink
Add support for generation of invite links with custom OAuth2 scopes (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
vicky5124 authored and arqunis committed Jul 4, 2021
1 parent 695bbef commit 50cd285
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 18 deletions.
99 changes: 99 additions & 0 deletions src/builder/bot_auth_parameters.rs
@@ -0,0 +1,99 @@
use url::Url;

use crate::http::client::Http;
use crate::internal::prelude::*;
use crate::model::prelude::*;

/// A builder for constructing an invite link with custom OAuth2 scopes.
#[derive(Debug, Clone, Default)]
pub struct CreateBotAuthParameters {
client_id: UserId,
scopes: Vec<OAuth2Scope>,
permissions: Permissions,
guild_id: GuildId,
disable_guild_select: bool,
}

impl CreateBotAuthParameters {
/// Builds the url with the provided data.
pub fn build(self) -> String {
let mut valid_data = vec![];
let bits = self.permissions.bits();

if self.client_id.0 != 0 {
valid_data.push(("cliend_id", self.client_id.0.to_string()));
}

if !self.scopes.is_empty() {
valid_data.push((
"scope",
self.scopes.iter().map(|i| i.to_string()).collect::<Vec<_>>().join(" "),
));
}

if bits != 0 {
valid_data.push(("permissions", bits.to_string()));
}

if self.guild_id.0 != 0 {
valid_data.push(("guild", self.guild_id.0.to_string()));
}

if self.disable_guild_select {
valid_data.push(("disable_guild_select", self.disable_guild_select.to_string()));
}

let url = Url::parse_with_params("https://discord.com/api/oauth2/authorize", &valid_data)
.expect("failed to construct URL");

url.to_string()
}

/// Specify the client Id of your application.
pub fn client_id<U: Into<UserId>>(&mut self, client_id: U) -> &mut Self {
self.client_id = client_id.into();
self
}

/// Automatically fetch and set the client Id of your application by inquiring Discord's API.
///
/// # Errors
///
/// Returns an
/// [`HttpError::UnsuccessfulRequest(Unauthorized)`][`HttpError::UnsuccessfulRequest`]
/// If the user is not authorized for this endpoint.
///
/// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest
pub async fn auto_client_id(&mut self, http: impl AsRef<Http>) -> Result<&mut Self> {
self.client_id = http.as_ref().get_current_application_info().await.map(|v| v.id)?;
Ok(self)
}

/// Specify the scopes for your application.
///
/// **Note**: This needs to include the [`Bot`] scope.
///
/// [`Bot`]: crate::model::oauth2::OAuth2Scope::Bot
pub fn scopes(&mut self, scopes: &[OAuth2Scope]) -> &mut Self {
self.scopes = scopes.to_vec();
self
}

/// Specify the permissions your application requires.
pub fn permissions(&mut self, permissions: Permissions) -> &mut Self {
self.permissions = permissions;
self
}

/// Specify the Id of the guild to prefill the dropdown picker for the user.
pub fn guild_id<G: Into<GuildId>>(&mut self, guild_id: G) -> &mut Self {
self.guild_id = guild_id.into();
self
}

/// Specify whether the user cannot change the guild in the dropdown picker.
pub fn disable_guild_select(&mut self, disable: bool) -> &mut Self {
self.disable_guild_select = disable;
self
}
}
2 changes: 2 additions & 0 deletions src/builder/mod.rs
Expand Up @@ -15,6 +15,7 @@ mod create_application_command;
#[cfg_attr(docsrs, doc(cfg(feature = "unstable_discord_api")))]
mod create_application_command_permission;

mod bot_auth_parameters;
mod create_allowed_mentions;
#[cfg(feature = "unstable_discord_api")]
#[cfg_attr(docsrs, doc(cfg(feature = "unstable_discord_api")))]
Expand Down Expand Up @@ -47,6 +48,7 @@ mod execute_webhook;
mod get_messages;

pub use self::{
bot_auth_parameters::CreateBotAuthParameters,
create_allowed_mentions::CreateAllowedMentions,
create_allowed_mentions::ParseValue,
create_channel::CreateChannel,
Expand Down
5 changes: 4 additions & 1 deletion src/http/client.rs
Expand Up @@ -2834,7 +2834,10 @@ impl Http {

/// Gets the current user's third party connections.
///
/// This method only works for user tokens with the `connections` OAuth2 scope.
/// This method only works for user tokens with the
/// [`Connections`] OAuth2 scope.
///
/// [`Connections`]: crate::model::oauth2::OAuth2Scope::Connections
pub async fn get_user_connections(&self) -> Result<Vec<Connection>> {
self.fire(Request {
body: None,
Expand Down
10 changes: 3 additions & 7 deletions src/model/guild/mod.rs
Expand Up @@ -1649,13 +1649,9 @@ impl Guild {
.filter_map(|member| async move {
let name = &member.user.name;

if case_sensitive {
if name.contains(substring) {
Some((member, name.to_string()))
} else {
None
}
} else if contains_case_insensitive(name, substring) {
if (case_sensitive && name.contains(substring))
|| contains_case_insensitive(name, substring)
{
Some((member, name.to_string()))
} else {
None
Expand Down
1 change: 1 addition & 0 deletions src/model/mod.rs
Expand Up @@ -35,6 +35,7 @@ pub mod id;
pub mod interactions;
pub mod invite;
pub mod misc;
pub mod oauth2;
pub mod permissions;
pub mod prelude;
pub mod user;
Expand Down
85 changes: 85 additions & 0 deletions src/model/oauth2.rs
@@ -0,0 +1,85 @@
use std::fmt;

/// The available OAuth2 Scopes.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum OAuth2Scope {
/// For oauth2 bots, this puts the bot in the user's selected guild by default.
Bot,
/// Allows your app to use Slash Commands in a guild.
ApplicationsCommands,
/// Allows your app to update its Slash Commands via this bearer token - client credentials grant only.
ApplicationsCommandsUpdate,

/// Allows `/users/@me` without [`Self::Email`].
Identify,
/// Enables `/users/@me` to return an `email` field.
Email,
/// Allows `/users/@me/connections` to return linked third-party accounts.
Connections,
/// Allows `/users/@me/guilds` to return basic information about all of a user's guilds.
Guilds,
/// Allows `/guilds/{guild.id}/members/{user.id}` to be used for joining users to a guild.
GuildsJoin,
/// Allows your app to join users to a group dm.
GdmJoin,
/// For local rpc server access, this allows you to control a user's local Discord client -
/// requires Discord approval.
Rpc,
/// For local rpc server api access, this allows you to receive notifications pushed out to the user - requires Discord approval.
RpcNotificationsRead,
RpcVoiceRead,
RpcVoiceWrite,
RpcActivitiesWrite,
/// This generates a webhook that is returned in the oauth token response for authorization code grants.
WebhookIncomming,
/// For local rpc server api access, this allows you to read messages from all client channels
/// (otherwise restricted to channels/guilds your app creates).
MessagesRead,
/// Allows your app to upload/update builds for a user's applications - requires Discord approval.
ApplicationsBuildsUpload,
/// Allows your app to read build data for a user's applications.
ApplicationsBuildsRead,
/// Allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications.
ApplicationsStoreUpdate,
/// Allows your app to read entitlements for a user's applications.
ApplicationsEntitlements,
/// Allows your app to fetch data from a user's "Now Playing/Recently Played" list - requires Discord approval.
ActivitiesRead,
/// allows your app to update a user's activity - requires Discord approval (Not required for gamesdk activity manager!).
ActivitiesWrite,
/// Allows your app to know a user's friends and implicit relationships - requires Discord approval.
RelactionshipsRead,
}

impl fmt::Display for OAuth2Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let val = match self {
Self::Bot => "bot",
Self::ApplicationsCommands => "applications.commands",
Self::ApplicationsCommandsUpdate => "applications.commands.update",
Self::Identify => "identify",
Self::Email => "email",
Self::Connections => "connections",
Self::Guilds => "guilds",
Self::GuildsJoin => "guilds.join",
Self::GdmJoin => "gdm.join",
Self::Rpc => "rpc",
Self::RpcNotificationsRead => "rpc.notifications.read",
Self::RpcVoiceRead => "rpc.voice.read",
Self::RpcVoiceWrite => "rpc.voice.write",
Self::RpcActivitiesWrite => "rpc.activities.write",
Self::WebhookIncomming => "webhook.incoming",
Self::MessagesRead => "messages.read",
Self::ApplicationsBuildsUpload => "applications.builds.upload",
Self::ApplicationsBuildsRead => "applications.builds.read",
Self::ApplicationsStoreUpdate => "applications.store.update",
Self::ApplicationsEntitlements => "applications.entitlements",
Self::ActivitiesRead => "activities.read",
Self::ActivitiesWrite => "activities.write",
Self::RelactionshipsRead => "relationships.read",
};

write!(f, "{}", val)
}
}
1 change: 1 addition & 0 deletions src/model/prelude.rs
Expand Up @@ -22,6 +22,7 @@ pub use super::id::*;
pub use super::interactions::*;
pub use super::invite::*;
pub use super::misc::*;
pub use super::oauth2::*;
pub use super::permissions::*;
pub use super::user::*;
pub use super::voice::*;
Expand Down
69 changes: 59 additions & 10 deletions src/model/user.rs
Expand Up @@ -13,7 +13,7 @@ use serde_json::json;
use super::prelude::*;
use super::utils::deserialize_u16;
#[cfg(feature = "model")]
use crate::builder::{CreateMessage, EditProfile};
use crate::builder::{CreateBotAuthParameters, CreateMessage, EditProfile};
#[cfg(all(feature = "cache", feature = "model"))]
use crate::cache::Cache;
#[cfg(feature = "collector")]
Expand Down Expand Up @@ -211,6 +211,9 @@ impl CurrentUser {
///
/// If the permissions passed are empty, the permissions part will be dropped.
///
/// Only the `bot` scope is used, if you wish to use more, such as slash commands, see
/// [`Self::invite_url_with_oauth2_scopes`]
///
/// # Examples
///
/// Get the invite url with no permissions set:
Expand Down Expand Up @@ -273,25 +276,71 @@ impl CurrentUser {
/// [`HttpError::UnsuccessfulRequest(Unauthorized)`][`HttpError::UnsuccessfulRequest`]
/// If the user is not authorized for this end point.
///
/// May return [`Error::Format`] while writing url to the buffer.
/// Should never return [`Error::Url`] as all the data is controlled over.
///
/// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest
pub async fn invite_url(
&self,
http: impl AsRef<Http>,
permissions: Permissions,
) -> Result<String> {
let bits = permissions.bits();
let client_id = http.as_ref().get_current_application_info().await.map(|v| v.id)?;
self.invite_url_with_oauth2_scopes(http, permissions, &[OAuth2Scope::Bot]).await
}

let mut url =
format!("https://discord.com/api/oauth2/authorize?client_id={}&scope=bot", client_id);
/// Generate an invite url, but with custom scopes.
///
/// # Examples
///
/// Get the invite url with no permissions set and slash commands support:
///
/// ```rust,no_run
/// # use serenity::http::Http;
/// # use serenity::model::user::CurrentUser;
/// #
/// # async fn run() {
/// # let user = CurrentUser::default();
/// # let http = Http::default();
/// use serenity::model::Permissions;
/// use serenity::model::oauth2::OAuth2Scope;
///
/// let scopes = vec![OAuth2Scope::Bot, OAuth2Scope::ApplicationsCommands];
///
/// // assuming the user has been bound
/// let url = match user.invite_url_with_oauth2_scopes(&http, Permissions::empty(), &scopes).await {
/// Ok(v) => v,
/// Err(why) => {
/// println!("Error getting invite url: {:?}", why);
///
/// return;
/// },
/// };
///
/// assert_eq!(url, "https://discordapp.com/api/oauth2/authorize? \
/// client_id=249608697955745802&scope=bot%20applications.commands");
/// # }
/// ```
/// # Errors
///
/// Returns an
/// [`HttpError::UnsuccessfulRequest(Unauthorized)`][`HttpError::UnsuccessfulRequest`]
/// If the user is not authorized for this end point.
///
/// Should never return [`Error::Url`] as all the data is controlled over.
///
/// [`HttpError::UnsuccessfulRequest`]: crate::http::HttpError::UnsuccessfulRequest
pub async fn invite_url_with_oauth2_scopes(
&self,
http: impl AsRef<Http>,
permissions: Permissions,
scopes: &[OAuth2Scope],
) -> Result<String> {
let mut builder = CreateBotAuthParameters::default();

if bits != 0 {
write!(url, "&permissions={}", bits)?;
}
builder.permissions(permissions);
builder.auto_client_id(http).await?;
builder.scopes(scopes);

Ok(url)
Ok(builder.build())
}

/// Returns a static formatted URL of the user's icon, if one exists.
Expand Down

0 comments on commit 50cd285

Please sign in to comment.