Browse Source

Merge branch 'dev' into feature/cheaper-rest-on-socket

pull/1198/head
Christopher F GitHub 6 years ago
parent
commit
b87ec3aaf1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 729 additions and 324 deletions
  1. +10
    -3
      samples/01_basic_ping_bot/Program.cs
  2. +18
    -10
      samples/02_commands_framework/Program.cs
  3. +21
    -16
      samples/03_sharded_client/Program.cs
  4. +23
    -3
      src/Discord.Net.Commands/CommandService.cs
  5. +1
    -1
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  6. +3
    -3
      src/Discord.Net.Commands/Extensions/MessageExtensions.cs
  7. +3
    -0
      src/Discord.Net.Core/Discord.Net.Core.csproj
  8. +0
    -39
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  9. +41
    -1
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  10. +2
    -1
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  11. +23
    -3
      src/Discord.Net.Core/Entities/Image.cs
  12. +54
    -0
      src/Discord.Net.Core/Extensions/MessageExtensions.cs
  13. +4
    -0
      src/Discord.Net.Core/Format.cs
  14. +2
    -1
      src/Discord.Net.Core/Net/Rest/IRestClient.cs
  15. +1
    -1
      src/Discord.Net.Core/Net/Udp/IUdpSocket.cs
  16. +1
    -1
      src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs
  17. +77
    -4
      src/Discord.Net.Core/Utils/TokenUtils.cs
  18. +17
    -5
      src/Discord.Net.Providers.WS4Net/WS4NetClient.cs
  19. +2
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs
  20. +8
    -5
      src/Discord.Net.Rest/BaseDiscordClient.cs
  21. +6
    -2
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  22. +7
    -5
      src/Discord.Net.Rest/DiscordRestClient.cs
  23. +4
    -4
      src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs
  24. +0
    -10
      src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs
  25. +0
    -33
      src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs
  26. +8
    -0
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  27. +10
    -0
      src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs
  28. +12
    -3
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  29. +5
    -4
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  30. +8
    -6
      src/Discord.Net.Rest/Net/Converters/ImageConverter.cs
  31. +20
    -8
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  32. +28
    -11
      src/Discord.Net.Rest/Net/Queue/RequestQueue.cs
  33. +13
    -12
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  34. +20
    -6
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  35. +11
    -1
      src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs
  36. +38
    -15
      src/Discord.Net.WebSocket/ConnectionManager.cs
  37. +29
    -6
      src/Discord.Net.WebSocket/DiscordShardedClient.cs
  38. +4
    -1
      src/Discord.Net.WebSocket/DiscordSocketApiClient.cs
  39. +29
    -12
      src/Discord.Net.WebSocket/DiscordSocketClient.cs
  40. +8
    -6
      src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs
  41. +0
    -8
      src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs
  42. +0
    -32
      src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs
  43. +8
    -1
      src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs
  44. +8
    -0
      src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs
  45. +23
    -6
      src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs
  46. +28
    -7
      src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs
  47. +24
    -11
      src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs
  48. +1
    -1
      src/Discord.Net.Webhook/Discord.Net.Webhook.csproj
  49. +10
    -7
      test/Discord.Net.Tests/Discord.Net.Tests.csproj
  50. +7
    -4
      test/Discord.Net.Tests/Net/CachedRestClient.cs
  51. +49
    -5
      test/Discord.Net.Tests/Tests.TokenUtils.cs

+ 10
- 3
samples/01_basic_ping_bot/Program.cs View File

@@ -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();


+ 18
- 10
samples/02_commands_framework/Program.cs View File

@@ -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>()


+ 21
- 16
samples/03_sharded_client/Program.cs View File

@@ -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();


+ 23
- 3
src/Discord.Net.Commands/CommandService.cs View File

@@ -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);
}
}
}

+ 1
- 1
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -16,4 +16,4 @@
<ItemGroup Condition=" '$(TargetFramework)' != 'netstandard2.0' ">
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" />
</ItemGroup>
</Project>
</Project>

+ 3
- 3
src/Discord.Net.Commands/Extensions/MessageExtensions.cs View File

@@ -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;


+ 3
- 0
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -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>

+ 0
- 39
src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs View File

@@ -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>


+ 41
- 1
src/Discord.Net.Core/Entities/Channels/INestedChannel.cs View File

@@ -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);
}
}

+ 2
- 1
src/Discord.Net.Core/Entities/Guilds/IGuild.cs View File

@@ -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.


+ 23
- 3
src/Discord.Net.Core/Entities/Image.cs View File

@@ -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;
}
}
}
}

+ 54
- 0
src/Discord.Net.Core/Extensions/MessageExtensions.cs View File

@@ -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);
}
}
}

+ 4
- 0
src/Discord.Net.Core/Format.cs View File

@@ -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)


+ 2
- 1
src/Discord.Net.Core/Net/Rest/IRestClient.cs View File

@@ -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.


+ 1
- 1
src/Discord.Net.Core/Net/Udp/IUdpSocket.cs View File

@@ -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;



+ 1
- 1
src/Discord.Net.Core/Net/WebSockets/IWebSocketClient.cs View File

@@ -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;


+ 77
- 4
src/Discord.Net.Core/Utils/TokenUtils.cs View File

@@ -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));
}
}

}
}

+ 17
- 5
src/Discord.Net.Providers.WS4Net/WS4NetClient.cs View File

@@ -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)


+ 2
- 0
src/Discord.Net.Rest/API/Rest/CreateGuildChannelParams.cs View File

@@ -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")]


+ 8
- 5
src/Discord.Net.Rest/BaseDiscordClient.cs View File

@@ -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 />


+ 6
- 2
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -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();


+ 7
- 5
src/Discord.Net.Rest/DiscordRestClient.cs View File

@@ -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)


+ 4
- 4
src/Discord.Net.Rest/Entities/AuditLogs/DataTypes/ChannelCreateAuditLogData.cs View File

@@ -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)));
}


+ 0
- 10
src/Discord.Net.Rest/Entities/Channels/RestCategoryChannel.cs View File

@@ -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>


+ 0
- 33
src/Discord.Net.Rest/Entities/Channels/RestGuildChannel.cs View File

@@ -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);


+ 8
- 0
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -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


+ 10
- 0
src/Discord.Net.Rest/Entities/Channels/RestVoiceChannel.cs View File

@@ -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)


+ 12
- 3
src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs View File

@@ -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);
}


+ 5
- 4
src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs View File

@@ -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)


+ 8
- 6
src/Discord.Net.Rest/Net/Converters/ImageConverter.cs View File

@@ -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);


+ 20
- 8
src/Discord.Net.Rest/Net/DefaultRestClient.cs View File

@@ -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");


+ 28
- 11
src/Discord.Net.Rest/Net/Queue/RequestQueue.cs View File

@@ -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();
}
}
}

+ 13
- 12
src/Discord.Net.WebSocket/Audio/AudioClient.cs View File

@@ -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 />


+ 20
- 6
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs View File

@@ -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)


+ 11
- 1
src/Discord.Net.WebSocket/Audio/Streams/InputStream.cs View File

@@ -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);
}
}
}

+ 38
- 15
src/Discord.Net.WebSocket/ConnectionManager.cs View File

@@ -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);
}
}
}
}

+ 29
- 6
src/Discord.Net.WebSocket/DiscordShardedClient.cs View File

@@ -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);
}
}
}

+ 4
- 1
src/Discord.Net.WebSocket/DiscordSocketApiClient.cs View File

@@ -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);


+ 29
- 12
src/Discord.Net.WebSocket/DiscordSocketClient.cs View File

@@ -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);
}


+ 8
- 6
src/Discord.Net.WebSocket/DiscordVoiceApiClient.cs View File

@@ -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 { }



+ 0
- 8
src/Discord.Net.WebSocket/Entities/Channels/SocketCategoryChannel.cs View File

@@ -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 />


+ 0
- 32
src/Discord.Net.WebSocket/Entities/Channels/SocketGuildChannel.cs View File

@@ -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);


+ 8
- 1
src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs View File

@@ -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;



+ 8
- 0
src/Discord.Net.WebSocket/Entities/Channels/SocketVoiceChannel.cs View File

@@ -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;



+ 23
- 6
src/Discord.Net.WebSocket/Entities/Guilds/SocketGuild.cs View File

@@ -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();
}
}
}

+ 28
- 7
src/Discord.Net.WebSocket/Net/DefaultUdpSocket.cs View File

@@ -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;


+ 24
- 11
src/Discord.Net.WebSocket/Net/DefaultWebSocketClient.cs View File

@@ -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);


+ 1
- 1
src/Discord.Net.Webhook/Discord.Net.Webhook.csproj View File

@@ -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>

+ 10
- 7
test/Discord.Net.Tests/Discord.Net.Tests.csproj View File

@@ -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>

+ 7
- 4
test/Discord.Net.Tests/Net/CachedRestClient.cs View File

@@ -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);
}
}
}
}

+ 49
- 5
test/Discord.Net.Tests/Tests.TokenUtils.cs View File

@@ -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);
}
}
}

Loading…
Cancel
Save