From e0d2b65fffbebd1f7d2a3aded59271504d693437 Mon Sep 17 00:00:00 2001 From: Dmitri Shimanski Date: Fri, 22 Aug 2025 06:48:43 +0300 Subject: [PATCH] Refactor and rename project to yawaflua.Discord.Net; add core entities and interfaces for Discord OAuth2 integration --- .github/workflows/dotnet.yml | 23 ++ .github/workflows/nuget.yml | 48 ++++ DiscordConfig.cs | 11 + DiscordOAuth.cs | 220 +++++------------- Entities/AvatarDecoration.cs | 12 + Entities/DiscordConnection.cs | 59 ++--- Entities/DiscordGuild.cs | 45 +++- Entities/DiscordGuildMember.cs | 39 ++++ Entities/DiscordRole.cs | 26 +++ Entities/DiscordSession.cs | 103 ++++++++ Entities/DiscordUser.cs | 75 ++++-- Entities/Enums/ConnectionType.cs | 24 ++ Entities/Enums/ConnectionVisibility.cs | 7 + Entities/Enums/GuildFeature.cs | 2 +- Entities/Enums/OAuthScope.cs | 2 +- Entities/Enums/PremiumType.cs | 2 +- Entities/Enums/RoleTags.cs | 11 + Entities/Enums/UserFlag.cs | 2 +- Entities/GuildFeatures.cs | 13 -- Entities/OAuthToken.cs | 69 +++++- Entities/RoleColor.cs | 10 + Interfaces/IDiscord.cs | 9 + Interfaces/Models/IAvatarDecoration.cs | 7 + Interfaces/Models/IConnection.cs | 17 ++ Interfaces/Models/IGuild.cs | 19 ++ Interfaces/Models/IGuildMember.cs | 18 ++ Interfaces/Models/IRole.cs | 25 ++ Interfaces/Models/IRoleColor.cs | 8 + Interfaces/Models/ISession.cs | 16 ++ Interfaces/Models/IToken.cs | 14 ++ Interfaces/Models/IUser.cs | 115 +++++++++ LICENSE | 28 +-- Options/GuildOptions.cs | 69 ------ README.md | 79 ++++--- ScopesBuilder.cs | 4 +- ...uth2.csproj => yawaflua.Discord.Net.csproj | 16 +- ...cordOAuth2.sln => yawaflua.Discord.Net.sln | 2 +- 37 files changed, 867 insertions(+), 382 deletions(-) create mode 100644 .github/workflows/dotnet.yml create mode 100644 .github/workflows/nuget.yml create mode 100644 DiscordConfig.cs create mode 100644 Entities/AvatarDecoration.cs create mode 100644 Entities/DiscordGuildMember.cs create mode 100644 Entities/DiscordRole.cs create mode 100644 Entities/DiscordSession.cs create mode 100644 Entities/Enums/ConnectionType.cs create mode 100644 Entities/Enums/ConnectionVisibility.cs create mode 100644 Entities/Enums/RoleTags.cs delete mode 100644 Entities/GuildFeatures.cs create mode 100644 Entities/RoleColor.cs create mode 100644 Interfaces/IDiscord.cs create mode 100644 Interfaces/Models/IAvatarDecoration.cs create mode 100644 Interfaces/Models/IConnection.cs create mode 100644 Interfaces/Models/IGuild.cs create mode 100644 Interfaces/Models/IGuildMember.cs create mode 100644 Interfaces/Models/IRole.cs create mode 100644 Interfaces/Models/IRoleColor.cs create mode 100644 Interfaces/Models/ISession.cs create mode 100644 Interfaces/Models/IToken.cs create mode 100644 Interfaces/Models/IUser.cs delete mode 100644 Options/GuildOptions.cs rename x3rt.DiscordOAuth2.csproj => yawaflua.Discord.Net.csproj (66%) rename x3rt.DiscordOAuth2.sln => yawaflua.Discord.Net.sln (81%) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..075a828 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,23 @@ +name: .NET Tests + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "*" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore /p:TreatWarningsAsErrors=false + - name: Test + run: dotnet test --no-build --verbosity normal \ No newline at end of file diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml new file mode 100644 index 0000000..af2b97d --- /dev/null +++ b/.github/workflows/nuget.yml @@ -0,0 +1,48 @@ +name: CI/CD NuGet Package + +on: + release: + types: [created] + +env: + NUGET_SOURCE: https://api.nuget.org/v3/index.json + GITHUB_SOURCE: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 9.0.x + + - name: Extract Package Version + id: get_version + run: | + VERSION=${GITHUB_REF#refs/tags/v} + echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV + echo "Using version: $VERSION" + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Test + run: dotnet test --no-build --verbosity normal --configuration Release -e telegram_test_token=${{secrets.TELEGRAM_TEST_TOKEN}} + + - name: Pack + run: dotnet pack --no-build --configuration Release -p:PackageVersion=${{ env.PACKAGE_VERSION }} + - name: Add GitHub source + run: dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GHCR_TOKEN }} --store-password-in-clear-text --name github ${{ env.GITHUB_SOURCE }} + + - name: Publish to GitHub Packages + run: dotnet nuget push "**/*.nupkg" --source "github" + + - name: Publish to NuGet + run: dotnet nuget push "**/*.nupkg" --source ${{ env.NUGET_SOURCE }} --api-key ${{ secrets.NUGET_API_KEY }} \ No newline at end of file diff --git a/DiscordConfig.cs b/DiscordConfig.cs new file mode 100644 index 0000000..e317174 --- /dev/null +++ b/DiscordConfig.cs @@ -0,0 +1,11 @@ +namespace yawaflua.Discord.Net; + +public sealed class DiscordConfig +{ + public ulong ClientId { get; set; } + public string ClientSecret { get; set; } = string.Empty; + public string RedirectUri { get; set; } = string.Empty; + public ScopesBuilder Scope { get; set; } + public bool Prompt { get; set; } = true; + public string Token { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/DiscordOAuth.cs b/DiscordOAuth.cs index 9a88f43..7c5c82d 100644 --- a/DiscordOAuth.cs +++ b/DiscordOAuth.cs @@ -1,36 +1,70 @@ using System.Collections.Specialized; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Web; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -using Newtonsoft.Json; -using x3rt.DiscordOAuth2.Entities; -using x3rt.DiscordOAuth2.Options; +using yawaflua.Discord.Net.Entities; +using yawaflua.Discord.Net.Entities.Enums; +using yawaflua.Discord.Net.Interfaces; +using yawaflua.Discord.Net.Interfaces.Models; +using ISession = yawaflua.Discord.Net.Interfaces.Models.ISession; -namespace x3rt.DiscordOAuth2; +namespace yawaflua.Discord.Net; -public class DiscordOAuth +public class DiscordOAuth : IDiscord { - private static ulong ClientId { get; set; } - private static string ClientSecret { get; set; } = string.Empty; - private static string? BotToken { get; set; } + private ulong ClientId { get; set; } + private string ClientSecret { get; set; } = string.Empty; + private 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) + private IToken? token { get; set; } + + private readonly HttpClient _httpClient = new HttpClient(); + + /// + /// Now deprecated, use DiscordConfig instead. + /// + /// + /// + /// + [Obsolete] + public static DiscordOAuth Configure(ulong clientId, string clientSecret, string? botToken = null) + { + return new DiscordOAuth(clientId, clientSecret, string.Empty, new ScopesBuilder(OAuthScope.Identify)) + { + BotToken = botToken + }; + } + + public DiscordOAuth(ulong clientId, string clientSecret, string redirectUri, ScopesBuilder scopes, string? botToken = null, bool prompt = true) { ClientId = clientId; ClientSecret = clientSecret; + RedirectUri = redirectUri; + Scopes = scopes; + Prompt = prompt; BotToken = botToken; } - - private readonly HttpClient _httpClient = new HttpClient(); - + + public DiscordOAuth(DiscordConfig config) + { + ClientId = config.ClientId; + ClientSecret = config.ClientSecret; + RedirectUri = config.RedirectUri; + Scopes = config.Scope; + Prompt = config.Prompt; + BotToken = config.Token; + } + + [Obsolete] public DiscordOAuth(string redirectUri, ScopesBuilder scopes, bool prompt = true) { RedirectUri = redirectUri; @@ -38,30 +72,12 @@ public class DiscordOAuth 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)) + if (request.Query.TryGetValue("code", out var codeQuery)) { - code = codeValues; + code = codeQuery; return true; } @@ -70,12 +86,10 @@ public class DiscordOAuth public static bool TryGetCode(HttpContext context, out string? code) { - var b = TryGetCode(context.Request, out var a); - code = a; - return b; + return TryGetCode(context.Request, out code); } - public async Task GetTokenAsync(string code) + public async Task GetTokenAsync(string code) { var content = new FormUrlEncodedContent(new Dictionary { @@ -89,135 +103,19 @@ public class DiscordOAuth var response = await _httpClient.PostAsync("https://discord.com/api/oauth2/token", content); var responseString = await response.Content.ReadAsStringAsync(); - var authToken = JsonConvert.DeserializeObject(responseString); + var authToken = JsonSerializer.Deserialize(responseString); AccessToken = authToken?.AccessToken; + token = authToken; return authToken; } - private async Task GetInformationAsync(string accessToken, string endpoint) where T : class + public ISession CreateSession() { - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - var response = await _httpClient.GetAsync($"https://discord.com/api/{endpoint}"); - if (!response.IsSuccessStatusCode) - { - return null; - } - - var responseString = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject(responseString) ?? null; + if (token != null) + return new DiscordSession(token, _httpClient, Scopes, ClientId, ClientSecret, RedirectUri, Prompt); + else + throw new InvalidOperationException("Token is not set. Please call GetTokenAsync first."); } - private async Task GetInformationAsync(HttpContext context, string endpoint) where T : class - { - if (AccessToken is null) - { - if (!TryGetCode(context, out var code)) return null; - var accessToken = await GetTokenAsync(code!); - if (accessToken is null) return null; - return await GetInformationAsync(accessToken.AccessToken, endpoint); - } - else - { - return await GetInformationAsync(AccessToken, endpoint); - } - } - - private async Task GetInformationAsync(OAuthToken token, string endpoint) where T : class - { - 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) - { - if (BotToken is null) throw new InvalidOperationException("Bot token is not set"); - 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/AvatarDecoration.cs b/Entities/AvatarDecoration.cs new file mode 100644 index 0000000..7b88e96 --- /dev/null +++ b/Entities/AvatarDecoration.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; +using yawaflua.Discord.Net.Interfaces.Models; + +namespace yawaflua.Discord.Net.Entities; + +internal class AvatarDecoration : IAvatarDecoration +{ + [JsonPropertyName("asset")] + public string AssetHash { get; set; } + [JsonPropertyName("sku_id")] + public ulong AssetArticular { get; set; } +} \ No newline at end of file diff --git a/Entities/DiscordConnection.cs b/Entities/DiscordConnection.cs index 47e80ce..497fe02 100644 --- a/Entities/DiscordConnection.cs +++ b/Entities/DiscordConnection.cs @@ -1,48 +1,19 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using yawaflua.Discord.Net.Entities.Enums; +using yawaflua.Discord.Net.Interfaces.Models; -namespace x3rt.DiscordOAuth2.Entities; +namespace yawaflua.Discord.Net.Entities; -public class DiscordConnection +internal class DiscordConnection : IConnection { - [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 - } + [JsonPropertyName("id")] public string Id { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } + [JsonPropertyName("type")] public ConnectionType Type { get; set; } + [JsonPropertyName("revoked")] public bool? Revoked { get; set; } + [JsonPropertyName("integrations")] public object[] Integrations { get; set; } + [JsonPropertyName("verified")] public bool? Verified { get; set; } + [JsonPropertyName("friend_sync")] public bool? FriendSync { get; set; } + [JsonPropertyName("show_activity")] public bool? ShowActivity { get; set; } + [JsonPropertyName("two_way_link")] public bool? TwoWayLink { get; set; } + [JsonPropertyName("visibility")] public ConnectionVisibility? Visibility { get; set; } } \ No newline at end of file diff --git a/Entities/DiscordGuild.cs b/Entities/DiscordGuild.cs index 0060d1f..38763d4 100644 --- a/Entities/DiscordGuild.cs +++ b/Entities/DiscordGuild.cs @@ -1,24 +1,45 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using yawaflua.Discord.Net.Entities.Enums; +using yawaflua.Discord.Net.Interfaces.Models; -namespace x3rt.DiscordOAuth2.Entities; +namespace yawaflua.Discord.Net.Entities; -public class DiscordGuild +internal class DiscordGuild : IGuild { - [JsonProperty("id")] public ulong Id { get; set; } + [JsonPropertyName("id")] public ulong Id { get; set; } - [JsonProperty("name")] public string Name { get; set; } + [JsonPropertyName("name")] public string Name { get; set; } - [JsonProperty("icon")] public string? Icon { get; set; } + [JsonPropertyName("icon")] public string? IconHash { get; set; } + [JsonPropertyName("banner")] public string? BannerHash { get; set; } - [JsonProperty("owner")] public bool Owner { get; set; } + [JsonPropertyName("owner")] public bool IsOwner { get; set; } - [JsonProperty("permissions")] public string Permissions { get; set; } + [JsonPropertyName("permissions")] public string Permissions { get; set; } - [JsonProperty("features")] public GuildFeatures Features { get; set; } + [JsonPropertyName("features")] public IEnumerable Features { get; set; } = []; + + [JsonPropertyName("approximate_member_count")] public int ApproximateMemberCount { get; set; } - public override string ToString() + [JsonPropertyName("approximate_presence_count")] public int ApproximatePresenceCount { get; set; } + + public string GetIconUrl(int size = 128) { - return - $"Id: {Id}; Name: {Name}; Icon: {Icon}; Owner: {Owner}; Permissions: {Permissions}; Features: {Features}"; + if (string.IsNullOrEmpty(IconHash)) + { + return string.Empty; + } + + return $"https://cdn.discordapp.com/icons/{Id}/{IconHash}.png?size={size}"; + } + + public string GetBannerUrl(int size = 128) + { + if (string.IsNullOrEmpty(BannerHash)) + { + return string.Empty; + } + + return $"https://cdn.discordapp.com/banners/{Id}/{BannerHash}.png?size={size}"; } } \ No newline at end of file diff --git a/Entities/DiscordGuildMember.cs b/Entities/DiscordGuildMember.cs new file mode 100644 index 0000000..bb6d659 --- /dev/null +++ b/Entities/DiscordGuildMember.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; +using yawaflua.Discord.Net.Interfaces.Models; + +namespace yawaflua.Discord.Net.Entities; + +internal class DiscordGuildMember : IGuildMember +{ + private DiscordUser User { get; set; } + public string? Nick { get; set; } + [JsonPropertyName("avatar")] + public string? AvatarHash { get; set; } + [JsonPropertyName("banner")] + public string? BannerHash { get; set; } + public List Roles { get; set; } + public DateTime JoinedAt { get; set; } + public DateTime? PremiumSince { get; set; } + [JsonPropertyName("deaf")] + public bool IsDeaf { get; set; } + [JsonPropertyName("mute")] + public bool IsMute { get; set; } + public int Flags { get; set; } + [JsonPropertyName("pending")] + public bool IsPending { get; set; } + [JsonPropertyName("communication_disabled_until")] + public DateTime? CommunicationDisabledUntil { get; set; } + private AvatarDecoration? AvatarDecoration { get; set; } + + IUser IGuildMember.User + { + get => User; + set => User = value as DiscordUser; + } + + IAvatarDecoration IGuildMember.AvatarDecoration + { + get => AvatarDecoration; + set => AvatarDecoration = value as AvatarDecoration; + } +} \ No newline at end of file diff --git a/Entities/DiscordRole.cs b/Entities/DiscordRole.cs new file mode 100644 index 0000000..c5a06f5 --- /dev/null +++ b/Entities/DiscordRole.cs @@ -0,0 +1,26 @@ +using yawaflua.Discord.Net.Entities.Enums; +using yawaflua.Discord.Net.Interfaces.Models; + +namespace yawaflua.Discord.Net.Entities; + +internal class DiscordRole : IRole +{ + public ulong Id { get; set; } + public string Name { get; set; } + public int Color { get; set; } + private RoleColor Colors { get; set; } + public bool IsHoisted { get; set; } + public bool IsManaged { get; set; } + public bool IsMentionable { get; set; } + public int Position { get; set; } + public ulong? Permissions { get; set; } + public string? IconHash { get; set; } + public string? UnicodeEmoji { get; set; } + public Dictionary? Tags { get; set; } + + IRoleColor IRole.Colors + { + get => Colors; + set => Colors = (RoleColor)value; + } +} \ No newline at end of file diff --git a/Entities/DiscordSession.cs b/Entities/DiscordSession.cs new file mode 100644 index 0000000..c98f4e1 --- /dev/null +++ b/Entities/DiscordSession.cs @@ -0,0 +1,103 @@ +using System.Collections.Specialized; +using System.Net.Http.Headers; +using System.Text.Json; +using yawaflua.Discord.Net.Interfaces.Models; + +namespace yawaflua.Discord.Net.Entities; + +internal class DiscordSession (IToken token, HttpClient httpClient, ScopesBuilder scopes, ulong clientId, string clientSecret, string redirectUri, bool prompt) : ISession +{ + private async Task _req(string endpoint, HttpMethod? method = null) where T : class + { + using var request = new HttpRequestMessage(method ?? HttpMethod.Get, $"https://discord.com/api/{endpoint}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); + var response = await httpClient.SendAsync(request); + if (!response.IsSuccessStatusCode) + { + return null; + } + + var responseString = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize(responseString) ?? null; + } + + public async Task?> GetGuildsAsync(CancellationToken cancellationToken = default) + { + if (token.AccessToken is null) + { + throw new ArgumentNullException(nameof(token), "Token cannot be null."); + } + + return await _req("users/@me/guilds"); + } + + public async Task GetGuildMemberAsync(ulong guildId, CancellationToken cancellationToken = default) + { + if (token.AccessToken is null) + { + throw new ArgumentNullException(nameof(token), "Token cannot be null."); + } + + return await _req($"users/@me/guilds/{guildId}/member"); + } + + public async Task AddMemberToGuildAsync(ulong guildId, ulong userId, CancellationToken cancellationToken = default) + { + if (token.AccessToken is null) + { + throw new ArgumentNullException(nameof(token), "Token cannot be null."); + } + + return await _req($"guilds/{guildId}/members/{userId}", HttpMethod.Put); + } + + public async Task GetCurrentUserAsync(CancellationToken cancellationToken = default) + { + if (token.AccessToken is null) + { + throw new ArgumentNullException(nameof(token), "Token cannot be null."); + } + + return await _req("users/@me"); + } + + public async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + if (token.AccessToken is null) + { + throw new ArgumentNullException(nameof(token), "Token cannot be null."); + } + return await _req("users/@me/connections"); + } + + public string GetAuthorizationUrl(string state) + { + + NameValueCollection query = new() + { + ["client_id"] = clientId.ToString(), + ["redirect_uri"] = redirectUri, + ["response_type"] = "code", + ["scope"] = scopes.ToString(), + ["state"] = state, + ["prompt"] = prompt ? "consent" : "none" + }; + + var uriBuilder = new UriBuilder("https://discord.com/api/oauth2/authorize") + { + Query = query.ToString() + }; + + return uriBuilder.ToString(); + + } + + public IToken GetToken(CancellationToken cancellationToken = default) + { + if (token.AccessToken is null) + { + throw new ArgumentNullException(nameof(token), "Token cannot be null."); + } + return token; + } +} \ No newline at end of file diff --git a/Entities/DiscordUser.cs b/Entities/DiscordUser.cs index 3ae62dd..8f798df 100644 --- a/Entities/DiscordUser.cs +++ b/Entities/DiscordUser.cs @@ -1,38 +1,85 @@ -using x3rt.DiscordOAuth2.Entities.Enums; +using System.Text.Json.Serialization; +using yawaflua.Discord.Net.Entities.Enums; +using yawaflua.Discord.Net.Interfaces.Models; -namespace x3rt.DiscordOAuth2.Entities; +namespace yawaflua.Discord.Net.Entities; -public class DiscordUser +internal class DiscordUser : IUser { + [JsonPropertyName("id")] public ulong Id { get; set; } + + [JsonPropertyName("username")] public string Username { get; set; } + + [JsonPropertyName("global_name")] + public string GlobalName { get; set; } + + [JsonPropertyName("discriminator")] public string Discriminator { get; set; } - public string? Avatar { get; set; } + + [JsonPropertyName("avatar")] + public string? AvatarHash { get; set; } + + [JsonPropertyName("bot")] public bool? Bot { get; set; } + + [JsonPropertyName("system")] public bool? System { get; set; } + + [JsonPropertyName("mfa_enabled")] public bool? MfaEnabled { get; set; } + + [JsonPropertyName("banner")] public string? Banner { get; set; } + + [JsonPropertyName("accent_color")] public int? AccentColor { get; set; } + + [JsonPropertyName("locale")] public string? Locale { get; set; } + + [JsonPropertyName("verified")] public bool? Verified { get; set; } + + [JsonPropertyName("email")] public string? Email { get; set; } + + [JsonPropertyName("flags")] public UserFlag? Flags { get; set; } + + [JsonPropertyName("premium_type")] public PremiumType? PremiumType { get; set; } + + [JsonPropertyName("public_flags")] public UserFlag? PublicFlags { get; set; } + + [JsonPropertyName("avatar_decoration")] + public AvatarDecoration? AvatarDecoration { get; set; } - public override string ToString() + IAvatarDecoration IUser.AvatarDecoration { - string result = ""; - foreach (var property in GetType().GetProperties()) + get => AvatarDecoration; + set => AvatarDecoration = value as AvatarDecoration; + } + + public string GetAvatarUrl(int size = 128) + { + if (string.IsNullOrEmpty(AvatarHash)) { - var value = property.GetValue(this); - if (value is not null) - { - result += $"{property.Name}: {value}; "; - } + return string.Empty; } - result = result.TrimEnd(' ', ';'); - return result; + return $"https://cdn.discordapp.com/avatars/{Id}/{AvatarHash}.png?size={size}"; + } + + public string GetBannerUrl(int size = 128) + { + if (string.IsNullOrEmpty(AvatarHash)) + { + return string.Empty; + } + + return $"https://cdn.discordapp.com/banners/{Id}/{AvatarHash}.png?size={size}"; } } \ No newline at end of file diff --git a/Entities/Enums/ConnectionType.cs b/Entities/Enums/ConnectionType.cs new file mode 100644 index 0000000..a6fa50a --- /dev/null +++ b/Entities/Enums/ConnectionType.cs @@ -0,0 +1,24 @@ +namespace yawaflua.Discord.Net.Entities.Enums; + +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/Enums/ConnectionVisibility.cs b/Entities/Enums/ConnectionVisibility.cs new file mode 100644 index 0000000..c67be24 --- /dev/null +++ b/Entities/Enums/ConnectionVisibility.cs @@ -0,0 +1,7 @@ +namespace yawaflua.Discord.Net.Entities.Enums; + +public enum ConnectionVisibility +{ + None, + Everyone +} \ No newline at end of file diff --git a/Entities/Enums/GuildFeature.cs b/Entities/Enums/GuildFeature.cs index 32cca01..9f0b50f 100644 --- a/Entities/Enums/GuildFeature.cs +++ b/Entities/Enums/GuildFeature.cs @@ -1,4 +1,4 @@ -namespace x3rt.DiscordOAuth2.Entities.Enums; +namespace yawaflua.Discord.Net.Entities.Enums; [Flags] // Credit: Discord.Net diff --git a/Entities/Enums/OAuthScope.cs b/Entities/Enums/OAuthScope.cs index 2d37eb5..9db9aae 100644 --- a/Entities/Enums/OAuthScope.cs +++ b/Entities/Enums/OAuthScope.cs @@ -1,4 +1,4 @@ -namespace x3rt.DiscordOAuth2.Entities.Enums; +namespace yawaflua.Discord.Net.Entities.Enums; /// /// Represents the OAuth2 scopes available for a Discord application. diff --git a/Entities/Enums/PremiumType.cs b/Entities/Enums/PremiumType.cs index 3e0cfb5..b656329 100644 --- a/Entities/Enums/PremiumType.cs +++ b/Entities/Enums/PremiumType.cs @@ -1,4 +1,4 @@ -namespace x3rt.DiscordOAuth2.Entities.Enums; +namespace yawaflua.Discord.Net.Entities.Enums; public enum PremiumType { diff --git a/Entities/Enums/RoleTags.cs b/Entities/Enums/RoleTags.cs new file mode 100644 index 0000000..5173ed9 --- /dev/null +++ b/Entities/Enums/RoleTags.cs @@ -0,0 +1,11 @@ +namespace yawaflua.Discord.Net.Entities.Enums; + +public enum RoleTags +{ + bot_id, + integration_id, + premium_subscriber, + subscription_listing_id, + available_for_purchase, + guild_connections, +} \ No newline at end of file diff --git a/Entities/Enums/UserFlag.cs b/Entities/Enums/UserFlag.cs index 6f2cb9f..1d72d27 100644 --- a/Entities/Enums/UserFlag.cs +++ b/Entities/Enums/UserFlag.cs @@ -1,4 +1,4 @@ -namespace x3rt.DiscordOAuth2.Entities.Enums; +namespace yawaflua.Discord.Net.Entities.Enums; [Flags] public enum UserFlag : ulong diff --git a/Entities/GuildFeatures.cs b/Entities/GuildFeatures.cs deleted file mode 100644 index 92b154f..0000000 --- a/Entities/GuildFeatures.cs +++ /dev/null @@ -1,13 +0,0 @@ -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 index 9252d8a..ae76381 100644 --- a/Entities/OAuthToken.cs +++ b/Entities/OAuthToken.cs @@ -1,16 +1,69 @@ -using Newtonsoft.Json; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Serialization; +using yawaflua.Discord.Net.Interfaces.Models; -namespace x3rt.DiscordOAuth2.Entities; +namespace yawaflua.Discord.Net.Entities; -public class OAuthToken +internal class OAuthToken (HttpClient client, ulong ClientId, string ClientSecret) : IToken { - [JsonProperty("access_token")] public string AccessToken { get; set; } + [JsonPropertyName("access_token")] public string AccessToken { get; set; } - [JsonProperty("expires_in")] public int ExpiresIn { get; set; } + [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } - [JsonProperty("refresh_token")] public string RefreshToken { get; set; } + [JsonPropertyName("refresh_token")] public string RefreshToken { get; set; } - [JsonProperty("scope")] public string Scope { get; set; } + [JsonPropertyName("scope")] public string Scope { get; set; } - [JsonProperty("token_type")] public string TokenType { get; set; } + [JsonPropertyName("token_type")] public string TokenType { get; set; } + public Task RevokeAsync(CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Post, "https://discord.com/api/oauth2/token/revoke") + { + Content = new FormUrlEncodedContent(new Dictionary + { + { "token", AccessToken }, + { "client_id", ClientId.ToString() }, + { "client_secret", ClientSecret } + }) + }; + return client.SendAsync(request, cancellationToken) + .ContinueWith(task => + { + if (!task.Result.IsSuccessStatusCode) + { + throw new Exception("Failed to revoke token."); + } + }, cancellationToken); + } + + public async Task RefreshAsync(CancellationToken cancellationToken = default) + { + using var request = new HttpRequestMessage(HttpMethod.Post, "https://discord.com/api/oauth2/token") + { + Content = new FormUrlEncodedContent(new Dictionary + { + { "grant_type", "refresh_token" }, + { "refresh_token", RefreshToken }, + }) + }; + var byteArray = System.Text.Encoding.ASCII.GetBytes($"{ClientId}:{ClientSecret}"); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); + var req = await client.SendAsync(request, cancellationToken); + if (!req.IsSuccessStatusCode) + { + throw new Exception("Failed to refresh token."); + } + var responseString = await req.Content.ReadAsStringAsync(); + var newToken = JsonSerializer.Deserialize(responseString); + if (newToken is null) + { + throw new Exception("Failed to deserialize token."); + } + AccessToken = newToken.AccessToken; + ExpiresIn = newToken.ExpiresIn; + RefreshToken = newToken.RefreshToken; + Scope = newToken.Scope; + TokenType = newToken.TokenType; + } } \ No newline at end of file diff --git a/Entities/RoleColor.cs b/Entities/RoleColor.cs new file mode 100644 index 0000000..c65b96e --- /dev/null +++ b/Entities/RoleColor.cs @@ -0,0 +1,10 @@ +using yawaflua.Discord.Net.Interfaces.Models; + +namespace yawaflua.Discord.Net.Entities; + +internal class RoleColor : IRoleColor +{ + public int Primary { get; set; } + public int? Secondary { get; set; } + public int? Tertiary { get; set; } +} \ No newline at end of file diff --git a/Interfaces/IDiscord.cs b/Interfaces/IDiscord.cs new file mode 100644 index 0000000..ae70bc1 --- /dev/null +++ b/Interfaces/IDiscord.cs @@ -0,0 +1,9 @@ +using yawaflua.Discord.Net.Interfaces.Models; + +namespace yawaflua.Discord.Net.Interfaces; + +public interface IDiscord +{ + Task GetTokenAsync(string code); + ISession? CreateSession(); +} \ No newline at end of file diff --git a/Interfaces/Models/IAvatarDecoration.cs b/Interfaces/Models/IAvatarDecoration.cs new file mode 100644 index 0000000..0e32f3f --- /dev/null +++ b/Interfaces/Models/IAvatarDecoration.cs @@ -0,0 +1,7 @@ +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IAvatarDecoration +{ + public string AssetHash { get; set; } + public ulong AssetArticular { get; set; } +} \ No newline at end of file diff --git a/Interfaces/Models/IConnection.cs b/Interfaces/Models/IConnection.cs new file mode 100644 index 0000000..bc83b4b --- /dev/null +++ b/Interfaces/Models/IConnection.cs @@ -0,0 +1,17 @@ +using yawaflua.Discord.Net.Entities.Enums; + +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IConnection +{ + public string Id { get; set; } + public string Name { get; set; } + public ConnectionType Type { get; set; } + public bool? Revoked { get; set; } + public object[] Integrations { get; set; } + public bool? Verified { get; set; } + public bool? FriendSync { get; set; } + public bool? ShowActivity { get; set; } + public bool? TwoWayLink { get; set; } + public ConnectionVisibility? Visibility { get; set; } +} \ No newline at end of file diff --git a/Interfaces/Models/IGuild.cs b/Interfaces/Models/IGuild.cs new file mode 100644 index 0000000..6560b42 --- /dev/null +++ b/Interfaces/Models/IGuild.cs @@ -0,0 +1,19 @@ +using yawaflua.Discord.Net.Entities.Enums; + +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IGuild +{ + public ulong Id { get; set; } + public string Name { get; set; } + public string? IconHash { get; set; } + public string? BannerHash { get; set; } + public bool IsOwner { get; set; } + public string Permissions { get; set; } + public IEnumerable Features { get; set; } + public int ApproximateMemberCount { get; set; } + public int ApproximatePresenceCount { get; set; } + + public string GetIconUrl(int size = 128); + public string GetBannerUrl(int size = 128); +} \ No newline at end of file diff --git a/Interfaces/Models/IGuildMember.cs b/Interfaces/Models/IGuildMember.cs new file mode 100644 index 0000000..f62a00c --- /dev/null +++ b/Interfaces/Models/IGuildMember.cs @@ -0,0 +1,18 @@ +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IGuildMember +{ + public IUser User { get; set; } + public string? Nick { get; set; } + public string? AvatarHash { get; set; } + public string? BannerHash { get; set; } + public List Roles { get; set; } + public DateTime JoinedAt { get; set; } + public DateTime? PremiumSince { get; set; } + public bool IsDeaf { get; set; } + public bool IsMute { get; set; } + public int Flags { get; set; } + public bool IsPending { get; set; } + public DateTime? CommunicationDisabledUntil { get; set; } + public IAvatarDecoration? AvatarDecoration { get; set; } +} \ No newline at end of file diff --git a/Interfaces/Models/IRole.cs b/Interfaces/Models/IRole.cs new file mode 100644 index 0000000..1064836 --- /dev/null +++ b/Interfaces/Models/IRole.cs @@ -0,0 +1,25 @@ +using yawaflua.Discord.Net.Entities.Enums; + +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IRole +{ + public ulong Id { get; set; } + public string Name { get; set; } + [Obsolete("Deprecated integer representation of hexadecimal color code")] + public int Color { get; set; } + + public IRoleColor Colors { get; set; } + + public bool IsHoisted { get; set; } + public bool IsManaged { get; set; } + public bool IsMentionable { get; set; } + + public int Position { get; set; } + public ulong? Permissions { get; set; } + + public string? IconHash { get; set; } + public string? UnicodeEmoji { get; set; } + + public Dictionary? Tags { get; set; } +} \ No newline at end of file diff --git a/Interfaces/Models/IRoleColor.cs b/Interfaces/Models/IRoleColor.cs new file mode 100644 index 0000000..876911c --- /dev/null +++ b/Interfaces/Models/IRoleColor.cs @@ -0,0 +1,8 @@ +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IRoleColor +{ + public int Primary { get; set; } + public int? Secondary { get; set; } + public int? Tertiary { get; set; } +} \ No newline at end of file diff --git a/Interfaces/Models/ISession.cs b/Interfaces/Models/ISession.cs new file mode 100644 index 0000000..58f57f0 --- /dev/null +++ b/Interfaces/Models/ISession.cs @@ -0,0 +1,16 @@ +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface ISession +{ + Task?> GetGuildsAsync(CancellationToken cancellationToken = default); + Task GetGuildMemberAsync(ulong guildId, CancellationToken cancellationToken = default); + Task AddMemberToGuildAsync(ulong guildId, ulong userId, CancellationToken cancellationToken = default); + Task GetCurrentUserAsync(CancellationToken cancellationToken = default); + + Task GetConnectionAsync(CancellationToken cancellationToken = default); + + + string GetAuthorizationUrl(string state); + IToken GetToken(CancellationToken cancellationToken = default); + +} \ No newline at end of file diff --git a/Interfaces/Models/IToken.cs b/Interfaces/Models/IToken.cs new file mode 100644 index 0000000..c922745 --- /dev/null +++ b/Interfaces/Models/IToken.cs @@ -0,0 +1,14 @@ +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IToken +{ + public string? AccessToken { get; set; } + public int ExpiresIn { get; set; } + public string? RefreshToken { get; set; } + public string? Scope { get; set; } + public string? TokenType { get; set; } + + Task RevokeAsync(CancellationToken cancellationToken = default); + Task RefreshAsync(CancellationToken cancellationToken = default); + +} \ No newline at end of file diff --git a/Interfaces/Models/IUser.cs b/Interfaces/Models/IUser.cs new file mode 100644 index 0000000..2a7b6d2 --- /dev/null +++ b/Interfaces/Models/IUser.cs @@ -0,0 +1,115 @@ +using yawaflua.Discord.Net.Entities.Enums; + +namespace yawaflua.Discord.Net.Interfaces.Models; + +public interface IUser +{ + /// + /// the user's id + /// + /// User-object + public ulong Id { get; set; } + + /// + /// the user's username, not unique across the platform + /// + /// User-object + public string Username { get; set; } + + /// + /// the user's display name, if it is set. For bots, this is the application name + /// + public string GlobalName { get; set; } + + /// + /// the user's Discord-tag (Obsolete) + /// + /// User-object + public string Discriminator { get; set; } + + /// + /// the user's avatar hash + /// + /// Image formatting + /// User-object + public string? AvatarHash { get; set; } + + /// + /// the user's banner hash + /// + /// Image formatting + /// User-object + /// Available in the User object only if the user has a banner set. + /// https://cdn.discordapp.com/banners/{user.id}/{banner}.png?size=512 + public string? Banner { get; set; } + + /// + /// whether the user belongs to an OAuth2 application + /// + /// User-object + public bool? Bot { get; set; } + + /// + /// whether the user is an Official Discord System user (part of the urgent message system) + /// + /// User-object + public bool? System { get; set; } + + /// + /// whether the user has two factor enabled on their account + /// + /// User-object + public bool? MfaEnabled { get; set; } + + + /// + /// the user's banner color encoded as an integer representation of hexadecimal color code + /// + /// User-object + public int? AccentColor { get; set; } + + /// + /// the user's chosen language option + /// + /// User-object + public string? Locale { get; set; } + + /// + /// whether the email on this account has been verified + /// + /// User-object + public bool? Verified { get; set; } + /// + /// the user's email + /// + /// User-object + public string? Email { get; set; } + + /// + /// the flags on a user's account + /// + /// User-object + /// User flags + public UserFlag? Flags { get; set; } + + /// + /// the type of Nitro subscription on a user's account + /// + /// User-object + public PremiumType? PremiumType { get; set; } + + /// + /// the public flags on a user's account + /// + /// User-object + public UserFlag? PublicFlags { get; set; } + + /// + /// data for the user's avatar decoration + /// + /// User-object + public IAvatarDecoration? AvatarDecoration { get; set; } + + public string GetAvatarUrl(int size = 128); + public string GetBannerUrl(int size = 128); +} \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8d240bd..545dac6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,13 @@ -MIT License +Copyright 2025 Dmitrii Shimanskii -Copyright (c) 2023 x3rt +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + http://www.apache.org/licenses/LICENSE-2.0 -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/Options/GuildOptions.cs b/Options/GuildOptions.cs deleted file mode 100644 index 3571b34..0000000 --- a/Options/GuildOptions.cs +++ /dev/null @@ -1,69 +0,0 @@ -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 index 5483b65..2514251 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,64 @@ -# x3rt.DiscordOAuth2 +# yawaflua.Discord.Net -[![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/) +[![NuGet](https://img.shields.io/nuget/v/yawaflua.Discord.Net.svg)](https://www.nuget.org/packages/yawaflua.Discord.Net/) +[![NuGet](https://img.shields.io/nuget/dt/yawaflua.Discord.Net.svg)](https://www.nuget.org/packages/yawaflua.Discord.Net/) -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 and one that can be used by people that are trying to avoid ASP.NETs cluttered and bulky Identity system. +yawaflua.Discord.Net is a library for integrating Discord OAuth2 into your .NET applications. It provides a simple and easy-to-use interface for authenticating users with Discord, retrieving user information, and managing sessions. + +## Features +- **OAuth2 Authentication**: Easily authenticate users with Discord using OAuth2. +- **User Information**: Retrieve user information such as username, avatar, and guilds. +- **Session Management**: Create and manage sessions for authenticated users. +- **Dependency Injection**: Seamlessly integrate with your .NET application's dependency injection system. ## 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("state"); -/* Redirect user to url via preferred method */ -``` -```csharp -// Your callback method -if (DiscordOAuth.TryGetCode(HttpContext, out var code)) +// Configure configuration to DI +builder.Services.AddSingleton( + new DiscordConfig + { + ClientId = "your-client-id", + ClientSecret = "shhhhh!", + RedirectUri = "https://example.com/Login", + Scopes = new ScopesBuilder(OAuthScope.Identify, OAuthScope.Guilds), + Token = "MyVeryCool.BotToken.For.Discord" // Optional, for bot interactions + }); +builder.Services.AddSingleton(); + +// And use it in you very cool controller or smth + +public class MyController (IDiscord discord) : ControllerBase { - 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; - // ... + public async Task Login() + { + var url = discord.GetAuthorizationUrl("state"); + return Redirect(url); + } + public async Task Callback() + { + if (discord.TryGetCode(HttpContext, out var code)) + { + var token = await discord.GetTokenAsync(code); + var user = await discord.GetUserAsync(token); + + var session = discord.CreateSession(); + var guilds = await session.GetGuildsAsync(); + // Do something with the user and guilds, like storing them in a database or session + // ... + + return Ok(); + } + + return BadRequest("Invalid code"); + } } ``` ## Feedback -DiscordOAuth2 is a work in progress and any feedback or suggestions are welcome. -This library is released under the [MIT License](LICENSE). +`yawaflua.Discord.Net` is a library still in development, and your feedback or support is very welcome. If you have any issues, suggestions, or questions, please open an issue on the [GitHub repository](https://github.com/yawaflua/Discord.Net/) + + +This library is released under the [Apache 2.0](LICENSE). diff --git a/ScopesBuilder.cs b/ScopesBuilder.cs index 74d2d9e..5d00c51 100644 --- a/ScopesBuilder.cs +++ b/ScopesBuilder.cs @@ -1,6 +1,6 @@ -using x3rt.DiscordOAuth2.Entities.Enums; +using yawaflua.Discord.Net.Entities.Enums; -namespace x3rt.DiscordOAuth2; +namespace yawaflua.Discord.Net; public class ScopesBuilder { diff --git a/x3rt.DiscordOAuth2.csproj b/yawaflua.Discord.Net.csproj similarity index 66% rename from x3rt.DiscordOAuth2.csproj rename to yawaflua.Discord.Net.csproj index 6c4378a..381ffb6 100644 --- a/x3rt.DiscordOAuth2.csproj +++ b/yawaflua.Discord.Net.csproj @@ -1,28 +1,28 @@ - net7.0 + net9.0 enable enable true Discord OAuth2 - x3rt + yawaflua Discord OAuth2 implementation for C# - x3rt.DiscordOAuth2 - x3rt.DiscordOAuth2 + yawaflua.Discord.Net + yawaflua.Discord.Net README.md LICENSE + httpsL//github.com/yawaflua/Discord.Net true - https://github.com/x3rt/x3rt.DiscordOAuth2 + https://github.com/yawaflua/Discord.Net GIT - Discord-OAuth2;Discord-OAuth-2;Discord-OAuth;DiscordOAuth;Discord;OAuth;OAuth-2;OAuth2 + Discord-OAuth2;Discord-OAuth-2;Discord-OAuth;DiscordOAuth;Discord;yawaflua;OAuth;OAuth-2;OAuth2 true - 1.0.4 + 1.0.5 - diff --git a/x3rt.DiscordOAuth2.sln b/yawaflua.Discord.Net.sln similarity index 81% rename from x3rt.DiscordOAuth2.sln rename to yawaflua.Discord.Net.sln index bf29364..f5d9809 100644 --- a/x3rt.DiscordOAuth2.sln +++ b/yawaflua.Discord.Net.sln @@ -1,5 +1,5 @@ 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "yawaflua.Discord.Net", "yawaflua.Discord.Net.csproj", "{09C8F24A-5CC8-42E3-9D86-3DD68D6642E0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution