| @@ -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 | // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library | ||||
| class Program | class Program | ||||
| { | { | ||||
| private DiscordSocketClient _client; | |||||
| private readonly DiscordSocketClient _client; | |||||
| // Discord.Net heavily utilizes TAP for async, so we create | // Discord.Net heavily utilizes TAP for async, so we create | ||||
| // an asynchronous context from the beginning. | // an asynchronous context from the beginning. | ||||
| static void Main(string[] args) | 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 = new DiscordSocketClient(); | ||||
| _client.Log += LogAsync; | _client.Log += LogAsync; | ||||
| _client.Ready += ReadyAsync; | _client.Ready += ReadyAsync; | ||||
| _client.MessageReceived += MessageReceivedAsync; | _client.MessageReceived += MessageReceivedAsync; | ||||
| } | |||||
| public async Task MainAsync() | |||||
| { | |||||
| // Tokens should be considered secret data, and never hard-coded. | // Tokens should be considered secret data, and never hard-coded. | ||||
| await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token")); | ||||
| await _client.StartAsync(); | await _client.StartAsync(); | ||||
| @@ -19,24 +19,32 @@ namespace _02_commands_framework | |||||
| // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library | // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library | ||||
| class Program | 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) | static void Main(string[] args) | ||||
| => new Program().MainAsync().GetAwaiter().GetResult(); | => new Program().MainAsync().GetAwaiter().GetResult(); | ||||
| public async Task MainAsync() | 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<DiscordSocketClient>(); | |||||
| var client = services.GetRequiredService<DiscordSocketClient>(); | |||||
| client.Log += LogAsync; | |||||
| services.GetRequiredService<CommandService>().Log += LogAsync; | |||||
| client.Log += LogAsync; | |||||
| services.GetRequiredService<CommandService>().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<CommandHandlingService>().InitializeAsync(); | |||||
| await services.GetRequiredService<CommandHandlingService>().InitializeAsync(); | |||||
| await Task.Delay(-1); | |||||
| await Task.Delay(-1); | |||||
| } | |||||
| } | } | ||||
| private Task LogAsync(LogMessage log) | private Task LogAsync(LogMessage log) | ||||
| @@ -46,7 +54,7 @@ namespace _02_commands_framework | |||||
| return Task.CompletedTask; | return Task.CompletedTask; | ||||
| } | } | ||||
| private IServiceProvider ConfigureServices() | |||||
| private ServiceProvider ConfigureServices() | |||||
| { | { | ||||
| return new ServiceCollection() | return new ServiceCollection() | ||||
| .AddSingleton<DiscordSocketClient>() | .AddSingleton<DiscordSocketClient>() | ||||
| @@ -13,41 +13,46 @@ namespace _03_sharded_client | |||||
| // DiscordSocketClient instances (or shards) to serve a large number of guilds. | // DiscordSocketClient instances (or shards) to serve a large number of guilds. | ||||
| class Program | class Program | ||||
| { | { | ||||
| private DiscordShardedClient _client; | |||||
| static void Main(string[] args) | static void Main(string[] args) | ||||
| => new Program().MainAsync().GetAwaiter().GetResult(); | => new Program().MainAsync().GetAwaiter().GetResult(); | ||||
| public async Task MainAsync() | public async Task MainAsync() | ||||
| { | { | ||||
| // You specify the amount of shards you'd like to have with the | // 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. | // have 1 shard per 1500-2000 guilds your bot is in. | ||||
| var config = new DiscordSocketConfig | var config = new DiscordSocketConfig | ||||
| { | { | ||||
| TotalShards = 2 | 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<DiscordShardedClient>(); | |||||
| // 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<CommandHandlingService>().InitializeAsync(); | |||||
| await services.GetRequiredService<CommandHandlingService>().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() | return new ServiceCollection() | ||||
| .AddSingleton(_client) | |||||
| .AddSingleton(new DiscordShardedClient(config)) | |||||
| .AddSingleton<CommandService>() | .AddSingleton<CommandService>() | ||||
| .AddSingleton<CommandHandlingService>() | .AddSingleton<CommandHandlingService>() | ||||
| .BuildServiceProvider(); | .BuildServiceProvider(); | ||||
| @@ -27,7 +27,7 @@ namespace Discord.Commands | |||||
| /// been successfully executed. | /// been successfully executed. | ||||
| /// </para> | /// </para> | ||||
| /// </remarks> | /// </remarks> | ||||
| public class CommandService | |||||
| public class CommandService : IDisposable | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Occurs when a command-related information is received. | /// Occurs when a command-related information is received. | ||||
| @@ -67,6 +67,8 @@ namespace Discord.Commands | |||||
| internal readonly LogManager _logManager; | internal readonly LogManager _logManager; | ||||
| internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; | internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; | ||||
| internal bool _isDisposed; | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents all modules loaded within <see cref="CommandService"/>. | /// Represents all modules loaded within <see cref="CommandService"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -330,9 +332,9 @@ namespace Discord.Commands | |||||
| //Type Readers | //Type Readers | ||||
| /// <summary> | /// <summary> | ||||
| /// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object | |||||
| /// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object | |||||
| /// type. | /// type. | ||||
| /// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will | |||||
| /// If <typeparamref name="T" /> is a <see cref="ValueType" />, a nullable <see cref="TypeReader" /> will | |||||
| /// also be added. | /// also be added. | ||||
| /// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged | /// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged | ||||
| /// and the default <see cref="TypeReader" /> will be replaced. | /// and the default <see cref="TypeReader" /> will be replaced. | ||||
| @@ -607,5 +609,23 @@ namespace Discord.Commands | |||||
| await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); | await _commandExecutedEvent.InvokeAsync(chosenOverload.Key.Command, context, result); | ||||
| return result; | return result; | ||||
| } | } | ||||
| protected virtual void Dispose(bool disposing) | |||||
| { | |||||
| if (!_isDisposed) | |||||
| { | |||||
| if (disposing) | |||||
| { | |||||
| _moduleLock?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | |||||
| } | |||||
| } | |||||
| void IDisposable.Dispose() | |||||
| { | |||||
| Dispose(true); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -16,4 +16,4 @@ | |||||
| <ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' "> | <ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' "> | ||||
| <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" /> | <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| </Project> | |||||
| </Project> | |||||
| @@ -19,7 +19,7 @@ namespace Discord.Commands | |||||
| public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) | public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) | ||||
| { | { | ||||
| var text = msg.Content; | var text = msg.Content; | ||||
| if (text.Length > 0 && text[0] == c) | |||||
| if (!string.IsNullOrEmpty(text) && text[0] == c) | |||||
| { | { | ||||
| argPos = 1; | argPos = 1; | ||||
| return true; | 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) | public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal) | ||||
| { | { | ||||
| var text = msg.Content; | var text = msg.Content; | ||||
| if (text.StartsWith(str, comparisonType)) | |||||
| if (!string.IsNullOrEmpty(text) && text.StartsWith(str, comparisonType)) | |||||
| { | { | ||||
| argPos = str.Length; | argPos = str.Length; | ||||
| return true; | return true; | ||||
| @@ -45,7 +45,7 @@ namespace Discord.Commands | |||||
| public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) | public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) | ||||
| { | { | ||||
| var text = msg.Content; | 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('>'); | int endPos = text.IndexOf('>'); | ||||
| if (endPos == -1) return false; | if (endPos == -1) return false; | ||||
| @@ -12,4 +12,7 @@ | |||||
| <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> | <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> | ||||
| <PackageReference Include="System.Interactive.Async" Version="3.1.1" /> | <PackageReference Include="System.Interactive.Async" Version="3.1.1" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup Condition=" '$(Configuration)' != 'Release' "> | |||||
| <PackageReference Include="IDisposableAnalyzers" Version="2.0.3.3" /> | |||||
| </ItemGroup> | |||||
| </Project> | </Project> | ||||
| @@ -44,45 +44,6 @@ namespace Discord | |||||
| /// </returns> | /// </returns> | ||||
| IReadOnlyCollection<Overwrite> PermissionOverwrites { get; } | IReadOnlyCollection<Overwrite> PermissionOverwrites { get; } | ||||
| /// <summary> | |||||
| /// Creates a new invite to this channel. | |||||
| /// </summary> | |||||
| /// <example> | |||||
| /// 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. | |||||
| /// <code language="cs"> | |||||
| /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | |||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | |||||
| /// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param> | |||||
| /// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous invite creation operation. The task result contains an invite | |||||
| /// metadata object containing information for the created invite. | |||||
| /// </returns> | |||||
| Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | |||||
| /// <summary> | |||||
| /// Gets a collection of all invites to this channel. | |||||
| /// </summary> | |||||
| /// <example> | |||||
| /// The following example gets all of the invites that have been created in this channel and selects the | |||||
| /// most used invite. | |||||
| /// <code language="cs"> | |||||
| /// var invites = await channel.GetInvitesAsync(); | |||||
| /// if (invites.Count == 0) return; | |||||
| /// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault(); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// 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. | |||||
| /// </returns> | |||||
| Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Modifies this guild channel. | /// Modifies this guild channel. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -1,3 +1,4 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord | namespace Discord | ||||
| @@ -25,10 +26,49 @@ namespace Discord | |||||
| /// representing the parent of this channel; <c>null</c> if none is set. | /// representing the parent of this channel; <c>null</c> if none is set. | ||||
| /// </returns> | /// </returns> | ||||
| Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | ||||
| /// <summary> | /// <summary> | ||||
| /// Syncs the permissions of this nested channel with its parent's. | /// Syncs the permissions of this nested channel with its parent's. | ||||
| /// </summary> | /// </summary> | ||||
| Task SyncPermissionsAsync(RequestOptions options = null); | Task SyncPermissionsAsync(RequestOptions options = null); | ||||
| /// <summary> | |||||
| /// Creates a new invite to this channel. | |||||
| /// </summary> | |||||
| /// <example> | |||||
| /// 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. | |||||
| /// <code language="cs"> | |||||
| /// await guildChannel.CreateInviteAsync(maxAge: 43200, maxUses: 3); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | |||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | |||||
| /// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param> | |||||
| /// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous invite creation operation. The task result contains an invite | |||||
| /// metadata object containing information for the created invite. | |||||
| /// </returns> | |||||
| Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null); | |||||
| /// <summary> | |||||
| /// Gets a collection of all invites to this channel. | |||||
| /// </summary>B | |||||
| /// <example> | |||||
| /// The following example gets all of the invites that have been created in this channel and selects the | |||||
| /// most used invite. | |||||
| /// <code language="cs"> | |||||
| /// var invites = await channel.GetInvitesAsync(); | |||||
| /// if (invites.Count == 0) return; | |||||
| /// var invite = invites.OrderByDescending(x => x.Uses).FirstOrDefault(); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// 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. | |||||
| /// </returns> | |||||
| Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null); | |||||
| } | } | ||||
| } | } | ||||
| @@ -474,12 +474,13 @@ namespace Discord | |||||
| /// Creates a new channel category in this guild. | /// Creates a new channel category in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="name">The new name for the category.</param> | /// <param name="name">The new name for the category.</param> | ||||
| /// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. The task result contains the newly created | /// A task that represents the asynchronous creation operation. The task result contains the newly created | ||||
| /// category channel. | /// category channel. | ||||
| /// </returns> | /// </returns> | ||||
| Task<ICategoryChannel> CreateCategoryAsync(string name, RequestOptions options = null); | |||||
| Task<ICategoryChannel> CreateCategoryAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of all the voice regions this guild can access. | /// Gets a collection of all the voice regions this guild can access. | ||||
| @@ -1,15 +1,21 @@ | |||||
| using System; | |||||
| using System.IO; | using System.IO; | ||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// An image that will be uploaded to Discord. | /// An image that will be uploaded to Discord. | ||||
| /// </summary> | /// </summary> | ||||
| public struct Image | |||||
| public struct Image : IDisposable | |||||
| { | { | ||||
| private bool _isDisposed; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the stream to be uploaded to Discord. | /// Gets the stream to be uploaded to Discord. | ||||
| /// </summary> | /// </summary> | ||||
| #pragma warning disable IDISP008 | |||||
| public Stream Stream { get; } | public Stream Stream { get; } | ||||
| #pragma warning restore IDISP008 | |||||
| /// <summary> | /// <summary> | ||||
| /// Create the image with a <see cref="System.IO.Stream"/>. | /// Create the image with a <see cref="System.IO.Stream"/>. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -19,6 +25,7 @@ namespace Discord | |||||
| /// </param> | /// </param> | ||||
| public Image(Stream stream) | public Image(Stream stream) | ||||
| { | { | ||||
| _isDisposed = false; | |||||
| Stream = stream; | Stream = stream; | ||||
| } | } | ||||
| @@ -45,15 +52,28 @@ namespace Discord | |||||
| /// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive). | /// The specified <paramref name="path"/> is invalid, (for example, it is on an unmapped drive). | ||||
| /// </exception> | /// </exception> | ||||
| /// <exception cref="System.UnauthorizedAccessException"> | /// <exception cref="System.UnauthorizedAccessException"> | ||||
| /// <paramref name="path" /> specified a directory.-or- The caller does not have the required permission. | |||||
| /// <paramref name="path" /> specified a directory.-or- The caller does not have the required permission. | |||||
| /// </exception> | /// </exception> | ||||
| /// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found. | |||||
| /// <exception cref="FileNotFoundException">The file specified in <paramref name="path" /> was not found. | |||||
| /// </exception> | /// </exception> | ||||
| /// <exception cref="IOException">An I/O error occurred while opening the file. </exception> | /// <exception cref="IOException">An I/O error occurred while opening the file. </exception> | ||||
| public Image(string path) | public Image(string path) | ||||
| { | { | ||||
| _isDisposed = false; | |||||
| Stream = File.OpenRead(path); | Stream = File.OpenRead(path); | ||||
| } | } | ||||
| /// <inheritdoc/> | |||||
| public void Dispose() | |||||
| { | |||||
| if (!_isDisposed) | |||||
| { | |||||
| #pragma warning disable IDISP007 | |||||
| Stream?.Dispose(); | |||||
| #pragma warning restore IDISP007 | |||||
| _isDisposed = true; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,3 +1,5 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| @@ -17,5 +19,57 @@ namespace Discord | |||||
| var channel = msg.Channel; | var channel = msg.Channel; | ||||
| return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; | return $"https://discordapp.com/channels/{(channel is IDMChannel ? "@me" : $"{(channel as ITextChannel).GuildId}")}/{channel.Id}/{msg.Id}"; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Add multiple reactions to a message. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This method does not bulk add reactions! It will send a request for each reaction inculded. | |||||
| /// </remarks> | |||||
| /// <example> | |||||
| /// <code language="cs"> | |||||
| /// IEmote A = new Emoji("🅰"); | |||||
| /// IEmote B = new Emoji("🅱"); | |||||
| /// await msg.AddReactionsAsync(new[] { A, B }); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="msg">The message to add reactions to.</param> | |||||
| /// <param name="reactions">An array of reactions to add to the message</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous operation for adding a reaction to this message. | |||||
| /// </returns> | |||||
| /// <seealso cref="IUserMessage.AddReactionAsync(IEmote, RequestOptions)"/> | |||||
| /// <seealso cref="IEmote"/> | |||||
| 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); | |||||
| } | |||||
| /// <summary> | |||||
| /// Remove multiple reactions from a message. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This method does not bulk remove reactions! If you want to clear reactions from a message, | |||||
| /// <see cref="IUserMessage.RemoveAllReactionsAsync(RequestOptions)"/> | |||||
| /// </remarks> | |||||
| /// <example> | |||||
| /// <code language="cs"> | |||||
| /// await msg.RemoveReactionsAsync(currentUser, new[] { A, B }); | |||||
| /// </code> | |||||
| /// </example> | |||||
| /// <param name="msg">The message to remove reactions from.</param> | |||||
| /// <param name="reactions">An array of reactions to remove from the message</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous operation for removing a reaction to this message. | |||||
| /// </returns> | |||||
| /// <seealso cref="IUserMessage.RemoveReactionAsync(IEmote, IUser, RequestOptions)"/> | |||||
| /// <seealso cref="IEmote"/> | |||||
| 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); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -14,6 +14,10 @@ namespace Discord | |||||
| public static string Underline(string text) => $"__{text}__"; | public static string Underline(string text) => $"__{text}__"; | ||||
| /// <summary> Returns a markdown-formatted string with strikethrough formatting. </summary> | /// <summary> Returns a markdown-formatted string with strikethrough formatting. </summary> | ||||
| public static string Strikethrough(string text) => $"~~{text}~~"; | public static string Strikethrough(string text) => $"~~{text}~~"; | ||||
| /// <summary> Returns a markdown-formatted URL. Only works in <see cref="EmbedBuilder"/> descriptions and fields. </summary> | |||||
| public static string Url(string text, string url) => $"[{text}]({url})"; | |||||
| /// <summary> Escapes a URL so that a preview is not generated. </summary> | |||||
| public static string EscapeUrl(string url) => $"<{url}>"; | |||||
| /// <summary> Returns a markdown-formatted string with codeblock formatting. </summary> | /// <summary> Returns a markdown-formatted string with codeblock formatting. </summary> | ||||
| public static string Code(string text, string language = null) | public static string Code(string text, string language = null) | ||||
| @@ -1,3 +1,4 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Threading; | using System.Threading; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -7,7 +8,7 @@ namespace Discord.Net.Rest | |||||
| /// <summary> | /// <summary> | ||||
| /// Represents a generic REST-based client. | /// Represents a generic REST-based client. | ||||
| /// </summary> | /// </summary> | ||||
| public interface IRestClient | |||||
| public interface IRestClient : IDisposable | |||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| /// Sets the HTTP header of this client for all requests. | /// Sets the HTTP header of this client for all requests. | ||||
| @@ -4,7 +4,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.Net.Udp | namespace Discord.Net.Udp | ||||
| { | { | ||||
| public interface IUdpSocket | |||||
| public interface IUdpSocket : IDisposable | |||||
| { | { | ||||
| event Func<byte[], int, int, Task> ReceivedDatagram; | event Func<byte[], int, int, Task> ReceivedDatagram; | ||||
| @@ -4,7 +4,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.Net.WebSockets | namespace Discord.Net.WebSockets | ||||
| { | { | ||||
| public interface IWebSocketClient | |||||
| public interface IWebSocketClient : IDisposable | |||||
| { | { | ||||
| event Func<byte[], int, int, Task> BinaryMessage; | event Func<byte[], int, int, Task> BinaryMessage; | ||||
| event Func<string, Task> TextMessage; | event Func<string, Task> TextMessage; | ||||
| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Text; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| @@ -7,6 +8,74 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| public static class TokenUtils | public static class TokenUtils | ||||
| { | { | ||||
| /// <summary> | |||||
| /// The minimum length of a Bot token. | |||||
| /// </summary> | |||||
| /// <remarks> | |||||
| /// This value was determined by comparing against the examples in the Discord | |||||
| /// documentation, and pre-existing tokens. | |||||
| /// </remarks> | |||||
| internal const int MinBotTokenLength = 58; | |||||
| /// <summary> | |||||
| /// Decodes a base 64 encoded string into a ulong value. | |||||
| /// </summary> | |||||
| /// <param name="encoded"> A base 64 encoded string containing a User Id.</param> | |||||
| /// <returns> A ulong containing the decoded value of the string, or null if the value was invalid. </returns> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | |||||
| /// Checks the validity of a bot token by attempting to decode a ulong userid | |||||
| /// from the bot token. | |||||
| /// </summary> | |||||
| /// <param name="message"> | |||||
| /// The bot token to validate. | |||||
| /// </param> | |||||
| /// <returns> | |||||
| /// True if the bot token was valid, false if it was not. | |||||
| /// </returns> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Checks the validity of the supplied token of a specific type. | /// Checks the validity of the supplied token of a specific type. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -29,17 +98,21 @@ namespace Discord | |||||
| // no validation is performed on Bearer tokens | // no validation is performed on Bearer tokens | ||||
| break; | break; | ||||
| case TokenType.Bot: | 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 | // this value was determined by referencing examples in the discord documentation, and by comparing with | ||||
| // pre-existing tokens | // 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. " + | |||||
| "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; | break; | ||||
| default: | default: | ||||
| // All unrecognized TokenTypes (including User tokens) are considered to be invalid. | // All unrecognized TokenTypes (including User tokens) are considered to be invalid. | ||||
| throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); | throw new ArgumentException(message: "Unrecognized TokenType.", paramName: nameof(token)); | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -19,6 +19,7 @@ namespace Discord.Net.Providers.WS4Net | |||||
| private readonly SemaphoreSlim _lock; | private readonly SemaphoreSlim _lock; | ||||
| private readonly Dictionary<string, string> _headers; | private readonly Dictionary<string, string> _headers; | ||||
| private WS4NetSocket _client; | private WS4NetSocket _client; | ||||
| private CancellationTokenSource _disconnectCancelTokenSource; | |||||
| private CancellationTokenSource _cancelTokenSource; | private CancellationTokenSource _cancelTokenSource; | ||||
| private CancellationToken _cancelToken, _parentToken; | private CancellationToken _cancelToken, _parentToken; | ||||
| private ManualResetEventSlim _waitUntilConnect; | private ManualResetEventSlim _waitUntilConnect; | ||||
| @@ -28,7 +29,7 @@ namespace Discord.Net.Providers.WS4Net | |||||
| { | { | ||||
| _headers = new Dictionary<string, string>(); | _headers = new Dictionary<string, string>(); | ||||
| _lock = new SemaphoreSlim(1, 1); | _lock = new SemaphoreSlim(1, 1); | ||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _disconnectCancelTokenSource = new CancellationTokenSource(); | |||||
| _cancelToken = CancellationToken.None; | _cancelToken = CancellationToken.None; | ||||
| _parentToken = CancellationToken.None; | _parentToken = CancellationToken.None; | ||||
| _waitUntilConnect = new ManualResetEventSlim(); | _waitUntilConnect = new ManualResetEventSlim(); | ||||
| @@ -38,7 +39,11 @@ namespace Discord.Net.Providers.WS4Net | |||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | |||||
| DisconnectInternalAsync(true).GetAwaiter().GetResult(); | DisconnectInternalAsync(true).GetAwaiter().GetResult(); | ||||
| _lock?.Dispose(); | |||||
| _cancelTokenSource?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| @@ -63,8 +68,13 @@ namespace Discord.Net.Providers.WS4Net | |||||
| { | { | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | 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()) | _client = new WS4NetSocket(host, "", customHeaderItems: _headers.ToList()) | ||||
| { | { | ||||
| @@ -96,7 +106,7 @@ namespace Discord.Net.Providers.WS4Net | |||||
| } | } | ||||
| private Task DisconnectInternalAsync(bool isDisposing = false) | private Task DisconnectInternalAsync(bool isDisposing = false) | ||||
| { | { | ||||
| _cancelTokenSource.Cancel(); | |||||
| _disconnectCancelTokenSource.Cancel(); | |||||
| if (_client == null) | if (_client == null) | ||||
| return Task.Delay(0); | return Task.Delay(0); | ||||
| @@ -125,8 +135,10 @@ namespace Discord.Net.Providers.WS4Net | |||||
| } | } | ||||
| public void SetCancelToken(CancellationToken cancelToken) | public void SetCancelToken(CancellationToken cancelToken) | ||||
| { | { | ||||
| _cancelTokenSource?.Dispose(); | |||||
| _parentToken = cancelToken; | _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) | public async Task SendAsync(byte[] data, int index, int count, bool isText) | ||||
| @@ -12,6 +12,8 @@ namespace Discord.API.Rest | |||||
| public ChannelType Type { get; } | public ChannelType Type { get; } | ||||
| [JsonProperty("parent_id")] | [JsonProperty("parent_id")] | ||||
| public Optional<ulong?> CategoryId { get; set; } | public Optional<ulong?> CategoryId { get; set; } | ||||
| [JsonProperty("position")] | |||||
| public Optional<int> Position { get; set; } | |||||
| //Text channels | //Text channels | ||||
| [JsonProperty("topic")] | [JsonProperty("topic")] | ||||
| @@ -34,7 +34,7 @@ namespace Discord.Rest | |||||
| public ISelfUser CurrentUser { get; protected set; } | public ISelfUser CurrentUser { get; protected set; } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public TokenType TokenType => ApiClient.AuthTokenType; | public TokenType TokenType => ApiClient.AuthTokenType; | ||||
| /// <summary> Creates a new REST-only Discord client. </summary> | /// <summary> Creates a new REST-only Discord client. </summary> | ||||
| internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) | internal BaseDiscordClient(DiscordRestConfig config, API.DiscordRestApiClient client) | ||||
| { | { | ||||
| @@ -106,9 +106,9 @@ namespace Discord.Rest | |||||
| await _loggedInEvent.InvokeAsync().ConfigureAwait(false); | await _loggedInEvent.InvokeAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| internal virtual Task OnLoginAsync(TokenType tokenType, string token) | |||||
| internal virtual Task OnLoginAsync(TokenType tokenType, string token) | |||||
| => Task.Delay(0); | => Task.Delay(0); | ||||
| public async Task LogoutAsync() | public async Task LogoutAsync() | ||||
| { | { | ||||
| await _stateLock.WaitAsync().ConfigureAwait(false); | await _stateLock.WaitAsync().ConfigureAwait(false); | ||||
| @@ -131,14 +131,17 @@ namespace Discord.Rest | |||||
| await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); | await _loggedOutEvent.InvokeAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| internal virtual Task OnLogoutAsync() | |||||
| internal virtual Task OnLogoutAsync() | |||||
| => Task.Delay(0); | => Task.Delay(0); | ||||
| internal virtual void Dispose(bool disposing) | internal virtual void Dispose(bool disposing) | ||||
| { | { | ||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | { | ||||
| #pragma warning disable IDISP007 | |||||
| ApiClient.Dispose(); | ApiClient.Dispose(); | ||||
| #pragma warning restore IDISP007 | |||||
| _stateLock?.Dispose(); | |||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| @@ -156,7 +159,7 @@ namespace Discord.Rest | |||||
| ISelfUser IDiscordClient.CurrentUser => CurrentUser; | ISelfUser IDiscordClient.CurrentUser => CurrentUser; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) | |||||
| Task<IApplication> IDiscordClient.GetApplicationInfoAsync(RequestOptions options) | |||||
| => throw new NotSupportedException(); | => throw new NotSupportedException(); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -66,6 +66,7 @@ namespace Discord.API | |||||
| /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | /// <exception cref="ArgumentException">Unknown OAuth token type.</exception> | ||||
| internal void SetBaseUrl(string baseUrl) | internal void SetBaseUrl(string baseUrl) | ||||
| { | { | ||||
| RestClient?.Dispose(); | |||||
| RestClient = _restClientProvider(baseUrl); | RestClient = _restClientProvider(baseUrl); | ||||
| RestClient.SetHeader("accept", "*/*"); | RestClient.SetHeader("accept", "*/*"); | ||||
| RestClient.SetHeader("user-agent", UserAgent); | RestClient.SetHeader("user-agent", UserAgent); | ||||
| @@ -93,7 +94,9 @@ namespace Discord.API | |||||
| if (disposing) | if (disposing) | ||||
| { | { | ||||
| _loginCancelToken?.Dispose(); | _loginCancelToken?.Dispose(); | ||||
| (RestClient as IDisposable)?.Dispose(); | |||||
| RestClient?.Dispose(); | |||||
| RequestQueue?.Dispose(); | |||||
| _stateLock?.Dispose(); | |||||
| } | } | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| @@ -117,6 +120,7 @@ namespace Discord.API | |||||
| try | try | ||||
| { | { | ||||
| _loginCancelToken?.Dispose(); | |||||
| _loginCancelToken = new CancellationTokenSource(); | _loginCancelToken = new CancellationTokenSource(); | ||||
| AuthToken = null; | AuthToken = null; | ||||
| @@ -242,7 +246,7 @@ namespace Discord.API | |||||
| internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, | internal Task<TResponse> SendMultipartAsync<TResponse>(string method, Expression<Func<string>> endpointExpr, IReadOnlyDictionary<string, object> multipartArgs, BucketIds ids, | ||||
| ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) | ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) | ||||
| => SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); | => SendMultipartAsync<TResponse>(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, funcName), clientBucket, options); | ||||
| public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, | |||||
| public async Task<TResponse> SendMultipartAsync<TResponse>(string method, string endpoint, IReadOnlyDictionary<string, object> multipartArgs, | |||||
| string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) | string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) | ||||
| { | { | ||||
| options = options ?? new RequestOptions(); | options = options ?? new RequestOptions(); | ||||
| @@ -33,6 +33,8 @@ namespace Discord.Rest | |||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| ApiClient.Dispose(); | ApiClient.Dispose(); | ||||
| base.Dispose(disposing); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -48,12 +50,12 @@ namespace Discord.Rest | |||||
| _applicationInfo = null; | _applicationInfo = null; | ||||
| return Task.Delay(0); | return Task.Delay(0); | ||||
| } | } | ||||
| public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null) | public async Task<RestApplication> GetApplicationInfoAsync(RequestOptions options = null) | ||||
| { | { | ||||
| return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); | return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this, options).ConfigureAwait(false)); | ||||
| } | } | ||||
| public Task<RestChannel> GetChannelAsync(ulong id, RequestOptions options = null) | public Task<RestChannel> GetChannelAsync(ulong id, RequestOptions options = null) | ||||
| => ClientHelper.GetChannelAsync(this, id, options); | => ClientHelper.GetChannelAsync(this, id, options); | ||||
| public Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<IRestPrivateChannel>> GetPrivateChannelsAsync(RequestOptions options = null) | ||||
| @@ -62,7 +64,7 @@ namespace Discord.Rest | |||||
| => ClientHelper.GetDMChannelsAsync(this, options); | => ClientHelper.GetDMChannelsAsync(this, options); | ||||
| public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestGroupChannel>> GetGroupChannelsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetGroupChannelsAsync(this, options); | => ClientHelper.GetGroupChannelsAsync(this, options); | ||||
| public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestConnection>> GetConnectionsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetConnectionsAsync(this, options); | => ClientHelper.GetConnectionsAsync(this, options); | ||||
| @@ -81,12 +83,12 @@ namespace Discord.Rest | |||||
| => ClientHelper.GetGuildsAsync(this, options); | => ClientHelper.GetGuildsAsync(this, options); | ||||
| public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) | public Task<RestGuild> CreateGuildAsync(string name, IVoiceRegion region, Stream jpegIcon = null, RequestOptions options = null) | ||||
| => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); | => ClientHelper.CreateGuildAsync(this, name, region, jpegIcon, options); | ||||
| public Task<RestUser> GetUserAsync(ulong id, RequestOptions options = null) | public Task<RestUser> GetUserAsync(ulong id, RequestOptions options = null) | ||||
| => ClientHelper.GetUserAsync(this, id, options); | => ClientHelper.GetUserAsync(this, id, options); | ||||
| public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | public Task<RestGuildUser> GetGuildUserAsync(ulong guildId, ulong id, RequestOptions options = null) | ||||
| => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | => ClientHelper.GetGuildUserAsync(this, guildId, id, options); | ||||
| public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestVoiceRegion>> GetVoiceRegionsAsync(RequestOptions options = null) | ||||
| => ClientHelper.GetVoiceRegionsAsync(this, options); | => ClientHelper.GetVoiceRegionsAsync(this, options); | ||||
| public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null) | public Task<RestVoiceRegion> GetVoiceRegionAsync(string id, RequestOptions options = null) | ||||
| @@ -33,10 +33,10 @@ namespace Discord.Rest | |||||
| foreach (var overwrite in overwritesModel.NewValue) | foreach (var overwrite in overwritesModel.NewValue) | ||||
| { | { | ||||
| var deny = overwrite.Value<ulong>("deny"); | |||||
| var permType = overwrite.Value<PermissionTarget>("type"); | |||||
| var id = overwrite.Value<ulong>("id"); | |||||
| var allow = overwrite.Value<ulong>("allow"); | |||||
| var deny = overwrite["deny"].ToObject<ulong>(discord.ApiClient.Serializer); | |||||
| var permType = overwrite["type"].ToObject<PermissionTarget>(discord.ApiClient.Serializer); | |||||
| var id = overwrite["id"].ToObject<ulong>(discord.ApiClient.Serializer); | |||||
| var allow = overwrite["allow"].ToObject<ulong>(discord.ApiClient.Serializer); | |||||
| overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); | overwrites.Add(new Overwrite(id, permType, new OverwritePermissions(allow, deny))); | ||||
| } | } | ||||
| @@ -25,16 +25,6 @@ namespace Discord.Rest | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Category)"; | private string DebuggerDisplay => $"{Name} ({Id}, Category)"; | ||||
| // IGuildChannel | |||||
| /// <inheritdoc /> | |||||
| /// <exception cref="NotSupportedException">This method is not supported with category channels.</exception> | |||||
| Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | |||||
| => throw new NotSupportedException(); | |||||
| /// <inheritdoc /> | |||||
| /// <exception cref="NotSupportedException">This method is not supported with category channels.</exception> | |||||
| Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options) | |||||
| => throw new NotSupportedException(); | |||||
| //IChannel | //IChannel | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| /// <exception cref="NotSupportedException">This method is not supported with category channels.</exception> | /// <exception cref="NotSupportedException">This method is not supported with category channels.</exception> | ||||
| @@ -178,32 +178,6 @@ namespace Discord.Rest | |||||
| } | } | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Gets a collection of all invites to this channel. | |||||
| /// </summary> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// 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. | |||||
| /// </returns> | |||||
| public async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||||
| /// <summary> | |||||
| /// Creates a new invite to this channel. | |||||
| /// </summary> | |||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | |||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | |||||
| /// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param> | |||||
| /// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous invite creation operation. The task result contains an invite | |||||
| /// metadata object containing information for the created invite. | |||||
| /// </returns> | |||||
| public async Task<RestInviteMetadata> 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); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the name of this channel. | /// Gets the name of this channel. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -224,13 +198,6 @@ namespace Discord.Rest | |||||
| } | } | ||||
| } | } | ||||
| /// <inheritdoc /> | |||||
| async Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options) | |||||
| => await GetInvitesAsync(options).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | |||||
| async Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | |||||
| => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) | OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) | ||||
| => GetPermissionOverwrite(role); | => GetPermissionOverwrite(role); | ||||
| @@ -204,6 +204,14 @@ namespace Discord.Rest | |||||
| public Task SyncPermissionsAsync(RequestOptions options = null) | public Task SyncPermissionsAsync(RequestOptions options = null) | ||||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | ||||
| //Invites | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | ||||
| //ITextChannel | //ITextChannel | ||||
| @@ -57,8 +57,17 @@ namespace Discord.Rest | |||||
| /// </returns> | /// </returns> | ||||
| public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | public Task<ICategoryChannel> GetCategoryAsync(RequestOptions options = null) | ||||
| => ChannelHelper.GetCategoryAsync(this, Discord, options); | => ChannelHelper.GetCategoryAsync(this, Discord, options); | ||||
| public Task SyncPermissionsAsync(RequestOptions options = null) | public Task SyncPermissionsAsync(RequestOptions options = null) | ||||
| => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | => ChannelHelper.SyncPermissionsAsync(this, Discord, options); | ||||
| //Invites | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | ||||
| @@ -77,6 +86,7 @@ namespace Discord.Rest | |||||
| => AsyncEnumerable.Empty<IReadOnlyCollection<IGuildUser>>(); | => AsyncEnumerable.Empty<IReadOnlyCollection<IGuildUser>>(); | ||||
| // INestedChannel | // INestedChannel | ||||
| /// <inheritdoc /> | |||||
| async Task<ICategoryChannel> INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) | async Task<ICategoryChannel> INestedChannel.GetCategoryAsync(CacheMode mode, RequestOptions options) | ||||
| { | { | ||||
| if (CategoryId.HasValue && mode == CacheMode.AllowDownload) | if (CategoryId.HasValue && mode == CacheMode.AllowDownload) | ||||
| @@ -163,6 +163,7 @@ namespace Discord.Rest | |||||
| CategoryId = props.CategoryId, | CategoryId = props.CategoryId, | ||||
| Topic = props.Topic, | Topic = props.Topic, | ||||
| IsNsfw = props.IsNsfw, | IsNsfw = props.IsNsfw, | ||||
| Position = props.Position | |||||
| }; | }; | ||||
| var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); | var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); | ||||
| return RestTextChannel.Create(client, guild, model); | return RestTextChannel.Create(client, guild, model); | ||||
| @@ -180,18 +181,26 @@ namespace Discord.Rest | |||||
| { | { | ||||
| CategoryId = props.CategoryId, | CategoryId = props.CategoryId, | ||||
| Bitrate = props.Bitrate, | Bitrate = props.Bitrate, | ||||
| UserLimit = props.UserLimit | |||||
| UserLimit = props.UserLimit, | |||||
| Position = props.Position | |||||
| }; | }; | ||||
| var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); | var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); | ||||
| return RestVoiceChannel.Create(client, guild, model); | return RestVoiceChannel.Create(client, guild, model); | ||||
| } | } | ||||
| /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception> | /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception> | ||||
| public static async Task<RestCategoryChannel> CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, | public static async Task<RestCategoryChannel> CreateCategoryChannelAsync(IGuild guild, BaseDiscordClient client, | ||||
| string name, RequestOptions options) | |||||
| string name, RequestOptions options, Action<GuildChannelProperties> func = null) | |||||
| { | { | ||||
| if (name == null) throw new ArgumentNullException(paramName: nameof(name)); | 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); | var model = await client.ApiClient.CreateGuildChannelAsync(guild.Id, args, options).ConfigureAwait(false); | ||||
| return RestCategoryChannel.Create(client, guild, model); | return RestCategoryChannel.Create(client, guild, model); | ||||
| } | } | ||||
| @@ -441,13 +441,14 @@ namespace Discord.Rest | |||||
| /// Creates a category channel with the provided name. | /// Creates a category channel with the provided name. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="name">The name of the new channel.</param> | /// <param name="name">The name of the new channel.</param> | ||||
| /// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
| /// <exception cref="ArgumentNullException"><paramref name="name" /> is <c>null</c>.</exception> | /// <exception cref="ArgumentNullException"><paramref name="name" /> is <c>null</c>.</exception> | ||||
| /// <returns> | /// <returns> | ||||
| /// The created category channel. | /// The created category channel. | ||||
| /// </returns> | /// </returns> | ||||
| public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, RequestOptions options = null) | |||||
| => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); | |||||
| public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | |||||
| => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets a collection of all the voice regions this guild can access. | /// Gets a collection of all the voice regions this guild can access. | ||||
| @@ -776,8 +777,8 @@ namespace Discord.Rest | |||||
| async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, Action<VoiceChannelProperties> func, RequestOptions options) | async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, Action<VoiceChannelProperties> func, RequestOptions options) | ||||
| => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); | => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, RequestOptions options) | |||||
| => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); | |||||
| async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, Action<GuildChannelProperties> func, RequestOptions options) | |||||
| => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IReadOnlyCollection<IVoiceRegion>> IGuild.GetVoiceRegionsAsync(RequestOptions options) | async Task<IReadOnlyCollection<IVoiceRegion>> IGuild.GetVoiceRegionsAsync(RequestOptions options) | ||||
| @@ -34,12 +34,14 @@ namespace Discord.Net.Converters | |||||
| } | } | ||||
| else | 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); | string base64 = Convert.ToBase64String(bytes, 0, length); | ||||
| @@ -27,12 +27,14 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| _baseUrl = baseUrl; | _baseUrl = baseUrl; | ||||
| #pragma warning disable IDISP014 | |||||
| _client = new HttpClient(new HttpClientHandler | _client = new HttpClient(new HttpClientHandler | ||||
| { | { | ||||
| AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, | ||||
| UseCookies = false, | UseCookies = false, | ||||
| UseProxy = useProxy, | UseProxy = useProxy, | ||||
| }); | }); | ||||
| #pragma warning restore IDISP014 | |||||
| SetHeader("accept-encoding", "gzip, deflate"); | SetHeader("accept-encoding", "gzip, deflate"); | ||||
| _cancelToken = CancellationToken.None; | _cancelToken = CancellationToken.None; | ||||
| @@ -91,12 +93,14 @@ namespace Discord.Net.Rest | |||||
| { | { | ||||
| if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason)); | ||||
| var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); | ||||
| MemoryStream memoryStream = null; | |||||
| if (multipartParams != null) | if (multipartParams != null) | ||||
| { | { | ||||
| foreach (var p in multipartParams) | foreach (var p in multipartParams) | ||||
| { | { | ||||
| switch (p.Value) | switch (p.Value) | ||||
| { | { | ||||
| #pragma warning disable IDISP004 | |||||
| case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } | case string stringValue: { content.Add(new StringContent(stringValue), p.Key); continue; } | ||||
| case byte[] byteArrayValue: { content.Add(new ByteArrayContent(byteArrayValue), 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; } | case Stream streamValue: { content.Add(new StreamContent(streamValue), p.Key); continue; } | ||||
| @@ -105,12 +109,15 @@ namespace Discord.Net.Rest | |||||
| var stream = fileValue.Stream; | var stream = fileValue.Stream; | ||||
| if (!stream.CanSeek) | if (!stream.CanSeek) | ||||
| { | { | ||||
| var memoryStream = new MemoryStream(); | |||||
| memoryStream = new MemoryStream(); | |||||
| await stream.CopyToAsync(memoryStream).ConfigureAwait(false); | await stream.CopyToAsync(memoryStream).ConfigureAwait(false); | ||||
| memoryStream.Position = 0; | memoryStream.Position = 0; | ||||
| #pragma warning disable IDISP001 | |||||
| stream = memoryStream; | stream = memoryStream; | ||||
| #pragma warning restore IDISP001 | |||||
| } | } | ||||
| content.Add(new StreamContent(stream), p.Key, fileValue.Filename); | content.Add(new StreamContent(stream), p.Key, fileValue.Filename); | ||||
| #pragma warning restore IDISP004 | |||||
| continue; | continue; | ||||
| } | } | ||||
| default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); | default: throw new InvalidOperationException($"Unsupported param type \"{p.Value.GetType().Name}\"."); | ||||
| @@ -118,19 +125,24 @@ namespace Discord.Net.Rest | |||||
| } | } | ||||
| } | } | ||||
| restRequest.Content = content; | 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<RestResponse> SendInternalAsync(HttpRequestMessage request, CancellationToken cancelToken, bool headerOnly) | private async Task<RestResponse> 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"); | private static readonly HttpMethod Patch = new HttpMethod("PATCH"); | ||||
| @@ -16,23 +16,24 @@ namespace Discord.Net.Queue | |||||
| private readonly ConcurrentDictionary<string, RequestBucket> _buckets; | private readonly ConcurrentDictionary<string, RequestBucket> _buckets; | ||||
| private readonly SemaphoreSlim _tokenLock; | private readonly SemaphoreSlim _tokenLock; | ||||
| private readonly CancellationTokenSource _cancelToken; //Dispose token | |||||
| private readonly CancellationTokenSource _cancelTokenSource; //Dispose token | |||||
| private CancellationTokenSource _clearToken; | private CancellationTokenSource _clearToken; | ||||
| private CancellationToken _parentToken; | private CancellationToken _parentToken; | ||||
| private CancellationTokenSource _requestCancelTokenSource; | |||||
| private CancellationToken _requestCancelToken; //Parent token + Clear token | private CancellationToken _requestCancelToken; //Parent token + Clear token | ||||
| private DateTimeOffset _waitUntil; | private DateTimeOffset _waitUntil; | ||||
| private Task _cleanupTask; | private Task _cleanupTask; | ||||
| public RequestQueue() | public RequestQueue() | ||||
| { | { | ||||
| _tokenLock = new SemaphoreSlim(1, 1); | _tokenLock = new SemaphoreSlim(1, 1); | ||||
| _clearToken = new CancellationTokenSource(); | _clearToken = new CancellationTokenSource(); | ||||
| _cancelToken = new CancellationTokenSource(); | |||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _requestCancelToken = CancellationToken.None; | _requestCancelToken = CancellationToken.None; | ||||
| _parentToken = CancellationToken.None; | _parentToken = CancellationToken.None; | ||||
| _buckets = new ConcurrentDictionary<string, RequestBucket>(); | _buckets = new ConcurrentDictionary<string, RequestBucket>(); | ||||
| _cleanupTask = RunCleanup(); | _cleanupTask = RunCleanup(); | ||||
| @@ -44,7 +45,9 @@ namespace Discord.Net.Queue | |||||
| try | try | ||||
| { | { | ||||
| _parentToken = cancelToken; | _parentToken = cancelToken; | ||||
| _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token).Token; | |||||
| _requestCancelTokenSource?.Dispose(); | |||||
| _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _clearToken.Token); | |||||
| _requestCancelToken = _requestCancelTokenSource.Token; | |||||
| } | } | ||||
| finally { _tokenLock.Release(); } | finally { _tokenLock.Release(); } | ||||
| } | } | ||||
| @@ -54,9 +57,14 @@ namespace Discord.Net.Queue | |||||
| try | try | ||||
| { | { | ||||
| _clearToken?.Cancel(); | _clearToken?.Cancel(); | ||||
| _clearToken?.Dispose(); | |||||
| _clearToken = new CancellationTokenSource(); | _clearToken = new CancellationTokenSource(); | ||||
| if (_parentToken != null) | if (_parentToken != null) | ||||
| _requestCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken).Token; | |||||
| { | |||||
| _requestCancelTokenSource?.Dispose(); | |||||
| _requestCancelTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_clearToken.Token, _parentToken); | |||||
| _requestCancelToken = _requestCancelTokenSource.Token; | |||||
| } | |||||
| else | else | ||||
| _requestCancelToken = _clearToken.Token; | _requestCancelToken = _clearToken.Token; | ||||
| } | } | ||||
| @@ -65,13 +73,19 @@ namespace Discord.Net.Queue | |||||
| public async Task<Stream> SendAsync(RestRequest request) | public async Task<Stream> SendAsync(RestRequest request) | ||||
| { | { | ||||
| CancellationTokenSource createdTokenSource = null; | |||||
| if (request.Options.CancelToken.CanBeCanceled) | 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 | else | ||||
| request.Options.CancelToken = _requestCancelToken; | request.Options.CancelToken = _requestCancelToken; | ||||
| var bucket = GetOrCreateBucket(request.Options.BucketId, request); | 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) | public async Task SendAsync(WebSocketRequest request) | ||||
| { | { | ||||
| @@ -109,7 +123,7 @@ namespace Discord.Net.Queue | |||||
| { | { | ||||
| try | try | ||||
| { | { | ||||
| while (!_cancelToken.IsCancellationRequested) | |||||
| while (!_cancelTokenSource.IsCancellationRequested) | |||||
| { | { | ||||
| var now = DateTimeOffset.UtcNow; | var now = DateTimeOffset.UtcNow; | ||||
| foreach (var bucket in _buckets.Select(x => x.Value)) | foreach (var bucket in _buckets.Select(x => x.Value)) | ||||
| @@ -117,7 +131,7 @@ namespace Discord.Net.Queue | |||||
| if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) | if ((now - bucket.LastAttemptAt).TotalMinutes > 1.0) | ||||
| _buckets.TryRemove(bucket.Id, out _); | _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) { } | catch (OperationCanceledException) { } | ||||
| @@ -126,7 +140,10 @@ namespace Discord.Net.Queue | |||||
| public void Dispose() | public void Dispose() | ||||
| { | { | ||||
| _cancelToken.Dispose(); | |||||
| _cancelTokenSource?.Dispose(); | |||||
| _tokenLock?.Dispose(); | |||||
| _clearToken?.Dispose(); | |||||
| _requestCancelTokenSource?.Dispose(); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -71,7 +71,7 @@ namespace Discord.Audio | |||||
| ApiClient.ReceivedPacket += ProcessPacketAsync; | ApiClient.ReceivedPacket += ProcessPacketAsync; | ||||
| _stateLock = new SemaphoreSlim(1, 1); | _stateLock = new SemaphoreSlim(1, 1); | ||||
| _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, | |||||
| _connection = new ConnectionManager(_stateLock, _audioLogger, 30000, | |||||
| OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); | OnConnectingAsync, OnDisconnectingAsync, x => ApiClient.Disconnected += x); | ||||
| _connection.Connected += () => _connectedEvent.InvokeAsync(); | _connection.Connected += () => _connectedEvent.InvokeAsync(); | ||||
| _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); | _connection.Disconnected += (ex, recon) => _disconnectedEvent.InvokeAsync(ex); | ||||
| @@ -79,7 +79,7 @@ namespace Discord.Audio | |||||
| _keepaliveTimes = new ConcurrentQueue<KeyValuePair<ulong, int>>(); | _keepaliveTimes = new ConcurrentQueue<KeyValuePair<ulong, int>>(); | ||||
| _ssrcMap = new ConcurrentDictionary<uint, ulong>(); | _ssrcMap = new ConcurrentDictionary<uint, ulong>(); | ||||
| _streams = new ConcurrentDictionary<ulong, StreamPair>(); | _streams = new ConcurrentDictionary<ulong, StreamPair>(); | ||||
| _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | _serializer = new JsonSerializer { ContractResolver = new DiscordContractResolver() }; | ||||
| _serializer.Error += (s, e) => | _serializer.Error += (s, e) => | ||||
| { | { | ||||
| @@ -91,7 +91,7 @@ namespace Discord.Audio | |||||
| UdpLatencyUpdated += async (old, val) => await _audioLogger.DebugAsync($"UDP Latency = {val} ms").ConfigureAwait(false); | 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; | _url = url; | ||||
| _userId = userId; | _userId = userId; | ||||
| @@ -100,7 +100,7 @@ namespace Discord.Audio | |||||
| await _connection.StartAsync().ConfigureAwait(false); | await _connection.StartAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task StopAsync() | public async Task StopAsync() | ||||
| { | |||||
| { | |||||
| await _connection.StopAsync().ConfigureAwait(false); | await _connection.StopAsync().ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -225,11 +225,11 @@ namespace Discord.Audio | |||||
| if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) | if (!data.Modes.Contains(DiscordVoiceAPIClient.Mode)) | ||||
| throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | throw new InvalidOperationException($"Discord does not support {DiscordVoiceAPIClient.Mode}"); | ||||
| ApiClient.SetUdpEndpoint(data.Ip, data.Port); | ApiClient.SetUdpEndpoint(data.Ip, data.Port); | ||||
| await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | await ApiClient.SendDiscoveryAsync(_ssrc).ConfigureAwait(false); | ||||
| _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); | _heartbeatTask = RunHeartbeatAsync(41250, _connection.CancelToken); | ||||
| } | } | ||||
| break; | break; | ||||
| @@ -305,9 +305,9 @@ namespace Discord.Audio | |||||
| catch (Exception ex) | catch (Exception ex) | ||||
| { | { | ||||
| await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); | await _audioLogger.DebugAsync("Malformed Packet", ex).ConfigureAwait(false); | ||||
| return; | |||||
| return; | |||||
| } | } | ||||
| await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); | await _audioLogger.DebugAsync("Received Discovery").ConfigureAwait(false); | ||||
| await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); | await ApiClient.SendSelectProtocol(ip, port).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -317,7 +317,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); | await _audioLogger.DebugAsync("Received Keepalive").ConfigureAwait(false); | ||||
| ulong value = | |||||
| ulong value = | |||||
| ((ulong)packet[0] >> 0) | | ((ulong)packet[0] >> 0) | | ||||
| ((ulong)packet[1] >> 8) | | ((ulong)packet[1] >> 8) | | ||||
| ((ulong)packet[2] >> 16) | | ((ulong)packet[2] >> 16) | | ||||
| @@ -341,7 +341,7 @@ namespace Discord.Audio | |||||
| } | } | ||||
| } | } | ||||
| else | else | ||||
| { | |||||
| { | |||||
| if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) | if (!RTPReadStream.TryReadSsrc(packet, 0, out var ssrc)) | ||||
| { | { | ||||
| await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); | await _audioLogger.DebugAsync("Malformed Frame").ConfigureAwait(false); | ||||
| @@ -388,7 +388,7 @@ namespace Discord.Audio | |||||
| var now = Environment.TickCount; | var now = Environment.TickCount; | ||||
| //Did server respond to our last heartbeat? | //Did server respond to our last heartbeat? | ||||
| if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && | |||||
| if (_heartbeatTimes.Count != 0 && (now - _lastMessageTime) > intervalMillis && | |||||
| ConnectionState == ConnectionState.Connected) | ConnectionState == ConnectionState.Connected) | ||||
| { | { | ||||
| _connection.Error(new Exception("Server missed last heartbeat")); | _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 _audioLogger.WarningAsync("Failed to send keepalive", ex).ConfigureAwait(false); | ||||
| } | } | ||||
| await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); | await Task.Delay(intervalMillis, cancelToken).ConfigureAwait(false); | ||||
| } | } | ||||
| await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); | await _audioLogger.DebugAsync("Keepalive Stopped").ConfigureAwait(false); | ||||
| @@ -467,6 +467,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| StopAsync().GetAwaiter().GetResult(); | StopAsync().GetAwaiter().GetResult(); | ||||
| ApiClient.Dispose(); | ApiClient.Dispose(); | ||||
| _stateLock?.Dispose(); | |||||
| } | } | ||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -27,7 +27,7 @@ namespace Discord.Audio.Streams | |||||
| private readonly AudioClient _client; | private readonly AudioClient _client; | ||||
| private readonly AudioStream _next; | private readonly AudioStream _next; | ||||
| private readonly CancellationTokenSource _cancelTokenSource; | |||||
| private readonly CancellationTokenSource _disposeTokenSource, _cancelTokenSource; | |||||
| private readonly CancellationToken _cancelToken; | private readonly CancellationToken _cancelToken; | ||||
| private readonly Task _task; | private readonly Task _task; | ||||
| private readonly ConcurrentQueue<Frame> _queuedFrames; | private readonly ConcurrentQueue<Frame> _queuedFrames; | ||||
| @@ -49,12 +49,13 @@ namespace Discord.Audio.Streams | |||||
| _logger = logger; | _logger = logger; | ||||
| _queueLength = (bufferMillis + (_ticksPerFrame - 1)) / _ticksPerFrame; //Round up | _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<Frame>(); | _queuedFrames = new ConcurrentQueue<Frame>(); | ||||
| _bufferPool = new ConcurrentQueue<byte[]>(); | _bufferPool = new ConcurrentQueue<byte[]>(); | ||||
| for (int i = 0; i < _queueLength; i++) | for (int i = 0; i < _queueLength; i++) | ||||
| _bufferPool.Enqueue(new byte[maxFrameSize]); | |||||
| _bufferPool.Enqueue(new byte[maxFrameSize]); | |||||
| _queueLock = new SemaphoreSlim(_queueLength, _queueLength); | _queueLock = new SemaphoreSlim(_queueLength, _queueLength); | ||||
| _silenceFrames = MaxSilenceFrames; | _silenceFrames = MaxSilenceFrames; | ||||
| @@ -63,7 +64,12 @@ namespace Discord.Audio.Streams | |||||
| protected override void Dispose(bool disposing) | protected override void Dispose(bool disposing) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| _cancelTokenSource.Cancel(); | |||||
| { | |||||
| _disposeTokenSource?.Cancel(); | |||||
| _disposeTokenSource?.Dispose(); | |||||
| _cancelTokenSource?.Dispose(); | |||||
| _queueLock?.Dispose(); | |||||
| } | |||||
| base.Dispose(disposing); | 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 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) | public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken) | ||||
| { | { | ||||
| CancellationTokenSource writeCancelToken = null; | |||||
| if (cancelToken.CanBeCanceled) | if (cancelToken.CanBeCanceled) | ||||
| cancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken).Token; | |||||
| { | |||||
| writeCancelToken = CancellationTokenSource.CreateLinkedTokenSource(cancelToken, _cancelToken); | |||||
| cancelToken = writeCancelToken.Token; | |||||
| } | |||||
| else | else | ||||
| cancelToken = _cancelToken; | cancelToken = _cancelToken; | ||||
| @@ -142,6 +152,9 @@ namespace Discord.Audio.Streams | |||||
| #if DEBUG | #if DEBUG | ||||
| var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock | var _ = _logger?.DebugAsync("Buffer overflow"); //Should never happen because of the queueLock | ||||
| #endif | #endif | ||||
| #pragma warning disable IDISP016 | |||||
| writeCancelToken?.Dispose(); | |||||
| #pragma warning restore IDISP016 | |||||
| return; | return; | ||||
| } | } | ||||
| Buffer.BlockCopy(data, offset, buffer, 0, count); | Buffer.BlockCopy(data, offset, buffer, 0, count); | ||||
| @@ -153,6 +166,7 @@ namespace Discord.Audio.Streams | |||||
| #endif | #endif | ||||
| _isPreloaded = true; | _isPreloaded = true; | ||||
| } | } | ||||
| writeCancelToken?.Dispose(); | |||||
| } | } | ||||
| public override async Task FlushAsync(CancellationToken cancelToken) | public override async Task FlushAsync(CancellationToken cancelToken) | ||||
| @@ -96,7 +96,17 @@ namespace Discord.Audio.Streams | |||||
| protected override void Dispose(bool isDisposing) | protected override void Dispose(bool isDisposing) | ||||
| { | { | ||||
| _isDisposed = true; | |||||
| if (!_isDisposed) | |||||
| { | |||||
| if (isDisposing) | |||||
| { | |||||
| _signal?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | |||||
| } | |||||
| base.Dispose(isDisposing); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -6,7 +6,7 @@ using Discord.Net; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| internal class ConnectionManager | |||||
| internal class ConnectionManager : IDisposable | |||||
| { | { | ||||
| public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } | public event Func<Task> Connected { add { _connectedEvent.Add(value); } remove { _connectedEvent.Remove(value); } } | ||||
| private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | private readonly AsyncEvent<Func<Task>> _connectedEvent = new AsyncEvent<Func<Task>>(); | ||||
| @@ -23,10 +23,12 @@ namespace Discord | |||||
| private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; | private CancellationTokenSource _combinedCancelToken, _reconnectCancelToken, _connectionCancelToken; | ||||
| private Task _task; | private Task _task; | ||||
| private bool _isDisposed; | |||||
| public ConnectionState State { get; private set; } | public ConnectionState State { get; private set; } | ||||
| public CancellationToken CancelToken { 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<Task> onConnecting, Func<Exception, Task> onDisconnecting, Action<Func<Exception, Task>> clientDisconnectHandler) | Func<Task> onConnecting, Func<Exception, Task> onDisconnecting, Action<Func<Exception, Task>> clientDisconnectHandler) | ||||
| { | { | ||||
| _stateLock = stateLock; | _stateLock = stateLock; | ||||
| @@ -55,6 +57,7 @@ namespace Discord | |||||
| { | { | ||||
| await AcquireConnectionLock().ConfigureAwait(false); | await AcquireConnectionLock().ConfigureAwait(false); | ||||
| var reconnectCancelToken = new CancellationTokenSource(); | var reconnectCancelToken = new CancellationTokenSource(); | ||||
| _reconnectCancelToken?.Dispose(); | |||||
| _reconnectCancelToken = reconnectCancelToken; | _reconnectCancelToken = reconnectCancelToken; | ||||
| _task = Task.Run(async () => | _task = Task.Run(async () => | ||||
| { | { | ||||
| @@ -67,16 +70,16 @@ namespace Discord | |||||
| try | try | ||||
| { | { | ||||
| await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); | await ConnectAsync(reconnectCancelToken).ConfigureAwait(false); | ||||
| nextReconnectDelay = 1000; //Reset delay | |||||
| nextReconnectDelay = 1000; //Reset delay | |||||
| await _connectionPromise.Task.ConfigureAwait(false); | await _connectionPromise.Task.ConfigureAwait(false); | ||||
| } | } | ||||
| catch (OperationCanceledException ex) | |||||
| { | |||||
| catch (OperationCanceledException ex) | |||||
| { | |||||
| Cancel(); //In case this exception didn't come from another Error call | Cancel(); //In case this exception didn't come from another Error call | ||||
| await DisconnectAsync(ex, !reconnectCancelToken.IsCancellationRequested).ConfigureAwait(false); | 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 | Error(ex); //In case this exception didn't come from another Error call | ||||
| if (!reconnectCancelToken.IsCancellationRequested) | if (!reconnectCancelToken.IsCancellationRequested) | ||||
| { | { | ||||
| @@ -103,16 +106,16 @@ namespace Discord | |||||
| finally { _stateLock.Release(); } | finally { _stateLock.Release(); } | ||||
| }); | }); | ||||
| } | } | ||||
| public virtual async Task StopAsync() | |||||
| public virtual Task StopAsync() | |||||
| { | { | ||||
| Cancel(); | Cancel(); | ||||
| var task = _task; | |||||
| if (task != null) | |||||
| await task.ConfigureAwait(false); | |||||
| return Task.CompletedTask; | |||||
| } | } | ||||
| private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) | private async Task ConnectAsync(CancellationTokenSource reconnectCancelToken) | ||||
| { | { | ||||
| _connectionCancelToken?.Dispose(); | |||||
| _combinedCancelToken?.Dispose(); | |||||
| _connectionCancelToken = new CancellationTokenSource(); | _connectionCancelToken = new CancellationTokenSource(); | ||||
| _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); | _combinedCancelToken = CancellationTokenSource.CreateLinkedTokenSource(_connectionCancelToken.Token, reconnectCancelToken.Token); | ||||
| CancelToken = _combinedCancelToken.Token; | CancelToken = _combinedCancelToken.Token; | ||||
| @@ -120,7 +123,7 @@ namespace Discord | |||||
| _connectionPromise = new TaskCompletionSource<bool>(); | _connectionPromise = new TaskCompletionSource<bool>(); | ||||
| State = ConnectionState.Connecting; | State = ConnectionState.Connecting; | ||||
| await _logger.InfoAsync("Connecting").ConfigureAwait(false); | await _logger.InfoAsync("Connecting").ConfigureAwait(false); | ||||
| try | try | ||||
| { | { | ||||
| var readyPromise = new TaskCompletionSource<bool>(); | var readyPromise = new TaskCompletionSource<bool>(); | ||||
| @@ -159,9 +162,9 @@ namespace Discord | |||||
| await _onDisconnecting(ex).ConfigureAwait(false); | await _onDisconnecting(ex).ConfigureAwait(false); | ||||
| await _logger.InfoAsync("Disconnected").ConfigureAwait(false); | |||||
| State = ConnectionState.Disconnected; | |||||
| await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false); | await _disconnectedEvent.InvokeAsync(ex, isReconnecting).ConfigureAwait(false); | ||||
| State = ConnectionState.Disconnected; | |||||
| await _logger.InfoAsync("Disconnected").ConfigureAwait(false); | |||||
| } | } | ||||
| public async Task CompleteAsync() | public async Task CompleteAsync() | ||||
| @@ -206,5 +209,25 @@ namespace Discord | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||
| protected virtual void Dispose(bool disposing) | |||||
| { | |||||
| if (!_isDisposed) | |||||
| { | |||||
| if (disposing) | |||||
| { | |||||
| _combinedCancelToken?.Dispose(); | |||||
| _reconnectCancelToken?.Dispose(); | |||||
| _connectionCancelToken?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | |||||
| } | |||||
| } | |||||
| public void Dispose() | |||||
| { | |||||
| Dispose(true); | |||||
| } | |||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -18,7 +18,9 @@ namespace Discord.WebSocket | |||||
| private int[] _shardIds; | private int[] _shardIds; | ||||
| private DiscordSocketClient[] _shards; | private DiscordSocketClient[] _shards; | ||||
| private int _totalShards; | private int _totalShards; | ||||
| private bool _isDisposed; | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override int Latency { get => GetLatency(); protected set { } } | public override int Latency { get => GetLatency(); protected set { } } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -38,11 +40,15 @@ namespace Discord.WebSocket | |||||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | /// <summary> Creates a new REST/WebSocket Discord client. </summary> | ||||
| public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } | public DiscordShardedClient() : this(null, new DiscordSocketConfig()) { } | ||||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | /// <summary> Creates a new REST/WebSocket Discord client. </summary> | ||||
| #pragma warning disable IDISP004 | |||||
| public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } | public DiscordShardedClient(DiscordSocketConfig config) : this(null, config, CreateApiClient(config)) { } | ||||
| #pragma warning restore IDISP004 | |||||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | /// <summary> Creates a new REST/WebSocket Discord client. </summary> | ||||
| public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } | public DiscordShardedClient(int[] ids) : this(ids, new DiscordSocketConfig()) { } | ||||
| /// <summary> Creates a new REST/WebSocket Discord client. </summary> | /// <summary> Creates a new REST/WebSocket Discord client. </summary> | ||||
| #pragma warning disable IDISP004 | |||||
| public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } | public DiscordShardedClient(int[] ids, DiscordSocketConfig config) : this(ids, config, CreateApiClient(config)) { } | ||||
| #pragma warning restore IDISP004 | |||||
| private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) | private DiscordShardedClient(int[] ids, DiscordSocketConfig config, API.DiscordSocketApiClient client) | ||||
| : base(config, client) | : base(config, client) | ||||
| { | { | ||||
| @@ -119,10 +125,10 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override async Task StartAsync() | |||||
| public override async Task StartAsync() | |||||
| => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); | => await Task.WhenAll(_shards.Select(x => x.StartAsync())).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override async Task StopAsync() | |||||
| public override async Task StopAsync() | |||||
| => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); | => await Task.WhenAll(_shards.Select(x => x.StopAsync())).ConfigureAwait(false); | ||||
| public DiscordSocketClient GetShard(int id) | public DiscordSocketClient GetShard(int id) | ||||
| @@ -145,7 +151,7 @@ namespace Discord.WebSocket | |||||
| => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); | => await _shards[0].GetApplicationInfoAsync(options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override SocketGuild GetGuild(ulong id) | |||||
| public override SocketGuild GetGuild(ulong id) | |||||
| => GetShardFor(id).GetGuild(id); | => GetShardFor(id).GetGuild(id); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -173,7 +179,7 @@ namespace Discord.WebSocket | |||||
| for (int i = 0; i < _shards.Length; i++) | for (int i = 0; i < _shards.Length; i++) | ||||
| result += _shards[i].PrivateChannels.Count; | result += _shards[i].PrivateChannels.Count; | ||||
| return result; | return result; | ||||
| } | |||||
| } | |||||
| private IEnumerable<SocketGuild> GetGuilds() | private IEnumerable<SocketGuild> GetGuilds() | ||||
| { | { | ||||
| @@ -189,7 +195,7 @@ namespace Discord.WebSocket | |||||
| for (int i = 0; i < _shards.Length; i++) | for (int i = 0; i < _shards.Length; i++) | ||||
| result += _shards[i].Guilds.Count; | result += _shards[i].Guilds.Count; | ||||
| return result; | return result; | ||||
| } | |||||
| } | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override SocketUser GetUser(ulong id) | public override SocketUser GetUser(ulong id) | ||||
| @@ -369,5 +375,22 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) | Task<IVoiceRegion> IDiscordClient.GetVoiceRegionAsync(string id, RequestOptions options) | ||||
| => Task.FromResult<IVoiceRegion>(GetVoiceRegion(id)); | => Task.FromResult<IVoiceRegion>(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); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -108,6 +108,8 @@ namespace Discord.API | |||||
| } | } | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| base.Dispose(disposing); | |||||
| } | } | ||||
| public async Task ConnectAsync() | public async Task ConnectAsync() | ||||
| @@ -137,6 +139,7 @@ namespace Discord.API | |||||
| ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
| try | try | ||||
| { | { | ||||
| _connectCancelToken?.Dispose(); | |||||
| _connectCancelToken = new CancellationTokenSource(); | _connectCancelToken = new CancellationTokenSource(); | ||||
| if (WebSocketClient != null) | if (WebSocketClient != null) | ||||
| WebSocketClient.SetCancelToken(_connectCancelToken.Token); | WebSocketClient.SetCancelToken(_connectCancelToken.Token); | ||||
| @@ -209,7 +212,7 @@ namespace Discord.API | |||||
| await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); | await RequestQueue.SendAsync(new WebSocketRequest(WebSocketClient, null, bytes, true, options)).ConfigureAwait(false); | ||||
| await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | await _sentGatewayMessageEvent.InvokeAsync(opCode).ConfigureAwait(false); | ||||
| } | } | ||||
| public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) | public async Task SendIdentifyAsync(int largeThreshold = 100, int shardID = 0, int totalShards = 1, RequestOptions options = null) | ||||
| { | { | ||||
| options = RequestOptions.CreateOrClone(options); | options = RequestOptions.CreateOrClone(options); | ||||
| @@ -42,6 +42,7 @@ namespace Discord.WebSocket | |||||
| private int _nextAudioId; | private int _nextAudioId; | ||||
| private DateTimeOffset? _statusSince; | private DateTimeOffset? _statusSince; | ||||
| private RestApplication _applicationInfo; | private RestApplication _applicationInfo; | ||||
| private bool _isDisposed; | |||||
| /// <summary> Provides access to a REST-only client with a shared state from this client. </summary> | /// <summary> Provides access to a REST-only client with a shared state from this client. </summary> | ||||
| public DiscordSocketRestClient Rest { get; } | public DiscordSocketRestClient Rest { get; } | ||||
| @@ -65,7 +66,7 @@ namespace Discord.WebSocket | |||||
| internal WebSocketProvider WebSocketProvider { get; private set; } | internal WebSocketProvider WebSocketProvider { get; private set; } | ||||
| internal bool AlwaysDownloadUsers { get; private set; } | internal bool AlwaysDownloadUsers { get; private set; } | ||||
| internal int? HandlerTimeout { get; private set; } | internal int? HandlerTimeout { get; private set; } | ||||
| internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; | internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | public override IReadOnlyCollection<SocketGuild> Guilds => State.Guilds; | ||||
| @@ -112,8 +113,10 @@ namespace Discord.WebSocket | |||||
| /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. | /// Initializes a new REST/WebSocket-based Discord client with the provided configuration. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="config">The configuration to be used with the client.</param> | /// <param name="config">The configuration to be used with the client.</param> | ||||
| #pragma warning disable IDISP004 | |||||
| public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } | public DiscordSocketClient(DiscordSocketConfig config) : this(config, CreateApiClient(config), null, null) { } | ||||
| internal DiscordSocketClient(DiscordSocketConfig config, SemaphoreSlim groupLock, DiscordSocketClient parentClient) : this(config, CreateApiClient(config), groupLock, parentClient) { } | 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) | private DiscordSocketClient(DiscordSocketConfig config, API.DiscordSocketApiClient client, SemaphoreSlim groupLock, DiscordSocketClient parentClient) | ||||
| : base(config, client) | : base(config, client) | ||||
| { | { | ||||
| @@ -173,11 +176,18 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| internal override void Dispose(bool disposing) | 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); | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -200,10 +210,10 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override async Task StartAsync() | |||||
| public override async Task StartAsync() | |||||
| => await _connection.StartAsync().ConfigureAwait(false); | => await _connection.StartAsync().ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public override async Task StopAsync() | |||||
| public override async Task StopAsync() | |||||
| => await _connection.StopAsync().ConfigureAwait(false); | => await _connection.StopAsync().ConfigureAwait(false); | ||||
| private async Task OnConnectingAsync() | private async Task OnConnectingAsync() | ||||
| @@ -707,6 +717,7 @@ namespace Discord.WebSocket | |||||
| { | { | ||||
| await GuildUnavailableAsync(guild).ConfigureAwait(false); | await GuildUnavailableAsync(guild).ConfigureAwait(false); | ||||
| await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); | await TimedInvokeAsync(_leftGuildEvent, nameof(LeftGuild), guild).ConfigureAwait(false); | ||||
| (guild as IDisposable).Dispose(); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| @@ -1209,16 +1220,22 @@ namespace Discord.WebSocket | |||||
| cachedMsg.Update(State, data); | cachedMsg.Update(State, data); | ||||
| after = cachedMsg; | after = cachedMsg; | ||||
| } | } | ||||
| else if (data.Author.IsSpecified) | |||||
| else | |||||
| { | { | ||||
| //Edited message isnt in cache, create a detached one | //Edited message isnt in cache, create a detached one | ||||
| SocketUser author; | 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 | 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); | after = SocketMessage.Create(this, State, author, channel, data); | ||||
| } | } | ||||
| @@ -16,7 +16,7 @@ using System.Threading.Tasks; | |||||
| namespace Discord.Audio | namespace Discord.Audio | ||||
| { | { | ||||
| internal class DiscordVoiceAPIClient | |||||
| internal class DiscordVoiceAPIClient : IDisposable | |||||
| { | { | ||||
| public const int MaxBitrate = 128 * 1024; | public const int MaxBitrate = 128 * 1024; | ||||
| public const string Mode = "xsalsa20_poly1305"; | public const string Mode = "xsalsa20_poly1305"; | ||||
| @@ -36,7 +36,7 @@ namespace Discord.Audio | |||||
| private readonly AsyncEvent<Func<byte[], Task>> _receivedPacketEvent = new AsyncEvent<Func<byte[], Task>>(); | private readonly AsyncEvent<Func<byte[], Task>> _receivedPacketEvent = new AsyncEvent<Func<byte[], Task>>(); | ||||
| public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | public event Func<Exception, Task> Disconnected { add { _disconnectedEvent.Add(value); } remove { _disconnectedEvent.Remove(value); } } | ||||
| private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | private readonly AsyncEvent<Func<Exception, Task>> _disconnectedEvent = new AsyncEvent<Func<Exception, Task>>(); | ||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private readonly SemaphoreSlim _connectionLock; | private readonly SemaphoreSlim _connectionLock; | ||||
| private readonly IUdpSocket _udp; | private readonly IUdpSocket _udp; | ||||
| @@ -103,8 +103,9 @@ namespace Discord.Audio | |||||
| if (disposing) | if (disposing) | ||||
| { | { | ||||
| _connectCancelToken?.Dispose(); | _connectCancelToken?.Dispose(); | ||||
| (_udp as IDisposable)?.Dispose(); | |||||
| (WebSocketClient as IDisposable)?.Dispose(); | |||||
| _udp?.Dispose(); | |||||
| WebSocketClient?.Dispose(); | |||||
| _connectionLock?.Dispose(); | |||||
| } | } | ||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| @@ -122,7 +123,7 @@ namespace Discord.Audio | |||||
| } | } | ||||
| public async Task SendAsync(byte[] data, int offset, int bytes) | 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); | await _sentDataEvent.InvokeAsync(bytes).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -177,6 +178,7 @@ namespace Discord.Audio | |||||
| ConnectionState = ConnectionState.Connecting; | ConnectionState = ConnectionState.Connecting; | ||||
| try | try | ||||
| { | { | ||||
| _connectCancelToken?.Dispose(); | |||||
| _connectCancelToken = new CancellationTokenSource(); | _connectCancelToken = new CancellationTokenSource(); | ||||
| var cancelToken = _connectCancelToken.Token; | var cancelToken = _connectCancelToken.Token; | ||||
| @@ -208,7 +210,7 @@ namespace Discord.Audio | |||||
| { | { | ||||
| if (ConnectionState == ConnectionState.Disconnected) return; | if (ConnectionState == ConnectionState.Disconnected) return; | ||||
| ConnectionState = ConnectionState.Disconnecting; | ConnectionState = ConnectionState.Disconnecting; | ||||
| try { _connectCancelToken?.Cancel(false); } | try { _connectCancelToken?.Cancel(false); } | ||||
| catch { } | catch { } | ||||
| @@ -67,14 +67,6 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | Task<IGuildUser> IGuildChannel.GetUserAsync(ulong id, CacheMode mode, RequestOptions options) | ||||
| => Task.FromResult<IGuildUser>(GetUser(id)); | => Task.FromResult<IGuildUser>(GetUser(id)); | ||||
| /// <inheritdoc /> | |||||
| /// <exception cref="NotSupportedException">This method is not supported with category channels.</exception> | |||||
| Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | |||||
| => throw new NotSupportedException(); | |||||
| /// <inheritdoc /> | |||||
| /// <exception cref="NotSupportedException">This method is not supported with category channels.</exception> | |||||
| Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options) | |||||
| => throw new NotSupportedException(); | |||||
| //IChannel | //IChannel | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| @@ -184,31 +184,6 @@ namespace Discord.WebSocket | |||||
| } | } | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Returns a collection of all invites to this channel. | |||||
| /// </summary> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// 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. | |||||
| /// </returns> | |||||
| public async Task<IReadOnlyCollection<RestInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||||
| /// <summary> | |||||
| /// Creates a new invite to this channel. | |||||
| /// </summary> | |||||
| /// <param name="maxAge">The time (in seconds) until the invite expires. Set to <c>null</c> to never expire.</param> | |||||
| /// <param name="maxUses">The max amount of times this invite may be used. Set to <c>null</c> to have unlimited uses.</param> | |||||
| /// <param name="isTemporary">If <c>true</c>, the user accepting this invite will be kicked from the guild after closing their client.</param> | |||||
| /// <param name="isUnique">If <c>true</c>, don't try to reuse a similar invite (useful for creating many unique one time use invites).</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | |||||
| /// <returns> | |||||
| /// A task that represents the asynchronous invite creation operation. The task result contains an invite | |||||
| /// metadata object containing information for the created invite. | |||||
| /// </returns> | |||||
| public async Task<RestInviteMetadata> 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; | public new virtual SocketGuildUser GetUser(ulong id) => null; | ||||
| /// <summary> | /// <summary> | ||||
| @@ -233,13 +208,6 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| ulong IGuildChannel.GuildId => Guild.Id; | ulong IGuildChannel.GuildId => Guild.Id; | ||||
| /// <inheritdoc /> | |||||
| async Task<IReadOnlyCollection<IInviteMetadata>> IGuildChannel.GetInvitesAsync(RequestOptions options) | |||||
| => await GetInvitesAsync(options).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | |||||
| async Task<IInviteMetadata> IGuildChannel.CreateInviteAsync(int? maxAge, int? maxUses, bool isTemporary, bool isUnique, RequestOptions options) | |||||
| => await CreateInviteAsync(maxAge, maxUses, isTemporary, isUnique, options).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) | OverwritePermissions? IGuildChannel.GetPermissionOverwrite(IRole role) | ||||
| => GetPermissionOverwrite(role); | => GetPermissionOverwrite(role); | ||||
| @@ -126,7 +126,6 @@ namespace Discord.WebSocket | |||||
| /// <returns> | /// <returns> | ||||
| /// Paged collection of messages. | /// Paged collection of messages. | ||||
| /// </returns> | /// </returns> | ||||
| public IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | public IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) | ||||
| => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); | => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); | ||||
| /// <summary> | /// <summary> | ||||
| @@ -304,6 +303,14 @@ namespace Discord.WebSocket | |||||
| public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null) | public Task<IReadOnlyCollection<RestWebhook>> GetWebhooksAsync(RequestOptions options = null) | ||||
| => ChannelHelper.GetWebhooksAsync(this, Discord, options); | => ChannelHelper.GetWebhooksAsync(this, Discord, options); | ||||
| //Invites | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | private string DebuggerDisplay => $"{Name} ({Id}, Text)"; | ||||
| internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; | internal new SocketTextChannel Clone() => MemberwiseClone() as SocketTextChannel; | ||||
| @@ -79,6 +79,14 @@ namespace Discord.WebSocket | |||||
| return null; | return null; | ||||
| } | } | ||||
| //Invites | |||||
| /// <inheritdoc /> | |||||
| public async Task<IInviteMetadata> 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); | |||||
| /// <inheritdoc /> | |||||
| public async Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null) | |||||
| => await ChannelHelper.GetInvitesAsync(this, Discord, options).ConfigureAwait(false); | |||||
| private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | private string DebuggerDisplay => $"{Name} ({Id}, Voice)"; | ||||
| internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; | internal new SocketVoiceChannel Clone() => MemberwiseClone() as SocketVoiceChannel; | ||||
| @@ -25,8 +25,9 @@ namespace Discord.WebSocket | |||||
| /// Represents a WebSocket-based guild object. | /// Represents a WebSocket-based guild object. | ||||
| /// </summary> | /// </summary> | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | ||||
| public class SocketGuild : SocketEntity<ulong>, IGuild | |||||
| public class SocketGuild : SocketEntity<ulong>, IGuild, IDisposable | |||||
| { | { | ||||
| #pragma warning disable IDISP002, IDISP006 | |||||
| private readonly SemaphoreSlim _audioLock; | private readonly SemaphoreSlim _audioLock; | ||||
| private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | private TaskCompletionSource<bool> _syncPromise, _downloaderPromise; | ||||
| private TaskCompletionSource<AudioClient> _audioConnectPromise; | private TaskCompletionSource<AudioClient> _audioConnectPromise; | ||||
| @@ -37,6 +38,7 @@ namespace Discord.WebSocket | |||||
| private ImmutableArray<GuildEmote> _emotes; | private ImmutableArray<GuildEmote> _emotes; | ||||
| private ImmutableArray<string> _features; | private ImmutableArray<string> _features; | ||||
| private AudioClient _audioClient; | private AudioClient _audioClient; | ||||
| #pragma warning restore IDISP002, IDISP006 | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| public string Name { get; private set; } | 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. | /// number here is the most accurate in terms of counting the number of users within this guild. | ||||
| /// </para> | /// </para> | ||||
| /// <para> | /// <para> | ||||
| /// Use this instead of enumerating the count of the | |||||
| /// Use this instead of enumerating the count of the | |||||
| /// <see cref="Discord.WebSocket.SocketGuild.Users" /> collection, as you may see discrepancy | /// <see cref="Discord.WebSocket.SocketGuild.Users" /> collection, as you may see discrepancy | ||||
| /// between that and this property. | /// between that and this property. | ||||
| /// </para> | /// </para> | ||||
| @@ -561,14 +563,15 @@ namespace Discord.WebSocket | |||||
| /// Creates a new channel category in this guild. | /// Creates a new channel category in this guild. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="name">The new name for the category.</param> | /// <param name="name">The new name for the category.</param> | ||||
| /// <param name="func">The delegate containing the properties to be applied to the channel upon its creation.</param> | |||||
| /// <param name="options">The options to be used when sending the request.</param> | /// <param name="options">The options to be used when sending the request.</param> | ||||
| /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception> | /// <exception cref="ArgumentNullException"><paramref name="name"/> is <c>null</c>.</exception> | ||||
| /// <returns> | /// <returns> | ||||
| /// A task that represents the asynchronous creation operation. The task result contains the newly created | /// A task that represents the asynchronous creation operation. The task result contains the newly created | ||||
| /// category channel. | /// category channel. | ||||
| /// </returns> | /// </returns> | ||||
| public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, RequestOptions options = null) | |||||
| => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options); | |||||
| public Task<RestCategoryChannel> CreateCategoryChannelAsync(string name, Action<GuildChannelProperties> func = null, RequestOptions options = null) | |||||
| => GuildHelper.CreateCategoryChannelAsync(this, Discord, name, options, func); | |||||
| internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) | internal SocketGuildChannel AddChannel(ClientState state, ChannelModel model) | ||||
| { | { | ||||
| @@ -871,9 +874,11 @@ namespace Discord.WebSocket | |||||
| if (external) | if (external) | ||||
| { | { | ||||
| #pragma warning disable IDISP001 | |||||
| var _ = promise.TrySetResultAsync(null); | var _ = promise.TrySetResultAsync(null); | ||||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | ||||
| return null; | return null; | ||||
| #pragma warning restore IDISP001 | |||||
| } | } | ||||
| if (_audioClient == null) | if (_audioClient == null) | ||||
| @@ -896,10 +901,14 @@ namespace Discord.WebSocket | |||||
| }; | }; | ||||
| audioClient.Connected += () => | audioClient.Connected += () => | ||||
| { | { | ||||
| #pragma warning disable IDISP001 | |||||
| var _ = promise.TrySetResultAsync(_audioClient); | var _ = promise.TrySetResultAsync(_audioClient); | ||||
| #pragma warning restore IDISP001 | |||||
| return Task.Delay(0); | return Task.Delay(0); | ||||
| }; | }; | ||||
| #pragma warning disable IDISP003 | |||||
| _audioClient = audioClient; | _audioClient = audioClient; | ||||
| #pragma warning restore IDISP003 | |||||
| } | } | ||||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, channelId, selfDeaf, selfMute).ConfigureAwait(false); | ||||
| @@ -947,6 +956,7 @@ namespace Discord.WebSocket | |||||
| if (_audioClient != null) | if (_audioClient != null) | ||||
| await _audioClient.StopAsync().ConfigureAwait(false); | await _audioClient.StopAsync().ConfigureAwait(false); | ||||
| await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); | await Discord.ApiClient.SendVoiceStateUpdateAsync(Id, null, false, false).ConfigureAwait(false); | ||||
| _audioClient?.Dispose(); | |||||
| _audioClient = null; | _audioClient = null; | ||||
| } | } | ||||
| internal async Task FinishConnectAudio(string url, string token) | internal async Task FinishConnectAudio(string url, string token) | ||||
| @@ -1069,8 +1079,8 @@ namespace Discord.WebSocket | |||||
| async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, Action<VoiceChannelProperties> func, RequestOptions options) | async Task<IVoiceChannel> IGuild.CreateVoiceChannelAsync(string name, Action<VoiceChannelProperties> func, RequestOptions options) | ||||
| => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); | => await CreateVoiceChannelAsync(name, func, options).ConfigureAwait(false); | ||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, RequestOptions options) | |||||
| => await CreateCategoryChannelAsync(name, options).ConfigureAwait(false); | |||||
| async Task<ICategoryChannel> IGuild.CreateCategoryAsync(string name, Action<GuildChannelProperties> func, RequestOptions options) | |||||
| => await CreateCategoryChannelAsync(name, func, options).ConfigureAwait(false); | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IReadOnlyCollection<IVoiceRegion>> IGuild.GetVoiceRegionsAsync(RequestOptions options) | async Task<IReadOnlyCollection<IVoiceRegion>> IGuild.GetVoiceRegionsAsync(RequestOptions options) | ||||
| @@ -1129,5 +1139,12 @@ namespace Discord.WebSocket | |||||
| /// <inheritdoc /> | /// <inheritdoc /> | ||||
| async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | async Task<IReadOnlyCollection<IWebhook>> IGuild.GetWebhooksAsync(RequestOptions options) | ||||
| => await GetWebhooksAsync(options).ConfigureAwait(false); | => await GetWebhooksAsync(options).ConfigureAwait(false); | ||||
| void IDisposable.Dispose() | |||||
| { | |||||
| DisconnectAudioAsync().GetAwaiter().GetResult(); | |||||
| _audioLock?.Dispose(); | |||||
| _audioClient?.Dispose(); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -13,24 +13,29 @@ namespace Discord.Net.Udp | |||||
| private readonly SemaphoreSlim _lock; | private readonly SemaphoreSlim _lock; | ||||
| private UdpClient _udp; | private UdpClient _udp; | ||||
| private IPEndPoint _destination; | private IPEndPoint _destination; | ||||
| private CancellationTokenSource _cancelTokenSource; | |||||
| private CancellationTokenSource _stopCancelTokenSource, _cancelTokenSource; | |||||
| private CancellationToken _cancelToken, _parentToken; | private CancellationToken _cancelToken, _parentToken; | ||||
| private Task _task; | private Task _task; | ||||
| private bool _isDisposed; | private bool _isDisposed; | ||||
| public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); | public ushort Port => (ushort)((_udp?.Client.LocalEndPoint as IPEndPoint)?.Port ?? 0); | ||||
| public DefaultUdpSocket() | public DefaultUdpSocket() | ||||
| { | { | ||||
| _lock = new SemaphoreSlim(1, 1); | _lock = new SemaphoreSlim(1, 1); | ||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _stopCancelTokenSource = new CancellationTokenSource(); | |||||
| } | } | ||||
| private void Dispose(bool disposing) | private void Dispose(bool disposing) | ||||
| { | { | ||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | |||||
| StopInternalAsync(true).GetAwaiter().GetResult(); | StopInternalAsync(true).GetAwaiter().GetResult(); | ||||
| _stopCancelTokenSource?.Dispose(); | |||||
| _cancelTokenSource?.Dispose(); | |||||
| _lock?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| @@ -56,9 +61,14 @@ namespace Discord.Net.Udp | |||||
| { | { | ||||
| await StopInternalAsync().ConfigureAwait(false); | 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); | _udp = new UdpClient(0); | ||||
| _task = RunAsync(_cancelToken); | _task = RunAsync(_cancelToken); | ||||
| @@ -77,7 +87,7 @@ namespace Discord.Net.Udp | |||||
| } | } | ||||
| public async Task StopInternalAsync(bool isDisposing = false) | public async Task StopInternalAsync(bool isDisposing = false) | ||||
| { | { | ||||
| try { _cancelTokenSource.Cancel(false); } catch { } | |||||
| try { _stopCancelTokenSource.Cancel(false); } catch { } | |||||
| if (!isDisposing) | if (!isDisposing) | ||||
| await (_task ?? Task.Delay(0)).ConfigureAwait(false); | await (_task ?? Task.Delay(0)).ConfigureAwait(false); | ||||
| @@ -96,8 +106,11 @@ namespace Discord.Net.Udp | |||||
| } | } | ||||
| public void SetCancelToken(CancellationToken cancelToken) | public void SetCancelToken(CancellationToken cancelToken) | ||||
| { | { | ||||
| _cancelTokenSource?.Dispose(); | |||||
| _parentToken = cancelToken; | _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) | public async Task SendAsync(byte[] data, int index, int count) | ||||
| @@ -117,6 +130,14 @@ namespace Discord.Net.Udp | |||||
| while (!cancelToken.IsCancellationRequested) | while (!cancelToken.IsCancellationRequested) | ||||
| { | { | ||||
| var receiveTask = _udp.ReceiveAsync(); | 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); | var task = await Task.WhenAny(closeTask, receiveTask).ConfigureAwait(false); | ||||
| if (task == closeTask) | if (task == closeTask) | ||||
| break; | break; | ||||
| @@ -25,14 +25,14 @@ namespace Discord.Net.WebSockets | |||||
| private readonly IWebProxy _proxy; | private readonly IWebProxy _proxy; | ||||
| private ClientWebSocket _client; | private ClientWebSocket _client; | ||||
| private Task _task; | private Task _task; | ||||
| private CancellationTokenSource _cancelTokenSource; | |||||
| private CancellationTokenSource _disconnectTokenSource, _cancelTokenSource; | |||||
| private CancellationToken _cancelToken, _parentToken; | private CancellationToken _cancelToken, _parentToken; | ||||
| private bool _isDisposed, _isDisconnecting; | private bool _isDisposed, _isDisconnecting; | ||||
| public DefaultWebSocketClient(IWebProxy proxy = null) | public DefaultWebSocketClient(IWebProxy proxy = null) | ||||
| { | { | ||||
| _lock = new SemaphoreSlim(1, 1); | _lock = new SemaphoreSlim(1, 1); | ||||
| _cancelTokenSource = new CancellationTokenSource(); | |||||
| _disconnectTokenSource = new CancellationTokenSource(); | |||||
| _cancelToken = CancellationToken.None; | _cancelToken = CancellationToken.None; | ||||
| _parentToken = CancellationToken.None; | _parentToken = CancellationToken.None; | ||||
| _headers = new Dictionary<string, string>(); | _headers = new Dictionary<string, string>(); | ||||
| @@ -43,7 +43,12 @@ namespace Discord.Net.WebSockets | |||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | |||||
| DisconnectInternalAsync(true).GetAwaiter().GetResult(); | DisconnectInternalAsync(true).GetAwaiter().GetResult(); | ||||
| _disconnectTokenSource?.Dispose(); | |||||
| _cancelTokenSource?.Dispose(); | |||||
| _lock?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| @@ -68,9 +73,14 @@ namespace Discord.Net.WebSockets | |||||
| { | { | ||||
| await DisconnectInternalAsync().ConfigureAwait(false); | 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 = new ClientWebSocket(); | ||||
| _client.Options.Proxy = _proxy; | _client.Options.Proxy = _proxy; | ||||
| _client.Options.KeepAliveInterval = TimeSpan.Zero; | _client.Options.KeepAliveInterval = TimeSpan.Zero; | ||||
| @@ -98,7 +108,7 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| private async Task DisconnectInternalAsync(bool isDisposing = false) | private async Task DisconnectInternalAsync(bool isDisposing = false) | ||||
| { | { | ||||
| try { _cancelTokenSource.Cancel(false); } catch { } | |||||
| try { _disconnectTokenSource.Cancel(false); } catch { } | |||||
| _isDisconnecting = true; | _isDisconnecting = true; | ||||
| try | try | ||||
| @@ -117,7 +127,7 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| try { _client.Dispose(); } | try { _client.Dispose(); } | ||||
| catch { } | catch { } | ||||
| _client = null; | _client = null; | ||||
| } | } | ||||
| } | } | ||||
| @@ -144,8 +154,11 @@ namespace Discord.Net.WebSockets | |||||
| } | } | ||||
| public void SetCancelToken(CancellationToken cancelToken) | public void SetCancelToken(CancellationToken cancelToken) | ||||
| { | { | ||||
| _cancelTokenSource?.Dispose(); | |||||
| _parentToken = cancelToken; | _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) | public async Task SendAsync(byte[] data, int index, int count, bool isText) | ||||
| @@ -166,7 +179,7 @@ namespace Discord.Net.WebSockets | |||||
| frameSize = count - (i * SendChunkSize); | frameSize = count - (i * SendChunkSize); | ||||
| else | else | ||||
| frameSize = SendChunkSize; | frameSize = SendChunkSize; | ||||
| var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; | var type = isText ? WebSocketMessageType.Text : WebSocketMessageType.Binary; | ||||
| await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); | await _client.SendAsync(new ArraySegment<byte>(data, index, count), type, isLast, _cancelToken).ConfigureAwait(false); | ||||
| } | } | ||||
| @@ -176,7 +189,7 @@ namespace Discord.Net.WebSockets | |||||
| _lock.Release(); | _lock.Release(); | ||||
| } | } | ||||
| } | } | ||||
| private async Task RunAsync(CancellationToken cancelToken) | private async Task RunAsync(CancellationToken cancelToken) | ||||
| { | { | ||||
| var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); | var buffer = new ArraySegment<byte>(new byte[ReceiveChunkSize]); | ||||
| @@ -188,7 +201,7 @@ namespace Discord.Net.WebSockets | |||||
| WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); | WebSocketReceiveResult socketResult = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); | ||||
| byte[] result; | byte[] result; | ||||
| int resultCount; | int resultCount; | ||||
| if (socketResult.MessageType == WebSocketMessageType.Close) | if (socketResult.MessageType == WebSocketMessageType.Close) | ||||
| throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); | throw new WebSocketClosedException((int)socketResult.CloseStatus, socketResult.CloseStatusDescription); | ||||
| @@ -219,7 +232,7 @@ namespace Discord.Net.WebSockets | |||||
| resultCount = socketResult.Count; | resultCount = socketResult.Count; | ||||
| result = buffer.Array; | result = buffer.Array; | ||||
| } | } | ||||
| if (socketResult.MessageType == WebSocketMessageType.Text) | if (socketResult.MessageType == WebSocketMessageType.Text) | ||||
| { | { | ||||
| string text = Encoding.UTF8.GetString(result, 0, resultCount); | string text = Encoding.UTF8.GetString(result, 0, resultCount); | ||||
| @@ -10,4 +10,4 @@ | |||||
| <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> | ||||
| <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | <ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| </Project> | |||||
| </Project> | |||||
| @@ -2,9 +2,9 @@ | |||||
| <PropertyGroup> | <PropertyGroup> | ||||
| <OutputType>Exe</OutputType> | <OutputType>Exe</OutputType> | ||||
| <RootNamespace>Discord</RootNamespace> | <RootNamespace>Discord</RootNamespace> | ||||
| <TargetFramework>netcoreapp1.1</TargetFramework> | |||||
| <TargetFramework>netcoreapp2.1</TargetFramework> | |||||
| <DebugType>portable</DebugType> | <DebugType>portable</DebugType> | ||||
| <PackageTargetFallback>$(PackageTargetFallback);portable-net45+win8+wp8+wpa81</PackageTargetFallback> | |||||
| <NoWarn>IDISP001,IDISP002,IDISP004,IDISP005</NoWarn> | |||||
| </PropertyGroup> | </PropertyGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <Content Include="xunit.runner.json"> | <Content Include="xunit.runner.json"> | ||||
| @@ -21,11 +21,14 @@ | |||||
| <ProjectReference Include="../../src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj" /> | <ProjectReference Include="../../src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| <ItemGroup> | <ItemGroup> | ||||
| <PackageReference Include="Akavache" Version="5.0.0" /> | |||||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.7.2" /> | |||||
| <PackageReference Include="Akavache" Version="6.0.31" /> | |||||
| <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> | |||||
| <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> | <PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> | ||||
| <PackageReference Include="xunit" Version="2.4.0" /> | |||||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" /> | |||||
| <PackageReference Include="xunit.runner.reporters" Version="2.4.0" /> | |||||
| <PackageReference Include="xunit" Version="2.4.1" /> | |||||
| <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1"> | |||||
| <PrivateAssets>all</PrivateAssets> | |||||
| <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> | |||||
| </PackageReference> | |||||
| <PackageReference Include="xunit.runner.reporters" Version="2.4.1" /> | |||||
| </ItemGroup> | </ItemGroup> | ||||
| </Project> | </Project> | ||||
| @@ -43,7 +43,10 @@ namespace Discord.Net | |||||
| if (!_isDisposed) | if (!_isDisposed) | ||||
| { | { | ||||
| if (disposing) | if (disposing) | ||||
| { | |||||
| _blobCache.Dispose(); | _blobCache.Dispose(); | ||||
| _cancelTokenSource?.Dispose(); | |||||
| } | |||||
| _isDisposed = true; | _isDisposed = true; | ||||
| } | } | ||||
| } | } | ||||
| @@ -70,7 +73,7 @@ namespace Discord.Net | |||||
| { | { | ||||
| if (method != "GET") | if (method != "GET") | ||||
| throw new InvalidOperationException("This RestClient only supports GET requests."); | throw new InvalidOperationException("This RestClient only supports GET requests."); | ||||
| string uri = Path.Combine(_baseUrl, endpoint); | string uri = Path.Combine(_baseUrl, endpoint); | ||||
| var bytes = await _blobCache.DownloadUrl(uri, _headers); | var bytes = await _blobCache.DownloadUrl(uri, _headers); | ||||
| return new RestResponse(HttpStatusCode.OK, _headers, new MemoryStream(bytes)); | 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."); | throw new InvalidOperationException("This RestClient does not support multipart requests."); | ||||
| } | } | ||||
| public async Task ClearAsync() | |||||
| public async Task ClearAsync() | |||||
| { | { | ||||
| await _blobCache.InvalidateAll(); | await _blobCache.InvalidateAll(); | ||||
| } | } | ||||
| @@ -93,7 +96,7 @@ namespace Discord.Net | |||||
| { | { | ||||
| if (Info != null) | if (Info != null) | ||||
| return; | return; | ||||
| bool needsReset = false; | bool needsReset = false; | ||||
| try | try | ||||
| { | { | ||||
| @@ -117,4 +120,4 @@ namespace Discord.Net | |||||
| await _blobCache.InsertObject<CacheInfo>("info", Info); | await _blobCache.InsertObject<CacheInfo>("info", Info); | ||||
| } | } | ||||
| } | } | ||||
| } | |||||
| } | |||||
| @@ -69,13 +69,14 @@ namespace Discord | |||||
| /// <summary> | /// <summary> | ||||
| /// Tests the behavior of <see cref="TokenUtils.ValidateToken(TokenType, string)"/> | /// Tests the behavior of <see cref="TokenUtils.ValidateToken(TokenType, string)"/> | ||||
| /// to see that valid Bot tokens do not throw Exceptions. | /// 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. | |||||
| /// </summary> | /// </summary> | ||||
| [Theory] | [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("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.ZnCjm1XVW7vRze4b7Cq4se7kKWss")] | ||||
| [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWsMTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")] | |||||
| public void TestBotTokenDoesNotThrowExceptions(string token) | public void TestBotTokenDoesNotThrowExceptions(string token) | ||||
| { | { | ||||
| // This example token is pulled from the Discord Docs | // This example token is pulled from the Discord Docs | ||||
| @@ -90,12 +91,15 @@ namespace Discord | |||||
| /// </summary> | /// </summary> | ||||
| [Theory] | [Theory] | ||||
| [InlineData("This is invalid")] | [InlineData("This is invalid")] | ||||
| // missing a single character from the end | |||||
| [InlineData("MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKW")] | |||||
| // bearer token | // bearer token | ||||
| [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] | [InlineData("6qrZcUqja7812RVdnEKjpzOL4CvHBFG")] | ||||
| // client secret | // client secret | ||||
| [InlineData("937it3ow87i4ery69876wqire")] | [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) | public void TestBotTokenInvalidThrowsArgumentException(string token) | ||||
| { | { | ||||
| Assert.Throws<ArgumentException>(() => TokenUtils.ValidateToken(TokenType.Bot, token)); | Assert.Throws<ArgumentException>(() => TokenUtils.ValidateToken(TokenType.Bot, token)); | ||||
| @@ -113,6 +117,7 @@ namespace Discord | |||||
| // TokenType.User | // TokenType.User | ||||
| [InlineData(0)] | [InlineData(0)] | ||||
| // out of range TokenType | // out of range TokenType | ||||
| [InlineData(-1)] | |||||
| [InlineData(4)] | [InlineData(4)] | ||||
| [InlineData(7)] | [InlineData(7)] | ||||
| public void TestUnrecognizedTokenType(int type) | public void TestUnrecognizedTokenType(int type) | ||||
| @@ -120,5 +125,44 @@ namespace Discord | |||||
| Assert.Throws<ArgumentException>(() => | Assert.Throws<ArgumentException>(() => | ||||
| TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); | TokenUtils.ValidateToken((TokenType)type, "MTk4NjIyNDgzNDcxOTI1MjQ4.Cl2FMQ.ZnCjm1XVW7vRze4b7Cq4se7kKWs")); | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Checks the <see cref="TokenUtils.CheckBotTokenValidity(string)"/> method for expected output. | |||||
| /// </summary> | |||||
| /// <param name="token"> The Bot Token to test.</param> | |||||
| /// <param name="expected"> The expected result. </param> | |||||
| [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); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||