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