diff --git a/.github/workflows/publish_release_master.yml b/.github/workflows/publish_release_master.yml index 5da43bb075..880d9ee2de 100644 --- a/.github/workflows/publish_release_master.yml +++ b/.github/workflows/publish_release_master.yml @@ -2,14 +2,18 @@ name: "Publish Release" on: release: types: ["published"] + env: - DOTNET_NOLOGO: true - DOTNET_CLI_TELEMETRY_OPTOUT: true + DOTNET_NOLOGO: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 + jobs: publish_release: - name: Publish Release (Master) - runs-on: "self-hosted" - if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]') && contains(github.event.release.target_commitish, 'master')" + name: Publish Release + runs-on: "ubuntu-latest" + if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" steps: - name: Checkout uses: actions/checkout@v3 @@ -19,16 +23,16 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.103 - - name: Build Nuget Packages + - name: Build NuGet packages run: "mkdir build && dotnet pack --include-symbols -p:SymbolPackageFormat=snupkg -c Release -o build" - - name: Publish Nuget Packages + - name: Publish NuGet packages run: "dotnet nuget push \"build/*\" -k ${{ secrets.NUGET_ORG_API_KEY }} -s https://api.nuget.org/v3/index.json" - - name: Upload Nuget Packages To Github Actions - uses: actions/upload-artifact@v3 + - name: Upload NuGet packages to GitHub Actions + uses: actions/upload-artifact@v4 with: - name: PR Nuget Packages + name: PR NuGet Packages path: build/* - - name: Upload Nuget Packages To Github Release + - name: Upload NuGet Packages To Github Release uses: "ncipollo/release-action@v1" with: allowUpdates: true @@ -37,8 +41,8 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} omitBodyDuringUpdate: true # We don't want to update the body of the release. omitNameDuringUpdate: true # We don't want to update the name of the release. - - name: Export Latest Tag - run: echo LATEST_STABLE_VERSION=$(git describe --abbrev=0 --tags) >> $GITHUB_ENV + - name: Export latest tag + run: echo LATEST_STABLE_VERSION=$(git describe --tags $(git rev-list --tags --max-count=1)) >> $GITHUB_ENV - name: Update Discord Channel Topic run: "dotnet run --project ./tools/AutoUpdateChannelDescription -p:Nightly=$(printf \"%0*d\n\" 5 $(( 1195 + ${{ github.run_number }} ))" env: diff --git a/DSharpPlus.CommandsNext/CommandsNextExtension.cs b/DSharpPlus.CommandsNext/CommandsNextExtension.cs index fc52c0775a..06bf12fea8 100644 --- a/DSharpPlus.CommandsNext/CommandsNextExtension.cs +++ b/DSharpPlus.CommandsNext/CommandsNextExtension.cs @@ -166,11 +166,11 @@ internal CommandsNextExtension(CommandsNextConfiguration cfg) /// Disposes of this the resources used by CNext. /// public override void Dispose() - => this.Config.CommandExecutor.Dispose(); - - ~CommandsNextExtension() { - this.Dispose(); + this.Config.CommandExecutor.Dispose(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } #region DiscordClient Registration diff --git a/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs b/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs index 2fd04f61d9..606f957977 100644 --- a/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs +++ b/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs @@ -133,31 +133,33 @@ private Task HandleEvent(DiscordClient client, T eventargs) return Task.CompletedTask; } - ~EventWaiter() - { - this.Dispose(); - } - /// /// Disposes this EventWaiter /// public void Dispose() { + if (this._disposed) + return; + this._disposed = true; - if (this._event != null) + + if (this._event != null && this._handler != null) this._event.Unregister(this._handler); - this._event = null; - this._handler = null; - this._client = null; + this._event = null!; + this._handler = null!; + this._client = null!; if (this._matchrequests != null) this._matchrequests.Clear(); if (this._collectrequests != null) this._collectrequests.Clear(); - this._matchrequests = null; - this._collectrequests = null; + this._matchrequests = null!; + this._collectrequests = null!; + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } } diff --git a/DSharpPlus.Interactivity/EventHandling/Paginator.cs b/DSharpPlus.Interactivity/EventHandling/Paginator.cs index 65c7f0d018..9d9d27548b 100644 --- a/DSharpPlus.Interactivity/EventHandling/Paginator.cs +++ b/DSharpPlus.Interactivity/EventHandling/Paginator.cs @@ -260,22 +260,23 @@ private async Task PaginateAsync(IPaginationRequest p, DiscordEmoji emoji) await builder.ModifyAsync(msg).ConfigureAwait(false); } - ~Paginator() - { - this.Dispose(); - } - /// /// Disposes this EventWaiter /// public void Dispose() { - this._client.MessageReactionAdded -= this.HandleReactionAdd; - this._client.MessageReactionRemoved -= this.HandleReactionRemove; - this._client.MessageReactionsCleared -= this.HandleReactionClear; - this._client = null; - this._requests.Clear(); - this._requests = null; + // Why doesn't this class implement IDisposable? + + if (this._client != null) + { + this._client.MessageReactionAdded -= this.HandleReactionAdd; + this._client.MessageReactionRemoved -= this.HandleReactionRemove; + this._client.MessageReactionsCleared -= this.HandleReactionClear; + this._client = null!; + } + + this._requests?.Clear(); + this._requests = null!; } } } diff --git a/DSharpPlus.Interactivity/EventHandling/Poller.cs b/DSharpPlus.Interactivity/EventHandling/Poller.cs index af86b2300e..0caa81a4c7 100644 --- a/DSharpPlus.Interactivity/EventHandling/Poller.cs +++ b/DSharpPlus.Interactivity/EventHandling/Poller.cs @@ -128,22 +128,26 @@ private Task HandleReactionClear(DiscordClient client, MessageReactionsClearEven return Task.CompletedTask; } - ~Poller() - { - this.Dispose(); - } - /// /// Disposes this EventWaiter /// public void Dispose() { - this._client.MessageReactionAdded -= this.HandleReactionAdd; - this._client.MessageReactionRemoved -= this.HandleReactionRemove; - this._client.MessageReactionsCleared -= this.HandleReactionClear; - this._client = null; - this._requests.Clear(); - this._requests = null; + // Why doesn't this class implement IDisposable? + + if (this._client != null) + { + this._client.MessageReactionAdded -= this.HandleReactionAdd; + this._client.MessageReactionRemoved -= this.HandleReactionRemove; + this._client.MessageReactionsCleared -= this.HandleReactionClear; + this._client = null!; + } + + if (this._requests != null) + { + this._requests.Clear(); + this._requests = null!; + } } } } diff --git a/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs b/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs index b229d238e3..b7938549e9 100644 --- a/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs +++ b/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs @@ -168,31 +168,34 @@ private Task HandleReactionClear(DiscordClient client, MessageReactionsClearEven return Task.CompletedTask; } - ~ReactionCollector() - { - this.Dispose(); - } - /// /// Disposes this EventWaiter /// public void Dispose() { - this._client = null; + this._client = null!; + + if (this._reactionAddHandler != null) + this._reactionAddEvent?.Unregister(this._reactionAddHandler); - this._reactionAddEvent.Unregister(this._reactionAddHandler); - this._reactionRemoveEvent.Unregister(this._reactionRemoveHandler); - this._reactionClearEvent.Unregister(this._reactionClearHandler); + if (this._reactionRemoveHandler != null) + this._reactionRemoveEvent?.Unregister(this._reactionRemoveHandler); - this._reactionAddEvent = null; - this._reactionAddHandler = null; - this._reactionRemoveEvent = null; - this._reactionRemoveHandler = null; - this._reactionClearEvent = null; - this._reactionClearHandler = null; + if (this._reactionClearHandler != null) + this._reactionClearEvent?.Unregister(this._reactionClearHandler); - this._requests.Clear(); - this._requests = null; + this._reactionAddEvent = null!; + this._reactionAddHandler = null!; + this._reactionRemoveEvent = null!; + this._reactionRemoveHandler = null!; + this._reactionClearEvent = null!; + this._reactionClearHandler = null!; + + this._requests?.Clear(); + this._requests = null!; + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } @@ -214,19 +217,16 @@ public ReactionCollectRequest(DiscordMessage msg, TimeSpan timeout) this._ct.Token.Register(() => this._tcs.TrySetResult(null)); } - ~ReactionCollectRequest() - { - this.Dispose(); - } - public void Dispose() { - GC.SuppressFinalize(this); this._ct.Dispose(); this._tcs = null; this._message = null; this._collected?.Clear(); this._collected = null; + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs index 013fd1836e..2e25649904 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs @@ -57,24 +57,19 @@ public CollectRequest(Func predicate, TimeSpan timeout) this._collected = new ConcurrentHashSet(); } - ~CollectRequest() - { - this.Dispose(); - } - /// /// Disposes this CollectRequest. /// public void Dispose() { this._ct.Dispose(); - this._tcs = null; - this._predicate = null; + this._tcs = null!; + this._predicate = null!; if (this._collected != null) { this._collected.Clear(); - this._collected = null; + this._collected = null!; } } } diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs index 08a812a95f..f1d6387910 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs @@ -54,19 +54,17 @@ public MatchRequest(Func predicate, TimeSpan timeout) this._timeout = timeout; } - ~MatchRequest() - { - this.Dispose(); - } - /// /// Disposes this MatchRequest. /// public void Dispose() { - this._ct.Dispose(); - this._tcs = null; - this._predicate = null; + this._ct?.Dispose(); + this._tcs = null!; + this._predicate = null!; + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } } diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs index e293819026..2033a04079 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs @@ -195,18 +195,15 @@ public async Task> GetTaskCompletionSourceAsync() return this._tcs; } - ~PaginationRequest() - { - this.Dispose(); - } - /// /// Disposes this PaginationRequest. /// public void Dispose() { - this._ct.Dispose(); - this._tcs = null; + // Why doesn't this class implement IDisposable? + + this._ct?.Dispose(); + this._tcs = null!; } } } diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs index 3b8b195eaa..3c1959d61e 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs @@ -99,18 +99,15 @@ internal void AddReaction(DiscordEmoji emoji, DiscordUser member) } } - ~PollRequest() - { - this.Dispose(); - } - /// /// Disposes this PollRequest. /// public void Dispose() { - this._ct.Dispose(); - this._tcs = null; + // Why doesn't this class implement IDisposable? + + this._ct?.Dispose(); + this._tcs = null!; } } diff --git a/DSharpPlus.Interactivity/InteractivityExtension.cs b/DSharpPlus.Interactivity/InteractivityExtension.cs index c458667c7e..edf6f9997d 100644 --- a/DSharpPlus.Interactivity/InteractivityExtension.cs +++ b/DSharpPlus.Interactivity/InteractivityExtension.cs @@ -206,7 +206,7 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) + if (!message.FilterComponents().Any()) throw new ArgumentException("Provided message does not contain any button components."); @@ -246,10 +246,12 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) + IReadOnlyList buttons = message.FilterComponents(); + + if (!buttons.Any()) throw new ArgumentException("Provided message does not contain any button components."); - var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId); + var ids = buttons.Select(c => c.CustomId); var result = await this @@ -289,7 +291,7 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) + if (!message.FilterComponents().Any()) throw new ArgumentException("Provided message does not contain any button components."); var result = await this @@ -330,10 +332,10 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) + if (!message.FilterComponents().Any()) throw new ArgumentException("Provided message does not contain any button components."); - if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id)) + if (!message.FilterComponents().Any(c => c.CustomId == id)) throw new ArgumentException($"Provided message does not contain button with Id of '{id}'."); var result = await this @@ -367,7 +369,7 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) + if (!message.FilterComponents().Any()) throw new ArgumentException("Provided message does not contain any button components."); var result = await this @@ -403,7 +405,7 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(this.IsSelect)) + if (!message.FilterComponents().Any(this.IsSelect)) throw new ArgumentException("Provided message does not contain any select components."); @@ -442,10 +444,10 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(this.IsSelect)) + if (!message.FilterComponents().Any(this.IsSelect)) throw new ArgumentException("Provided message does not contain any select components."); - if (message.Components.SelectMany(c => c.Components).Where(this.IsSelect).All(c => c.CustomId != id)) + if (message.FilterComponents().Where(this.IsSelect).All(c => c.CustomId != id)) throw new ArgumentException($"Provided message does not contain select component with Id of '{id}'."); var result = await this @@ -495,10 +497,10 @@ public async Task> Wait if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); - if (!message.Components.SelectMany(c => c.Components).Any(this.IsSelect)) + if (!message.FilterComponents().Any(this.IsSelect)) throw new ArgumentException("Provided message does not contain any select components."); - if (message.Components.SelectMany(c => c.Components).Where(this.IsSelect).All(c => c.CustomId != id)) + if (message.FilterComponents().Where(this.IsSelect).All(c => c.CustomId != id)) throw new ArgumentException($"Provided message does not contain button with Id of '{id}'."); var result = await this @@ -1060,21 +1062,19 @@ private async Task HandleInvalidInteraction(DiscordInteraction interaction) public override void Dispose() { - this.ComponentEventWaiter.Dispose(); - this.ModalEventWaiter.Dispose(); - this.ReactionCollector.Dispose(); - this.ComponentInteractionWaiter.Dispose(); - this.MessageCreatedWaiter.Dispose(); - this.MessageReactionAddWaiter.Dispose(); - this.Paginator.Dispose(); - this.Poller.Dispose(); - this.TypingStartWaiter.Dispose(); - this._compPaginator.Dispose(); - } - - ~InteractivityExtension() - { - this.Dispose(); + this.ComponentEventWaiter?.Dispose(); + this.ModalEventWaiter?.Dispose(); + this.ReactionCollector?.Dispose(); + this.ComponentInteractionWaiter?.Dispose(); + this.MessageCreatedWaiter?.Dispose(); + this.MessageReactionAddWaiter?.Dispose(); + this.Paginator?.Dispose(); + this.Poller?.Dispose(); + this.TypingStartWaiter?.Dispose(); + this._compPaginator?.Dispose(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } } diff --git a/DSharpPlus.Lavalink/LavalinkExtension.cs b/DSharpPlus.Lavalink/LavalinkExtension.cs index 7e16fe51b0..daefcfe752 100644 --- a/DSharpPlus.Lavalink/LavalinkExtension.cs +++ b/DSharpPlus.Lavalink/LavalinkExtension.cs @@ -199,22 +199,19 @@ private Task Con_Disconnected(LavalinkNodeConnection node, NodeDisconnectedEvent public override void Dispose() { - foreach(var node in this._connectedNodes) + foreach (var node in this._connectedNodes) { // undoubtedly there will be some GitHub comments about this. Help. node.Value.StopAsync().GetAwaiter().GetResult(); } - this._connectedNodes.Clear(); - // unhook events - _nodeDisconnected.UnregisterAll(); + this._connectedNodes?.Clear(); - // Hi GC! <3 😘 clean me up uwu - } + // unhook events + this._nodeDisconnected?.UnregisterAll(); - ~LavalinkExtension() - { - this.Dispose(); + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } } diff --git a/DSharpPlus.Rest/DiscordRestClient.cs b/DSharpPlus.Rest/DiscordRestClient.cs index d4c569c33d..e48e9bf1fc 100644 --- a/DSharpPlus.Rest/DiscordRestClient.cs +++ b/DSharpPlus.Rest/DiscordRestClient.cs @@ -2293,7 +2293,7 @@ public override void Dispose() return; this._disposed = true; this._guilds = null; - this.ApiClient._rest.Dispose(); + this.ApiClient?._rest?.Dispose(); } } } diff --git a/DSharpPlus.SlashCommands/SlashCommandsExtension.cs b/DSharpPlus.SlashCommands/SlashCommandsExtension.cs index 5ffec91ddb..31fc75fa77 100644 --- a/DSharpPlus.SlashCommands/SlashCommandsExtension.cs +++ b/DSharpPlus.SlashCommands/SlashCommandsExtension.cs @@ -50,7 +50,7 @@ public sealed class SlashCommandsExtension : BaseExtension internal SlashCommandsExtension(SlashCommandsConfiguration configuration) { - this._configuration = configuration; + this._configuration = configuration ?? new SlashCommandsConfiguration(); } /// @@ -1238,23 +1238,24 @@ public event AsyncEventHandler /// Connects to the specified voice channel. /// @@ -724,15 +719,15 @@ public void Dispose() this.IsDisposed = true; this.IsInitialized = false; - this.TokenSource.Cancel(); - this.SenderTokenSource.Cancel(); + this.TokenSource?.Cancel(); + this.SenderTokenSource?.Cancel(); this.ReceiverTokenSource?.Cancel(); - this.KeepaliveTokenSource.Cancel(); + this.KeepaliveTokenSource?.Cancel(); - this.TokenSource.Dispose(); - this.SenderTokenSource.Dispose(); + this.TokenSource?.Dispose(); + this.SenderTokenSource?.Dispose(); this.ReceiverTokenSource?.Dispose(); - this.KeepaliveTokenSource.Dispose(); + this.KeepaliveTokenSource?.Dispose(); try { @@ -742,11 +737,11 @@ public void Dispose() catch { } this.Opus?.Dispose(); - this.Opus = null; + this.Opus = null!; this.Sodium?.Dispose(); - this.Sodium = null; + this.Sodium = null!; this.Rtp?.Dispose(); - this.Rtp = null; + this.Rtp = null!; this.VoiceDisconnected?.Invoke(this.Guild); } diff --git a/DSharpPlus.VoiceNext/VoiceNextExtension.cs b/DSharpPlus.VoiceNext/VoiceNextExtension.cs index b96d7e7ca9..cdb1a014fd 100644 --- a/DSharpPlus.VoiceNext/VoiceNextExtension.cs +++ b/DSharpPlus.VoiceNext/VoiceNextExtension.cs @@ -229,18 +229,20 @@ private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpd public override void Dispose() { - foreach(var conn in this.ActiveConnections) + foreach (var conn in this.ActiveConnections) { - conn.Value.Dispose(); + conn.Value?.Dispose(); + } + + if (this.Client != null) + { + this.Client.VoiceStateUpdated -= this.Client_VoiceStateUpdate; + this.Client.VoiceServerUpdated -= this.Client_VoiceServerUpdate; } - this.Client.VoiceStateUpdated -= this.Client_VoiceStateUpdate; - this.Client.VoiceServerUpdated -= this.Client_VoiceServerUpdate; // Lo and behold, the audacious man who dared lay his hand upon VoiceNext hath once more trespassed upon its profane ground! - } - ~VoiceNextExtension() - { - this.Dispose(); + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); } } } diff --git a/DSharpPlus.targets b/DSharpPlus.targets index 168293733f..89fef6cd1b 100644 --- a/DSharpPlus.targets +++ b/DSharpPlus.targets @@ -1,7 +1,7 @@ - 4.4.0 + 4.5.1 1591 9.0 True diff --git a/DSharpPlus/AsyncEvents/AsyncEvent`2.cs b/DSharpPlus/AsyncEvents/AsyncEvent`2.cs index bb3f8490fc..ecc7af4f46 100644 --- a/DSharpPlus/AsyncEvents/AsyncEvent`2.cs +++ b/DSharpPlus/AsyncEvents/AsyncEvent`2.cs @@ -30,6 +30,7 @@ namespace DSharpPlus.AsyncEvents { + /// /// Provides an implementation of an asynchronous event. Registered handlers are executed asynchronously, /// in parallel, and potential exceptions are caught and sent to the specified exception handler. @@ -57,7 +58,9 @@ public AsyncEvent(string name, AsyncEventExceptionHandler except public void Register(AsyncEventHandler handler) { if (handler is null) + { throw new ArgumentNullException(nameof(handler)); + } this._lock.Wait(); try @@ -77,8 +80,9 @@ public void Register(AsyncEventHandler handler) public void Unregister(AsyncEventHandler handler) { if (handler is null) + { throw new ArgumentNullException(nameof(handler)); - + } this._lock.Wait(); try @@ -105,30 +109,25 @@ public void UnregisterAll() public async Task InvokeAsync(TSender sender, TArgs args) { if (this._handlers.Count == 0) + { return; + } await this._lock.WaitAsync(); List> copiedHandlers = new(this._handlers); this._lock.Release(); - try + _ = Task.WhenAll(copiedHandlers.Select(async (handler) => { - await Task.WhenAll(copiedHandlers.Select(async (handler) => + try { - try - { - await handler(sender, args); - } - catch (Exception ex) - { - this._exceptionHandler?.Invoke(this, ex, handler, sender, args); - } - })); - } - finally - { - this._lock.Release(); - } + await handler(sender, args); + } + catch (Exception ex) + { + this._exceptionHandler?.Invoke(this, ex, handler, sender, args); + } + })); return; } diff --git a/DSharpPlus/Clients/BaseDiscordClient.cs b/DSharpPlus/Clients/BaseDiscordClient.cs index 2dcce3017b..3b3fa1c5e0 100644 --- a/DSharpPlus/Clients/BaseDiscordClient.cs +++ b/DSharpPlus/Clients/BaseDiscordClient.cs @@ -89,7 +89,7 @@ public IReadOnlyDictionary VoiceRegions /// Initializes this Discord API client. /// /// Configuration for this client. - protected BaseDiscordClient(DiscordConfiguration config) + internal BaseDiscordClient(DiscordConfiguration config, RestClient restClient = null) { this.Configuration = new DiscordConfiguration(config); @@ -100,7 +100,7 @@ protected BaseDiscordClient(DiscordConfiguration config) } this.Logger = this.Configuration.LoggerFactory.CreateLogger(); - this.ApiClient = new DiscordApiClient(this); + this.ApiClient = new DiscordApiClient(this, restClient); this.UserCache = new ConcurrentDictionary(); this.InternalVoiceRegions = new ConcurrentDictionary(); this._voice_regions_lazy = new Lazy>(() => new ReadOnlyDictionary(this.InternalVoiceRegions)); diff --git a/DSharpPlus/Clients/DiscordClient.cs b/DSharpPlus/Clients/DiscordClient.cs index 180bd93b03..b74c282964 100644 --- a/DSharpPlus/Clients/DiscordClient.cs +++ b/DSharpPlus/Clients/DiscordClient.cs @@ -150,6 +150,28 @@ public DiscordClient(DiscordConfiguration config) this.PrivateChannels = new ReadOnlyConcurrentDictionary(this._privateChannels); } + /// + /// This constructor is used when constructing a sharded client to use a shared rest client. + /// + /// Specifies configuration parameters. + /// Restclient which will be used for the underlying ApiClients + internal DiscordClient(DiscordConfiguration config, RestClient restClient) + : base(config, restClient) + { + if (this.Configuration.MessageCacheSize > 0) + { + var intents = this.Configuration.Intents; + this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages) + ? new RingBuffer(this.Configuration.MessageCacheSize) + : null; + } + + this.InternalSetup(); + + this.Guilds = new ReadOnlyConcurrentDictionary(this._guilds); + this.PrivateChannels = new ReadOnlyConcurrentDictionary(this._privateChannels); + } + internal void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", this.Goof); @@ -1030,13 +1052,8 @@ private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportU #region Disposal - ~DiscordClient() - { - this.Dispose(); - } - - private bool _disposed; + /// /// Disposes your DiscordClient. /// @@ -1046,17 +1063,19 @@ public override void Dispose() return; this._disposed = true; - GC.SuppressFinalize(this); - this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - this.ApiClient._rest.Dispose(); - this.CurrentUser = null; + this.DisconnectAsync().GetAwaiter().GetResult(); + this.ApiClient?._rest?.Dispose(); + this.CurrentUser = null!; var extensions = this._extensions; // prevent _extensions being modified during dispose - this._extensions = null; + this._extensions = null!; + foreach (var extension in extensions) + { if (extension is IDisposable disposable) disposable.Dispose(); + } try { @@ -1065,9 +1084,9 @@ public override void Dispose() } catch { } - this._guilds = null; - this._heartbeatTask = null; - this._privateChannels = null; + this._guilds = null!; + this._heartbeatTask = null!; + this._privateChannels = null!; } #endregion diff --git a/DSharpPlus/Clients/DiscordShardedClient.cs b/DSharpPlus/Clients/DiscordShardedClient.cs index 25d80fa124..3091ee5734 100644 --- a/DSharpPlus/Clients/DiscordShardedClient.cs +++ b/DSharpPlus/Clients/DiscordShardedClient.cs @@ -241,24 +241,25 @@ public async Task InitializeShardsAsync() if (this._shards.Count != 0) return this._shards.Count; - this.GatewayInfo = await this.GetGatewayInfoAsync().ConfigureAwait(false); - var shardc = this.Configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; - var lf = new ShardedLoggerFactory(this.Logger); - for (var i = 0; i < shardc; i++) + this.GatewayInfo = await this.GetGatewayInfoAsync(); + int shardCount = this.Configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; + ShardedLoggerFactory loggerFactory = new ShardedLoggerFactory(this.Configuration.LoggerFactory); + RestClient restClient = new RestClient(this.Configuration, loggerFactory.CreateLogger("Rest")); + for (int i = 0; i < shardCount; i++) + { + DiscordConfiguration cfg = new DiscordConfiguration(this.Configuration) { - var cfg = new DiscordConfiguration(this.Configuration) - { - ShardId = i, - ShardCount = shardc, - LoggerFactory = lf - }; + ShardId = i, + ShardCount = shardCount, + LoggerFactory = loggerFactory + }; - var client = new DiscordClient(cfg); + var client = new DiscordClient(cfg, restClient); if (!this._shards.TryAdd(i, client)) throw new InvalidOperationException("Could not initialize shards."); } - return shardc; + return shardCount; } #endregion @@ -402,12 +403,12 @@ private Task InternalStopAsync(bool enableLogger = true) if (!this._isStarted) throw new InvalidOperationException("This client has not been started."); + this._isStarted = false; + if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disposing {ShardCount} shards.", this._shards.Count); - this._isStarted = false; this._voiceRegionsLazy = null; - this.GatewayInfo = null; this.CurrentUser = null; this.CurrentApplication = null; @@ -671,7 +672,10 @@ private int GetShardIdFromGuilds(ulong id) #region Destructor ~DiscordShardedClient() - => this.InternalStopAsync(false).GetAwaiter().GetResult(); + { + if (this._isStarted) + this.InternalStopAsync(false).GetAwaiter().GetResult(); + } #endregion } diff --git a/DSharpPlus/Clients/DiscordWebhookClient.cs b/DSharpPlus/Clients/DiscordWebhookClient.cs index 869404f098..6e6823aa51 100644 --- a/DSharpPlus/Clients/DiscordWebhookClient.cs +++ b/DSharpPlus/Clients/DiscordWebhookClient.cs @@ -272,8 +272,8 @@ public async Task> BroadcastMessageAs ~DiscordWebhookClient() { - this._hooks.Clear(); - this._hooks = null; + this._hooks?.Clear(); + this._hooks = null!; this._apiclient._rest.Dispose(); } } diff --git a/DSharpPlus/Entities/Channel/DiscordChannel.cs b/DSharpPlus/Entities/Channel/DiscordChannel.cs index 21d136d820..0fb8f28a9a 100644 --- a/DSharpPlus/Entities/Channel/DiscordChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordChannel.cs @@ -236,6 +236,7 @@ public DiscordVoiceRegion RtcRegion /// Only sent on the resolved channels of interaction responses for application commands. /// [JsonProperty("permissions")] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions? UserPermissions { get; internal set; } internal DiscordChannel() diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs index a4ffc54c1b..182fc8e330 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs @@ -26,6 +26,7 @@ using System.Collections.ObjectModel; using System.Globalization; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Newtonsoft.Json; @@ -102,7 +103,7 @@ public DiscordChannel Channel /// Gets the components this message was sent with. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyCollection Components { get; internal set; } + public IReadOnlyCollection Components { get; internal set; } /// /// Gets the user or member that sent the message. @@ -440,6 +441,35 @@ internal void PopulateMentions() this._mentionedUsers = mentionedUsers.ToList(); } + /// + /// Searches the components on this message for an aggregate of all components of a certain type. + /// + public IReadOnlyList FilterComponents() + where T : DiscordComponent + { + List components = new(); + + foreach (DiscordComponent component in this.Components) + { + if (component is DiscordActionRowComponent actionRowComponent) + { + foreach (DiscordComponent subComponent in actionRowComponent.Components) + { + if (subComponent is T filteredComponent) + { + components.Add(filteredComponent); + } + } + } + else if (component is T filteredComponent) + { + components.Add(filteredComponent); + } + } + + return components; + } + /// /// Edits the message. /// diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs index bdf388bb08..44e6a4240e 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs @@ -115,7 +115,10 @@ public DiscordMessageBuilder(DiscordMessage baseMessage) { this.IsTTS = baseMessage.IsTTS; this.ReplyId = baseMessage.ReferencedMessage?.Id; - this._components = baseMessage.Components.ToList(); // Calling ToList copies the list instead of referencing it + + // calling tolist copies the list, which is good since we inaccurately reflect the message in case of + // unknown components + this._components = baseMessage.Components.OfType().ToList(); this._content = baseMessage.Content; this._embeds = baseMessage.Embeds.ToList(); this._stickers = baseMessage.Stickers.ToList(); diff --git a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs index 134d6dbc6c..a890b5e45b 100644 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs +++ b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs @@ -23,6 +23,7 @@ using System; using System.Threading.Tasks; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Entities @@ -42,12 +43,14 @@ public class DiscordOverwrite : SnowflakeObject /// Gets the allowed permission set. /// [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions Allowed { get; internal set; } /// /// Gets the denied permission set. /// [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions Denied { get; internal set; } [JsonIgnore] diff --git a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs index 22bd400c63..5c0a2974ea 100644 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs +++ b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs @@ -23,6 +23,7 @@ using System; using System.Threading.Tasks; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Entities @@ -158,9 +159,11 @@ internal DiscordRestOverwrite Build() internal struct DiscordRestOverwrite { [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] internal Permissions Allow { get; set; } [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] internal Permissions Deny { get; set; } [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] diff --git a/DSharpPlus/Entities/Guild/DiscordGuild.cs b/DSharpPlus/Entities/Guild/DiscordGuild.cs index aeeaa3e0b4..2b8f57ac41 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuild.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuild.cs @@ -109,6 +109,7 @@ public string DiscoverySplashUrl /// Gets permissions for the user in the guild (does not include channel overrides) /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions? Permissions { get; set; } /// diff --git a/DSharpPlus/Entities/Guild/DiscordRole.cs b/DSharpPlus/Entities/Guild/DiscordRole.cs index 9ae6a145db..880c278011 100644 --- a/DSharpPlus/Entities/Guild/DiscordRole.cs +++ b/DSharpPlus/Entities/Guild/DiscordRole.cs @@ -27,6 +27,7 @@ using System.Threading.Tasks; using DSharpPlus.Net.Abstractions; using DSharpPlus.Net.Models; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Entities @@ -87,6 +88,7 @@ public DiscordColor Color /// Gets the permissions set for this role. /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions Permissions { get; internal set; } /// diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs index 776f34af05..f86592561b 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs @@ -26,6 +26,7 @@ using System.Collections.ObjectModel; using System.Globalization; using System.Linq; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Entities @@ -81,6 +82,7 @@ public sealed class DiscordApplicationCommand : SnowflakeObject, IEquatable [JsonProperty("default_member_permissions")] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions? DefaultMemberPermissions { get; internal set; } @@ -126,11 +128,8 @@ public DiscordApplicationCommand(string name, string description, IEnumerable 100) throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); } - else + else if (type == ApplicationCommandType.UserContextMenu || type == ApplicationCommandType.MessageContextMenu) { - if (!string.IsNullOrWhiteSpace(description)) - throw new ArgumentException("Context menus do not support descriptions."); - if (options?.Any() ?? false) throw new ArgumentException("Context menus do not support options."); } diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs index 1767ae3fa4..c63b999c17 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs @@ -65,10 +65,10 @@ public sealed class DiscordInteractionData : SnowflakeObject /// /// Components on this interaction. Only applies to modal interactions. /// - public IReadOnlyList Components => this._components; + public IReadOnlyList Components => this._components; [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - internal List _components; + internal List _components; /// /// The Id of the target. Applicable for context menus. diff --git a/DSharpPlus/EventArgs/Interaction/ModalSubmitEventArgs.cs b/DSharpPlus/EventArgs/Interaction/ModalSubmitEventArgs.cs index f3bd769b45..4adfbf466b 100644 --- a/DSharpPlus/EventArgs/Interaction/ModalSubmitEventArgs.cs +++ b/DSharpPlus/EventArgs/Interaction/ModalSubmitEventArgs.cs @@ -46,7 +46,7 @@ internal ModalSubmitEventArgs(DiscordInteraction interaction) var dict = new Dictionary(); foreach (var component in interaction.Data._components) - if (component.Components.First() is TextInputComponent input) + if ((component as DiscordActionRowComponent)?.Components.First() is TextInputComponent input) dict.Add(input.CustomId, input.Value); this.Values = dict; diff --git a/DSharpPlus/Logging/DefaultLoggerFactory.cs b/DSharpPlus/Logging/DefaultLoggerFactory.cs index 1303a625c9..a498ca66e5 100644 --- a/DSharpPlus/Logging/DefaultLoggerFactory.cs +++ b/DSharpPlus/Logging/DefaultLoggerFactory.cs @@ -34,15 +34,10 @@ internal class DefaultLoggerFactory : ILoggerFactory public void AddProvider(ILoggerProvider provider) => this.Providers.Add(provider); - public ILogger CreateLogger(string categoryName) - { - if (this._isDisposed) - throw new InvalidOperationException("This logger factory is already disposed."); - - return categoryName != typeof(BaseDiscordClient).FullName && categoryName != typeof(DiscordWebhookClient).FullName - ? throw new ArgumentException($"This factory can only provide instances of loggers for {typeof(BaseDiscordClient).FullName} or {typeof(DiscordWebhookClient).FullName}.", nameof(categoryName)) - : new CompositeDefaultLogger(this.Providers); - } + public ILogger CreateLogger(string categoryName) => + this._isDisposed + ? throw new InvalidOperationException("This logger factory is already disposed.") + : new CompositeDefaultLogger(this.Providers); public void Dispose() { diff --git a/DSharpPlus/Logging/ShardedLoggerFactory.cs b/DSharpPlus/Logging/ShardedLoggerFactory.cs index aa8d974245..e865523a00 100644 --- a/DSharpPlus/Logging/ShardedLoggerFactory.cs +++ b/DSharpPlus/Logging/ShardedLoggerFactory.cs @@ -23,26 +23,20 @@ using System; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; namespace DSharpPlus { internal class ShardedLoggerFactory : ILoggerFactory { - private ILogger Logger { get; } + private ConcurrentDictionary Loggers { get; } = new(); + private ILoggerFactory Factory { get; } - public ShardedLoggerFactory(ILogger instance) - { - this.Logger = instance; - } + public ShardedLoggerFactory(ILoggerFactory factory) => this.Factory = factory; public void AddProvider(ILoggerProvider provider) => throw new InvalidOperationException("This is a passthrough logger container, it cannot register new providers."); - public ILogger CreateLogger(string categoryName) - { - return categoryName != typeof(BaseDiscordClient).FullName - ? throw new ArgumentException($"This factory can only provide instances of loggers for {typeof(BaseDiscordClient).FullName}.", nameof(categoryName)) - : this.Logger; - } + public ILogger CreateLogger(string categoryName) => this.Loggers.GetOrAdd(categoryName, this.Factory.CreateLogger(categoryName)); public void Dispose() { } diff --git a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index 4c7607c67c..99d192aacb 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using DSharpPlus.Entities; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Net.Abstractions @@ -54,8 +55,8 @@ internal class RestApplicationCommandCreatePayload public bool? AllowDMUsage { get; set; } [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions? DefaultMemberPermissions { get; set; } - } internal class RestApplicationCommandEditPayload diff --git a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs index 243d3b50b9..5a0b802ec3 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Generic; using DSharpPlus.Entities; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Net.Abstractions @@ -287,9 +288,11 @@ internal sealed class RestChannelInviteCreatePayload internal sealed class RestChannelPermissionEditPayload { [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions Allow { get; set; } [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions Deny { get; set; } [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] diff --git a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs index 77c107bcb8..e9d1ae4dae 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Generic; using DSharpPlus.Entities; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Net.Abstractions @@ -261,6 +262,7 @@ internal sealed class RestGuildRolePayload public string Name { get; set; } [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions? Permissions { get; set; } [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] diff --git a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs index 1a1bbb3216..10e2ac2bc2 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs @@ -22,6 +22,7 @@ // SOFTWARE. using System.Collections.Generic; +using DSharpPlus.Net.Serialization; using Newtonsoft.Json; namespace DSharpPlus.Net.Abstractions @@ -71,6 +72,7 @@ internal sealed class RestUserGuild public bool IsOwner { get; set; } [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsJsonConverter))] public Permissions Permissions { get; set; } } diff --git a/DSharpPlus/Net/Rest/DiscordApiClient.cs b/DSharpPlus/Net/Rest/DiscordApiClient.cs index 507eddbe9b..b0905398c2 100644 --- a/DSharpPlus/Net/Rest/DiscordApiClient.cs +++ b/DSharpPlus/Net/Rest/DiscordApiClient.cs @@ -46,10 +46,10 @@ public sealed class DiscordApiClient internal BaseDiscordClient _discord { get; } internal RestClient _rest { get; } - internal DiscordApiClient(BaseDiscordClient client) + internal DiscordApiClient(BaseDiscordClient client, RestClient rest = null) { this._discord = client; - this._rest = new RestClient(client); + this._rest = rest ?? new RestClient(client.Configuration, client.Logger); } internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client diff --git a/DSharpPlus/Net/Rest/RestClient.cs b/DSharpPlus/Net/Rest/RestClient.cs index abed35bb40..6f495dc76b 100644 --- a/DSharpPlus/Net/Rest/RestClient.cs +++ b/DSharpPlus/Net/Rest/RestClient.cs @@ -60,11 +60,10 @@ internal sealed class RestClient : IDisposable private Task _cleanerTask; private volatile bool _disposed; - internal RestClient(BaseDiscordClient client) - : this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger) + internal RestClient(DiscordConfiguration config, ILogger logger) + : this(config.Proxy, config.HttpTimeout, config.UseRelativeRatelimit, logger) { - this.Discord = client; - this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client)); + this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(config)); } internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit, @@ -677,9 +676,6 @@ private async Task CleanupBucketsAsync() this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } - ~RestClient() - => this.Dispose(); - public void Dispose() { if (this._disposed) @@ -687,7 +683,7 @@ public void Dispose() this._disposed = true; - this.GlobalRateLimitEvent.Reset(); + this.GlobalRateLimitEvent?.Reset(); if (this._bucketCleanerTokenSource?.IsCancellationRequested == false) { @@ -703,9 +699,9 @@ public void Dispose() } catch { } - this.RoutesToHashes.Clear(); - this.HashesToBuckets.Clear(); - this.RequestQueue.Clear(); + this.RoutesToHashes?.Clear(); + this.HashesToBuckets?.Clear(); + this.RequestQueue?.Clear(); } } } diff --git a/DSharpPlus/Net/Serialization/DiscordJson.cs b/DSharpPlus/Net/Serialization/DiscordJson.cs index 67db38ad0b..e09b405e11 100644 --- a/DSharpPlus/Net/Serialization/DiscordJson.cs +++ b/DSharpPlus/Net/Serialization/DiscordJson.cs @@ -37,7 +37,7 @@ public static class DiscordJson { ContractResolver = new OptionalJsonContractResolver(), DateParseHandling = DateParseHandling.None, - Converters = new[] { new ISO8601DateTimeOffsetJsonConverter() } + Converters = new JsonConverter[] { new ISO8601DateTimeOffsetJsonConverter(), new DiscordPermissionsJsonConverter() } }); /// Serializes the specified object to a JSON string. diff --git a/DSharpPlus/Net/Serialization/DiscordPermissionsJsonConverter.cs b/DSharpPlus/Net/Serialization/DiscordPermissionsJsonConverter.cs new file mode 100644 index 0000000000..963f1f174c --- /dev/null +++ b/DSharpPlus/Net/Serialization/DiscordPermissionsJsonConverter.cs @@ -0,0 +1,51 @@ +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2023 DSharpPlus Contributors +// +// 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: +// +// 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. + +using System; +using System.Globalization; +using System.Numerics; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Serialization +{ + internal class DiscordPermissionsJsonConverter : JsonConverter + { + public override Permissions ReadJson(JsonReader reader, Type objectType, Permissions existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + + if (token == null || token.Type == JTokenType.Null) + { + return Permissions.None; + } + + var value = token.ToObject(); + + return (Permissions)(ulong)(value & ulong.MaxValue); + } + + public override void WriteJson(JsonWriter writer, Permissions value, JsonSerializer serializer) + => writer.WriteValue(((ulong)value).ToString(CultureInfo.InvariantCulture)); + } +}