From d946bdc47c977e165bd7fde5157974e4380c1dfc Mon Sep 17 00:00:00 2001 From: x3rt <51767230+x3rt@users.noreply.github.com> Date: Wed, 26 Apr 2023 18:19:56 -0600 Subject: [PATCH] Initial release --- .gitignore | 6 + DiscordOAuth.cs | 217 +++++++++++++++++++++++++++ Entities/DiscordConnection.cs | 48 ++++++ Entities/DiscordGuild.cs | 24 +++ Entities/DiscordUser.cs | 38 +++++ Entities/Enums/GuildFeature.cs | 266 +++++++++++++++++++++++++++++++++ Entities/Enums/OAuthScope.cs | 134 +++++++++++++++++ Entities/Enums/PremiumType.cs | 9 ++ Entities/Enums/UserFlag.cs | 21 +++ Entities/GuildFeatures.cs | 13 ++ Entities/OAuthToken.cs | 16 ++ Options/GuildOptions.cs | 69 +++++++++ README.md | 42 ++++++ ScopesBuilder.cs | 89 +++++++++++ x3rt.DiscordOAuth2.csproj | 20 +++ x3rt.DiscordOAuth2.sln | 15 ++ 16 files changed, 1027 insertions(+) create mode 100644 .gitignore create mode 100644 DiscordOAuth.cs create mode 100644 Entities/DiscordConnection.cs create mode 100644 Entities/DiscordGuild.cs create mode 100644 Entities/DiscordUser.cs create mode 100644 Entities/Enums/GuildFeature.cs create mode 100644 Entities/Enums/OAuthScope.cs create mode 100644 Entities/Enums/PremiumType.cs create mode 100644 Entities/Enums/UserFlag.cs create mode 100644 Entities/GuildFeatures.cs create mode 100644 Entities/OAuthToken.cs create mode 100644 Options/GuildOptions.cs create mode 100644 README.md create mode 100644 ScopesBuilder.cs create mode 100644 x3rt.DiscordOAuth2.csproj create mode 100644 x3rt.DiscordOAuth2.sln diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91953df --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +.idea \ No newline at end of file diff --git a/DiscordOAuth.cs b/DiscordOAuth.cs new file mode 100644 index 0000000..cf9ac3f --- /dev/null +++ b/DiscordOAuth.cs @@ -0,0 +1,217 @@ +using System.Collections.Specialized; +using System.Net.Http.Headers; +using System.Text; +using System.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; +using x3rt.DiscordOAuth2.Entities; +using x3rt.DiscordOAuth2.Options; + +namespace x3rt.DiscordOAuth2; + +public class DiscordOAuth +{ + private static ulong ClientId { get; set; } + private static string ClientSecret { get; set; } = string.Empty; + private static string? BotToken { get; set; } + + private string RedirectUri { get; set; } + private bool Prompt { get; set; } + private ScopesBuilder Scopes { get; set; } + + private string? AccessToken { get; set; } + + public static void Configure(ulong clientId, string clientSecret, string? botToken = null) + { + ClientId = clientId; + ClientSecret = clientSecret; + BotToken = botToken; + } + + private readonly HttpClient _httpClient = new HttpClient(); + + public DiscordOAuth(string redirectUri, ScopesBuilder scopes, bool prompt = true) + { + RedirectUri = redirectUri; + Scopes = scopes; + Prompt = prompt; + } + + public string GetAuthorizationUrl(string state) + { + NameValueCollection query = HttpUtility.ParseQueryString(string.Empty); + query["client_id"] = ClientId.ToString(); + query["redirect_uri"] = RedirectUri; + query["response_type"] = "code"; + query["scope"] = Scopes.ToString(); + query["state"] = state; + query["prompt"] = Prompt ? "consent" : "none"; + + var uriBuilder = new UriBuilder("https://discord.com/api/oauth2/authorize") + { + Query = query.ToString() + }; + + return uriBuilder.ToString(); + } + + public static bool TryGetCode(HttpRequest request, out string? code) + { + code = null; + if (request.Query.TryGetValue("code", out StringValues codeValues)) + { + code = codeValues[0]; + return true; + } + + return false; + } + + public static bool TryGetCode(HttpContext context, out string? code) + { + var b = TryGetCode(context.Request, out var a); + code = a; + return b; + } + + public async Task GetTokenAsync(string code) + { + var content = new FormUrlEncodedContent(new Dictionary + { + { "client_id", ClientId.ToString() }, + { "client_secret", ClientSecret }, + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", RedirectUri }, + { "scope", Scopes.ToString() } + }); + + var response = await _httpClient.PostAsync("https://discord.com/api/oauth2/token", content); + var responseString = await response.Content.ReadAsStringAsync(); + var authToken = JsonConvert.DeserializeObject(responseString); + AccessToken = authToken?.AccessToken; + return authToken; + } + + private async Task GetInformationAsync(string accessToken, string endpoint) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + var response = await _httpClient.GetAsync($"https://discord.com/api/{endpoint}"); + var responseString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseString) ?? default!; + } + + private async Task GetInformationAsync(HttpContext context, string endpoint) + { + if (AccessToken is null) + { + if (!TryGetCode(context, out var code)) return default; + var accessToken = await GetTokenAsync(code!); + if (accessToken is null) return default; + return await GetInformationAsync(accessToken.AccessToken, endpoint); + } + else + { + return await GetInformationAsync(AccessToken, endpoint); + } + } + + private async Task GetInformationAsync(OAuthToken token, string endpoint) + { + return await GetInformationAsync(token.AccessToken, endpoint); + } + + public async Task GetUserAsync(string accessToken) + { + return await GetInformationAsync(accessToken, "users/@me"); + } + + public async Task GetUserAsync(HttpContext context) + { + return await GetInformationAsync(context, "users/@me"); + } + + public async Task GetUserAsync(OAuthToken token) + { + return await GetInformationAsync(token, "users/@me"); + } + + public async Task GetGuildsAsync(string accessToken) + { + return await GetInformationAsync(accessToken, "users/@me/guilds"); + } + + public async Task GetGuildsAsync(HttpContext context) + { + return await GetInformationAsync(context, "users/@me/guilds"); + } + + public async Task GetGuildsAsync(OAuthToken token) + { + return await GetInformationAsync(token, "users/@me/guilds"); + } + + public async Task GetConnectionsAsync(string accessToken) + { + return await GetInformationAsync(accessToken, "users/@me/connections"); + } + + public async Task GetConnectionsAsync(HttpContext context) + { + return await GetInformationAsync(context, "users/@me/connections"); + } + + public async Task GetConnectionsAsync(OAuthToken token) + { + return await GetInformationAsync(token, "users/@me/connections"); + } + + public async Task JoinGuildAsync(string accessToken, ulong userId, GuildOptions options) + { + var request = + new HttpRequestMessage(HttpMethod.Put, + $"https://discord.com/api/guilds/{options.GuildId}/members/{userId}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bot", BotToken); + + var content = new Dictionary + { + ["access_token"] = accessToken + }; + if (options.Nickname is not null) content["nick"] = options.Nickname; + if (options.RoleIds is not null) content["roles"] = options.RoleIds; + if (options.Muted is not null) content["mute"] = options.Muted; + if (options.Deafened is not null) content["deaf"] = options.Deafened; + + var json = JsonConvert.SerializeObject(content); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await _httpClient.SendAsync(request); + return response.IsSuccessStatusCode; + } + + public async Task JoinGuildAsync(HttpContext context, GuildOptions options) + { + if (AccessToken is null) + { + if (!TryGetCode(context, out var code)) return false; + var accessToken = await GetTokenAsync(code!); + if (accessToken is null) return false; + var user = await GetUserAsync(accessToken.AccessToken); + if (user is null) return false; + return await JoinGuildAsync(accessToken.AccessToken, user.Id, options); + } + else + { + var user = await GetUserAsync(AccessToken); + if (user is null) return false; + return await JoinGuildAsync(AccessToken, user.Id, options); + } + } + + public async Task JoinGuildAsync(OAuthToken token, GuildOptions options) + { + var user = await GetUserAsync(token.AccessToken); + if (user is null) return false; + return await JoinGuildAsync(token.AccessToken, user.Id, options); + } +} \ No newline at end of file diff --git a/Entities/DiscordConnection.cs b/Entities/DiscordConnection.cs new file mode 100644 index 0000000..47e80ce --- /dev/null +++ b/Entities/DiscordConnection.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace x3rt.DiscordOAuth2.Entities; + +public class DiscordConnection +{ + [JsonProperty("id")] public string Id { get; set; } + [JsonProperty("name")] public string Name { get; set; } + [JsonProperty("type")] public ConnectionType Type { get; set; } + [JsonProperty("revoked")] public bool? Revoked { get; set; } + [JsonProperty("integrations")] public object[] Integrations { get; set; } + [JsonProperty("verified")] public bool? Verified { get; set; } + [JsonProperty("friend_sync")] public bool? FriendSync { get; set; } + [JsonProperty("show_activity")] public bool? ShowActivity { get; set; } + [JsonProperty("two_way_link")] public bool? TwoWayLink { get; set; } + [JsonProperty("visibility")] public ConnectionVisibility? Visibility { get; set; } + + + public enum ConnectionVisibility + { + None, + Everyone + } + + + public enum ConnectionType + { + BattleNet, + Ebay, + EpicGames, + Facebook, + GitHub, + Instagram, + LeagueOfLegends, + PayPal, + PlayStation, + Reddit, + RiotGames, + Spotify, + Skype, + Steam, + TikTok, + Twitch, + Twitter, + Xbox, + YouTube + } +} \ No newline at end of file diff --git a/Entities/DiscordGuild.cs b/Entities/DiscordGuild.cs new file mode 100644 index 0000000..0060d1f --- /dev/null +++ b/Entities/DiscordGuild.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; + +namespace x3rt.DiscordOAuth2.Entities; + +public class DiscordGuild +{ + [JsonProperty("id")] public ulong Id { get; set; } + + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("icon")] public string? Icon { get; set; } + + [JsonProperty("owner")] public bool Owner { get; set; } + + [JsonProperty("permissions")] public string Permissions { get; set; } + + [JsonProperty("features")] public GuildFeatures Features { get; set; } + + public override string ToString() + { + return + $"Id: {Id}; Name: {Name}; Icon: {Icon}; Owner: {Owner}; Permissions: {Permissions}; Features: {Features}"; + } +} \ No newline at end of file diff --git a/Entities/DiscordUser.cs b/Entities/DiscordUser.cs new file mode 100644 index 0000000..3ae62dd --- /dev/null +++ b/Entities/DiscordUser.cs @@ -0,0 +1,38 @@ +using x3rt.DiscordOAuth2.Entities.Enums; + +namespace x3rt.DiscordOAuth2.Entities; + +public class DiscordUser +{ + public ulong Id { get; set; } + public string Username { get; set; } + public string Discriminator { get; set; } + public string? Avatar { get; set; } + public bool? Bot { get; set; } + public bool? System { get; set; } + public bool? MfaEnabled { get; set; } + public string? Banner { get; set; } + public int? AccentColor { get; set; } + public string? Locale { get; set; } + public bool? Verified { get; set; } + public string? Email { get; set; } + public UserFlag? Flags { get; set; } + public PremiumType? PremiumType { get; set; } + public UserFlag? PublicFlags { get; set; } + + public override string ToString() + { + string result = ""; + foreach (var property in GetType().GetProperties()) + { + var value = property.GetValue(this); + if (value is not null) + { + result += $"{property.Name}: {value}; "; + } + } + + result = result.TrimEnd(' ', ';'); + return result; + } +} \ No newline at end of file diff --git a/Entities/Enums/GuildFeature.cs b/Entities/Enums/GuildFeature.cs new file mode 100644 index 0000000..32cca01 --- /dev/null +++ b/Entities/Enums/GuildFeature.cs @@ -0,0 +1,266 @@ +namespace x3rt.DiscordOAuth2.Entities.Enums; + +[Flags] +// Credit: Discord.Net +public enum GuildFeature : long +{ + /// + /// The guild has no features. + /// + None = 0L, + + /// + /// The guild has access to animated banners. + /// + AnimatedBanner = 1L << 0, + + /// + /// The guild has access to set an animated guild icon. + /// + AnimatedIcon = 1L << 1, + + /// + /// The guild has access to set a guild banner image. + /// + Banner = 1L << 2, + + /// + /// The guild has access to channel banners. + /// + ChannelBanner = 1L << 3, + + /// + /// The guild has access to use commerce features (i.e. create store channels). + /// + Commerce = 1L << 4, + + /// + /// The guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates. + /// + /// + /// This feature is mutable. + /// + Community = 1L << 5, + + /// + /// The guild is able to be discovered in the directory. + /// + /// + /// This feature is mutable. + /// + Discoverable = 1L << 6, + + /// + /// The guild has discoverable disabled. + /// + DiscoverableDisabled = 1L << 7, + + /// + /// The guild has enabled discoverable before. + /// + EnabledDiscoverableBefore = 1L << 8, + + /// + /// The guild is able to be featured in the directory. + /// + Featureable = 1L << 9, + + /// + /// The guild has a force relay. + /// + ForceRelay = 1L << 10, + + /// + /// The guild has a directory entry. + /// + HasDirectoryEntry = 1L << 11, + + /// + /// The guild is a hub. + /// + Hub = 1L << 12, + + /// + /// You shouldn't be here... + /// + InternalEmployeeOnly = 1L << 13, + + /// + /// The guild has access to set an invite splash background. + /// + InviteSplash = 1L << 14, + + /// + /// The guild is linked to a hub. + /// + LinkedToHub = 1L << 15, + + /// + /// The guild has member profiles. + /// + MemberProfiles = 1L << 16, + + /// + /// The guild has enabled Membership Screening. + /// + MemberVerificationGateEnabled = 1L << 17, + + /// + /// The guild has enabled monetization. + /// + MonetizationEnabled = 1L << 18, + + /// + /// The guild has more emojis. + /// + MoreEmoji = 1L << 19, + + /// + /// The guild has increased custom sticker slots. + /// + MoreStickers = 1L << 20, + + /// + /// The guild has access to create news channels. + /// + News = 1L << 21, + + /// + /// The guild has new thread permissions. + /// + NewThreadPermissions = 1L << 22, + + /// + /// The guild is partnered. + /// + Partnered = 1L << 23, + + /// + /// The guild has a premium tier three override; guilds made by Discord usually have this. + /// + PremiumTier3Override = 1L << 24, + + /// + /// The guild can be previewed before joining via Membership Screening or the directory. + /// + PreviewEnabled = 1L << 25, + + /// + /// The guild has access to create private threads. + /// + PrivateThreads = 1L << 26, + + /// + /// The guild has relay enabled. + /// + RelayEnabled = 1L << 27, + + /// + /// The guild is able to set role icons. + /// + RoleIcons = 1L << 28, + + /// + /// The guild has role subscriptions available for purchase. + /// + RoleSubscriptionsAvailableForPurchase = 1L << 29, + + /// + /// The guild has role subscriptions enabled. + /// + RoleSubscriptionsEnabled = 1L << 30, + + /// + /// The guild has access to the seven day archive time for threads. + /// + SevenDayThreadArchive = 1L << 31, + + /// + /// The guild has text in voice enabled. + /// + TextInVoiceEnabled = 1L << 32, + + /// + /// The guild has threads enabled. + /// + ThreadsEnabled = 1L << 33, + + /// + /// The guild has testing threads enabled. + /// + ThreadsEnabledTesting = 1L << 34, + + /// + /// The guild has the default thread auto archive. + /// + ThreadsDefaultAutoArchiveDuration = 1L << 35, + + /// + /// The guild has access to the three day archive time for threads. + /// + ThreeDayThreadArchive = 1L << 36, + + /// + /// The guild has enabled ticketed events. + /// + TicketedEventsEnabled = 1L << 37, + + /// + /// The guild has access to set a vanity URL. + /// + VanityUrl = 1L << 38, + + /// + /// The guild is verified. + /// + Verified = 1L << 39, + + /// + /// The guild has access to set 384kbps bitrate in voice (previously VIP voice servers). + /// + VIPRegions = 1L << 40, + + /// + /// The guild has enabled the welcome screen. + /// + WelcomeScreenEnabled = 1L << 41, + + /// + /// The guild has been set as a support server on the App Directory. + /// + DeveloperSupportServer = 1L << 42, + + /// + /// The guild has invites disabled. + /// + /// + /// This feature is mutable. + /// + InvitesDisabled = 1L << 43, + + /// + /// The guild has auto moderation enabled. + /// + AutoModeration = 1L << 44, + + /// + /// This guild has alerts for join raids disabled. + /// + /// + /// This feature is mutable. + /// + RaidAlertsDisabled = 1L << 45, + + /// + /// This guild has Clyde AI enabled. + /// + /// + /// This feature is mutable. + /// + ClydeEnabled = 1L << 46, + + /// + /// This guild has a guild web page vanity url. + /// + GuildWebPageVanityUrl = 1L << 47 +} \ No newline at end of file diff --git a/Entities/Enums/OAuthScope.cs b/Entities/Enums/OAuthScope.cs new file mode 100644 index 0000000..2d37eb5 --- /dev/null +++ b/Entities/Enums/OAuthScope.cs @@ -0,0 +1,134 @@ +namespace x3rt.DiscordOAuth2.Entities.Enums; + +/// +/// Represents the OAuth2 scopes available for a Discord application. +/// +///

+/// Credit to DSharpPlus +///
+///
+public enum OAuthScope +{ + /// + /// Allows /users/@me without email. + /// + Identify, + + /// + /// Enables /users/@me to return email. + /// + 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 into a guild. + /// + GuildsJoin, + + /// + /// Allows /users/@me/guilds/{guild.id}/members to return a user's member information in a guild. + /// + GuildsMembersRead, + + /// Allows your app to join users into a group DM. + GdmJoin, + + /// + /// For local RPC server access, this allows you to control a user's local Discord client. + /// + /// This scope requires Discord approval. + Rpc, + + /// + /// For local RPC server access, this allows you to receive notifications pushed to the user. + /// + /// This scope requires Discord approval. + RpcNotificationsRead, + + /// + /// For local RPC server access, this allows you to read a user's voice settings and listen for voice events. + /// + /// This scope requires Discord approval. + RpcVoiceRead, + + /// + /// For local RPC server access, this allows you to update a user's voice settings. + /// + /// This scope requires Discord approval. + RpcVoiceWrite, + + /// + /// For local RPC server access, this allows you to update a user's activity. + /// + /// This scope requires Discord approval. + RpcActivitiesWrite, + + /// + /// For OAuth2 bots, this puts the bot in the user's selected guild by default. + /// + Bot, + + /// + /// This generates a webhook that is returned in the OAuth token response for authorization code grants. + /// + WebhookIncoming, + + /// + /// For local RPC server access, this allows you to read messages from all client channels + /// (otherwise restricted to channels/guilds your application creates). + /// + MessagesRead, + + /// + /// Allows your application to upload/update builds for a user's applications. + /// + /// This scope requires Discord approval. + ApplicationsBuildsUpload, + + /// + /// Allows your application to read build data for a user's applications. + /// + ApplicationsBuildsRead, + + /// + /// Allows your application to use application commands in a guild. + /// + ApplicationsCommands, + + /// + /// Allows your application to read and update store data (SKUs, store listings, achievements etc.) for a user's applications. + /// + ApplicationsStoreUpdate, + + /// + /// Allows your application to read entitlements for a user's applications. + /// + ApplicationsEntitlements, + + /// + /// Allows your application to fetch data from a user's "Now Playing/Recently Played" list. + /// + /// This scope requires Discord approval. + ActivitiesRead, + + /// Allows your application to update a user's activity. + /// + /// Outside of the GameSDK activity manager, this scope requires Discord approval. + /// + ActivitiesWrite, + + /// + /// Allows your application to know a user's friends and implicit relationships. + /// + /// This scope requires Discord approval. + RelationshipsRead, +} \ No newline at end of file diff --git a/Entities/Enums/PremiumType.cs b/Entities/Enums/PremiumType.cs new file mode 100644 index 0000000..3e0cfb5 --- /dev/null +++ b/Entities/Enums/PremiumType.cs @@ -0,0 +1,9 @@ +namespace x3rt.DiscordOAuth2.Entities.Enums; + +public enum PremiumType +{ + None = 0, + NitroClassic = 1, + Nitro = 2, + NitroBasic = 3 +} \ No newline at end of file diff --git a/Entities/Enums/UserFlag.cs b/Entities/Enums/UserFlag.cs new file mode 100644 index 0000000..6f2cb9f --- /dev/null +++ b/Entities/Enums/UserFlag.cs @@ -0,0 +1,21 @@ +namespace x3rt.DiscordOAuth2.Entities.Enums; + +[Flags] +public enum UserFlag : ulong +{ + Staff = 1 << 0, + Partner = 1 << 1, + HypeSquad = 1 << 2, + BugHunterLevel1 = 1 << 3, + HouseBraveryMember = 1 << 6, + HouseBrillianceMember = 1 << 7, + HouseBalanceMember = 1 << 8, + EarlyNitroSupporter = 1 << 9, + TeamPseudoUser = 1 << 10, + BugHunterLevel2 = 1 << 14, + VerifiedBot = 1 << 16, + VerifiedDeveloper = 1 << 17, + CertifiedModerator = 1 << 18, + BotHttpInteractions = 1 << 19, + ActiveDeveloper = 1 << 22 +} \ No newline at end of file diff --git a/Entities/GuildFeatures.cs b/Entities/GuildFeatures.cs new file mode 100644 index 0000000..92b154f --- /dev/null +++ b/Entities/GuildFeatures.cs @@ -0,0 +1,13 @@ +using x3rt.DiscordOAuth2.Entities.Enums; + +namespace x3rt.DiscordOAuth2.Entities; + +public record GuildFeatures +{ + IEnumerable Features { get; set; } + + public override string ToString() + { + return string.Join(", ", Features); + } +} \ No newline at end of file diff --git a/Entities/OAuthToken.cs b/Entities/OAuthToken.cs new file mode 100644 index 0000000..9252d8a --- /dev/null +++ b/Entities/OAuthToken.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace x3rt.DiscordOAuth2.Entities; + +public class OAuthToken +{ + [JsonProperty("access_token")] public string AccessToken { get; set; } + + [JsonProperty("expires_in")] public int ExpiresIn { get; set; } + + [JsonProperty("refresh_token")] public string RefreshToken { get; set; } + + [JsonProperty("scope")] public string Scope { get; set; } + + [JsonProperty("token_type")] public string TokenType { get; set; } +} \ No newline at end of file diff --git a/Options/GuildOptions.cs b/Options/GuildOptions.cs new file mode 100644 index 0000000..3571b34 --- /dev/null +++ b/Options/GuildOptions.cs @@ -0,0 +1,69 @@ +namespace x3rt.DiscordOAuth2.Options; + +public class GuildOptions +{ + public ulong GuildId { get; set; } + public string? Nickname { get; set; } + public IEnumerable? RoleIds { get; set; } + public bool? Muted { get; set; } + public bool? Deafened { get; set; } + + public GuildOptions(ulong guildId, string? nickname = null, IEnumerable? roleIds = null, bool? mute = null, bool? deafened = null) + { + GuildId = guildId; + Nickname = nickname; + RoleIds = roleIds; + Muted = mute; + Deafened = deafened; + } + + public GuildOptions Mute(bool mute = true) + { + Muted = mute; + return this; + } + + public GuildOptions Deafen(bool deafen = true) + { + Deafened = deafen; + return this; + } + + public GuildOptions WithNickname(string nickname) + { + Nickname = nickname; + return this; + } + + public GuildOptions WithRoleIds(params ulong[] roleIds) + { + RoleIds = roleIds; + return this; + } + + public GuildOptions WithRoleIds(IEnumerable roleIds) + { + RoleIds = roleIds.ToArray(); + return this; + } + + public GuildOptions WithRoleId(ulong roleId) + { + if (RoleIds is null) + { + RoleIds = new[] { roleId }; + } + else + { + RoleIds = RoleIds.Append(roleId); + } + + return this; + } + + + public override string ToString() + { + return $"GuildId: {GuildId}; Nickname: {Nickname}; RoleIds: {RoleIds}"; + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae0ebba --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# x3rt.DiscordOAuth2 + +[![NuGet](https://img.shields.io/nuget/v/x3rt.DiscordOAuth2.svg)](https://www.nuget.org/packages/x3rt.DiscordOAuth2/) +[![NuGet](https://img.shields.io/nuget/dt/x3rt.DiscordOAuth2.svg)](https://www.nuget.org/packages/x3rt.DiscordOAuth2/) + +A **simple** library to handle Discord OAuth2 authentication. +Meant to serve as an alternative to +the [AspNet.Security.OAuth.Providers](https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers) library that +gives more control to the user. + +## Usage + +```csharp +// Configure OAuth ClientID and ClientSecret globally +DiscordOAuth.Configure(0123456789, "ClientSecret", "OptionalBotToken"); +``` +```csharp +var scopes = new ScopesBuilder(OAuthScope.Identify); +var oAuth = new DiscordOAuth("https://example.com/Login", scopes); +var url = oAuth.GetAuthorizationUrl(); +/* Redirect user to url via preferred method */ +``` +```csharp +// Your callback method +if (DiscordOAuth.TryGetCode(HttpContext, out var code)) +{ + var scopes = new ScopesBuilder(OAuthScope.Identify); + var oAuth = new DiscordOAuth("https://example.com/Login", scopes); + var token = await oAuth.GetTokenAsync(code); + var user = await oAuth.GetUserAsync(token); + + ulong userId = user.Id; + // ... + +} +``` + +## Feedback + +DiscordOAuth2 is a work in progress and any feedback or suggestions are welcome. +This library is released under the [MIT License](LICENSE). +``` \ No newline at end of file diff --git a/ScopesBuilder.cs b/ScopesBuilder.cs new file mode 100644 index 0000000..74d2d9e --- /dev/null +++ b/ScopesBuilder.cs @@ -0,0 +1,89 @@ +using x3rt.DiscordOAuth2.Entities.Enums; + +namespace x3rt.DiscordOAuth2; + +public class ScopesBuilder +{ + List _scopes = new List(); + + + public ScopesBuilder() + { + } + + public ScopesBuilder(OAuthScope scope) + { + _scopes.Add(scope); + } + + public ScopesBuilder(IEnumerable scopes) + { + _scopes.AddRange(scopes); + } + + public ScopesBuilder(params OAuthScope[] scopes) + { + _scopes.AddRange(scopes); + } + + public ScopesBuilder AddScope(OAuthScope scope) + { + _scopes.Add(scope); + return this; + } + + public ScopesBuilder AddScopes(IEnumerable scopes) + { + _scopes.AddRange(scopes); + return this; + } + + public ScopesBuilder AddScopes(params OAuthScope[] scopes) + { + _scopes.AddRange(scopes); + return this; + } + + public string Build() + { + return string.Join(" ", _scopes.Select(TranslateOAuthScope)); + } + + public override string ToString() + { + return Build(); + } + + + private string? TranslateOAuthScope(OAuthScope scope) + { + return scope switch + { + OAuthScope.Identify => "identify", + OAuthScope.Email => "email", + OAuthScope.Connections => "connections", + OAuthScope.Guilds => "guilds", + OAuthScope.GuildsJoin => "guilds.join", + OAuthScope.GuildsMembersRead => "guilds.members.read", + OAuthScope.GdmJoin => "gdm.join", + OAuthScope.Rpc => "rpc", + OAuthScope.RpcNotificationsRead => "rpc.notifications.read", + OAuthScope.RpcVoiceRead => "rpc.voice.read", + OAuthScope.RpcVoiceWrite => "rpc.voice.write", + OAuthScope.RpcActivitiesWrite => "rpc.activities.write", + OAuthScope.Bot => "bot", + OAuthScope.WebhookIncoming => "webhook.incoming", + OAuthScope.MessagesRead => "messages.read", + OAuthScope.ApplicationsBuildsUpload => "applications.builds.upload", + OAuthScope.ApplicationsBuildsRead => "applications.builds.read", + OAuthScope.ApplicationsCommands => "applications.commands", + OAuthScope.ApplicationsStoreUpdate => "applications.store.update", + OAuthScope.ApplicationsEntitlements => "applications.entitlements", + OAuthScope.ActivitiesRead => "activities.read", + OAuthScope.ActivitiesWrite => "activities.write", + OAuthScope.RelationshipsRead => "relationships.read", + _ => null + }; + } +} + diff --git a/x3rt.DiscordOAuth2.csproj b/x3rt.DiscordOAuth2.csproj new file mode 100644 index 0000000..ec59dec --- /dev/null +++ b/x3rt.DiscordOAuth2.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + enable + enable + true + DiscordOAuth2 + x3rt + Discord OAuth2 implementation for C# + x3rt.DiscordOAuth2 + x3rt.DiscordOAuth2 + + + + + + + + diff --git a/x3rt.DiscordOAuth2.sln b/x3rt.DiscordOAuth2.sln new file mode 100644 index 0000000..bf29364 --- /dev/null +++ b/x3rt.DiscordOAuth2.sln @@ -0,0 +1,15 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "x3rt.DiscordOAuth2", "x3rt.DiscordOAuth2.csproj", "{09C8F24A-5CC8-42E3-9D86-3DD68D6642E0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {09C8F24A-5CC8-42E3-9D86-3DD68D6642E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09C8F24A-5CC8-42E3-9D86-3DD68D6642E0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09C8F24A-5CC8-42E3-9D86-3DD68D6642E0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09C8F24A-5CC8-42E3-9D86-3DD68D6642E0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal