From a64ab6025b4da17fcfea1876d54fdd032d5361dd Mon Sep 17 00:00:00 2001 From: Nathan Solomon Date: Tue, 27 Nov 2018 16:16:32 -0600 Subject: [PATCH 01/13] Resolve Issue #1188 (Allow Users to specify position when creating a new channel) (#1196) * Added ability to specify position when creating a channel * Adjusted categories to include guildproperties and allow specifying position when creating channel categories * fixed unimplemented methods (for CreateCategoryChannelAsync) and added appropriate documentation --- src/Discord.Net.Core/Entities/Guilds/IGuild.cs | 3 ++- .../API/Rest/CreateGuildChannelParams.cs | 2 ++ .../Entities/Guilds/GuildHelper.cs | 15 ++++++++++++--- src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs | 9 +++++---- .../Entities/Guilds/SocketGuild.cs | 9 +++++---- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs index a7206bd59..3b35796b9 100644 --- a/src/Discord.Net.Core/Entities/Guilds/IGuild.cs +++ b/src/Discord.Net.Core/Entities/Guilds/IGuild.cs @@ -474,12 +474,13 @@ namespace Discord /// Creates a new channel category in this guild. /// /// The new name for the category. + /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// category channel. /// - Task CreateCategoryAsync(string name, RequestOptions options = null); + Task CreateCategoryAsync(string name, Action func = null, RequestOptions options = null); /// /// Gets a collection of all the voice regions this guild can access. diff --git a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs index 05cdf4b8a..a102bd38d 100644 --- a/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs +++ b/src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs @@ -12,6 +12,8 @@ namespace Discord.API.Rest public ChannelType Type { get; } [JsonProperty("parent_id")] public Optional CategoryId { get; set; } + [JsonProperty("position")] + public Optional Position { get; set; } //Text channels [JsonProperty("topic")] diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index c31fa89f2..affa74685 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -163,6 +163,7 @@ namespace Discord.Rest CategoryId = props.CategoryId, Topic = props.Topic, IsNsfw = props.IsNsfw, + Position = props.Position }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestTextChannel.Create(client, guild, model); @@ -180,18 +181,26 @@ namespace Discord.Rest { CategoryId = props.CategoryId, Bitrate = props.Bitrate, - UserLimit = props.UserLimit + UserLimit = props.UserLimit, + Position = props.Position }; var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestVoiceChannel.Create(client, guild, model); } /// is null. public static async Task CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, - string name, RequestOptions options) + string name, RequestOptions options, Action func = null) { if (name == null) throw new ArgumentNullException(paramName: nameof(name)); - var args = new CreateGuildChannelParams(name, ChannelType.Category); + var props = new GuildChannelProperties(); + func?.Invoke(props); + + var args = new CreateGuildChannelParams(name, ChannelType.Category) + { + Position = props.Position + }; + var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); return RestCategoryChannel.Create(client, guild, model); } diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index bd70bf96b..a847998b5 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -441,13 +441,14 @@ namespace Discord.Rest /// Creates a category channel with the provided name. /// /// The name of the new channel. + /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// is null. /// /// The created category channel. /// - public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); /// /// Gets a collection of all the voice regions this guild can access. @@ -776,8 +777,8 @@ namespace Discord.Rest async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); /// - async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) - => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) + => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 412f3acff..4e4124679 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -561,14 +561,15 @@ namespace Discord.WebSocket /// Creates a new channel category in this guild. /// /// The new name for the category. + /// The delegate containing the properties to be applied to the channel upon its creation. /// The options to be used when sending the request. /// is null. /// /// A task that represents the asynchronous creation operation. The task result contains the newly created /// category channel. /// - public Task CreateCategoryChannelAsync(string name, RequestOptions options = null) - => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); + public Task CreateCategoryChannelAsync(string name, Action func = null, RequestOptions options = null) + => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) { @@ -1069,8 +1070,8 @@ namespace Discord.WebSocket async Task IGuild.CreateVoiceChannelAsync(string name, Action func, RequestOptions options) => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); /// - async Task IGuild.CreateCategoryAsync(string name, RequestOptions options) - => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); + async Task IGuild.CreateCategoryAsync(string name, Action func, RequestOptions options) + => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); /// async Task> IGuild.GetVoiceRegionsAsync(RequestOptions options) From f005af37b401e921d5402a9d4db8519bdcdd5192 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Tue, 27 Nov 2018 17:31:47 -0500 Subject: [PATCH 02/13] feature: add Format.Url, Format.EscapeUrl Format.Url formats a URL into a markdown `[]()` masked URL. Format.EscapeUrl formats a URL into a Discord `<>` escaped URL. --- src/Discord.Net.Core/Format.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Discord.Net.Core/Format.cs b/src/Discord.Net.Core/Format.cs index 0b16daeb1..fec9b607a 100644 --- a/src/Discord.Net.Core/Format.cs +++ b/src/Discord.Net.Core/Format.cs @@ -14,6 +14,10 @@ namespace Discord public static string Underline(string text) => $"__{text}__"; /// Returns a markdown-formatted string with strikethrough formatting. public static string Strikethrough(string text) => $"~~{text}~~"; + /// Returns a markdown-formatted URL. Only works in descriptions and fields. + public static string Url(string text, string url) => $"[{text}]({url})"; + /// Escapes a URL so that a preview is not generated. + public static string EscapeUrl(string url) => $"<{url}>"; /// Returns a markdown-formatted string with codeblock formatting. public static string Code(string text, string language = null) From 5421df14fe5f0a90a07863bdf3690e8ff449e9ae Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Tue, 27 Nov 2018 17:49:30 -0500 Subject: [PATCH 03/13] feature: add extensions for bulk reactions this is not api-level bulk reactions (!) --- .../Extensions/MessageExtensions.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/Discord.Net.Core/Extensions/MessageExtensions.cs b/src/Discord.Net.Core/Extensions/MessageExtensions.cs index 90185cb6d..90ebea92f 100644 --- a/src/Discord.Net.Core/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Core/Extensions/MessageExtensions.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; + namespace Discord { /// @@ -17,5 +19,57 @@ namespace Discord var channel = msg.Channel; return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; } + + /// + /// Add multiple reactions to a message. + /// + /// + /// This method does not bulk add reactions! It will send a request for each reaction inculded. + /// + /// + /// + /// IEmote A = new Emoji("🅰"); + /// IEmote B = new Emoji("🅱"); + /// await msg.AddReactionsAsync(new[] { A, B }); + /// + /// + /// The message to add reactions to. + /// An array of reactions to add to the message + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for adding a reaction to this message. + /// + /// + /// + public static async Task AddReactionsAsync(this IUserMessage msg, IEmote[] reactions, RequestOptions options = null) + { + foreach (var rxn in reactions) + await msg.AddReactionAsync(rxn, options).ConfigureAwait(false); + } + /// + /// Remove multiple reactions from a message. + /// + /// + /// This method does not bulk remove reactions! If you want to clear reactions from a message, + /// + /// + /// + /// + /// await msg.RemoveReactionsAsync(currentUser, new[] { A, B }); + /// + /// + /// The message to remove reactions from. + /// An array of reactions to remove from the message + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous operation for removing a reaction to this message. + /// + /// + /// + public static async Task RemoveReactionsAsync(this IUserMessage msg, IUser user, IEmote[] reactions, RequestOptions options = null) + { + foreach (var rxn in reactions) + await msg.RemoveReactionAsync(rxn, user, options).ConfigureAwait(false); + } } } From dca6c33da334bf048afcc81ac8a13eb6905b8946 Mon Sep 17 00:00:00 2001 From: jaypaw549 <17625734+jaypaw549@users.noreply.github.com> Date: Wed, 28 Nov 2018 18:17:08 -0700 Subject: [PATCH 04/13] fix: Update ChannelCreateAuditLogData.cs (#1195) --- .../AuditLogs/DataTypes/ChannelCreateAuditLogData.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs index 51a19a0de..a692829f4 100644 --- a/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs +++ b/src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs @@ -33,10 +33,10 @@ namespace Discord.Rest foreach (var overwrite in overwritesModel.NewValue) { - var deny = overwrite.Value("deny"); - var permType = overwrite.Value("type"); - var id = overwrite.Value("id"); - var allow = overwrite.Value("allow"); + var deny = overwrite["deny"].ToObject(discord.ApiClient.Serializer); + var permType = overwrite["type"].ToObject(discord.ApiClient.Serializer); + var id = overwrite["id"].ToObject(discord.ApiClient.Serializer); + var allow = overwrite["allow"].ToObject(discord.ApiClient.Serializer); overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); } From 7366cd4361bc5052766ad85e87066ea54075a15c Mon Sep 17 00:00:00 2001 From: Monica S Date: Thu, 29 Nov 2018 01:18:16 +0000 Subject: [PATCH 05/13] feature: Implement Dispose for types which have disposable data (#1171) * Initial set of dispose implementations Not handled yet: - Discord.Net.Websocket/Entities/SocketGuild - Discord.Net.Tests * Refactor DiscordSocketClient init into ctor This way we remove an IDisposableAnalyzer warning for not disposing the client when we set the client variable. * Dispose of clients when disposing sharded client * Finish implementing IDisposable where appropriate I opted to use NoWarn in the Tests project as it wasn't really necessary considering that our tests only run once * Tweak samples after feedback --- samples/01_basic_ping_bot/Program.cs | 13 ++++-- samples/02_commands_framework/Program.cs | 28 +++++++----- samples/03_sharded_client/Program.cs | 37 +++++++++------- src/Discord.Net.Commands/CommandService.cs | 26 +++++++++-- .../Discord.Net.Commands.csproj | 2 +- src/Discord.Net.Core/Discord.Net.Core.csproj | 3 ++ src/Discord.Net.Core/Entities/Image.cs | 26 +++++++++-- src/Discord.Net.Core/Net/Rest/IRestClient.cs | 3 +- src/Discord.Net.Core/Net/Udp/IUdpSocket.cs | 2 +- .../Net/WebSockets/IWebSocketClient.cs | 2 +- .../WS4NetClient.cs | 22 +++++++--- src/Discord.Net.Rest/BaseDiscordClient.cs | 13 +++--- src/Discord.Net.Rest/DiscordRestApiClient.cs | 8 +++- src/Discord.Net.Rest/DiscordRestClient.cs | 12 +++--- .../Net/Converters/ImageConverter.cs | 14 +++--- src/Discord.Net.Rest/Net/DefaultRestClient.cs | 28 ++++++++---- .../Net/Queue/RequestQueue.cs | 39 ++++++++++++----- .../Audio/AudioClient.cs | 25 +++++------ .../Audio/Streams/BufferedWriteStream.cs | 26 ++++++++--- .../Audio/Streams/InputStream.cs | 12 +++++- .../ConnectionManager.cs | 43 +++++++++++++++---- .../DiscordShardedClient.cs | 35 ++++++++++++--- .../DiscordSocketApiClient.cs | 5 ++- .../DiscordSocketClient.cs | 24 ++++++++--- .../DiscordVoiceApiClient.cs | 14 +++--- .../Entities/Guilds/SocketGuild.cs | 20 ++++++++- .../Net/DefaultUdpSocket.cs | 27 +++++++++--- .../Net/DefaultWebSocketClient.cs | 35 ++++++++++----- .../Discord.Net.Webhook.csproj | 2 +- .../Discord.Net.Tests.csproj | 1 + .../Discord.Net.Tests/Net/CachedRestClient.cs | 11 +++-- 31 files changed, 405 insertions(+), 153 deletions(-) diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs index 973a6ce95..4d6674e97 100644 --- a/samples/01_basic_ping_bot/Program.cs +++ b/samples/01_basic_ping_bot/Program.cs @@ -16,21 +16,28 @@ namespace _01_basic_ping_bot // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library class Program { - private DiscordSocketClient _client; + private readonly DiscordSocketClient _client; // Discord.Net heavily utilizes TAP for async, so we create // an asynchronous context from the beginning. static void Main(string[] args) - => new Program().MainAsync().GetAwaiter().GetResult(); + { + new Program().MainAsync().GetAwaiter().GetResult(); + } - public async Task MainAsync() + public Program() { + // It is recommended to Dispose of a client when you are finished + // using it, at the end of your app's lifetime. _client = new DiscordSocketClient(); _client.Log += LogAsync; _client.Ready += ReadyAsync; _client.MessageReceived += MessageReceivedAsync; + } + public async Task MainAsync() + { // Tokens should be considered secret data, and never hard-coded. await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); await _client.StartAsync(); diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs index 136494bdf..76c11f9f0 100644 --- a/samples/02_commands_framework/Program.cs +++ b/samples/02_commands_framework/Program.cs @@ -19,24 +19,32 @@ namespace _02_commands_framework // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library class Program { + // There is no need to implement IDisposable like before as we are + // using dependency injection, which handles calling Dispose for us. static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public async Task MainAsync() { - var services = ConfigureServices(); + // You should dispose a service provider created using ASP.NET + // when you are finished using it, at the end of your app's lifetime. + // If you use another dependency injection framework, you should inspect + // its documentation for the best way to do this. + using (var services = ConfigureServices()) + { + var client = services.GetRequiredService(); - var client = services.GetRequiredService(); + client.Log += LogAsync; + services.GetRequiredService().Log += LogAsync; - client.Log += LogAsync; - services.GetRequiredService().Log += LogAsync; + // Tokens should be considered secret data, and never hard-coded. + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await client.StartAsync(); - await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); - await client.StartAsync(); + await services.GetRequiredService().InitializeAsync(); - await services.GetRequiredService().InitializeAsync(); - - await Task.Delay(-1); + await Task.Delay(-1); + } } private Task LogAsync(LogMessage log) @@ -46,7 +54,7 @@ namespace _02_commands_framework return Task.CompletedTask; } - private IServiceProvider ConfigureServices() + private ServiceProvider ConfigureServices() { return new ServiceCollection() .AddSingleton() diff --git a/samples/03_sharded_client/Program.cs b/samples/03_sharded_client/Program.cs index 048145f9f..7a2f99168 100644 --- a/samples/03_sharded_client/Program.cs +++ b/samples/03_sharded_client/Program.cs @@ -13,41 +13,46 @@ namespace _03_sharded_client // DiscordSocketClient instances (or shards) to serve a large number of guilds. class Program { - private DiscordShardedClient _client; - static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult(); public async Task MainAsync() { // You specify the amount of shards you'd like to have with the - // DiscordSocketConfig. Generally, it's recommended to + // DiscordSocketConfig. Generally, it's recommended to // have 1 shard per 1500-2000 guilds your bot is in. var config = new DiscordSocketConfig { TotalShards = 2 }; - _client = new DiscordShardedClient(config); - var services = ConfigureServices(); + // You should dispose a service provider created using ASP.NET + // when you are finished using it, at the end of your app's lifetime. + // If you use another dependency injection framework, you should inspect + // its documentation for the best way to do this. + using (var services = ConfigureServices(config)) + { + var client = services.GetRequiredService(); - // The Sharded Client does not have a Ready event. - // The ShardReady event is used instead, allowing for individual - // control per shard. - _client.ShardReady += ReadyAsync; - _client.Log += LogAsync; + // The Sharded Client does not have a Ready event. + // The ShardReady event is used instead, allowing for individual + // control per shard. + client.ShardReady += ReadyAsync; + client.Log += LogAsync; - await services.GetRequiredService().InitializeAsync(); + await services.GetRequiredService().InitializeAsync(); - await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); - await _client.StartAsync(); + // Tokens should be considered secret data, and never hard-coded. + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); + await client.StartAsync(); - await Task.Delay(-1); + await Task.Delay(-1); + } } - private IServiceProvider ConfigureServices() + private ServiceProvider ConfigureServices(DiscordSocketConfig config) { return new ServiceCollection() - .AddSingleton(_client) + .AddSingleton(new DiscordShardedClient(config)) .AddSingleton() .AddSingleton() .BuildServiceProvider(); diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d5b3f9ff4..c36aec4a5 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -27,7 +27,7 @@ namespace Discord.Commands /// been successfully executed. /// /// - public class CommandService + public class CommandService : IDisposable { /// /// Occurs when a command-related information is received. @@ -67,6 +67,8 @@ namespace Discord.Commands internal readonly LogManager _logManager; internal readonly IReadOnlyDictionary _quotationMarkAliasMap; + internal bool _isDisposed; + /// /// Represents all modules loaded within . /// @@ -330,9 +332,9 @@ namespace Discord.Commands //Type Readers /// - /// Adds a custom to this for the supplied object + /// Adds a custom to this for the supplied object /// type. - /// If is a , a nullable will + /// If is a , a nullable will /// also be added. /// If a default exists for , a warning will be logged /// and the default will be replaced. @@ -607,5 +609,23 @@ namespace Discord.Commands await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); return result; } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _moduleLock?.Dispose(); + } + + _isDisposed = true; + } + } + + void IDisposable.Dispose() + { + Dispose(true); + } } } diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj index a754486dd..1bef1bfea 100644 --- a/src/Discord.Net.Commands/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj index d4d450e1c..ff9b3c5e0 100644 --- a/src/Discord.Net.Core/Discord.Net.Core.csproj +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -12,4 +12,7 @@ + + + diff --git a/src/Discord.Net.Core/Entities/Image.cs b/src/Discord.Net.Core/Entities/Image.cs index d5a9e26a3..3f5a01f6a 100644 --- a/src/Discord.Net.Core/Entities/Image.cs +++ b/src/Discord.Net.Core/Entities/Image.cs @@ -1,15 +1,21 @@ +using System; using System.IO; + namespace Discord { /// /// An image that will be uploaded to Discord. /// - public struct Image + public struct Image : IDisposable { + private bool _isDisposed; + /// /// Gets the stream to be uploaded to Discord. /// +#pragma warning disable IDISP008 public Stream Stream { get; } +#pragma warning restore IDISP008 /// /// Create the image with a . /// @@ -19,6 +25,7 @@ namespace Discord /// public Image(Stream stream) { + _isDisposed = false; Stream = stream; } @@ -45,15 +52,28 @@ namespace Discord /// The specified is invalid, (for example, it is on an unmapped drive). /// /// - /// specified a directory.-or- The caller does not have the required permission. + /// specified a directory.-or- The caller does not have the required permission. /// - /// The file specified in was not found. + /// The file specified in was not found. /// /// An I/O error occurred while opening the file. public Image(string path) { + _isDisposed = false; Stream = File.OpenRead(path); } + /// + public void Dispose() + { + if (!_isDisposed) + { +#pragma warning disable IDISP007 + Stream?.Dispose(); +#pragma warning restore IDISP007 + + _isDisposed = true; + } + } } } diff --git a/src/Discord.Net.Core/Net/Rest/IRestClient.cs b/src/Discord.Net.Core/Net/Rest/IRestClient.cs index 2e30d2ef4..71010f70d 100644 --- a/src/Discord.Net.Core/Net/Rest/IRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/IRestClient.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -7,7 +8,7 @@ namespace Discord.Net.Rest /// /// Represents a generic REST-based client. /// - public interface IRestClient + public interface IRestClient : IDisposable { /// /// Sets the HTTP header of this client for all requests. diff --git a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs index 10ac652b3..ed2881d1f 100644 --- a/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs +++ b/src/Discord.Net.Core/Net/Udp/IUdpSocket.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.Udp { - public interface IUdpSocket + public interface IUdpSocket : IDisposable { event Func ReceivedDatagram; diff --git a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs index 7eccaabf2..14b41cce1 100644 --- a/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public interface IWebSocketClient + public interface IWebSocketClient : IDisposable { event Func BinaryMessage; event Func TextMessage; diff --git a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs index afc01f87a..ef99c8045 100644 --- a/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs +++ b/src/Discord.Net.Providers.WS4Net/WS4NetClient.cs @@ -19,6 +19,7 @@ namespace Discord.Net.Providers.WS4Net private readonly SemaphoreSlim _lock; private readonly Dictionary _headers; private WS4NetSocket _client; + private CancellationTokenSource _disconnectCancelTokenSource; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private ManualResetEventSlim _waitUntilConnect; @@ -28,7 +29,7 @@ namespace Discord.Net.Providers.WS4Net { _headers = new Dictionary(); _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); + _disconnectCancelTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _waitUntilConnect = new ManualResetEventSlim(); @@ -38,7 +39,11 @@ namespace Discord.Net.Providers.WS4Net if (!_isDisposed) { if (disposing) + { DisconnectInternalAsync(true).GetAwaiter().GetResult(); + _lock?.Dispose(); + _cancelTokenSource?.Dispose(); + } _isDisposed = true; } } @@ -63,8 +68,13 @@ namespace Discord.Net.Providers.WS4Net { await DisconnectInternalAsync().ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _disconnectCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _client?.Dispose(); + + _disconnectCancelTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; _client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList()) { @@ -96,7 +106,7 @@ namespace Discord.Net.Providers.WS4Net } private Task DisconnectInternalAsync(bool isDisposing = false) { - _cancelTokenSource.Cancel(); + _disconnectCancelTokenSource.Cancel(); if (_client == null) return Task.Delay(0); @@ -125,8 +135,10 @@ namespace Discord.Net.Providers.WS4Net } public void SetCancelToken(CancellationToken cancelToken) { + _cancelTokenSource?.Dispose(); _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; } public async Task SendAsync(byte[] data, int index, int count, bool isText) diff --git a/src/Discord.Net.Rest/BaseDiscordClient.cs b/src/Discord.Net.Rest/BaseDiscordClient.cs index b584f5764..fc938d04d 100644 --- a/src/Discord.Net.Rest/BaseDiscordClient.cs +++ b/src/Discord.Net.Rest/BaseDiscordClient.cs @@ -34,7 +34,7 @@ namespace Discord.Rest public ISelfUser CurrentUser { get; protected set; } /// public TokenType TokenType => ApiClient.AuthTokenType; - + /// Creates a new REST-only Discord client. internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) { @@ -106,9 +106,9 @@ namespace Discord.Rest await _loggedInEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLoginAsync(TokenType tokenType, string token) + internal virtual Task OnLoginAsync(TokenType tokenType, string token) => Task.Delay(0); - + public async Task LogoutAsync() { await _stateLock.WaitAsync().ConfigureAwait(false); @@ -131,14 +131,17 @@ namespace Discord.Rest await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); } - internal virtual Task OnLogoutAsync() + internal virtual Task OnLogoutAsync() => Task.Delay(0); internal virtual void Dispose(bool disposing) { if (!_isDisposed) { +#pragma warning disable IDISP007 ApiClient.Dispose(); +#pragma warning restore IDISP007 + _stateLock?.Dispose(); _isDisposed = true; } } @@ -156,7 +159,7 @@ namespace Discord.Rest ISelfUser IDiscordClient.CurrentUser => CurrentUser; /// - Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) + Task IDiscordClient.GetApplicationInfoAsync(RequestOptions options) => throw new NotSupportedException(); /// diff --git a/src/Discord.Net.Rest/DiscordRestApiClient.cs b/src/Discord.Net.Rest/DiscordRestApiClient.cs index 57d7c718a..eeeea4139 100644 --- a/src/Discord.Net.Rest/DiscordRestApiClient.cs +++ b/src/Discord.Net.Rest/DiscordRestApiClient.cs @@ -66,6 +66,7 @@ namespace Discord.API /// Unknown OAuth token type. internal void SetBaseUrl(string baseUrl) { + RestClient?.Dispose(); RestClient = _restClientProvider(baseUrl); RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("user-agent", UserAgent); @@ -93,7 +94,9 @@ namespace Discord.API if (disposing) { _loginCancelToken?.Dispose(); - (RestClient as IDisposable)?.Dispose(); + RestClient?.Dispose(); + RequestQueue?.Dispose(); + _stateLock?.Dispose(); } _isDisposed = true; } @@ -117,6 +120,7 @@ namespace Discord.API try { + _loginCancelToken?.Dispose(); _loginCancelToken = new CancellationTokenSource(); AuthToken = null; @@ -242,7 +246,7 @@ namespace Discord.API internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); - public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, + public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index e36353855..f96d5dd0b 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -31,6 +31,8 @@ namespace Discord.Rest { if (disposing) ApiClient.Dispose(); + + base.Dispose(disposing); } /// @@ -46,12 +48,12 @@ namespace Discord.Rest _applicationInfo = null; return Task.Delay(0); } - + public async Task GetApplicationInfoAsync(RequestOptions options = null) { return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); } - + public Task GetChannelAsync(ulong id, RequestOptions options = null) => ClientHelper.GetChannelAsync(this, id, options); public Task> GetPrivateChannelsAsync(RequestOptions options = null) @@ -60,7 +62,7 @@ namespace Discord.Rest => ClientHelper.GetDMChannelsAsync(this, options); public Task> GetGroupChannelsAsync(RequestOptions options = null) => ClientHelper.GetGroupChannelsAsync(this, options); - + public Task> GetConnectionsAsync(RequestOptions options = null) => ClientHelper.GetConnectionsAsync(this, options); @@ -79,12 +81,12 @@ namespace Discord.Rest => ClientHelper.GetGuildsAsync(this, options); public Task CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); - + public Task GetUserAsync(ulong id, RequestOptions options = null) => ClientHelper.GetUserAsync(this, id, options); public Task GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) => ClientHelper.GetGuildUserAsync(this, guildId, id, options); - + public Task> GetVoiceRegionsAsync(RequestOptions options = null) => ClientHelper.GetVoiceRegionsAsync(this, options); public Task GetVoiceRegionAsync(string id, RequestOptions options = null) diff --git a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs index 9bbcfb8a3..941a35bf1 100644 --- a/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs +++ b/src/Discord.Net.Rest/Net/Converters/ImageConverter.cs @@ -34,12 +34,14 @@ namespace Discord.Net.Converters } else { - var cloneStream = new MemoryStream(); - image.Stream.CopyTo(cloneStream); - bytes = new byte[cloneStream.Length]; - cloneStream.Position = 0; - cloneStream.Read(bytes, 0, bytes.Length); - length = (int)cloneStream.Length; + using (var cloneStream = new MemoryStream()) + { + image.Stream.CopyTo(cloneStream); + bytes = new byte[cloneStream.Length]; + cloneStream.Position = 0; + cloneStream.Read(bytes, 0, bytes.Length); + length = (int)cloneStream.Length; + } } string base64 = Convert.ToBase64String(bytes, 0, length); diff --git a/src/Discord.Net.Rest/Net/DefaultRestClient.cs b/src/Discord.Net.Rest/Net/DefaultRestClient.cs index 05a568198..b5036d94e 100644 --- a/src/Discord.Net.Rest/Net/DefaultRestClient.cs +++ b/src/Discord.Net.Rest/Net/DefaultRestClient.cs @@ -27,12 +27,14 @@ namespace Discord.Net.Rest { _baseUrl = baseUrl; +#pragma warning disable IDISP014 _client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, UseCookies = false, UseProxy = useProxy, }); +#pragma warning restore IDISP014 SetHeader("accept-encoding", "gzip, deflate"); _cancelToken = CancellationToken.None; @@ -91,12 +93,14 @@ namespace Discord.Net.Rest { if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + MemoryStream memoryStream = null; if (multipartParams != null) { foreach (var p in multipartParams) { switch (p.Value) { +#pragma warning disable IDISP004 case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), p.Key); continue; } case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } @@ -105,12 +109,15 @@ namespace Discord.Net.Rest var stream = fileValue.Stream; if (!stream.CanSeek) { - var memoryStream = new MemoryStream(); + memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; +#pragma warning disable IDISP001 stream = memoryStream; +#pragma warning restore IDISP001 } content.Add(new StreamContent(stream), p.Key, fileValue.Filename); +#pragma warning restore IDISP004 continue; } default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); @@ -118,19 +125,24 @@ namespace Discord.Net.Rest } } restRequest.Content = content; - return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + var result = await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); + memoryStream?.Dispose(); + return result; } } private async Task SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) { - cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; - HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - - var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); - var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + using (var cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken)) + { + cancelToken = cancelTokenSource.Token; + HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - return new RestResponse(response.StatusCode, headers, stream); + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); + var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; + + return new RestResponse(response.StatusCode, headers, stream); + } } private static readonly HttpMethod Patch = new HttpMethod("PATCH"); diff --git a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs index 1b4830da2..4baf76433 100644 --- a/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Rest/Net/Queue/RequestQueue.cs @@ -16,23 +16,24 @@ namespace Discord.Net.Queue private readonly ConcurrentDictionary _buckets; private readonly SemaphoreSlim _tokenLock; - private readonly CancellationTokenSource _cancelToken; //Dispose token + private readonly CancellationTokenSource _cancelTokenSource; //Dispose token private CancellationTokenSource _clearToken; private CancellationToken _parentToken; + private CancellationTokenSource _requestCancelTokenSource; private CancellationToken _requestCancelToken; //Parent token + Clear token private DateTimeOffset _waitUntil; private Task _cleanupTask; - + public RequestQueue() { _tokenLock = new SemaphoreSlim(1, 1); _clearToken = new CancellationTokenSource(); - _cancelToken = new CancellationTokenSource(); + _cancelTokenSource = new CancellationTokenSource(); _requestCancelToken = CancellationToken.None; _parentToken = CancellationToken.None; - + _buckets = new ConcurrentDictionary(); _cleanupTask = RunCleanup(); @@ -44,7 +45,9 @@ namespace Discord.Net.Queue try { _parentToken = cancelToken; - _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token); + _requestCancelToken = _requestCancelTokenSource.Token; } finally { _tokenLock.Release(); } } @@ -54,9 +57,14 @@ namespace Discord.Net.Queue try { _clearToken?.Cancel(); + _clearToken?.Dispose(); _clearToken = new CancellationTokenSource(); if (_parentToken != null) - _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; + { + _requestCancelTokenSource?.Dispose(); + _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); + _requestCancelToken = _requestCancelTokenSource.Token; + } else _requestCancelToken = _clearToken.Token; } @@ -65,13 +73,19 @@ namespace Discord.Net.Queue public async Task SendAsync(RestRequest request) { + CancellationTokenSource createdTokenSource = null; if (request.Options.CancelToken.CanBeCanceled) - request.Options.CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken).Token; + { + createdTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_requestCancelToken, request.Options.CancelToken); + request.Options.CancelToken = createdTokenSource.Token; + } else request.Options.CancelToken = _requestCancelToken; var bucket = GetOrCreateBucket(request.Options.BucketId, request); - return await bucket.SendAsync(request).ConfigureAwait(false); + var result = await bucket.SendAsync(request).ConfigureAwait(false); + createdTokenSource?.Dispose(); + return result; } public async Task SendAsync(WebSocketRequest request) { @@ -109,7 +123,7 @@ namespace Discord.Net.Queue { try { - while (!_cancelToken.IsCancellationRequested) + while (!_cancelTokenSource.IsCancellationRequested) { var now = DateTimeOffset.UtcNow; foreach (var bucket in _buckets.Select(x => x.Value)) @@ -117,7 +131,7 @@ namespace Discord.Net.Queue if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) _buckets.TryRemove(bucket.Id, out _); } - await Task.Delay(60000, _cancelToken.Token).ConfigureAwait(false); //Runs each minute + await Task.Delay(60000, _cancelTokenSource.Token).ConfigureAwait(false); //Runs each minute } } catch (OperationCanceledException) { } @@ -126,7 +140,10 @@ namespace Discord.Net.Queue public void Dispose() { - _cancelToken.Dispose(); + _cancelTokenSource?.Dispose(); + _tokenLock?.Dispose(); + _clearToken?.Dispose(); + _requestCancelTokenSource?.Dispose(); } } } diff --git a/src/Discord.Net.WebSocket/Audio/AudioClient.cs b/src/Discord.Net.WebSocket/Audio/AudioClient.cs index 800e04933..2210e019f 100644 --- a/src/Discord.Net.WebSocket/Audio/AudioClient.cs +++ b/src/Discord.Net.WebSocket/Audio/AudioClient.cs @@ -71,7 +71,7 @@ namespace Discord.Audio ApiClient.ReceivedPacket += ProcessPacketAsync; _stateLock = new SemaphoreSlim(1, 1); - _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, + _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); _connection.Connected += () => _connectedEvent.InvokeAsync(); _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); @@ -79,7 +79,7 @@ namespace Discord.Audio _keepaliveTimes = new ConcurrentQueue>(); _ssrcMap = new ConcurrentDictionary(); _streams = new ConcurrentDictionary(); - + _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; _serializer.Error += (s, e) => { @@ -91,7 +91,7 @@ namespace Discord.Audio UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); } - internal async Task StartAsync(string url, ulong userId, string sessionId, string token) + internal async Task StartAsync(string url, ulong userId, string sessionId, string token) { _url = url; _userId = userId; @@ -100,7 +100,7 @@ namespace Discord.Audio await _connection.StartAsync().ConfigureAwait(false); } public async Task StopAsync() - { + { await _connection.StopAsync().ConfigureAwait(false); } @@ -225,11 +225,11 @@ namespace Discord.Audio if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); - + ApiClient.SetUdpEndpoint(data.Ip, data.Port); await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); - + _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); } break; @@ -305,9 +305,9 @@ namespace Discord.Audio catch (Exception ex) { await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); - return; + return; } - + await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); } @@ -317,7 +317,7 @@ namespace Discord.Audio { await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); - ulong value = + ulong value = ((ulong)packet[0] >> 0) | ((ulong)packet[1] >> 8) | ((ulong)packet[2] >> 16) | @@ -341,7 +341,7 @@ namespace Discord.Audio } } else - { + { if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) { await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); @@ -388,7 +388,7 @@ namespace Discord.Audio var now = Environment.TickCount; //Did server respond to our last heartbeat? - if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && + if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && ConnectionState == ConnectionState.Connected) { _connection.Error(new Exception("Server missed last heartbeat")); @@ -437,7 +437,7 @@ namespace Discord.Audio { await _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); } - + await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); } await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); @@ -467,6 +467,7 @@ namespace Discord.Audio { StopAsync().GetAwaiter().GetResult(); ApiClient.Dispose(); + _stateLock?.Dispose(); } } /// diff --git a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs index 47a7e2809..16ad0ae89 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs @@ -27,7 +27,7 @@ namespace Discord.Audio.Streams private readonly AudioClient _client; private readonly AudioStream _next; - private readonly CancellationTokenSource _cancelTokenSource; + private readonly CancellationTokenSource _disposeTokenSource, _cancelTokenSource; private readonly CancellationToken _cancelToken; private readonly Task _task; private readonly ConcurrentQueue _queuedFrames; @@ -49,12 +49,13 @@ namespace Discord.Audio.Streams _logger = logger; _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, cancelToken).Token; + _disposeTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_disposeTokenSource.Token, cancelToken); + _cancelToken = _cancelTokenSource.Token; _queuedFrames = new ConcurrentQueue(); _bufferPool = new ConcurrentQueue(); for (int i = 0; i < _queueLength; i++) - _bufferPool.Enqueue(new byte[maxFrameSize]); + _bufferPool.Enqueue(new byte[maxFrameSize]); _queueLock = new SemaphoreSlim(_queueLength, _queueLength); _silenceFrames = MaxSilenceFrames; @@ -63,7 +64,12 @@ namespace Discord.Audio.Streams protected override void Dispose(bool disposing) { if (disposing) - _cancelTokenSource.Cancel(); + { + _disposeTokenSource?.Cancel(); + _disposeTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _queueLock?.Dispose(); + } base.Dispose(disposing); } @@ -131,8 +137,12 @@ namespace Discord.Audio.Streams public override void WriteHeader(ushort seq, uint timestamp, bool missed) { } //Ignore, we use our own timing public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) { + CancellationTokenSource writeCancelToken = null; if (cancelToken.CanBeCanceled) - cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; + { + writeCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken); + cancelToken = writeCancelToken.Token; + } else cancelToken = _cancelToken; @@ -142,6 +152,9 @@ namespace Discord.Audio.Streams #if DEBUG var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock #endif +#pragma warning disable IDISP016 + writeCancelToken?.Dispose(); +#pragma warning restore IDISP016 return; } Buffer.BlockCopy(data, offset, buffer, 0, count); @@ -153,6 +166,7 @@ namespace Discord.Audio.Streams #endif _isPreloaded = true; } + writeCancelToken?.Dispose(); } public override async Task FlushAsync(CancellationToken cancelToken) diff --git a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs index b9d6157ea..6233c47b5 100644 --- a/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs +++ b/src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs @@ -96,7 +96,17 @@ namespace Discord.Audio.Streams protected override void Dispose(bool isDisposing) { - _isDisposed = true; + if (!_isDisposed) + { + if (isDisposing) + { + _signal?.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(isDisposing); } } } diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index decae4163..66f847c50 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -6,7 +6,7 @@ using Discord.Net; namespace Discord { - internal class ConnectionManager + internal class ConnectionManager : IDisposable { public event Func Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } private readonly AsyncEvent> _connectedEvent = new AsyncEvent>(); @@ -23,10 +23,12 @@ namespace Discord private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; private Task _task; + private bool _isDisposed; + public ConnectionState State { get; private set; } public CancellationToken CancelToken { get; private set; } - internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, + internal ConnectionManager(SemaphoreSlim stateLock, Logger logger, int connectionTimeout, Func onConnecting, Func onDisconnecting, Action> clientDisconnectHandler) { _stateLock = stateLock; @@ -55,6 +57,7 @@ namespace Discord { await AcquireConnectionLock().ConfigureAwait(false); var reconnectCancelToken = new CancellationTokenSource(); + _reconnectCancelToken?.Dispose(); _reconnectCancelToken = reconnectCancelToken; _task = Task.Run(async () => { @@ -67,16 +70,16 @@ namespace Discord try { await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); - nextReconnectDelay = 1000; //Reset delay + nextReconnectDelay = 1000; //Reset delay await _connectionPromise.Task.ConfigureAwait(false); } - catch (OperationCanceledException ex) - { + catch (OperationCanceledException ex) + { Cancel(); //In case this exception didn't come from another Error call await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); } - catch (Exception ex) - { + catch (Exception ex) + { Error(ex); //In case this exception didn't come from another Error call if (!reconnectCancelToken.IsCancellationRequested) { @@ -113,6 +116,8 @@ namespace Discord private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) { + _connectionCancelToken?.Dispose(); + _combinedCancelToken?.Dispose(); _connectionCancelToken = new CancellationTokenSource(); _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); CancelToken = _combinedCancelToken.Token; @@ -120,7 +125,7 @@ namespace Discord _connectionPromise = new TaskCompletionSource(); State = ConnectionState.Connecting; await _logger.InfoAsync("Connecting").ConfigureAwait(false); - + try { var readyPromise = new TaskCompletionSource(); @@ -206,5 +211,25 @@ namespace Discord break; } } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _combinedCancelToken?.Dispose(); + _reconnectCancelToken?.Dispose(); + _connectionCancelToken?.Dispose(); + } + + _isDisposed = true; + } + } + + public void Dispose() + { + Dispose(true); + } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 4e497346f..03969f535 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -18,7 +18,9 @@ namespace Discord.WebSocket private int[] _shardIds; private DiscordSocketClient[] _shards; private int _totalShards; - + + private bool _isDisposed; + /// public override int Latency { get => GetLatency(); protected set { } } /// @@ -38,11 +40,15 @@ namespace Discord.WebSocket /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } +#pragma warning restore IDISP004 /// Creates a new REST/WebSocket Discord client. public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } /// Creates a new REST/WebSocket Discord client. +#pragma warning disable IDISP004 public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } +#pragma warning restore IDISP004 private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) : base(config, client) { @@ -119,10 +125,10 @@ namespace Discord.WebSocket } /// - public override async Task StartAsync() + public override async Task StartAsync() => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); /// - public override async Task StopAsync() + public override async Task StopAsync() => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); public DiscordSocketClient GetShard(int id) @@ -145,7 +151,7 @@ namespace Discord.WebSocket => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); /// - public override SocketGuild GetGuild(ulong id) + public override SocketGuild GetGuild(ulong id) => GetShardFor(id).GetGuild(id); /// @@ -173,7 +179,7 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].PrivateChannels.Count; return result; - } + } private IEnumerable GetGuilds() { @@ -189,7 +195,7 @@ namespace Discord.WebSocket for (int i = 0; i < _shards.Length; i++) result += _shards[i].Guilds.Count; return result; - } + } /// public override SocketUser GetUser(ulong id) @@ -369,5 +375,22 @@ namespace Discord.WebSocket /// Task IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) => Task.FromResult(GetVoiceRegion(id)); + + internal override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + foreach (var client in _shards) + client?.Dispose(); + _connectionGroupLock?.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(disposing); + } } } diff --git a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs index bc579793d..894ae66a8 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketApiClient.cs @@ -108,6 +108,8 @@ namespace Discord.API } _isDisposed = true; } + + base.Dispose(disposing); } public async Task ConnectAsync() @@ -137,6 +139,7 @@ namespace Discord.API ConnectionState = ConnectionState.Connecting; try { + _connectCancelToken?.Dispose(); _connectCancelToken = new CancellationTokenSource(); if (WebSocketClient != null) WebSocketClient.SetCancelToken(_connectCancelToken.Token); @@ -209,7 +212,7 @@ namespace Discord.API await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); } - + public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) { options = RequestOptions.CreateOrClone(options); diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 3d260d1a6..6b720645e 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -43,6 +43,8 @@ namespace Discord.WebSocket private DateTimeOffset? _statusSince; private RestApplication _applicationInfo; + private bool _isDisposed; + /// Gets the shard of of this client. public int ShardId { get; } /// Gets the current connection state of this client. @@ -63,7 +65,7 @@ namespace Discord.WebSocket internal WebSocketProvider WebSocketProvider { get; private set; } internal bool AlwaysDownloadUsers { get; private set; } internal int? HandlerTimeout { get; private set; } - + internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; /// public override IReadOnlyCollection Guilds => State.Guilds; @@ -110,8 +112,10 @@ namespace Discord.WebSocket /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. /// /// The configuration to be used with the client. +#pragma warning disable IDISP004 public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } +#pragma warning restore IDISP004 private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : base(config, client) { @@ -170,11 +174,18 @@ namespace Discord.WebSocket /// internal override void Dispose(bool disposing) { - if (disposing) + if (!_isDisposed) { - StopAsync().GetAwaiter().GetResult(); - ApiClient.Dispose(); + if (disposing) + { + StopAsync().GetAwaiter().GetResult(); + ApiClient?.Dispose(); + _stateLock?.Dispose(); + } + _isDisposed = true; } + + base.Dispose(disposing); } /// @@ -197,10 +208,10 @@ namespace Discord.WebSocket } /// - public override async Task StartAsync() + public override async Task StartAsync() => await _connection.StartAsync().ConfigureAwait(false); /// - public override async Task StopAsync() + public override async Task StopAsync() => await _connection.StopAsync().ConfigureAwait(false); private async Task OnConnectingAsync() @@ -704,6 +715,7 @@ namespace Discord.WebSocket { await GuildUnavailableAsync(guild).ConfigureAwait(false); await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); + (guild as IDisposable).Dispose(); } else { diff --git a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs index 80dec0fd4..f78145dbe 100644 --- a/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs +++ b/src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs @@ -16,7 +16,7 @@ using System.Threading.Tasks; namespace Discord.Audio { - internal class DiscordVoiceAPIClient + internal class DiscordVoiceAPIClient : IDisposable { public const int MaxBitrate = 128 * 1024; public const string Mode = "xsalsa20_poly1305"; @@ -36,7 +36,7 @@ namespace Discord.Audio private readonly AsyncEvent> _receivedPacketEvent = new AsyncEvent>(); public event Func Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } private readonly AsyncEvent> _disconnectedEvent = new AsyncEvent>(); - + private readonly JsonSerializer _serializer; private readonly SemaphoreSlim _connectionLock; private readonly IUdpSocket _udp; @@ -103,8 +103,9 @@ namespace Discord.Audio if (disposing) { _connectCancelToken?.Dispose(); - (_udp as IDisposable)?.Dispose(); - (WebSocketClient as IDisposable)?.Dispose(); + _udp?.Dispose(); + WebSocketClient?.Dispose(); + _connectionLock?.Dispose(); } _isDisposed = true; } @@ -122,7 +123,7 @@ namespace Discord.Audio } public async Task SendAsync(byte[] data, int offset, int bytes) { - await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); + await _udp.SendAsync(data, offset, bytes).ConfigureAwait(false); await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); } @@ -177,6 +178,7 @@ namespace Discord.Audio ConnectionState = ConnectionState.Connecting; try { + _connectCancelToken?.Dispose(); _connectCancelToken = new CancellationTokenSource(); var cancelToken = _connectCancelToken.Token; @@ -208,7 +210,7 @@ namespace Discord.Audio { if (ConnectionState == ConnectionState.Disconnected) return; ConnectionState = ConnectionState.Disconnecting; - + try { _connectCancelToken?.Cancel(false); } catch { } diff --git a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs index 4e4124679..ca2db1a77 100644 --- a/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs +++ b/src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs @@ -25,8 +25,9 @@ namespace Discord.WebSocket /// Represents a WebSocket-based guild object. /// [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class SocketGuild : SocketEntity, IGuild + public class SocketGuild : SocketEntity, IGuild, IDisposable { +#pragma warning disable IDISP002, IDISP006 private readonly SemaphoreSlim _audioLock; private TaskCompletionSource _syncPromise, _downloaderPromise; private TaskCompletionSource _audioConnectPromise; @@ -37,6 +38,7 @@ namespace Discord.WebSocket private ImmutableArray _emotes; private ImmutableArray _features; private AudioClient _audioClient; +#pragma warning restore IDISP002, IDISP006 /// public string Name { get; private set; } @@ -63,7 +65,7 @@ namespace Discord.WebSocket /// number here is the most accurate in terms of counting the number of users within this guild. /// /// - /// Use this instead of enumerating the count of the + /// Use this instead of enumerating the count of the /// collection, as you may see discrepancy /// between that and this property. /// @@ -872,9 +874,11 @@ namespace Discord.WebSocket if (external) { +#pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(null); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); return null; +#pragma warning restore IDISP001 } if (_audioClient == null) @@ -897,10 +901,14 @@ namespace Discord.WebSocket }; audioClient.Connected += () => { +#pragma warning disable IDISP001 var _ = promise.TrySetResultAsync(_audioClient); +#pragma warning restore IDISP001 return Task.Delay(0); }; +#pragma warning disable IDISP003 _audioClient = audioClient; +#pragma warning restore IDISP003 } await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); @@ -948,6 +956,7 @@ namespace Discord.WebSocket if (_audioClient != null) await _audioClient.StopAsync().ConfigureAwait(false); await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); + _audioClient?.Dispose(); _audioClient = null; } internal async Task FinishConnectAudio(string url, string token) @@ -1130,5 +1139,12 @@ namespace Discord.WebSocket /// async Task> IGuild.GetWebhooksAsync(RequestOptions options) => await GetWebhooksAsync(options).ConfigureAwait(false); + + void IDisposable.Dispose() + { + DisconnectAudioAsync().GetAwaiter().GetResult(); + _audioLock?.Dispose(); + _audioClient?.Dispose(); + } } } diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index 251a761d4..4b37de28f 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -13,24 +13,29 @@ namespace Discord.Net.Udp private readonly SemaphoreSlim _lock; private UdpClient _udp; private IPEndPoint _destination; - private CancellationTokenSource _cancelTokenSource; + private CancellationTokenSource _stopCancelTokenSource, _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private Task _task; private bool _isDisposed; - + public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); public DefaultUdpSocket() { _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); + _stopCancelTokenSource = new CancellationTokenSource(); } private void Dispose(bool disposing) { if (!_isDisposed) { if (disposing) + { StopInternalAsync(true).GetAwaiter().GetResult(); + _stopCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _lock?.Dispose(); + } _isDisposed = true; } } @@ -56,9 +61,14 @@ namespace Discord.Net.Udp { await StopInternalAsync().ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _stopCancelTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + + _stopCancelTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + _udp?.Dispose(); _udp = new UdpClient(0); _task = RunAsync(_cancelToken); @@ -77,7 +87,7 @@ namespace Discord.Net.Udp } public async Task StopInternalAsync(bool isDisposing = false) { - try { _cancelTokenSource.Cancel(false); } catch { } + try { _stopCancelTokenSource.Cancel(false); } catch { } if (!isDisposing) await (_task ?? Task.Delay(0)).ConfigureAwait(false); @@ -96,8 +106,11 @@ namespace Discord.Net.Udp } public void SetCancelToken(CancellationToken cancelToken) { + _cancelTokenSource?.Dispose(); + _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _stopCancelTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; } public async Task SendAsync(byte[] data, int index, int count) diff --git a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs index dc5201ac1..df2da5813 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs @@ -25,14 +25,14 @@ namespace Discord.Net.WebSockets private readonly IWebProxy _proxy; private ClientWebSocket _client; private Task _task; - private CancellationTokenSource _cancelTokenSource; + private CancellationTokenSource _disconnectTokenSource, _cancelTokenSource; private CancellationToken _cancelToken, _parentToken; private bool _isDisposed, _isDisconnecting; public DefaultWebSocketClient(IWebProxy proxy = null) { _lock = new SemaphoreSlim(1, 1); - _cancelTokenSource = new CancellationTokenSource(); + _disconnectTokenSource = new CancellationTokenSource(); _cancelToken = CancellationToken.None; _parentToken = CancellationToken.None; _headers = new Dictionary(); @@ -43,7 +43,12 @@ namespace Discord.Net.WebSockets if (!_isDisposed) { if (disposing) + { DisconnectInternalAsync(true).GetAwaiter().GetResult(); + _disconnectTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + _lock?.Dispose(); + } _isDisposed = true; } } @@ -68,9 +73,14 @@ namespace Discord.Net.WebSockets { await DisconnectInternalAsync().ConfigureAwait(false); - _cancelTokenSource = new CancellationTokenSource(); - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _disconnectTokenSource?.Dispose(); + _cancelTokenSource?.Dispose(); + + _disconnectTokenSource = new CancellationTokenSource(); + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; + _client?.Dispose(); _client = new ClientWebSocket(); _client.Options.Proxy = _proxy; _client.Options.KeepAliveInterval = TimeSpan.Zero; @@ -98,7 +108,7 @@ namespace Discord.Net.WebSockets } private async Task DisconnectInternalAsync(bool isDisposing = false) { - try { _cancelTokenSource.Cancel(false); } catch { } + try { _disconnectTokenSource.Cancel(false); } catch { } _isDisconnecting = true; try @@ -117,7 +127,7 @@ namespace Discord.Net.WebSockets } try { _client.Dispose(); } catch { } - + _client = null; } } @@ -144,8 +154,11 @@ namespace Discord.Net.WebSockets } public void SetCancelToken(CancellationToken cancelToken) { + _cancelTokenSource?.Dispose(); + _parentToken = cancelToken; - _cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _cancelTokenSource.Token).Token; + _cancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_parentToken, _disconnectTokenSource.Token); + _cancelToken = _cancelTokenSource.Token; } public async Task SendAsync(byte[] data, int index, int count, bool isText) @@ -166,7 +179,7 @@ namespace Discord.Net.WebSockets frameSize = count - (i * SendChunkSize); else frameSize = SendChunkSize; - + var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; await _client.SendAsync(new ArraySegment(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); } @@ -176,7 +189,7 @@ namespace Discord.Net.WebSockets _lock.Release(); } } - + private async Task RunAsync(CancellationToken cancelToken) { var buffer = new ArraySegment(new byte[ReceiveChunkSize]); @@ -188,7 +201,7 @@ namespace Discord.Net.WebSockets WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); byte[] result; int resultCount; - + if (socketResult.MessageType == WebSocketMessageType.Close) throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); @@ -219,7 +232,7 @@ namespace Discord.Net.WebSockets resultCount = socketResult.Count; result = buffer.Array; } - + if (socketResult.MessageType == WebSocketMessageType.Text) { string text = Encoding.UTF8.GetString(result, 0, resultCount); diff --git a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj index ba7bbcff8..58282d85b 100644 --- a/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj +++ b/src/Discord.Net.Webhook/Discord.Net.Webhook.csproj @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 0ee6f7e59..aa6f86a34 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -5,6 +5,7 @@ netcoreapp1.1 portable $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 + IDISP001,IDISP002,IDISP004,IDISP005 diff --git a/test/Discord.Net.Tests/Net/CachedRestClient.cs b/test/Discord.Net.Tests/Net/CachedRestClient.cs index 4bc8a386a..c465eaa01 100644 --- a/test/Discord.Net.Tests/Net/CachedRestClient.cs +++ b/test/Discord.Net.Tests/Net/CachedRestClient.cs @@ -43,7 +43,10 @@ namespace Discord.Net if (!_isDisposed) { if (disposing) + { _blobCache.Dispose(); + _cancelTokenSource?.Dispose(); + } _isDisposed = true; } } @@ -70,7 +73,7 @@ namespace Discord.Net { if (method != "GET") throw new InvalidOperationException("This RestClient only supports GET requests."); - + string uri = Path.Combine(_baseUrl, endpoint); var bytes = await _blobCache.DownloadUrl(uri, _headers); return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); @@ -84,7 +87,7 @@ namespace Discord.Net throw new InvalidOperationException("This RestClient does not support multipart requests."); } - public async Task ClearAsync() + public async Task ClearAsync() { await _blobCache.InvalidateAll(); } @@ -93,7 +96,7 @@ namespace Discord.Net { if (Info != null) return; - + bool needsReset = false; try { @@ -117,4 +120,4 @@ namespace Discord.Net await _blobCache.InsertObject("info", Info); } } -} \ No newline at end of file +} From 46e2674a9328abc994fcb9cd614f06e7e9c2ea97 Mon Sep 17 00:00:00 2001 From: ComputerMaster1st Date: Thu, 29 Nov 2018 22:13:50 +0000 Subject: [PATCH 06/13] fix: Added Msg.Content Null Check For Prefixes (#1200) * Added Msg.Content Null Check * Minor Change * Grouped Params In If Statement * Minor Change --- src/Discord.Net.Commands/Extensions/MessageExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs index f76df1f2a..f880e1d98 100644 --- a/src/Discord.Net.Commands/Extensions/MessageExtensions.cs +++ b/src/Discord.Net.Commands/Extensions/MessageExtensions.cs @@ -19,7 +19,7 @@ namespace Discord.Commands public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) { var text = msg.Content; - if (text.Length > 0 && text[0] == c) + if (!string.IsNullOrEmpty(text) && text[0] == c) { argPos = 1; return true; @@ -32,7 +32,7 @@ namespace Discord.Commands public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) { var text = msg.Content; - if (text.StartsWith(str, comparisonType)) + if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType)) { argPos = str.Length; return true; @@ -45,7 +45,7 @@ namespace Discord.Commands public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) { var text = msg.Content; - if (text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; + if (string.IsNullOrEmpty(text) || text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; int endPos = text.IndexOf('>'); if (endPos == -1) return false; From a3f5e0b3a7f9d74fd7e8665a80c2995150871a4b Mon Sep 17 00:00:00 2001 From: Still Hsu <341464@gmail.com> Date: Fri, 30 Nov 2018 06:15:11 +0800 Subject: [PATCH 07/13] api: [brk] Move Invites-related Methods from IGuildChannel to INestedChannel (#1172) * Move invites-related methods from IGuildChannel to INestedChannel * Add missing implementation ...because I somehow forgot it the first time around --- .../Entities/Channels/IGuildChannel.cs | 39 ----------------- .../Entities/Channels/INestedChannel.cs | 42 ++++++++++++++++++- .../Entities/Channels/RestCategoryChannel.cs | 10 ----- .../Entities/Channels/RestGuildChannel.cs | 33 --------------- .../Entities/Channels/RestTextChannel.cs | 8 ++++ .../Entities/Channels/RestVoiceChannel.cs | 10 +++++ .../Channels/SocketCategoryChannel.cs | 8 ---- .../Entities/Channels/SocketGuildChannel.cs | 32 -------------- .../Entities/Channels/SocketTextChannel.cs | 9 +++- .../Entities/Channels/SocketVoiceChannel.cs | 8 ++++ 10 files changed, 75 insertions(+), 124 deletions(-) diff --git a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs index c3a2161cc..1da1879de 100644 --- a/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs @@ -44,45 +44,6 @@ namespace Discord /// IReadOnlyCollection PermissionOverwrites { get; } - /// - /// Creates a new invite to this channel. - /// - /// - /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only - /// be used 3 times throughout its lifespan. - /// - /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); - /// - /// - /// The time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, the user accepting this invite will be kicked from the guild after closing their client. - /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous invite creation operation. The task result contains an invite - /// metadata object containing information for the created invite. - /// - Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); - /// - /// Gets a collection of all invites to this channel. - /// - /// - /// The following example gets all of the invites that have been created in this channel and selects the - /// most used invite. - /// - /// var invites = await channel.GetInvitesAsync(); - /// if (invites.Count == 0) return; - /// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault(); - /// - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection - /// of invite metadata that are created for this channel. - /// - Task> GetInvitesAsync(RequestOptions options = null); - /// /// Modifies this guild channel. /// diff --git a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs index 6719f91d4..f45a0114a 100644 --- a/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/INestedChannel.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord @@ -25,10 +26,49 @@ namespace Discord /// representing the parent of this channel; null if none is set. /// Task GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); - + /// /// Syncs the permissions of this nested channel with its parent's. /// Task SyncPermissionsAsync(RequestOptions options = null); + + /// + /// Creates a new invite to this channel. + /// + /// + /// The following example creates a new invite to this channel; the invite lasts for 12 hours and can only + /// be used 3 times throughout its lifespan. + /// + /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); + /// + /// + /// The time (in seconds) until the invite expires. Set to null to never expire. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. + /// If true, the user accepting this invite will be kicked from the guild after closing their client. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous invite creation operation. The task result contains an invite + /// metadata object containing information for the created invite. + /// + Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); + /// + /// Gets a collection of all invites to this channel. + /// B + /// + /// The following example gets all of the invites that have been created in this channel and selects the + /// most used invite. + /// + /// var invites = await channel.GetInvitesAsync(); + /// if (invites.Count == 0) return; + /// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault(); + /// + /// + /// The options to be used when sending the request. + /// + /// A task that represents the asynchronous get operation. The task result contains a read-only collection + /// of invite metadata that are created for this channel. + /// + Task> GetInvitesAsync(RequestOptions options = null); } } diff --git a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs index 9d69d6bdc..177bde21d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs @@ -25,16 +25,6 @@ namespace Discord.Rest private string DebuggerDisplay => $"{Name} ({Id}, Category)"; - // IGuildChannel - /// - /// This method is not supported with category channels. - Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => throw new NotSupportedException(); - /// - /// This method is not supported with category channels. - Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => throw new NotSupportedException(); - //IChannel /// /// This method is not supported with category channels. diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs index 380a64c8c..5f4db2eea 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs @@ -178,32 +178,6 @@ namespace Discord.Rest } } - /// - /// Gets a collection of all invites to this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection - /// of invite metadata that are created for this channel. - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - - /// - /// Creates a new invite to this channel. - /// - /// The time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, the user accepting this invite will be kicked from the guild after closing their client. - /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous invite creation operation. The task result contains an invite - /// metadata object containing information for the created invite. - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// /// Gets the name of this channel. /// @@ -224,13 +198,6 @@ namespace Discord.Rest } } - /// - async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => await GetInvitesAsync(options).ConfigureAwait(false); - /// - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index beeb8de19..58de066fb 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -204,6 +204,14 @@ namespace Discord.Rest public Task SyncPermissionsAsync(RequestOptions options = null) => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; //ITextChannel diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs index 1a2c5ceae..aaaaad373 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs @@ -57,8 +57,17 @@ namespace Discord.Rest /// public Task GetCategoryAsync(RequestOptions options = null) => ChannelHelper.GetCategoryAsync(this, Discord, options); + public Task SyncPermissionsAsync(RequestOptions options = null) => ChannelHelper.SyncPermissionsAsync(this, Discord, options); + + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; @@ -77,6 +86,7 @@ namespace Discord.Rest => AsyncEnumerable.Empty>(); // INestedChannel + /// async Task INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) { if (CategoryId.HasValue && mode == CacheMode.AllowDownload) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs index 0ae40820c..b90c1976a 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs @@ -67,14 +67,6 @@ namespace Discord.WebSocket /// Task IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) => Task.FromResult(GetUser(id)); - /// - /// This method is not supported with category channels. - Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => throw new NotSupportedException(); - /// - /// This method is not supported with category channels. - Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => throw new NotSupportedException(); //IChannel /// diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs index bf1b2260b..18401c593 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs @@ -184,31 +184,6 @@ namespace Discord.WebSocket } } - /// - /// Returns a collection of all invites to this channel. - /// - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous get operation. The task result contains a read-only collection - /// of invite metadata that are created for this channel. - /// - public async Task> GetInvitesAsync(RequestOptions options = null) - => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); - /// - /// Creates a new invite to this channel. - /// - /// The time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, the user accepting this invite will be kicked from the guild after closing their client. - /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites). - /// The options to be used when sending the request. - /// - /// A task that represents the asynchronous invite creation operation. The task result contains an invite - /// metadata object containing information for the created invite. - /// - public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) - => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - public new virtual SocketGuildUser GetUser(ulong id) => null; /// @@ -233,13 +208,6 @@ namespace Discord.WebSocket /// ulong IGuildChannel.GuildId => Guild.Id; - /// - async Task> IGuildChannel.GetInvitesAsync(RequestOptions options) - => await GetInvitesAsync(options).ConfigureAwait(false); - /// - async Task IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) - => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); - /// OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) => GetPermissionOverwrite(role); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index acd868020..728a4ad53 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -126,7 +126,6 @@ namespace Discord.WebSocket /// /// Paged collection of messages. /// - public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); /// @@ -304,6 +303,14 @@ namespace Discord.WebSocket public Task> GetWebhooksAsync(RequestOptions options = null) => ChannelHelper.GetWebhooksAsync(this, Discord, options); + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + private string DebuggerDisplay => $"{Name} ({Id}, Text)"; internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs index dd71416db..48a7d7c3b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs @@ -79,6 +79,14 @@ namespace Discord.WebSocket return null; } + //Invites + /// + public async Task CreateInviteAsync(int? maxAge = 86400, int? maxUses = null, bool isTemporary = false, bool isUnique = false, RequestOptions options = null) + => await ChannelHelper.CreateInviteAsync(this, Discord, maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); + /// + public async Task> GetInvitesAsync(RequestOptions options = null) + => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); + private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; From 2c93363653d7935cd142b664b7abc63fd2641110 Mon Sep 17 00:00:00 2001 From: ComputerMaster1st Date: Thu, 29 Nov 2018 22:16:46 +0000 Subject: [PATCH 08/13] fix: Solves AudioClient Lockup On Disconnect (#1203) * Solves Audio Disconnect Lockup * Execute Disconnected Event Before Logger & State --- src/Discord.Net.WebSocket/ConnectionManager.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/ConnectionManager.cs b/src/Discord.Net.WebSocket/ConnectionManager.cs index 66f847c50..8c9c743cb 100644 --- a/src/Discord.Net.WebSocket/ConnectionManager.cs +++ b/src/Discord.Net.WebSocket/ConnectionManager.cs @@ -106,12 +106,10 @@ namespace Discord finally { _stateLock.Release(); } }); } - public virtual async Task StopAsync() + public virtual Task StopAsync() { Cancel(); - var task = _task; - if (task != null) - await task.ConfigureAwait(false); + return Task.CompletedTask; } private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) @@ -164,9 +162,9 @@ namespace Discord await _onDisconnecting(ex).ConfigureAwait(false); - await _logger.InfoAsync("Disconnected").ConfigureAwait(false); - State = ConnectionState.Disconnected; await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false); + State = ConnectionState.Disconnected; + await _logger.InfoAsync("Disconnected").ConfigureAwait(false); } public async Task CompleteAsync() From ccb16e40c8f111b3690f84b410b2020676b5a40d Mon Sep 17 00:00:00 2001 From: ComputerMaster1st Date: Thu, 29 Nov 2018 22:20:35 +0000 Subject: [PATCH 09/13] fix: Solves UdpClient "ObjectDisposedException" (#1202) * Solves "ObjectDisposedException" * Corrected Spelling Error * Fixed Spelling --- src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs index 4b37de28f..82079e9bd 100644 --- a/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs +++ b/src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs @@ -130,6 +130,14 @@ namespace Discord.Net.Udp while (!cancelToken.IsCancellationRequested) { var receiveTask = _udp.ReceiveAsync(); + + _ = receiveTask.ContinueWith((receiveResult) => + { + //observe the exception as to not receive as unhandled exception + _ = receiveResult.Exception; + + }, TaskContinuationOptions.OnlyOnFaulted); + var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); if (task == closeTask) break; From 6f5693f486126413f7cbcd593b5b2539120385c4 Mon Sep 17 00:00:00 2001 From: Christopher Felegy Date: Thu, 29 Nov 2018 17:51:34 -0500 Subject: [PATCH 10/13] test: bump dependency versions --- test/Discord.Net.Tests/Discord.Net.Tests.csproj | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index aa6f86a34..46e37655e 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -2,9 +2,8 @@ Exe Discord - netcoreapp1.1 + netcoreapp2.1 portable - $(PackageTargetFallback);portable-net45+win8+wp8+wpa81 IDISP001,IDISP002,IDISP004,IDISP005 @@ -22,11 +21,14 @@ - - + + - - - + + + all + runtime; build; native; contentfiles; analyzers + + From f6413bac59ace5afdb9492e341e6bc28302ad6c7 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Fri, 30 Nov 2018 04:10:26 -0800 Subject: [PATCH 11/13] fix: Update minimum Bot Token length to 58 char (#1204) * Update the minimum bot token length to 58 char - Updates the minimum length of a bot token to be 58 characters. An older 58 char bot token was found by Moiph - Makes this value an internal const instead of a magic number * update the TokenUtils tests for 58 char min --- src/Discord.Net.Core/Utils/TokenUtils.cs | 15 ++++++++++++--- test/Discord.Net.Tests/Tests.TokenUtils.cs | 10 +++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Core/Utils/TokenUtils.cs b/src/Discord.Net.Core/Utils/TokenUtils.cs index bfc915252..8fa846267 100644 --- a/src/Discord.Net.Core/Utils/TokenUtils.cs +++ b/src/Discord.Net.Core/Utils/TokenUtils.cs @@ -7,6 +7,15 @@ namespace Discord /// public static class TokenUtils { + /// + /// The minimum length of a Bot token. + /// + /// + /// This value was determined by comparing against the examples in the Discord + /// documentation, and pre-existing tokens. + /// + internal const int MinBotTokenLength = 58; + /// /// Checks the validity of the supplied token of a specific type. /// @@ -29,11 +38,11 @@ namespace Discord // no validation is performed on Bearer tokens break; case TokenType.Bot: - // bot tokens are assumed to be at least 59 characters in length + // bot tokens are assumed to be at least 58 characters in length // this value was determined by referencing examples in the discord documentation, and by comparing with // pre-existing tokens - if (token.Length < 59) - throw new ArgumentException(message: "A Bot token must be at least 59 characters in length.", paramName: nameof(token)); + if (token.Length < MinBotTokenLength) + throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length.", paramName: nameof(token)); break; default: // All unrecognized TokenTypes (including User tokens) are considered to be invalid. diff --git a/test/Discord.Net.Tests/Tests.TokenUtils.cs b/test/Discord.Net.Tests/Tests.TokenUtils.cs index dc5a93e34..8cebc649d 100644 --- a/test/Discord.Net.Tests/Tests.TokenUtils.cs +++ b/test/Discord.Net.Tests/Tests.TokenUtils.cs @@ -69,9 +69,12 @@ namespace Discord /// /// Tests the behavior of /// to see that valid Bot tokens do not throw Exceptions. - /// Valid Bot tokens can be strings of length 59 or above. + /// Valid Bot tokens can be strings of length 58 or above. /// [Theory] + // missing a single character from the end, 58 char. still should be valid + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] + // 59 char token [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] [InlineData("This appears to be completely invalid, however the current validation rules are not very strict.")] [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWss")] @@ -90,12 +93,12 @@ namespace Discord /// [Theory] [InlineData("This is invalid")] - // missing a single character from the end - [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] // bearer token [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] // client secret [InlineData("937it3ow87i4ery69876wqire")] + // 57 char bot token + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK")] public void TestBotTokenInvalidThrowsArgumentException(string token) { Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); @@ -113,6 +116,7 @@ namespace Discord // TokenType.User [InlineData(0)] // out of range TokenType + [InlineData(-1)] [InlineData(4)] [InlineData(7)] public void TestUnrecognizedTokenType(int type) From 91e0f03bfdf6aae1a0e944a15aaf6a3a2a9a0d4c Mon Sep 17 00:00:00 2001 From: Alex Gravely Date: Sun, 2 Dec 2018 12:57:38 -0500 Subject: [PATCH 12/13] fix: Fix after message remaining in MessageUpdated if message author wasn't in the payload (#1209) * Fix leaving updated message as null when Discords doesn't include a message author in MESSAGE_UPDATE * Name! That! Parameter! --- .../DiscordSocketClient.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index 6b720645e..1e431ec92 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -1218,16 +1218,22 @@ namespace Discord.WebSocket cachedMsg.Update(State, data); after = cachedMsg; } - else if (data.Author.IsSpecified) + else { //Edited message isnt in cache, create a detached one SocketUser author; - if (guild != null) - author = guild.GetUser(data.Author.Value.Id); + if (data.Author.IsSpecified) + { + if (guild != null) + author = guild.GetUser(data.Author.Value.Id); + else + author = (channel as SocketChannel).GetUser(data.Author.Value.Id); + if (author == null) + author = SocketUnknownUser.Create(this, State, data.Author.Value); + } else - author = (channel as SocketChannel).GetUser(data.Author.Value.Id); - if (author == null) - author = SocketUnknownUser.Create(this, State, data.Author.Value); + // Message author wasn't specified in the payload, so create a completely anonymous unknown user + author = new SocketUnknownUser(this, id: 0); after = SocketMessage.Create(this, State, author, channel, data); } From f4b1a5f25bfd37b353bdadcc93377c60b48368a9 Mon Sep 17 00:00:00 2001 From: Chris Johnston Date: Sun, 2 Dec 2018 10:03:12 -0800 Subject: [PATCH 13/13] fix: Improve validation of Bot Tokens (#1206) * improve bot token validation by trying to decode user id from token Try to decode the user id from the supplied bot token as a way of validating the token. If this should fail, indicate that the token is invalid. * Update the tokenutils tests to pass the new validation checks * Add test case for CheckBotTokenValidity method * lint: clean up whitespace * Add check for null or whitespace string, lint whitespace * fix userid conversion * Add hint to user to check that token is not an oauth client secret * Catch exception that can be thrown by GetString * Refactor token conversion logic into it's own testable method --- src/Discord.Net.Core/Utils/TokenUtils.cs | 68 +++++++++++++++++++++- test/Discord.Net.Tests/Tests.TokenUtils.cs | 44 +++++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Core/Utils/TokenUtils.cs b/src/Discord.Net.Core/Utils/TokenUtils.cs index 8fa846267..68aad5d96 100644 --- a/src/Discord.Net.Core/Utils/TokenUtils.cs +++ b/src/Discord.Net.Core/Utils/TokenUtils.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace Discord { @@ -16,6 +17,65 @@ namespace Discord /// internal const int MinBotTokenLength = 58; + /// + /// Decodes a base 64 encoded string into a ulong value. + /// + /// A base 64 encoded string containing a User Id. + /// A ulong containing the decoded value of the string, or null if the value was invalid. + internal static ulong? DecodeBase64UserId(string encoded) + { + if (string.IsNullOrWhiteSpace(encoded)) + return null; + + try + { + // decode the base64 string + var bytes = Convert.FromBase64String(encoded); + var idStr = Encoding.UTF8.GetString(bytes); + // try to parse a ulong from the resulting string + if (ulong.TryParse(idStr, out var id)) + return id; + } + catch (DecoderFallbackException) + { + // ignore exception, can be thrown by GetString + } + catch (FormatException) + { + // ignore exception, can be thrown if base64 string is invalid + } + catch (ArgumentException) + { + // ignore exception, can be thrown by BitConverter + } + return null; + } + + /// + /// Checks the validity of a bot token by attempting to decode a ulong userid + /// from the bot token. + /// + /// + /// The bot token to validate. + /// + /// + /// True if the bot token was valid, false if it was not. + /// + internal static bool CheckBotTokenValidity(string message) + { + if (string.IsNullOrWhiteSpace(message)) + return false; + + // split each component of the JWT + var segments = message.Split('.'); + + // ensure that there are three parts + if (segments.Length != 3) + return false; + // return true if the user id could be determined + return DecodeBase64UserId(segments[0]).HasValue; + } + /// /// Checks the validity of the supplied token of a specific type. /// @@ -42,13 +102,17 @@ namespace Discord // this value was determined by referencing examples in the discord documentation, and by comparing with // pre-existing tokens if (token.Length < MinBotTokenLength) - throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length.", paramName: nameof(token)); + throw new ArgumentException(message: $"A Bot token must be at least {MinBotTokenLength} characters in length. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); + // check the validity of the bot token by decoding the ulong userid from the jwt + if (!CheckBotTokenValidity(token)) + throw new ArgumentException(message: "The Bot token was invalid. " + + "Ensure that the Bot Token provided is not an OAuth client secret.", paramName: nameof(token)); break; default: // All unrecognized TokenTypes (including User tokens) are considered to be invalid. throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); } } - } } diff --git a/test/Discord.Net.Tests/Tests.TokenUtils.cs b/test/Discord.Net.Tests/Tests.TokenUtils.cs index 8cebc649d..9a1102ec5 100644 --- a/test/Discord.Net.Tests/Tests.TokenUtils.cs +++ b/test/Discord.Net.Tests/Tests.TokenUtils.cs @@ -76,9 +76,7 @@ namespace Discord [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] // 59 char token [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] - [InlineData("This appears to be completely invalid, however the current validation rules are not very strict.")] [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWss")] - [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] public void TestBotTokenDoesNotThrowExceptions(string token) { // This example token is pulled from the Discord Docs @@ -99,6 +97,9 @@ namespace Discord [InlineData("937it3ow87i4ery69876wqire")] // 57 char bot token [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK")] + [InlineData("This is an invalid token, but it passes the check for string length.")] + // valid token, but passed in twice + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] public void TestBotTokenInvalidThrowsArgumentException(string token) { Assert.Throws(() => TokenUtils.ValidateToken(TokenType.Bot, token)); @@ -124,5 +125,44 @@ namespace Discord Assert.Throws(() => TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); } + + /// + /// Checks the method for expected output. + /// + /// The Bot Token to test. + /// The expected result. + [Theory] + // this method only checks the first part of the JWT + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4..", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kK", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4. this part is invalid. this part is also invalid", true)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.", false)] + [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4", false)] + [InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw.xxxx.xxxxx", true)] + // should not throw an unexpected exception + [InlineData("", false)] + [InlineData(null, false)] + public void TestCheckBotTokenValidity(string token, bool expected) + { + Assert.Equal(expected, TokenUtils.CheckBotTokenValidity(token)); + } + + [Theory] + // cannot pass a ulong? as a param in InlineData, so have to have a separate param + // indicating if a value is null + [InlineData("NDI4NDc3OTQ0MDA5MTk1NTIw", false, 428477944009195520)] + // should return null w/o throwing other exceptions + [InlineData("", true, 0)] + [InlineData(" ", true, 0)] + [InlineData(null, true, 0)] + [InlineData("these chars aren't allowed @U#)*@#!)*", true, 0)] + public void TestDecodeBase64UserId(string encodedUserId, bool isNull, ulong expectedUserId) + { + var result = TokenUtils.DecodeBase64UserId(encodedUserId); + if (isNull) + Assert.Null(result); + else + Assert.Equal(expectedUserId, result); + } } }