Browse Source

Merge branch 'dev' of https://github.com/RogueException/Discord.Net into breaking-change/better-async-collections

pull/658/head
ObsidianMinor 8 years ago
parent
commit
5cd5966444
100 changed files with 1005 additions and 394 deletions
  1. +2
    -2
      Discord.Net.targets
  2. +1
    -1
      README.md
  3. +1
    -1
      appveyor.yml
  4. +10
    -11
      docs/guides/commands/commands.md
  5. +17
    -13
      docs/guides/commands/samples/command_handler.cs
  6. +4
    -5
      docs/guides/commands/samples/dependency_map_setup.cs
  7. +4
    -2
      docs/guides/commands/samples/require_owner.cs
  8. +7
    -2
      docs/guides/concepts/samples/events.cs
  9. +2
    -2
      docs/guides/getting_started/intro.md
  10. +17
    -9
      docs/guides/getting_started/samples/intro/structure.cs
  11. +2
    -2
      docs/guides/migrating/migrating.md
  12. +1
    -1
      docs/guides/voice/samples/audio_ffmpeg.cs
  13. +7
    -0
      src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
  14. +5
    -3
      src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  15. +2
    -2
      src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  16. +13
    -5
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  17. +10
    -2
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  18. +103
    -53
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  19. +10
    -2
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  20. +4
    -1
      src/Discord.Net.Commands/CommandError.cs
  21. +4
    -4
      src/Discord.Net.Commands/CommandMatch.cs
  22. +5
    -4
      src/Discord.Net.Commands/CommandParser.cs
  23. +83
    -43
      src/Discord.Net.Commands/CommandService.cs
  24. +2
    -2
      src/Discord.Net.Commands/IModuleBase.cs
  25. +77
    -35
      src/Discord.Net.Commands/Info/CommandInfo.cs
  26. +17
    -0
      src/Discord.Net.Commands/Info/ModuleInfo.cs
  27. +5
    -2
      src/Discord.Net.Commands/Info/ParameterInfo.cs
  28. +5
    -7
      src/Discord.Net.Commands/ModuleBase.cs
  29. +0
    -5
      src/Discord.Net.Commands/PrimitiveParsers.cs
  30. +1
    -1
      src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
  31. +2
    -3
      src/Discord.Net.Commands/Readers/EnumTypeReader.cs
  32. +4
    -4
      src/Discord.Net.Commands/Readers/MessageTypeReader.cs
  33. +13
    -5
      src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs
  34. +1
    -1
      src/Discord.Net.Commands/Readers/RoleTypeReader.cs
  35. +3
    -2
      src/Discord.Net.Commands/Readers/TypeReader.cs
  36. +2
    -3
      src/Discord.Net.Commands/Readers/UserTypeReader.cs
  37. +27
    -0
      src/Discord.Net.Commands/Results/PreconditionGroupResult.cs
  38. +2
    -2
      src/Discord.Net.Commands/Results/PreconditionResult.cs
  39. +27
    -0
      src/Discord.Net.Commands/Results/RuntimeResult.cs
  40. +1
    -1
      src/Discord.Net.Commands/Utilities/ReflectionUtils.cs
  41. +4
    -1
      src/Discord.Net.Core/Audio/AudioStream.cs
  42. +2
    -2
      src/Discord.Net.Core/Audio/IAudioClient.cs
  43. +3
    -1
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  44. +21
    -5
      src/Discord.Net.Core/Entities/Emotes/Emoji.cs
  45. +20
    -1
      src/Discord.Net.Core/Entities/Emotes/Emote.cs
  46. +1
    -1
      src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs
  47. +2
    -2
      src/Discord.Net.Core/Entities/Guilds/IGuild.cs
  48. +3
    -1
      src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs
  49. +6
    -3
      src/Discord.Net.Core/Entities/Messages/Embed.cs
  50. +2
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs
  51. +2
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs
  52. +3
    -2
      src/Discord.Net.Core/Entities/Messages/EmbedImage.cs
  53. +2
    -1
      src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs
  54. +3
    -2
      src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs
  55. +13
    -0
      src/Discord.Net.Core/Entities/Messages/EmbedType.cs
  56. +3
    -2
      src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs
  57. +1
    -1
      src/Discord.Net.Core/Entities/Messages/IEmbed.cs
  58. +40
    -0
      src/Discord.Net.Core/Entities/Roles/Color.cs
  59. +1
    -1
      src/Discord.Net.Core/Entities/Users/IGuildUser.cs
  60. +1
    -3
      src/Discord.Net.Core/Entities/Users/IUser.cs
  61. +10
    -0
      src/Discord.Net.Core/Extensions/StringExtensions.cs
  62. +16
    -0
      src/Discord.Net.Core/Extensions/UserExtensions.cs
  63. +3
    -3
      src/Discord.Net.Core/Net/Rest/IRestClient.cs
  64. +4
    -0
      src/Discord.Net.Core/RequestOptions.cs
  65. +3
    -2
      src/Discord.Net.Rest/API/Common/Embed.cs
  66. +2
    -1
      src/Discord.Net.Rest/API/Common/EmbedAuthor.cs
  67. +2
    -1
      src/Discord.Net.Rest/API/Common/EmbedFooter.cs
  68. +1
    -0
      src/Discord.Net.Rest/API/Common/EmbedImage.cs
  69. +1
    -0
      src/Discord.Net.Rest/API/Common/EmbedProvider.cs
  70. +1
    -0
      src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs
  71. +1
    -0
      src/Discord.Net.Rest/API/Common/EmbedVideo.cs
  72. +1
    -0
      src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs
  73. +10
    -8
      src/Discord.Net.Rest/DiscordRestApiClient.cs
  74. +4
    -4
      src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs
  75. +3
    -1
      src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs
  76. +3
    -1
      src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs
  77. +3
    -1
      src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs
  78. +3
    -1
      src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs
  79. +2
    -2
      src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs
  80. +4
    -4
      src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs
  81. +180
    -21
      src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs
  82. +2
    -2
      src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs
  83. +3
    -5
      src/Discord.Net.Rest/Entities/Users/RestUser.cs
  84. +1
    -1
      src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs
  85. +2
    -2
      src/Discord.Net.Rest/Entities/Users/UserHelper.cs
  86. +23
    -0
      src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs
  87. +8
    -3
      src/Discord.Net.Rest/Net/DefaultRestClient.cs
  88. +1
    -1
      src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs
  89. +1
    -1
      src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs
  90. +1
    -1
      src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs
  91. +3
    -1
      src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs
  92. +3
    -1
      src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs
  93. +3
    -1
      src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs
  94. +3
    -5
      src/Discord.Net.Rpc/Entities/Users/RpcUser.cs
  95. +22
    -22
      src/Discord.Net.WebSocket/Audio/AudioClient.cs
  96. +2
    -2
      src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs
  97. +5
    -2
      src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs
  98. +2
    -2
      src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs
  99. +9
    -8
      src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs
  100. +18
    -10
      src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs

+ 2
- 2
Discord.Net.targets View File

@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>rc3</VersionSuffix>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<Authors>RogueException</Authors> <Authors>RogueException</Authors>
<PackageTags>discord;discordapp</PackageTags> <PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>


+ 1
- 1
README.md View File

@@ -1,4 +1,4 @@
# Discord.Net v1.0.0-rc
# Discord.Net
[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net) [![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net)
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net)
[![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev) [![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev)


+ 1
- 1
appveyor.yml View File

@@ -34,7 +34,7 @@ after_build:
if ($Env:APPVEYOR_REPO_TAG -eq "true") { if ($Env:APPVEYOR_REPO_TAG -eq "true") {
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="" nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix=""
} else { } else {
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-$Env:BUILD"
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD"
} }
- ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } - ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }




+ 10
- 11
docs/guides/commands/commands.md View File

@@ -45,7 +45,7 @@ Discord.Net's implementation of Modules is influenced heavily from
ASP.Net Core's Controller pattern. This means that the lifetime of a ASP.Net Core's Controller pattern. This means that the lifetime of a
module instance is only as long as the command being invoked. module instance is only as long as the command being invoked.


**Avoid using long-running code** in your modules whereever possible.
**Avoid using long-running code** in your modules wherever possible.
You should **not** be implementing very much logic into your modules; You should **not** be implementing very much logic into your modules;
outsource to a service for that. outsource to a service for that.


@@ -167,8 +167,8 @@ a dependency map.


Modules are constructed using Dependency Injection. Any parameters Modules are constructed using Dependency Injection. Any parameters
that are placed in the constructor must be injected into an that are placed in the constructor must be injected into an
@Discord.Commands.IDependencyMap. Alternatively, you may accept an
IDependencyMap as an argument and extract services yourself.
@System.IServiceProvider. Alternatively, you may accept an
IServiceProvider as an argument and extract services yourself.


### Module Properties ### Module Properties


@@ -205,21 +205,20 @@ you use DI when writing your modules.


### Setup ### Setup


First, you need to create an @Discord.Commands.IDependencyMap.
The library includes @Discord.Commands.DependencyMap to help with
this, however you may create your own IDependencyMap if you wish.
First, you need to create an @System.IServiceProvider
You may create your own IServiceProvider if you wish.


Next, add the dependencies your modules will use to the map. Next, add the dependencies your modules will use to the map.


Finally, pass the map into the `LoadAssembly` method. Finally, pass the map into the `LoadAssembly` method.
Your modules will automatically be loaded with this dependency map. Your modules will automatically be loaded with this dependency map.


[!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)]
[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)]


### Usage in Modules ### Usage in Modules


In the constructor of your module, any parameters will be filled in by In the constructor of your module, any parameters will be filled in by
the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`.
the @System.IServiceProvider you pass into `LoadAssembly`.


Any publicly settable properties will also be filled in the same manner. Any publicly settable properties will also be filled in the same manner.


@@ -228,12 +227,12 @@ Any publicly settable properties will also be filled in the same manner.
being injected. being injected.


>[!NOTE] >[!NOTE]
>If you accept `CommandService` or `IDependencyMap` as a parameter in
>If you accept `CommandService` or `IServiceProvider` as a parameter in
your constructor or as an injectable property, these entries will be filled your constructor or as an injectable property, these entries will be filled
by the CommandService the module was loaded from, and the DependencyMap passed
by the CommandService the module was loaded from, and the ServiceProvider passed
into it, respectively. into it, respectively.


[!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)]
[!code-csharp[ServiceProvider in Modules](samples/dependency_module.cs)]


# Preconditions # Preconditions




+ 17
- 13
docs/guides/commands/samples/command_handler.cs View File

@@ -1,14 +1,16 @@
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Reflection; using System.Reflection;
using Discord; using Discord;
using Discord.WebSocket; using Discord.WebSocket;
using Discord.Commands; using Discord.Commands;
using Microsoft.Extensions.DependencyInjection;


public class Program public class Program
{ {
private CommandService commands; private CommandService commands;
private DiscordSocketClient client; private DiscordSocketClient client;
private DependencyMap map;
private IServiceProvider services;


static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult();


@@ -19,38 +21,40 @@ public class Program


string token = "bot token here"; string token = "bot token here";


map = new DependencyMap();
services = new ServiceCollection()
.BuildServiceProvider();


await InstallCommands(); await InstallCommands();


await client.LoginAsync(TokenType.Bot, token); await client.LoginAsync(TokenType.Bot, token);
await client.ConnectAsync();
await client.StartAsync();


await Task.Delay(-1); await Task.Delay(-1);
} }

public async Task InstallCommands() public async Task InstallCommands()
{ {
// Hook the MessageReceived Event into our Command Handler // Hook the MessageReceived Event into our Command Handler
client.MessageReceived += HandleCommand; client.MessageReceived += HandleCommand;
// Discover all of the commands in this assembly and load them.
// Discover all of the commands in this assembly and load them.
await commands.AddModulesAsync(Assembly.GetEntryAssembly()); await commands.AddModulesAsync(Assembly.GetEntryAssembly());
} }

public async Task HandleCommand(SocketMessage messageParam) public async Task HandleCommand(SocketMessage messageParam)
{
{
// Don't process the command if it was a System Message // Don't process the command if it was a System Message
var message = messageParam as SocketUserMessage; var message = messageParam as SocketUserMessage;
if (message == null) return; if (message == null) return;
// Create a number to track where the prefix ends and the command begins
int argPos = 0;
// Determine if the message is a command, based on if it starts with '!' or a mention prefix
if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return;
// Create a number to track where the prefix ends and the command begins
int argPos = 0;
// Determine if the message is a command, based on if it starts with '!' or a mention prefix
if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return;
// Create a Command Context // Create a Command Context
var context = new CommandContext(client, message); var context = new CommandContext(client, message);
// Execute the command. (result does not indicate a return value, // Execute the command. (result does not indicate a return value,
// rather an object stating if the command executed succesfully)
var result = await commands.ExecuteAsync(context, argPos, map);
// rather an object stating if the command executed successfully)
var result = await commands.ExecuteAsync(context, argPos, service);
if (!result.IsSuccess) if (!result.IsSuccess)
await context.Channel.SendMessageAsync(result.ErrorReason); await context.Channel.SendMessageAsync(result.ErrorReason);
}

}
} }

+ 4
- 5
docs/guides/commands/samples/dependency_map_setup.cs View File

@@ -7,12 +7,11 @@ public class Commands
{ {
public async Task Install(DiscordSocketClient client) public async Task Install(DiscordSocketClient client)
{ {
// Here, we will inject the Dependency Map with
// Here, we will inject the ServiceProvider with
// all of the services our client will use. // all of the services our client will use.
_map.Add(client);
_map.Add(commands);
_map.Add(new NotificationService(_map));
_map.Add(new DatabaseService(_map));
_serviceCollection.AddSingleton(client)
_serviceCollection.AddSingleton(new NotificationService())
_serviceCollection.AddSingleton(new DatabaseService())
// ... // ...
await _commands.AddModulesAsync(Assembly.GetEntryAssembly()); await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
} }


+ 4
- 2
docs/guides/commands/samples/require_owner.cs View File

@@ -2,16 +2,18 @@


using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;


// Inherit from PreconditionAttribute // Inherit from PreconditionAttribute
public class RequireOwnerAttribute : PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute
{ {
// Override the CheckPermissions method // Override the CheckPermissions method
public async override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map)
public async override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{ {
// Get the ID of the bot's owner // Get the ID of the bot's owner
var ownerId = (await map.Get<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id;
var ownerId = (await services.GetService<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id;
// If this command was executed by that user, return a success // If this command was executed by that user, return a success
if (context.User.Id == ownerId) if (context.User.Id == ownerId)
return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();


+ 7
- 2
docs/guides/concepts/samples/events.cs View File

@@ -8,7 +8,11 @@ public class Program
public async Task MainAsync() public async Task MainAsync()
{ {
_client = new DiscordSocketClient();
// When working with events that have Cacheable<IMessage, ulong> parameters,
// you must enable the message cache in your config settings if you plan to
// use the cached message entity.
var _config = new DiscordSocketConfig { MessageCacheSize = 100 };
_client = new DiscordSocketClient(_config);


await _client.LoginAsync(TokenType.Bot, "bot token"); await _client.LoginAsync(TokenType.Bot, "bot token");
await _client.StartAsync(); await _client.StartAsync();
@@ -25,7 +29,8 @@ public class Program


private async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel) private async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel)
{ {
// If the message was not in the cache, downloading it will result in getting a copy of `after`.
var message = await before.GetOrDownloadAsync(); var message = await before.GetOrDownloadAsync();
Console.WriteLine($"{message} -> {after}"); Console.WriteLine($"{message} -> {after}");
} }
}
}

+ 2
- 2
docs/guides/getting_started/intro.md View File

@@ -211,7 +211,7 @@ For your reference, you may view the [completed program].
# Building a bot with commands # Building a bot with commands


This section will show you how to write a program that is ready for This section will show you how to write a program that is ready for
[commands](commands.md). Note that this will not be explaining _how_
[commands](commands/commands.md). Note that this will not be explaining _how_
to write commands or services, it will only be covering the general to write commands or services, it will only be covering the general
structure. structure.


@@ -224,4 +224,4 @@ should be to separate the program (initialization and command handler),
the modules (handle commands), and the services (persistent storage, the modules (handle commands), and the services (persistent storage,
pure functions, data manipulation). pure functions, data manipulation).


**todo:** diagram of bot structure
**todo:** diagram of bot structure

+ 17
- 9
docs/guides/getting_started/samples/intro/structure.cs View File

@@ -30,8 +30,8 @@ class Program
LogLevel = LogSeverity.Info, LogLevel = LogSeverity.Info,
// If you or another service needs to do anything with messages // If you or another service needs to do anything with messages
// (eg. checking Reactions), you should probably
// set the MessageCacheSize here.
// (eg. checking Reactions, checking the content of edited/deleted messages),
// you must set the MessageCacheSize. You may adjust the number as needed.
//MessageCacheSize = 50, //MessageCacheSize = 50,


// If your platform doesn't have native websockets, // If your platform doesn't have native websockets,
@@ -41,7 +41,7 @@ class Program
}); });
} }


// Create a named logging handler, so it can be re-used by addons
// Example of a logging handler. This can be re-used by addons
// that ask for a Func<LogMessage, Task>. // that ask for a Func<LogMessage, Task>.
private static Task Logger(LogMessage message) private static Task Logger(LogMessage message)
{ {
@@ -65,6 +65,13 @@ class Program
} }
Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}"); Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}");
Console.ForegroundColor = cc; Console.ForegroundColor = cc;
// If you get an error saying 'CompletedTask' doesn't exist,
// your project is targeting .NET 4.5.2 or lower. You'll need
// to adjust your project's target framework to 4.6 or higher
// (instructions for this are easily Googled).
// If you *need* to run on .NET 4.5 for compat/other reasons,
// the alternative is to 'return Task.Delay(0);' instead.
return Task.CompletedTask; return Task.CompletedTask;
} }


@@ -92,16 +99,17 @@ class Program
// and other dependencies that your commands might need. // and other dependencies that your commands might need.
_map.AddSingleton(new SomeServiceClass()); _map.AddSingleton(new SomeServiceClass());


// Either search the program and add all Module classes that can be found:
await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
// Or add Modules manually if you prefer to be a little more explicit:
await _commands.AddModuleAsync<SomeModule>();

// When all your required services are in the collection, build the container. // When all your required services are in the collection, build the container.
// Tip: There's an overload taking in a 'validateScopes' bool to make sure // Tip: There's an overload taking in a 'validateScopes' bool to make sure
// you haven't made any mistakes in your dependency graph. // you haven't made any mistakes in your dependency graph.
_services = _map.BuildServiceProvider(); _services = _map.BuildServiceProvider();


// Either search the program and add all Module classes that can be found.
// Module classes *must* be marked 'public' or they will be ignored.
await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
// Or add Modules manually if you prefer to be a little more explicit:
await _commands.AddModuleAsync<SomeModule>();

// Subscribe a handler to see if a message invokes a command. // Subscribe a handler to see if a message invokes a command.
_client.MessageReceived += HandleCommandAsync; _client.MessageReceived += HandleCommandAsync;
} }
@@ -120,7 +128,7 @@ class Program
// commands to be invoked by mentioning the bot instead. // commands to be invoked by mentioning the bot instead.
if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */) if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */)
{ {
// Create a Command Context
// Create a Command Context.
var context = new SocketCommandContext(_client, msg); var context = new SocketCommandContext(_client, msg);
// Execute the command. (result does not indicate a return value, // Execute the command. (result does not indicate a return value,


+ 2
- 2
docs/guides/migrating/migrating.md View File

@@ -42,7 +42,7 @@ events are delegates, but are still registered the same.
For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived) For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived)


To hook an event into MessageReceived, we now use the following code: To hook an event into MessageReceived, we now use the following code:
[!code-csharp[Event Registration](guides/samples/migrating/event.cs)]
[!code-csharp[Event Registration](samples/event.cs)]


> **All Event Handlers in 1.0 MUST return Task!** > **All Event Handlers in 1.0 MUST return Task!**


@@ -50,7 +50,7 @@ If your event handler is marked as `async`, it will automatically return `Task`.
if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead, if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead,
stick a `return Task.CompletedTask` at the bottom. stick a `return Task.CompletedTask` at the bottom.


[!code-csharp[Sync Event Registration](guides/samples/migrating/sync_event.cs)]
[!code-csharp[Sync Event Registration](samples/sync_event.cs)]


**Event handlers no longer require a sender.** The only arguments your event handler needs to accept **Event handlers no longer require a sender.** The only arguments your event handler needs to accept
are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the


+ 1
- 1
docs/guides/voice/samples/audio_ffmpeg.cs View File

@@ -3,7 +3,7 @@ private async Task SendAsync(IAudioClient client, string path)
// Create FFmpeg using the previous example // Create FFmpeg using the previous example
var ffmpeg = CreateStream(path); var ffmpeg = CreateStream(path);
var output = ffmpeg.StandardOutput.BaseStream; var output = ffmpeg.StandardOutput.BaseStream;
var discord = client.CreatePCMStream(AudioApplication.Mixed, 1920);
var discord = client.CreatePCMStream(AudioApplication.Mixed);
await output.CopyToAsync(discord); await output.CopyToAsync(discord);
await discord.FlushAsync(); await discord.FlushAsync();
} }

+ 7
- 0
src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs View File

@@ -6,6 +6,13 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public abstract class PreconditionAttribute : Attribute public abstract class PreconditionAttribute : Attribute
{ {
/// <summary>
/// Specify a group that this precondition belongs to. Preconditions of the same group require only one
/// of the preconditions to pass in order to be successful (A || B). Specifying <see cref="Group"/> = <see cref="null"/>
/// or not at all will require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// </summary>
public string Group { get; set; } = null;

public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services); public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services);
} }
} }

+ 5
- 3
src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -44,14 +44,16 @@ namespace Discord.Commands


public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services) public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{ {
var guildUser = await context.Guild.GetCurrentUserAsync();
IGuildUser guildUser = null;
if (context.Guild != null)
guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false);


if (GuildPermission.HasValue) if (GuildPermission.HasValue)
{ {
if (guildUser == null) if (guildUser == null)
return PreconditionResult.FromError("Command must be used in a guild channel"); return PreconditionResult.FromError("Command must be used in a guild channel");
if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}");
return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}");
} }


if (ChannelPermission.HasValue) if (ChannelPermission.HasValue)
@@ -65,7 +67,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(guildChannel); perms = ChannelPermissions.All(guildChannel);


if (!perms.Has(ChannelPermission.Value)) if (!perms.Has(ChannelPermission.Value))
return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}");
return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}");
} }


return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();


+ 2
- 2
src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -52,7 +52,7 @@ namespace Discord.Commands
if (guildUser == null) if (guildUser == null)
return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel")); return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel"));
if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"));
return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}"));
} }


if (ChannelPermission.HasValue) if (ChannelPermission.HasValue)
@@ -66,7 +66,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(guildChannel); perms = ChannelPermissions.All(guildChannel);


if (!perms.Has(ChannelPermission.Value)) if (!perms.Has(ChannelPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"));
return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}"));
} }


return Task.FromResult(PreconditionResult.FromSuccess()); return Task.FromResult(PreconditionResult.FromSuccess());


+ 13
- 5
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -10,10 +10,11 @@ namespace Discord.Commands.Builders
{ {
private readonly List<PreconditionAttribute> _preconditions; private readonly List<PreconditionAttribute> _preconditions;
private readonly List<ParameterBuilder> _parameters; private readonly List<ParameterBuilder> _parameters;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases; private readonly List<string> _aliases;


public ModuleBuilder Module { get; } public ModuleBuilder Module { get; }
internal Func<ICommandContext, object[], IServiceProvider, Task> Callback { get; set; }
internal Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> Callback { get; set; }


public string Name { get; set; } public string Name { get; set; }
public string Summary { get; set; } public string Summary { get; set; }
@@ -24,6 +25,7 @@ namespace Discord.Commands.Builders


public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<ParameterBuilder> Parameters => _parameters; public IReadOnlyList<ParameterBuilder> Parameters => _parameters;
public IReadOnlyList<Attribute> Attributes => _attributes;
public IReadOnlyList<string> Aliases => _aliases; public IReadOnlyList<string> Aliases => _aliases;


//Automatic //Automatic
@@ -33,10 +35,11 @@ namespace Discord.Commands.Builders


_preconditions = new List<PreconditionAttribute>(); _preconditions = new List<PreconditionAttribute>();
_parameters = new List<ParameterBuilder>(); _parameters = new List<ParameterBuilder>();
_attributes = new List<Attribute>();
_aliases = new List<string>(); _aliases = new List<string>();
} }
//User-defined //User-defined
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, Task> callback)
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback)
: this(module) : this(module)
{ {
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
@@ -77,12 +80,17 @@ namespace Discord.Commands.Builders
{ {
for (int i = 0; i < aliases.Length; i++) for (int i = 0; i < aliases.Length; i++)
{ {
var alias = aliases[i] ?? "";
string alias = aliases[i] ?? "";
if (!_aliases.Contains(alias)) if (!_aliases.Contains(alias))
_aliases.Add(alias); _aliases.Add(alias);
} }
return this; return this;
} }
public CommandBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public CommandBuilder AddPrecondition(PreconditionAttribute precondition) public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
{ {
_preconditions.Add(precondition); _preconditions.Add(precondition);
@@ -122,11 +130,11 @@ namespace Discord.Commands.Builders


var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple);
if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) if ((firstMultipleParam != null) && (firstMultipleParam != lastParam))
throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag.");
throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}");
var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder);
if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) if ((firstRemainderParam != null) && (firstRemainderParam != lastParam))
throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag.");
throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}");
} }


return new CommandInfo(this, info, service); return new CommandInfo(this, info, service);


+ 10
- 2
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

@@ -10,6 +10,7 @@ namespace Discord.Commands.Builders
private readonly List<CommandBuilder> _commands; private readonly List<CommandBuilder> _commands;
private readonly List<ModuleBuilder> _submodules; private readonly List<ModuleBuilder> _submodules;
private readonly List<PreconditionAttribute> _preconditions; private readonly List<PreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases; private readonly List<string> _aliases;


public CommandService Service { get; } public CommandService Service { get; }
@@ -21,6 +22,7 @@ namespace Discord.Commands.Builders
public IReadOnlyList<CommandBuilder> Commands => _commands; public IReadOnlyList<CommandBuilder> Commands => _commands;
public IReadOnlyList<ModuleBuilder> Modules => _submodules; public IReadOnlyList<ModuleBuilder> Modules => _submodules;
public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions; public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<Attribute> Attributes => _attributes;
public IReadOnlyList<string> Aliases => _aliases; public IReadOnlyList<string> Aliases => _aliases;


//Automatic //Automatic
@@ -32,6 +34,7 @@ namespace Discord.Commands.Builders
_commands = new List<CommandBuilder>(); _commands = new List<CommandBuilder>();
_submodules = new List<ModuleBuilder>(); _submodules = new List<ModuleBuilder>();
_preconditions = new List<PreconditionAttribute>(); _preconditions = new List<PreconditionAttribute>();
_attributes = new List<Attribute>();
_aliases = new List<string>(); _aliases = new List<string>();
} }
//User-defined //User-defined
@@ -63,18 +66,23 @@ namespace Discord.Commands.Builders
{ {
for (int i = 0; i < aliases.Length; i++) for (int i = 0; i < aliases.Length; i++)
{ {
var alias = aliases[i] ?? "";
string alias = aliases[i] ?? "";
if (!_aliases.Contains(alias)) if (!_aliases.Contains(alias))
_aliases.Add(alias); _aliases.Add(alias);
} }
return this; return this;
} }
public ModuleBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
{ {
_preconditions.Add(precondition); _preconditions.Add(precondition);
return this; return this;
} }
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, Task> callback, Action<CommandBuilder> createFunc)
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback, Action<CommandBuilder> createFunc)
{ {
var builder = new CommandBuilder(this, primaryAlias, callback); var builder = new CommandBuilder(this, primaryAlias, callback);
createFunc(builder); createFunc(builder);


+ 103
- 53
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -12,25 +12,42 @@ namespace Discord.Commands
{ {
private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo(); private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo();


public static IEnumerable<TypeInfo> Search(Assembly assembly)
public static async Task<IReadOnlyList<TypeInfo>> SearchAsync(Assembly assembly, CommandService service)
{ {
foreach (var type in assembly.ExportedTypes)
bool IsLoadableModule(TypeInfo info)
{ {
var typeInfo = type.GetTypeInfo();
if (IsValidModuleDefinition(typeInfo) &&
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) &&
info.GetCustomAttribute<DontAutoLoadAttribute>() == null;
}

var result = new List<TypeInfo>();

foreach (var typeInfo in assembly.DefinedTypes)
{
if (typeInfo.IsPublic || typeInfo.IsNestedPublic)
{ {
yield return typeInfo;
if (IsValidModuleDefinition(typeInfo) &&
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
{
result.Add(typeInfo);
}
}
else if (IsLoadableModule(typeInfo))
{
await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.");
} }
} }

return result;
} }


public static Dictionary<Type, ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service);
public static Dictionary<Type, ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service)

public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service);
public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service)
{ {
/*if (!validTypes.Any()) /*if (!validTypes.Any())
throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ throw new InvalidOperationException("Could not find any valid modules from the given selection");*/
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); var topLevelGroups = validTypes.Where(x => x.DeclaringType == null);
var subGroups = validTypes.Intersect(topLevelGroups); var subGroups = validTypes.Intersect(topLevelGroups);


@@ -48,10 +65,13 @@ namespace Discord.Commands


BuildModule(module, typeInfo, service); BuildModule(module, typeInfo, service);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
builtTypes.Add(typeInfo);


result[typeInfo.AsType()] = module.Build(service); result[typeInfo.AsType()] = module.Build(service);
} }


await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false);

return result; return result;
} }


@@ -102,6 +122,9 @@ namespace Discord.Commands
case PreconditionAttribute precondition: case PreconditionAttribute precondition:
builder.AddPrecondition(precondition); builder.AddPrecondition(precondition);
break; break;
default:
builder.AddAttributes(attribute);
break;
} }
} }


@@ -128,26 +151,35 @@ namespace Discord.Commands
foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
// TODO: C#7 type switch
if (attribute is CommandAttribute)
switch (attribute)
{ {
var cmdAttr = attribute as CommandAttribute;
builder.AddAliases(cmdAttr.Text);
builder.RunMode = cmdAttr.RunMode;
builder.Name = builder.Name ?? cmdAttr.Text;
case CommandAttribute command:
builder.AddAliases(command.Text);
builder.RunMode = command.RunMode;
builder.Name = builder.Name ?? command.Text;
break;
case NameAttribute name:
builder.Name = name.Text;
break;
case PriorityAttribute priority:
builder.Priority = priority.Priority;
break;
case SummaryAttribute summary:
builder.Summary = summary.Text;
break;
case RemarksAttribute remarks:
builder.Remarks = remarks.Text;
break;
case AliasAttribute alias:
builder.AddAliases(alias.Aliases);
break;
case PreconditionAttribute precondition:
builder.AddPrecondition(precondition);
break;
default:
builder.AddAttributes(attribute);
break;
} }
else if (attribute is NameAttribute)
builder.Name = (attribute as NameAttribute).Text;
else if (attribute is PriorityAttribute)
builder.Priority = (attribute as PriorityAttribute).Priority;
else if (attribute is SummaryAttribute)
builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is RemarksAttribute)
builder.Remarks = (attribute as RemarksAttribute).Text;
else if (attribute is AliasAttribute)
builder.AddAliases((attribute as AliasAttribute).Aliases);
else if (attribute is PreconditionAttribute)
builder.AddPrecondition(attribute as PreconditionAttribute);
} }


if (builder.Name == null) if (builder.Name == null)
@@ -165,22 +197,34 @@ namespace Discord.Commands


var createInstance = ReflectionUtils.CreateBuilder<IModuleBase>(typeInfo, service); var createInstance = ReflectionUtils.CreateBuilder<IModuleBase>(typeInfo, service);


builder.Callback = async (ctx, args, map) =>
async Task<IResult> ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd)
{ {
var instance = createInstance(map);
instance.SetContext(ctx);
var instance = createInstance(services);
instance.SetContext(context);

try try
{ {
instance.BeforeExecute();
instance.BeforeExecute(cmd);

var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); var task = method.Invoke(instance, args) as Task ?? Task.Delay(0);
await task.ConfigureAwait(false);
if (task is Task<RuntimeResult> resultTask)
{
return await resultTask.ConfigureAwait(false);
}
else
{
await task.ConfigureAwait(false);
return ExecuteResult.FromSuccess();
}
} }
finally finally
{ {
instance.AfterExecute();
instance.AfterExecute(cmd);
(instance as IDisposable)?.Dispose(); (instance as IDisposable)?.Dispose();
} }
};
}

builder.Callback = ExecuteCallback;
} }


private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service)
@@ -195,24 +239,30 @@ namespace Discord.Commands


foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
// TODO: C#7 type switch
if (attribute is SummaryAttribute)
builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is OverrideTypeReaderAttribute)
builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader);
else if (attribute is ParameterPreconditionAttribute)
builder.AddPrecondition(attribute as ParameterPreconditionAttribute);
else if (attribute is ParamArrayAttribute)
{
builder.IsMultiple = true;
paramType = paramType.GetElementType();
}
else if (attribute is RemainderAttribute)
switch (attribute)
{ {
if (position != count-1)
throw new InvalidOperationException("Remainder parameters must be the last parameter in a command.");
builder.IsRemainder = true;
case SummaryAttribute summary:
builder.Summary = summary.Text;
break;
case OverrideTypeReaderAttribute typeReader:
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader);
break;
case ParamArrayAttribute _:
builder.IsMultiple = true;
paramType = paramType.GetElementType();
break;
case ParameterPreconditionAttribute precon:
builder.AddPrecondition(precon);
break;
case RemainderAttribute _:
if (position != count - 1)
throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}");

builder.IsRemainder = true;
break;
default:
builder.AddAttributes(attribute);
break;
} }
} }


@@ -258,9 +308,9 @@ namespace Discord.Commands
private static bool IsValidCommandDefinition(MethodInfo methodInfo) private static bool IsValidCommandDefinition(MethodInfo methodInfo)
{ {
return methodInfo.IsDefined(typeof(CommandAttribute)) && return methodInfo.IsDefined(typeof(CommandAttribute)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(void)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic && !methodInfo.IsStatic &&
!methodInfo.IsGenericMethod; !methodInfo.IsGenericMethod;
} }
} }
}
}

+ 10
- 2
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -8,7 +8,8 @@ namespace Discord.Commands.Builders
{ {
public class ParameterBuilder public class ParameterBuilder
{ {
private readonly List<ParameterPreconditionAttribute> _preconditions;
private readonly List<ParameterPreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;


public CommandBuilder Command { get; } public CommandBuilder Command { get; }
public string Name { get; internal set; } public string Name { get; internal set; }
@@ -22,11 +23,13 @@ namespace Discord.Commands.Builders
public string Summary { get; set; } public string Summary { get; set; }


public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions; public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<Attribute> Attributes => _attributes;


//Automatic //Automatic
internal ParameterBuilder(CommandBuilder command) internal ParameterBuilder(CommandBuilder command)
{ {
_preconditions = new List<ParameterPreconditionAttribute>(); _preconditions = new List<ParameterPreconditionAttribute>();
_attributes = new List<Attribute>();


Command = command; Command = command;
} }
@@ -49,7 +52,7 @@ namespace Discord.Commands.Builders
TypeReader = Command.Module.Service.GetDefaultTypeReader(type); TypeReader = Command.Module.Service.GetDefaultTypeReader(type);


if (TypeReader == null) if (TypeReader == null)
throw new InvalidOperationException($"{type} does not have a TypeReader registered for it");
throw new InvalidOperationException($"{type} does not have a TypeReader registered for it. Parameter: {Name} in {Command.PrimaryAlias}");


if (type.GetTypeInfo().IsValueType) if (type.GetTypeInfo().IsValueType)
DefaultValue = Activator.CreateInstance(type); DefaultValue = Activator.CreateInstance(type);
@@ -84,6 +87,11 @@ namespace Discord.Commands.Builders
return this; return this;
} }


public ParameterBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition)
{ {
_preconditions.Add(precondition); _preconditions.Add(precondition);


+ 4
- 1
src/Discord.Net.Commands/CommandError.cs View File

@@ -18,6 +18,9 @@
UnmetPrecondition, UnmetPrecondition,


//Execute //Execute
Exception
Exception,

//Runtime
Unsuccessful
} }
} }

+ 4
- 4
src/Discord.Net.Commands/CommandMatch.cs View File

@@ -18,11 +18,11 @@ namespace Discord.Commands


public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
=> Command.CheckPreconditionsAsync(context, services); => Command.CheckPreconditionsAsync(context, services);
public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null)
=> Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult);
public Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)
=> Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services);
public Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
=> Command.ExecuteAsync(context, argList, paramList, services); => Command.ExecuteAsync(context, argList, paramList, services);
public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
=> Command.ExecuteAsync(context, parseResult, services); => Command.ExecuteAsync(context, parseResult, services);
} }
} }

+ 5
- 4
src/Discord.Net.Commands/CommandParser.cs View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System;
using System.Collections.Immutable;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -13,7 +14,7 @@ namespace Discord.Commands
QuotedParameter QuotedParameter
} }
public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos)
public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos)
{ {
ParameterInfo curParam = null; ParameterInfo curParam = null;
StringBuilder argBuilder = new StringBuilder(input.Length); StringBuilder argBuilder = new StringBuilder(input.Length);
@@ -110,7 +111,7 @@ namespace Discord.Commands
if (curParam == null) if (curParam == null)
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");


var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false);
var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false);
if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches) if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches)
return ParseResult.FromError(typeReaderResult); return ParseResult.FromError(typeReaderResult);


@@ -133,7 +134,7 @@ namespace Discord.Commands


if (curParam != null && curParam.IsRemainder) if (curParam != null && curParam.IsRemainder)
{ {
var typeReaderResult = await curParam.Parse(context, argBuilder.ToString()).ConfigureAwait(false);
var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false);
if (!typeReaderResult.IsSuccess) if (!typeReaderResult.IsSuccess)
return ParseResult.FromError(typeReaderResult); return ParseResult.FromError(typeReaderResult);
argList.Add(typeReaderResult); argList.Add(typeReaderResult);


+ 83
- 43
src/Discord.Net.Commands/CommandService.cs View File

@@ -33,7 +33,7 @@ namespace Discord.Commands


public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x); public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x);
public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands); public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands);
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value);
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new { y.Key, y.Value })).ToLookup(x => x.Key, x => x.Value);


public CommandService() : this(new CommandServiceConfig()) { } public CommandService() : this(new CommandServiceConfig()) { }
public CommandService(CommandServiceConfig config) public CommandService(CommandServiceConfig config)
@@ -59,6 +59,9 @@ namespace Discord.Commands
foreach (var type in PrimitiveParsers.SupportedTypes) foreach (var type in PrimitiveParsers.SupportedTypes)
_defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type);


_defaultTypeReaders[typeof(string)] =
new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0);

var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>(); var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>();
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>))); entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>))); entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>)));
@@ -95,7 +98,7 @@ namespace Discord.Commands
if (_typedModuleDefs.ContainsKey(type)) if (_typedModuleDefs.ContainsKey(type))
throw new ArgumentException($"This module has already been added."); throw new ArgumentException($"This module has already been added.");


var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault();
var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault();


if (module.Value == default(ModuleInfo)) if (module.Value == default(ModuleInfo))
throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?"); throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?");
@@ -114,8 +117,8 @@ namespace Discord.Commands
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
try try
{ {
var types = ModuleClassBuilder.Search(assembly).ToArray();
var moduleDefs = ModuleClassBuilder.Build(types, this);
var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false);
var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false);


foreach (var info in moduleDefs) foreach (var info in moduleDefs)
{ {
@@ -161,8 +164,7 @@ namespace Discord.Commands
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
try try
{ {
ModuleInfo module;
if (!_typedModuleDefs.TryRemove(type, out module))
if (!_typedModuleDefs.TryRemove(type, out var module))
return false; return false;


return RemoveModuleInternal(module); return RemoveModuleInternal(module);
@@ -196,20 +198,18 @@ namespace Discord.Commands
} }
public void AddTypeReader(Type type, TypeReader reader) public void AddTypeReader(Type type, TypeReader reader)
{ {
var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary<Type, TypeReader>());
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader; readers[reader.GetType()] = reader;
} }
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
{ {
ConcurrentDictionary<Type, TypeReader> definedTypeReaders;
if (_typeReaders.TryGetValue(type, out definedTypeReaders))
if (_typeReaders.TryGetValue(type, out var definedTypeReaders))
return definedTypeReaders; return definedTypeReaders;
return null; return null;
} }
internal TypeReader GetDefaultTypeReader(Type type) internal TypeReader GetDefaultTypeReader(Type type)
{ {
TypeReader reader;
if (_defaultTypeReaders.TryGetValue(type, out reader))
if (_defaultTypeReaders.TryGetValue(type, out var reader))
return reader; return reader;
var typeInfo = type.GetTypeInfo(); var typeInfo = type.GetTypeInfo();


@@ -235,13 +235,13 @@ namespace Discord.Commands
} }


//Execution //Execution
public SearchResult Search(ICommandContext context, int argPos)
public SearchResult Search(ICommandContext context, int argPos)
=> Search(context, context.Message.Content.Substring(argPos)); => Search(context, context.Message.Content.Substring(argPos));
public SearchResult Search(ICommandContext context, string input) public SearchResult Search(ICommandContext context, string input)
{ {
string searchInput = _caseSensitive ? input : input.ToLowerInvariant(); string searchInput = _caseSensitive ? input : input.ToLowerInvariant();
var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray();
if (matches.Length > 0) if (matches.Length > 0)
return SearchResult.FromSuccess(input, matches); return SearchResult.FromSuccess(input, matches);
else else
@@ -259,46 +259,86 @@ namespace Discord.Commands
return searchResult; return searchResult;


var commands = searchResult.Commands; var commands = searchResult.Commands;
for (int i = 0; i < commands.Count; i++)
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();

foreach (var match in commands)
{ {
var preconditionResult = await commands[i].CheckPreconditionsAsync(context, services).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
{
if (commands.Count == 1)
return preconditionResult;
else
continue;
}
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
}

var successfulPreconditions = preconditionResults
.Where(x => x.Value.IsSuccess)
.ToArray();

if (successfulPreconditions.Length == 0)
{
//All preconditions failed, return the one from the highest priority command
var bestCandidate = preconditionResults
.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);
return bestCandidate.Value;
}

//If we get this far, at least one precondition was successful.

var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions)
{
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);


var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false);
if (!parseResult.IsSuccess)
if (parseResult.Error == CommandError.MultipleMatches)
{ {
if (parseResult.Error == CommandError.MultipleMatches)
IReadOnlyList<TypeReaderValue> argList, paramList;
switch (multiMatchHandling)
{ {
IReadOnlyList<TypeReaderValue> argList, paramList;
switch (multiMatchHandling)
{
case MultiMatchHandling.Best:
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
case MultiMatchHandling.Best:
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
} }
}


if (!parseResult.IsSuccess)
{
if (commands.Count == 1)
return parseResult;
else
continue;
}
parseResultsDict[pair.Key] = parseResult;
}

// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;
if (match.Command.Parameters.Count > 0)
{
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;

argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
} }


return await commands[i].ExecuteAsync(context, parseResult, services).ConfigureAwait(false);
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + totalArgsScore * 0.99f;
}

//Order the parse results by their score so that we choose the most likely result to execute
var parseResults = parseResultsDict
.OrderByDescending(x => CalculateScore(x.Key, x.Value));

var successfulParses = parseResults
.Where(x => x.Value.IsSuccess)
.ToArray();

if (successfulParses.Length == 0)
{
//All parses failed, return the one from the highest priority command, using score as a tie breaker
var bestMatch = parseResults
.FirstOrDefault(x => !x.Value.IsSuccess);
return bestMatch.Value;
} }


return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload.");
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
} }
} }
} }

+ 2
- 2
src/Discord.Net.Commands/IModuleBase.cs View File

@@ -4,8 +4,8 @@
{ {
void SetContext(ICommandContext context); void SetContext(ICommandContext context);


void BeforeExecute();
void BeforeExecute(CommandInfo command);
void AfterExecute();
void AfterExecute(CommandInfo command);
} }
} }

+ 77
- 35
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -18,7 +18,7 @@ namespace Discord.Commands
private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));
private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>();


private readonly Func<ICommandContext, object[], IServiceProvider, Task> _action;
private readonly Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> _action;


public ModuleInfo Module { get; } public ModuleInfo Module { get; }
public string Name { get; } public string Name { get; }
@@ -31,18 +31,19 @@ namespace Discord.Commands
public IReadOnlyList<string> Aliases { get; } public IReadOnlyList<string> Aliases { get; }
public IReadOnlyList<ParameterInfo> Parameters { get; } public IReadOnlyList<ParameterInfo> Parameters { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; } public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }


internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service)
{ {
Module = module; Module = module;
Name = builder.Name; Name = builder.Name;
Summary = builder.Summary; Summary = builder.Summary;
Remarks = builder.Remarks; Remarks = builder.Remarks;


RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode);
Priority = builder.Priority; Priority = builder.Priority;
Aliases = module.Aliases Aliases = module.Aliases
.Permutate(builder.Aliases, (first, second) => .Permutate(builder.Aliases, (first, second) =>
{ {
@@ -57,6 +58,7 @@ namespace Discord.Commands
.ToImmutableArray(); .ToImmutableArray();


Preconditions = builder.Preconditions.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray();
Attributes = builder.Attributes.ToImmutableArray();


Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false;
@@ -68,58 +70,80 @@ namespace Discord.Commands
{ {
services = services ?? EmptyServiceProvider.Instance; services = services ?? EmptyServiceProvider.Instance;


foreach (PreconditionAttribute precondition in Module.Preconditions)
async Task<PreconditionResult> CheckGroups(IEnumerable<PreconditionAttribute> preconditions, string type)
{ {
var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}
foreach (IGrouping<string, PreconditionAttribute> preconditionGroup in preconditions.GroupBy(p => p.Group, StringComparer.Ordinal))
{
if (preconditionGroup.Key == null)
{
foreach (PreconditionAttribute precondition in preconditionGroup)
{
var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}
}
else
{
var results = new List<PreconditionResult>();
foreach (PreconditionAttribute precondition in preconditionGroup)
results.Add(await precondition.CheckPermissions(context, this, services).ConfigureAwait(false));


foreach (PreconditionAttribute precondition in Preconditions)
{
var result = await precondition.CheckPermissions(context, this, services).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
if (!results.Any(p => p.IsSuccess))
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
}
}
return PreconditionGroupResult.FromSuccess();
} }


var moduleResult = await CheckGroups(Module.Preconditions, "Module");
if (!moduleResult.IsSuccess)
return moduleResult;

var commandResult = await CheckGroups(Preconditions, "Command");
if (!commandResult.IsSuccess)
return commandResult;

return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();
} }
public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null)
public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)
{ {
services = services ?? EmptyServiceProvider.Instance;

if (!searchResult.IsSuccess) if (!searchResult.IsSuccess)
return ParseResult.FromError(searchResult); return ParseResult.FromError(searchResult);
if (preconditionResult != null && !preconditionResult.Value.IsSuccess)
return ParseResult.FromError(preconditionResult.Value);
if (preconditionResult != null && !preconditionResult.IsSuccess)
return ParseResult.FromError(preconditionResult);
string input = searchResult.Text.Substring(startIndex); string input = searchResult.Text.Substring(startIndex);
return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false);
return await CommandParser.ParseArgs(this, context, services, input, 0).ConfigureAwait(false);
} }


public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
{ {
if (!parseResult.IsSuccess) if (!parseResult.IsSuccess)
return Task.FromResult(ExecuteResult.FromError(parseResult));
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult));


var argList = new object[parseResult.ArgValues.Count]; var argList = new object[parseResult.ArgValues.Count];
for (int i = 0; i < parseResult.ArgValues.Count; i++) for (int i = 0; i < parseResult.ArgValues.Count; i++)
{ {
if (!parseResult.ArgValues[i].IsSuccess) if (!parseResult.ArgValues[i].IsSuccess)
return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i]));
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i]));
argList[i] = parseResult.ArgValues[i].Values.First().Value; argList[i] = parseResult.ArgValues[i].Values.First().Value;
} }
var paramList = new object[parseResult.ParamValues.Count]; var paramList = new object[parseResult.ParamValues.Count];
for (int i = 0; i < parseResult.ParamValues.Count; i++) for (int i = 0; i < parseResult.ParamValues.Count; i++)
{ {
if (!parseResult.ParamValues[i].IsSuccess) if (!parseResult.ParamValues[i].IsSuccess)
return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i]));
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i]));
paramList[i] = parseResult.ParamValues[i].Values.First().Value; paramList[i] = parseResult.ParamValues[i].Values.First().Value;
} }


return ExecuteAsync(context, argList, paramList, services); return ExecuteAsync(context, argList, paramList, services);
} }
public async Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
public async Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
{ {
services = services ?? EmptyServiceProvider.Instance; services = services ?? EmptyServiceProvider.Instance;


@@ -130,7 +154,7 @@ namespace Discord.Commands
for (int position = 0; position < Parameters.Count; position++) for (int position = 0; position < Parameters.Count; position++)
{ {
var parameter = Parameters[position]; var parameter = Parameters[position];
var argument = args[position];
object argument = args[position];
var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false); var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false);
if (!result.IsSuccess) if (!result.IsSuccess)
return ExecuteResult.FromError(result); return ExecuteResult.FromError(result);
@@ -139,10 +163,9 @@ namespace Discord.Commands
switch (RunMode) switch (RunMode)
{ {
case RunMode.Sync: //Always sync case RunMode.Sync: //Always sync
await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false);
break;
return await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false);
case RunMode.Async: //Always async case RunMode.Async: //Always async
var t2 = Task.Run(async () =>
var t2 = Task.Run(async () =>
{ {
await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false); await ExecuteAsyncInternal(context, args, services).ConfigureAwait(false);
}); });
@@ -156,12 +179,26 @@ namespace Discord.Commands
} }
} }


private async Task ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services)
private async Task<IResult> ExecuteAsyncInternal(ICommandContext context, object[] args, IServiceProvider services)
{ {
await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false);
try try
{ {
await _action(context, args, services).ConfigureAwait(false);
var task = _action(context, args, services, this);
if (task is Task<IResult> resultTask)
{
var result = await resultTask.ConfigureAwait(false);
if (result is RuntimeResult execResult)
return execResult;
}
else if (task is Task<ExecuteResult> execTask)
{
return await execTask.ConfigureAwait(false);
}
else
await task.ConfigureAwait(false);

return ExecuteResult.FromSuccess();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -178,8 +215,13 @@ namespace Discord.Commands
else else
ExceptionDispatchInfo.Capture(ex).Throw(); ExceptionDispatchInfo.Capture(ex).Throw();
} }

return ExecuteResult.FromError(CommandError.Exception, ex.Message);
}
finally
{
await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false);
} }
await Module.Service._cmdLogger.VerboseAsync($"Executed {GetLogText(context)}").ConfigureAwait(false);
} }


private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList) private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList)
@@ -190,7 +232,7 @@ namespace Discord.Commands
argCount--; argCount--;


int i = 0; int i = 0;
foreach (var arg in argList)
foreach (object arg in argList)
{ {
if (i == argCount) if (i == argCount)
throw new InvalidOperationException("Command was invoked with too many parameters"); throw new InvalidOperationException("Command was invoked with too many parameters");
@@ -216,11 +258,11 @@ namespace Discord.Commands
=> paramsList.Cast<T>().ToArray(); => paramsList.Cast<T>().ToArray();


internal string GetLogText(ICommandContext context) internal string GetLogText(ICommandContext context)
{
{
if (context.Guild != null) if (context.Guild != null)
return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}";
else else
return $"\"{Name}\" for {context.User} in {context.Channel}"; return $"\"{Name}\" for {context.User} in {context.Channel}";
} }
} }
}
}

+ 17
- 0
src/Discord.Net.Commands/Info/ModuleInfo.cs View File

@@ -1,3 +1,4 @@
using System;
using System.Linq; using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -16,6 +17,7 @@ namespace Discord.Commands
public IReadOnlyList<string> Aliases { get; } public IReadOnlyList<string> Aliases { get; }
public IReadOnlyList<CommandInfo> Commands { get; } public IReadOnlyList<CommandInfo> Commands { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; } public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }
public IReadOnlyList<ModuleInfo> Submodules { get; } public IReadOnlyList<ModuleInfo> Submodules { get; }
public ModuleInfo Parent { get; } public ModuleInfo Parent { get; }
public bool IsSubmodule => Parent != null; public bool IsSubmodule => Parent != null;
@@ -32,6 +34,7 @@ namespace Discord.Commands
Aliases = BuildAliases(builder, service).ToImmutableArray(); Aliases = BuildAliases(builder, service).ToImmutableArray();
Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray(); Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray();
Preconditions = BuildPreconditions(builder).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray();
Attributes = BuildAttributes(builder).ToImmutableArray();


Submodules = BuildSubmodules(builder, service).ToImmutableArray(); Submodules = BuildSubmodules(builder, service).ToImmutableArray();
} }
@@ -86,5 +89,19 @@ namespace Discord.Commands


return result; return result;
} }

private static List<Attribute> BuildAttributes(ModuleBuilder builder)
{
var result = new List<Attribute>();

ModuleBuilder parent = builder;
while (parent != null)
{
result.AddRange(parent.Attributes);
parent = parent.Parent;
}

return result;
}
} }
} }

+ 5
- 2
src/Discord.Net.Commands/Info/ParameterInfo.cs View File

@@ -21,6 +21,7 @@ namespace Discord.Commands
public object DefaultValue { get; } public object DefaultValue { get; }


public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; } public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }


internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service)
{ {
@@ -36,6 +37,7 @@ namespace Discord.Commands
DefaultValue = builder.DefaultValue; DefaultValue = builder.DefaultValue;


Preconditions = builder.Preconditions.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray();
Attributes = builder.Attributes.ToImmutableArray();


_reader = builder.TypeReader; _reader = builder.TypeReader;
} }
@@ -54,9 +56,10 @@ namespace Discord.Commands
return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();
} }


public async Task<TypeReaderResult> Parse(ICommandContext context, string input)
public async Task<TypeReaderResult> Parse(ICommandContext context, string input, IServiceProvider services = null)
{ {
return await _reader.Read(context, input).ConfigureAwait(false);
services = services ?? EmptyServiceProvider.Instance;
return await _reader.Read(context, input, services).ConfigureAwait(false);
} }


public override string ToString() => Name; public override string ToString() => Name;


+ 5
- 7
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -15,11 +15,11 @@ namespace Discord.Commands
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);
} }


protected virtual void BeforeExecute()
protected virtual void BeforeExecute(CommandInfo command)
{ {
} }


protected virtual void AfterExecute()
protected virtual void AfterExecute(CommandInfo command)
{ {
} }


@@ -27,13 +27,11 @@ namespace Discord.Commands
void IModuleBase.SetContext(ICommandContext context) void IModuleBase.SetContext(ICommandContext context)
{ {
var newValue = context as T; var newValue = context as T;
if (newValue == null)
throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}");
Context = newValue;
Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}");
} }


void IModuleBase.BeforeExecute() => BeforeExecute();
void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command);


void IModuleBase.AfterExecute() => AfterExecute();
void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command);
} }
} }

+ 0
- 5
src/Discord.Net.Commands/PrimitiveParsers.cs View File

@@ -31,11 +31,6 @@ namespace Discord.Commands
parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse; parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse;
parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse; parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse;
parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse; parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse;
parserBuilder[typeof(string)] = (TryParseDelegate<string>)delegate (string str, out string value)
{
value = str;
return true;
};
return parserBuilder.ToImmutable(); return parserBuilder.ToImmutable();
} }




+ 1
- 1
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs View File

@@ -9,7 +9,7 @@ namespace Discord.Commands
internal class ChannelTypeReader<T> : TypeReader internal class ChannelTypeReader<T> : TypeReader
where T : class, IChannel where T : class, IChannel
{ {
public override async Task<TypeReaderResult> Read(ICommandContext context, string input)
public override async Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services)
{ {
if (context.Guild != null) if (context.Guild != null)
{ {


+ 2
- 3
src/Discord.Net.Commands/Readers/EnumTypeReader.cs View File

@@ -44,12 +44,11 @@ namespace Discord.Commands
_enumsByValue = byValueBuilder.ToImmutable(); _enumsByValue = byValueBuilder.ToImmutable();
} }


public override Task<TypeReaderResult> Read(ICommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services)
{ {
T baseValue;
object enumValue; object enumValue;


if (_tryParse(input, out baseValue))
if (_tryParse(input, out T baseValue))
{ {
if (_enumsByValue.TryGetValue(baseValue, out enumValue)) if (_enumsByValue.TryGetValue(baseValue, out enumValue))
return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); return Task.FromResult(TypeReaderResult.FromSuccess(enumValue));


+ 4
- 4
src/Discord.Net.Commands/Readers/MessageTypeReader.cs View File

@@ -1,4 +1,5 @@
using System.Globalization;
using System;
using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
@@ -6,15 +7,14 @@ namespace Discord.Commands
internal class MessageTypeReader<T> : TypeReader internal class MessageTypeReader<T> : TypeReader
where T : class, IMessage where T : class, IMessage
{ {
public override async Task<TypeReaderResult> Read(ICommandContext context, string input)
public override async Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services)
{ {
ulong id; ulong id;


//By Id (1.0) //By Id (1.0)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
{ {
var msg = await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T;
if (msg != null)
if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg)
return TypeReaderResult.FromSuccess(msg); return TypeReaderResult.FromSuccess(msg);
} }




+ 13
- 5
src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs View File

@@ -15,17 +15,25 @@ namespace Discord.Commands
internal class PrimitiveTypeReader<T> : TypeReader internal class PrimitiveTypeReader<T> : TypeReader
{ {
private readonly TryParseDelegate<T> _tryParse; private readonly TryParseDelegate<T> _tryParse;
private readonly float _score;


public PrimitiveTypeReader() public PrimitiveTypeReader()
: this(PrimitiveParsers.Get<T>(), 1)
{ }

public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score)
{ {
_tryParse = PrimitiveParsers.Get<T>();
if (score < 0 || score > 1)
throw new ArgumentOutOfRangeException(nameof(score), score, "Scores must be within the range [0, 1]");

_tryParse = tryParse;
_score = score;
} }


public override Task<TypeReaderResult> Read(ICommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services)
{ {
T value;
if (_tryParse(input, out value))
return Task.FromResult(TypeReaderResult.FromSuccess(value));
if (_tryParse(input, out T value))
return Task.FromResult(TypeReaderResult.FromSuccess(new TypeReaderValue(value, _score)));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}")); return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}"));
} }
} }


+ 1
- 1
src/Discord.Net.Commands/Readers/RoleTypeReader.cs View File

@@ -9,7 +9,7 @@ namespace Discord.Commands
internal class RoleTypeReader<T> : TypeReader internal class RoleTypeReader<T> : TypeReader
where T : class, IRole where T : class, IRole
{ {
public override Task<TypeReaderResult> Read(ICommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services)
{ {
ulong id; ulong id;




+ 3
- 2
src/Discord.Net.Commands/Readers/TypeReader.cs View File

@@ -1,9 +1,10 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
public abstract class TypeReader public abstract class TypeReader
{ {
public abstract Task<TypeReaderResult> Read(ICommandContext context, string input);
public abstract Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services);
} }
} }

+ 2
- 3
src/Discord.Net.Commands/Readers/UserTypeReader.cs View File

@@ -10,7 +10,7 @@ namespace Discord.Commands
internal class UserTypeReader<T> : TypeReader internal class UserTypeReader<T> : TypeReader
where T : class, IUser where T : class, IUser
{ {
public override async Task<TypeReaderResult> Read(ICommandContext context, string input)
public override async Task<TypeReaderResult> Read(ICommandContext context, string input, IServiceProvider services)
{ {
var results = new Dictionary<ulong, TypeReaderValue>(); var results = new Dictionary<ulong, TypeReaderValue>();
IReadOnlyCollection<IUser> channelUsers = await context.Channel.GetUsersAsync(CacheMode.CacheOnly).ToArray().ConfigureAwait(false); //TODO: must be a better way? IReadOnlyCollection<IUser> channelUsers = await context.Channel.GetUsersAsync(CacheMode.CacheOnly).ToArray().ConfigureAwait(false); //TODO: must be a better way?
@@ -43,8 +43,7 @@ namespace Discord.Commands
if (index >= 0) if (index >= 0)
{ {
string username = input.Substring(0, index); string username = input.Substring(0, index);
ushort discriminator;
if (ushort.TryParse(input.Substring(index + 1), out discriminator))
if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator))
{ {
var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)); string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));


+ 27
- 0
src/Discord.Net.Commands/Results/PreconditionGroupResult.cs View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.Diagnostics;

namespace Discord.Commands
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class PreconditionGroupResult : PreconditionResult
{
public IReadOnlyCollection<PreconditionResult> PreconditionResults { get; }

protected PreconditionGroupResult(CommandError? error, string errorReason, ICollection<PreconditionResult> preconditions)
: base(error, errorReason)
{
PreconditionResults = (preconditions ?? new List<PreconditionResult>(0)).ToReadOnlyCollection();
}

public static new PreconditionGroupResult FromSuccess()
=> new PreconditionGroupResult(null, null, null);
public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> preconditions)
=> new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions);
public static new PreconditionGroupResult FromError(IResult result) //needed?
=> new PreconditionGroupResult(result.Error, result.ErrorReason, null);

public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
}
}

+ 2
- 2
src/Discord.Net.Commands/Results/PreconditionResult.cs View File

@@ -3,14 +3,14 @@
namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct PreconditionResult : IResult
public class PreconditionResult : IResult
{ {
public CommandError? Error { get; } public CommandError? Error { get; }
public string ErrorReason { get; } public string ErrorReason { get; }


public bool IsSuccess => !Error.HasValue; public bool IsSuccess => !Error.HasValue;


private PreconditionResult(CommandError? error, string errorReason)
protected PreconditionResult(CommandError? error, string errorReason)
{ {
Error = error; Error = error;
ErrorReason = errorReason; ErrorReason = errorReason;


+ 27
- 0
src/Discord.Net.Commands/Results/RuntimeResult.cs View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace Discord.Commands
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public abstract class RuntimeResult : IResult
{
protected RuntimeResult(CommandError? error, string reason)
{
Error = error;
Reason = reason;
}

public CommandError? Error { get; }
public string Reason { get; }

public bool IsSuccess => !Error.HasValue;

string IResult.ErrorReason => Reason;

public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful");
private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}";
}
}

+ 1
- 1
src/Discord.Net.Commands/Utilities/ReflectionUtils.cs View File

@@ -58,7 +58,7 @@ namespace Discord.Commands
{ {
foreach (var prop in ownerType.DeclaredProperties) foreach (var prop in ownerType.DeclaredProperties)
{ {
if (prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute<DontInjectAttribute>() == null)
if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute<DontInjectAttribute>() == null)
result.Add(prop); result.Add(prop);
} }
ownerType = ownerType.BaseType.GetTypeInfo(); ownerType = ownerType.BaseType.GetTypeInfo();


+ 4
- 1
src/Discord.Net.Core/Audio/AudioStream.cs View File

@@ -11,7 +11,10 @@ namespace Discord.Audio
public override bool CanSeek => false; public override bool CanSeek => false;
public override bool CanWrite => false; public override bool CanWrite => false;


public virtual void WriteHeader(ushort seq, uint timestamp, bool missed) { }
public virtual void WriteHeader(ushort seq, uint timestamp, bool missed)
{
throw new InvalidOperationException("This stream does not accept headers");
}
public override void Write(byte[] buffer, int offset, int count) public override void Write(byte[] buffer, int offset, int count)
{ {
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult(); WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();


+ 2
- 2
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -28,8 +28,8 @@ namespace Discord.Audio
/// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary> /// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary>
AudioOutStream CreateDirectOpusStream(); AudioOutStream CreateDirectOpusStream();
/// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary> /// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary>
AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000);
AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30);
/// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary> /// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary>
AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null);
AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30);
} }
} }

+ 3
- 1
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -30,7 +30,9 @@ namespace Discord
/// <summary> Gets a collection of pinned messages in this channel. </summary> /// <summary> Gets a collection of pinned messages in this channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null); Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null);
/// <summary> Bulk deletes multiple messages. </summary> /// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null);
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null);
/// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null);


/// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. </summary> /// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. </summary>
Task TriggerTypingAsync(RequestOptions options = null); Task TriggerTypingAsync(RequestOptions options = null);


+ 21
- 5
src/Discord.Net.Core/Entities/Emotes/Emoji.cs View File

@@ -6,8 +6,16 @@
public class Emoji : IEmote public class Emoji : IEmote
{ {
// TODO: need to constrain this to unicode-only emojis somehow // TODO: need to constrain this to unicode-only emojis somehow

/// <summary>
/// The unicode representation of this emote.
/// </summary>
public string Name { get; }

public override string ToString() => Name;

/// <summary> /// <summary>
/// Creates a unciode emoji.
/// Creates a unicode emoji.
/// </summary> /// </summary>
/// <param name="unicode">The pure UTF-8 encoding of an emoji</param> /// <param name="unicode">The pure UTF-8 encoding of an emoji</param>
public Emoji(string unicode) public Emoji(string unicode)
@@ -15,9 +23,17 @@
Name = unicode; Name = unicode;
} }


/// <summary>
/// The unicode representation of this emote.
/// </summary>
public string Name { get; }
public override bool Equals(object other)
{
if (other == null) return false;
if (other == this) return true;

var otherEmoji = other as Emoji;
if (otherEmoji == null) return false;

return string.Equals(Name, otherEmoji.Name);
}

public override int GetHashCode() => Name.GetHashCode();
} }
} }

+ 20
- 1
src/Discord.Net.Core/Entities/Emotes/Emote.cs View File

@@ -25,6 +25,25 @@ namespace Discord
Name = name; Name = name;
} }


public override bool Equals(object other)
{
if (other == null) return false;
if (other == this) return true;

var otherEmote = other as Emote;
if (otherEmote == null) return false;

return string.Equals(Name, otherEmote.Name) && Id == otherEmote.Id;
}

public override int GetHashCode()
{
unchecked
{
return (Name.GetHashCode() * 397) ^ Id.GetHashCode();
}
}

/// <summary> /// <summary>
/// Parse an Emote from its raw format /// Parse an Emote from its raw format
/// </summary> /// </summary>
@@ -58,6 +77,6 @@ namespace Discord
} }


private string DebuggerDisplay => $"{Name} ({Id})"; private string DebuggerDisplay => $"{Name} ({Id})";
public override string ToString() => Name;
public override string ToString() => $"<:{Name}:{Id}>";
} }
} }

+ 1
- 1
src/Discord.Net.Core/Entities/Emotes/GuildEmote.cs View File

@@ -20,7 +20,7 @@ namespace Discord
RoleIds = roleIds; RoleIds = roleIds;
} }


public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})"; private string DebuggerDisplay => $"{Name} ({Id})";
public override string ToString() => $"<:{Name}:{Id}>";
} }
} }

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

@@ -66,10 +66,10 @@ namespace Discord
Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null); Task<IReadOnlyCollection<IBan>> GetBansAsync(RequestOptions options = null);
/// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary> /// <summary> Bans the provided user from this guild and optionally prunes their recent messages. </summary>
/// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param>
Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null);
Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null);
/// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary> /// <summary> Bans the provided user id from this guild and optionally prunes their recent messages. </summary>
/// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param> /// <param name="pruneDays">The number of days to remove messages from this user for - must be between [0, 7]</param>
Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null);
Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null);
/// <summary> Unbans the provided user if it is currently banned. </summary> /// <summary> Unbans the provided user if it is currently banned. </summary>
Task RemoveBanAsync(IUser user, RequestOptions options = null); Task RemoveBanAsync(IUser user, RequestOptions options = null);
/// <summary> Unbans the provided user id if it is currently banned. </summary> /// <summary> Unbans the provided user id if it is currently banned. </summary>


+ 3
- 1
src/Discord.Net.Core/Entities/Guilds/VerificationLevel.cs View File

@@ -9,6 +9,8 @@
/// <summary> Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. </summary> /// <summary> Users must fulfill the requirements of Low, and be registered on Discord for at least 5 minutes. </summary>
Medium = 2, Medium = 2,
/// <summary> Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. </summary> /// <summary> Users must fulfill the requirements of Medium, and be a member of this guild for at least 10 minutes. </summary>
High = 3
High = 3,
/// <summary> Users must fulfill the requirements of High, and must have a verified phone on their Discord account. </summary>
Extreme = 4
} }
} }

+ 6
- 3
src/Discord.Net.Core/Entities/Messages/Embed.cs View File

@@ -1,13 +1,14 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;


namespace Discord namespace Discord
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] [DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public class Embed : IEmbed public class Embed : IEmbed
{ {
public string Type { get; }
public EmbedType Type { get; }


public string Description { get; internal set; } public string Description { get; internal set; }
public string Url { get; internal set; } public string Url { get; internal set; }
@@ -22,12 +23,12 @@ namespace Discord
public EmbedThumbnail? Thumbnail { get; internal set; } public EmbedThumbnail? Thumbnail { get; internal set; }
public ImmutableArray<EmbedField> Fields { get; internal set; } public ImmutableArray<EmbedField> Fields { get; internal set; }


internal Embed(string type)
internal Embed(EmbedType type)
{ {
Type = type; Type = type;
Fields = ImmutableArray.Create<EmbedField>(); Fields = ImmutableArray.Create<EmbedField>();
} }
internal Embed(string type,
internal Embed(EmbedType type,
string title, string title,
string description, string description,
string url, string url,
@@ -56,6 +57,8 @@ namespace Discord
Fields = fields; Fields = fields;
} }


public int Length => Title?.Length + Author?.Name?.Length + Description?.Length + Footer?.Text?.Length + Fields.Sum(f => f.Name.Length + f.Value.ToString().Length) ?? 0;

public override string ToString() => Title; public override string ToString() => Title;
private string DebuggerDisplay => $"{Title} ({Type})"; private string DebuggerDisplay => $"{Title} ({Type})";
} }


+ 2
- 1
src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;


namespace Discord namespace Discord
{ {


+ 2
- 1
src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;


namespace Discord namespace Discord
{ {


+ 3
- 2
src/Discord.Net.Core/Entities/Messages/EmbedImage.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;


namespace Discord namespace Discord
{ {
@@ -19,6 +20,6 @@ namespace Discord
} }


private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})";
public override string ToString() => Url;
public override string ToString() => Url.ToString();
} }
} }

+ 2
- 1
src/Discord.Net.Core/Entities/Messages/EmbedProvider.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;


namespace Discord namespace Discord
{ {


+ 3
- 2
src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;


namespace Discord namespace Discord
{ {
@@ -19,6 +20,6 @@ namespace Discord
} }


private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})";
public override string ToString() => Url;
public override string ToString() => Url.ToString();
} }
} }

+ 13
- 0
src/Discord.Net.Core/Entities/Messages/EmbedType.cs View File

@@ -0,0 +1,13 @@
namespace Discord
{
public enum EmbedType
{
Rich,
Link,
Video,
Image,
Gifv,
Article,
Tweet
}
}

+ 3
- 2
src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs View File

@@ -1,4 +1,5 @@
using System.Diagnostics;
using System;
using System.Diagnostics;


namespace Discord namespace Discord
{ {
@@ -17,6 +18,6 @@ namespace Discord
} }


private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})";
public override string ToString() => Url;
public override string ToString() => Url.ToString();
} }
} }

+ 1
- 1
src/Discord.Net.Core/Entities/Messages/IEmbed.cs View File

@@ -6,9 +6,9 @@ namespace Discord
public interface IEmbed public interface IEmbed
{ {
string Url { get; } string Url { get; }
string Type { get; }
string Title { get; } string Title { get; }
string Description { get; } string Description { get; }
EmbedType Type { get; }
DateTimeOffset? Timestamp { get; } DateTimeOffset? Timestamp { get; }
Color? Color { get; } Color? Color { get; }
EmbedImage? Image { get; } EmbedImage? Image { get; }


+ 40
- 0
src/Discord.Net.Core/Entities/Roles/Color.cs View File

@@ -8,6 +8,46 @@ namespace Discord
{ {
/// <summary> Gets the default user color value. </summary> /// <summary> Gets the default user color value. </summary>
public static readonly Color Default = new Color(0); public static readonly Color Default = new Color(0);
/// <summary> Gets the teal color value </summary>
public static readonly Color Teal = new Color(0x1ABC9C);
/// <summary> Gets the dark teal color value </summary>
public static readonly Color DarkTeal = new Color(0x11806A);
/// <summary> Gets the green color value </summary>
public static readonly Color Green = new Color(0x2ECC71);
/// <summary> Gets the dark green color value </summary>
public static readonly Color DarkGreen = new Color(0x1F8B4C);
/// <summary> Gets the blue color value </summary>
public static readonly Color Blue = new Color(0x3498DB);
/// <summary> Gets the dark blue color value </summary>
public static readonly Color DarkBlue = new Color(0x206694);
/// <summary> Gets the purple color value </summary>
public static readonly Color Purple = new Color(0x9B59B6);
/// <summary> Gets the dark purple color value </summary>
public static readonly Color DarkPurple = new Color(0x71368A);
/// <summary> Gets the magenta color value </summary>
public static readonly Color Magenta = new Color(0xE91E63);
/// <summary> Gets the dark magenta color value </summary>
public static readonly Color DarkMagenta = new Color(0xAD1457);
/// <summary> Gets the gold color value </summary>
public static readonly Color Gold = new Color(0xF1C40F);
/// <summary> Gets the light orange color value </summary>
public static readonly Color LightOrange = new Color(0xC27C0E);
/// <summary> Gets the orange color value </summary>
public static readonly Color Orange = new Color(0xE67E22);
/// <summary> Gets the dark orange color value </summary>
public static readonly Color DarkOrange = new Color(0xA84300);
/// <summary> Gets the red color value </summary>
public static readonly Color Red = new Color(0xE74C3C);
/// <summary> Gets the dark red color value </summary>
public static readonly Color DarkRed = new Color(0x992D22);
/// <summary> Gets the light grey color value </summary>
public static readonly Color LightGrey = new Color(0x979C9F);
/// <summary> Gets the lighter grey color value </summary>
public static readonly Color LighterGrey = new Color(0x95A5A6);
/// <summary> Gets the dark grey color value </summary>
public static readonly Color DarkGrey = new Color(0x607D8B);
/// <summary> Gets the darker grey color value </summary>
public static readonly Color DarkerGrey = new Color(0x546E7A);


/// <summary> Gets the encoded value for this color. </summary> /// <summary> Gets the encoded value for this color. </summary>
public uint RawValue { get; } public uint RawValue { get; }


+ 1
- 1
src/Discord.Net.Core/Entities/Users/IGuildUser.cs View File

@@ -25,7 +25,7 @@ namespace Discord
ChannelPermissions GetPermissions(IGuildChannel channel); ChannelPermissions GetPermissions(IGuildChannel channel);


/// <summary> Kicks this user from this guild. </summary> /// <summary> Kicks this user from this guild. </summary>
Task KickAsync(RequestOptions options = null);
Task KickAsync(string reason = null, RequestOptions options = null);
/// <summary> Modifies this user's properties in this guild. </summary> /// <summary> Modifies this user's properties in this guild. </summary>
Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null); Task ModifyAsync(Action<GuildUserProperties> func, RequestOptions options = null);




+ 1
- 3
src/Discord.Net.Core/Entities/Users/IUser.cs View File

@@ -20,8 +20,6 @@ namespace Discord
string Username { get; } string Username { get; }


/// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary> /// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary>
Task<IDMChannel> GetDMChannelAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
/// <summary> Returns a private message channel to this user, creating one if it does not already exist. </summary>
Task<IDMChannel> CreateDMChannelAsync(RequestOptions options = null);
Task<IDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null);
} }
} }

+ 10
- 0
src/Discord.Net.Core/Extensions/StringExtensions.cs View File

@@ -0,0 +1,10 @@
using System;

namespace Discord
{
internal static class StringExtensions
{
public static bool IsNullOrUri(this string url) =>
string.IsNullOrEmpty(url) || Uri.IsWellFormedUriString(url, UriKind.Absolute);
}
}

+ 16
- 0
src/Discord.Net.Core/Extensions/UserExtensions.cs View File

@@ -0,0 +1,16 @@
using System.Threading.Tasks;

namespace Discord
{
public static class UserExtensions
{
public static async Task<IUserMessage> SendMessageAsync(this IUser user,
string text,
bool isTTS = false,
Embed embed = null,
RequestOptions options = null)
{
return await (await user.GetOrCreateDMChannelAsync().ConfigureAwait(false)).SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false);
}
}
}

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

@@ -9,8 +9,8 @@ namespace Discord.Net.Rest
void SetHeader(string key, string value); void SetHeader(string key, string value);
void SetCancelToken(CancellationToken cancelToken); void SetCancelToken(CancellationToken cancelToken);


Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false);
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false);
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false);
Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly = false, string reason = null);
} }
} }

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

@@ -14,6 +14,10 @@ namespace Discord
public CancellationToken CancelToken { get; set; } = CancellationToken.None; public CancellationToken CancelToken { get; set; } = CancellationToken.None;
public RetryMode? RetryMode { get; set; } public RetryMode? RetryMode { get; set; }
public bool HeaderOnly { get; internal set; } public bool HeaderOnly { get; internal set; }
/// <summary>
/// The reason for this action in the guild's audit log
/// </summary>
public string AuditLogReason { get; set; }


internal bool IgnoreState { get; set; } internal bool IgnoreState { get; set; }
internal string BucketId { get; set; } internal string BucketId { get; set; }


+ 3
- 2
src/Discord.Net.Rest/API/Common/Embed.cs View File

@@ -1,6 +1,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;


namespace Discord.API namespace Discord.API
{ {
@@ -8,14 +9,14 @@ namespace Discord.API
{ {
[JsonProperty("title")] [JsonProperty("title")]
public string Title { get; set; } public string Title { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("description")] [JsonProperty("description")]
public string Description { get; set; } public string Description { get; set; }
[JsonProperty("url")] [JsonProperty("url")]
public string Url { get; set; } public string Url { get; set; }
[JsonProperty("color")] [JsonProperty("color")]
public uint? Color { get; set; } public uint? Color { get; set; }
[JsonProperty("type"), JsonConverter(typeof(StringEnumConverter))]
public EmbedType Type { get; set; }
[JsonProperty("timestamp")] [JsonProperty("timestamp")]
public DateTimeOffset? Timestamp { get; set; } public DateTimeOffset? Timestamp { get; set; }
[JsonProperty("author")] [JsonProperty("author")]


+ 2
- 1
src/Discord.Net.Rest/API/Common/EmbedAuthor.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;


namespace Discord.API namespace Discord.API
{ {


+ 2
- 1
src/Discord.Net.Rest/API/Common/EmbedFooter.cs View File

@@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;


namespace Discord.API namespace Discord.API
{ {


+ 1
- 0
src/Discord.Net.Rest/API/Common/EmbedImage.cs View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API


+ 1
- 0
src/Discord.Net.Rest/API/Common/EmbedProvider.cs View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API


+ 1
- 0
src/Discord.Net.Rest/API/Common/EmbedThumbnail.cs View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API


+ 1
- 0
src/Discord.Net.Rest/API/Common/EmbedVideo.cs View File

@@ -1,4 +1,5 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System;
using Newtonsoft.Json; using Newtonsoft.Json;


namespace Discord.API namespace Discord.API


+ 1
- 0
src/Discord.Net.Rest/API/Rest/CreateGuildBanParams.cs View File

@@ -4,5 +4,6 @@ namespace Discord.API.Rest
internal class CreateGuildBanParams internal class CreateGuildBanParams
{ {
public Optional<int> DeleteMessageDays { get; set; } public Optional<int> DeleteMessageDays { get; set; }
public string Reason { get; set; }
} }
} }

+ 10
- 8
src/Discord.Net.Rest/DiscordRestApiClient.cs View File

@@ -30,7 +30,7 @@ namespace Discord.API


protected readonly JsonSerializer _serializer; protected readonly JsonSerializer _serializer;
protected readonly SemaphoreSlim _stateLock; protected readonly SemaphoreSlim _stateLock;
private readonly RestClientProvider RestClientProvider;
private readonly RestClientProvider _restClientProvider;


protected bool _isDisposed; protected bool _isDisposed;
private CancellationTokenSource _loginCancelToken; private CancellationTokenSource _loginCancelToken;
@@ -48,7 +48,7 @@ namespace Discord.API
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry, public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
JsonSerializer serializer = null) JsonSerializer serializer = null)
{ {
RestClientProvider = restClientProvider;
_restClientProvider = restClientProvider;
UserAgent = userAgent; UserAgent = userAgent;
DefaultRetryMode = defaultRetryMode; DefaultRetryMode = defaultRetryMode;
_serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() };
@@ -60,7 +60,7 @@ namespace Discord.API
} }
internal void SetBaseUrl(string baseUrl) internal void SetBaseUrl(string baseUrl)
{ {
RestClient = RestClientProvider(baseUrl);
RestClient = _restClientProvider(baseUrl);
RestClient.SetHeader("accept", "*/*"); RestClient.SetHeader("accept", "*/*");
RestClient.SetHeader("user-agent", UserAgent); RestClient.SetHeader("user-agent", UserAgent);
RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken)); RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
@@ -189,7 +189,7 @@ namespace Discord.API
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
options.IsClientBucket = AuthTokenType == TokenType.User; options.IsClientBucket = AuthTokenType == TokenType.User;


var json = payload != null ? SerializeJson(payload) : null;
string json = payload != null ? SerializeJson(payload) : null;
var request = new JsonRestRequest(RestClient, method, endpoint, json, options); var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false);
} }
@@ -233,7 +233,7 @@ namespace Discord.API
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
options.IsClientBucket = AuthTokenType == TokenType.User; options.IsClientBucket = AuthTokenType == TokenType.User;


var json = payload != null ? SerializeJson(payload) : null;
string json = payload != null ? SerializeJson(payload) : null;
var request = new JsonRestRequest(RestClient, method, endpoint, json, options); var request = new JsonRestRequest(RestClient, method, endpoint, json, options);
return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
} }
@@ -803,7 +803,8 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);


var ids = new BucketIds(guildId: guildId); var ids = new BucketIds(guildId: guildId);
await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}", ids, options: options).ConfigureAwait(false);
string reason = string.IsNullOrWhiteSpace(args.Reason) ? "" : $"&reason={args.Reason}";
await SendAsync("PUT", () => $"guilds/{guildId}/bans/{userId}?delete-message-days={args.DeleteMessageDays}{reason}", ids, options: options).ConfigureAwait(false);
} }
public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null) public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, RequestOptions options = null)
{ {
@@ -980,14 +981,15 @@ namespace Discord.API
Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}"; Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}";
return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false); return await SendAsync<IReadOnlyCollection<GuildMember>>("GET", endpoint, ids, options: options).ConfigureAwait(false);
} }
public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, RequestOptions options = null)
public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason, RequestOptions options = null)
{ {
Preconditions.NotEqual(guildId, 0, nameof(guildId)); Preconditions.NotEqual(guildId, 0, nameof(guildId));
Preconditions.NotEqual(userId, 0, nameof(userId)); Preconditions.NotEqual(userId, 0, nameof(userId));
options = RequestOptions.CreateOrClone(options); options = RequestOptions.CreateOrClone(options);


var ids = new BucketIds(guildId: guildId); var ids = new BucketIds(guildId: guildId);
await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}", ids, options: options).ConfigureAwait(false);
reason = string.IsNullOrWhiteSpace(reason) ? "" : $"?reason={reason}";
await SendAsync("DELETE", () => $"guilds/{guildId}/members/{userId}{reason}", ids, options: options).ConfigureAwait(false);
} }
public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null) public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Rest.ModifyGuildMemberParams args, RequestOptions options = null)
{ {


+ 4
- 4
src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs View File

@@ -178,12 +178,12 @@ namespace Discord.Rest
var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS };
var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false);
return RestUserMessage.Create(client, channel, client.CurrentUser, model); return RestUserMessage.Create(client, channel, client.CurrentUser, model);
}
}


public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client,
IEnumerable<IMessage> messages, RequestOptions options)
public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client,
IEnumerable<ulong> messageIds, RequestOptions options)
{ {
var msgs = messages.Select(x => x.Id).ToArray();
var msgs = messageIds.ToArray();
if (msgs.Length < 100) if (msgs.Length < 100)
{ {
var args = new DeleteMessagesParams(msgs); var args = new DeleteMessagesParams(msgs);


+ 3
- 1
src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs View File

@@ -73,7 +73,9 @@ namespace Discord.Rest
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


+ 3
- 1
src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs View File

@@ -86,7 +86,9 @@ namespace Discord.Rest
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


+ 3
- 1
src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs View File

@@ -64,7 +64,9 @@ namespace Discord.Rest
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


+ 3
- 1
src/Discord.Net.Rest/Entities/Channels/RpcVirtualMessageChannel.cs View File

@@ -43,7 +43,9 @@ namespace Discord.Rest
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


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

@@ -107,9 +107,9 @@ namespace Discord.Rest
} }


public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client, public static async Task AddBanAsync(IGuild guild, BaseDiscordClient client,
ulong userId, int pruneDays, RequestOptions options)
ulong userId, int pruneDays, string reason, RequestOptions options)
{ {
var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays };
var args = new CreateGuildBanParams { DeleteMessageDays = pruneDays, Reason = reason };
await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false); await client.ApiClient.CreateGuildBanAsync(guild.Id, userId, args, options).ConfigureAwait(false);
} }
public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client, public static async Task RemoveBanAsync(IGuild guild, BaseDiscordClient client,


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

@@ -137,10 +137,10 @@ namespace Discord.Rest
public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null) public Task<IReadOnlyCollection<RestBan>> GetBansAsync(RequestOptions options = null)
=> GuildHelper.GetBansAsync(this, Discord, options); => GuildHelper.GetBansAsync(this, Discord, options);


public Task AddBanAsync(IUser user, int pruneDays = 0, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, options);
public Task AddBanAsync(ulong userId, int pruneDays = 0, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, options);
public Task AddBanAsync(IUser user, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, user.Id, pruneDays, reason, options);
public Task AddBanAsync(ulong userId, int pruneDays = 0, string reason = null, RequestOptions options = null)
=> GuildHelper.AddBanAsync(this, Discord, userId, pruneDays, reason, options);


public Task RemoveBanAsync(IUser user, RequestOptions options = null) public Task RemoveBanAsync(IUser user, RequestOptions options = null)
=> GuildHelper.RemoveBanAsync(this, Discord, user.Id, options); => GuildHelper.RemoveBanAsync(this, Discord, user.Id, options);


+ 180
- 21
src/Discord.Net.Rest/Entities/Messages/EmbedBuilder.cs View File

@@ -8,19 +8,66 @@ namespace Discord
{ {
private readonly Embed _embed; private readonly Embed _embed;


public const int MaxFieldCount = 25;
public const int MaxTitleLength = 256;
public const int MaxDescriptionLength = 2048;
public const int MaxEmbedLength = 6000; // user bot limit is 2000, but we don't validate that here.

public EmbedBuilder() public EmbedBuilder()
{ {
_embed = new Embed("rich");
_embed = new Embed(EmbedType.Rich);
Fields = new List<EmbedFieldBuilder>(); Fields = new List<EmbedFieldBuilder>();
} }


public string Title { get { return _embed.Title; } set { _embed.Title = value; } }
public string Description { get { return _embed.Description; } set { _embed.Description = value; } }
public string Url { get { return _embed.Url; } set { _embed.Url = value; } }
public string ThumbnailUrl { get { return _embed.Thumbnail?.Url; } set { _embed.Thumbnail = new EmbedThumbnail(value, null, null, null); } }
public string ImageUrl { get { return _embed.Image?.Url; } set { _embed.Image = new EmbedImage(value, null, null, null); } }
public DateTimeOffset? Timestamp { get { return _embed.Timestamp; } set { _embed.Timestamp = value; } }
public Color? Color { get { return _embed.Color; } set { _embed.Color = value; } }
public string Title
{
get => _embed.Title;
set
{
if (value?.Length > MaxTitleLength) throw new ArgumentException($"Title length must be less than or equal to {MaxTitleLength}.", nameof(Title));
_embed.Title = value;
}
}

public string Description
{
get => _embed.Description;
set
{
if (value?.Length > MaxDescriptionLength) throw new ArgumentException($"Description length must be less than or equal to {MaxDescriptionLength}.", nameof(Description));
_embed.Description = value;
}
}

public string Url
{
get => _embed.Url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url));
_embed.Url = value;
}
}
public string ThumbnailUrl
{
get => _embed.Thumbnail?.Url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ThumbnailUrl));
_embed.Thumbnail = new EmbedThumbnail(value, null, null, null);
}
}
public string ImageUrl
{
get => _embed.Image?.Url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(ImageUrl));
_embed.Image = new EmbedImage(value, null, null, null);
}
}
public DateTimeOffset? Timestamp { get => _embed.Timestamp; set { _embed.Timestamp = value; } }
public Color? Color { get => _embed.Color; set { _embed.Color = value; } }


public EmbedAuthorBuilder Author { get; set; } public EmbedAuthorBuilder Author { get; set; }
public EmbedFooterBuilder Footer { get; set; } public EmbedFooterBuilder Footer { get; set; }
@@ -30,8 +77,10 @@ namespace Discord
get => _fields; get => _fields;
set set
{ {
if (value != null) _fields = value;
else throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(value));

if (value == null) throw new ArgumentNullException("Cannot set an embed builder's fields collection to null", nameof(Fields));
if (value.Count > MaxFieldCount) throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(Fields));
_fields = value;
} }
} }


@@ -88,6 +137,17 @@ namespace Discord
Author = author; Author = author;
return this; return this;
} }
public EmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null)
{
var author = new EmbedAuthorBuilder
{
Name = name,
IconUrl = iconUrl,
Url = url
};
Author = author;
return this;
}
public EmbedBuilder WithFooter(EmbedFooterBuilder footer) public EmbedBuilder WithFooter(EmbedFooterBuilder footer)
{ {
Footer = footer; Footer = footer;
@@ -100,6 +160,16 @@ namespace Discord
Footer = footer; Footer = footer;
return this; return this;
} }
public EmbedBuilder WithFooter(string text, string iconUrl = null)
{
var footer = new EmbedFooterBuilder
{
Text = text,
IconUrl = iconUrl
};
Footer = footer;
return this;
}


public EmbedBuilder AddField(string name, object value) public EmbedBuilder AddField(string name, object value)
{ {
@@ -107,7 +177,7 @@ namespace Discord
.WithIsInline(false) .WithIsInline(false)
.WithName(name) .WithName(name)
.WithValue(value); .WithValue(value);
Fields.Add(field);
AddField(field);
return this; return this;
} }
public EmbedBuilder AddInlineField(string name, object value) public EmbedBuilder AddInlineField(string name, object value)
@@ -116,11 +186,16 @@ namespace Discord
.WithIsInline(true) .WithIsInline(true)
.WithName(name) .WithName(name)
.WithValue(value); .WithValue(value);
Fields.Add(field);
AddField(field);
return this; return this;
} }
public EmbedBuilder AddField(EmbedFieldBuilder field) public EmbedBuilder AddField(EmbedFieldBuilder field)
{ {
if (Fields.Count >= MaxFieldCount)
{
throw new ArgumentException($"Field count must be less than or equal to {MaxFieldCount}.", nameof(field));
}

Fields.Add(field); Fields.Add(field);
return this; return this;
} }
@@ -128,7 +203,18 @@ namespace Discord
{ {
var field = new EmbedFieldBuilder(); var field = new EmbedFieldBuilder();
action(field); action(field);
Fields.Add(field);
this.AddField(field);
return this;
}
public EmbedBuilder AddField(string title, string text, bool inline = false)
{
var field = new EmbedFieldBuilder
{
Name = title,
Value = text,
IsInline = inline
};
_fields.Add(field);
return this; return this;
} }


@@ -140,6 +226,12 @@ namespace Discord
for (int i = 0; i < Fields.Count; i++) for (int i = 0; i < Fields.Count; i++)
fields.Add(Fields[i].Build()); fields.Add(Fields[i].Build());
_embed.Fields = fields.ToImmutable(); _embed.Fields = fields.ToImmutable();

if (_embed.Length > MaxEmbedLength)
{
throw new InvalidOperationException($"Total embed length must be less than or equal to {MaxEmbedLength}");
}

return _embed; return _embed;
} }
public static implicit operator Embed(EmbedBuilder builder) => builder?.Build(); public static implicit operator Embed(EmbedBuilder builder) => builder?.Build();
@@ -149,9 +241,32 @@ namespace Discord
{ {
private EmbedField _field; private EmbedField _field;


public string Name { get { return _field.Name; } set { _field.Name = value; } }
public object Value { get { return _field.Value; } set { _field.Value = value.ToString(); } }
public bool IsInline { get { return _field.Inline; } set { _field.Inline = value; } }
public const int MaxFieldNameLength = 256;
public const int MaxFieldValueLength = 1024;

public string Name
{
get => _field.Name;
set
{
if (string.IsNullOrEmpty(value)) throw new ArgumentException($"Field name must not be null or empty.", nameof(Name));
if (value.Length > MaxFieldNameLength) throw new ArgumentException($"Field name length must be less than or equal to {MaxFieldNameLength}.", nameof(Name));
_field.Name = value;
}
}

public object Value
{
get => _field.Value;
set
{
var stringValue = value?.ToString();
if (string.IsNullOrEmpty(stringValue)) throw new ArgumentException($"Field value must not be null or empty.", nameof(Value));
if (stringValue.Length > MaxFieldValueLength) throw new ArgumentException($"Field value length must be less than or equal to {MaxFieldValueLength}.", nameof(Value));
_field.Value = stringValue;
}
}
public bool IsInline { get => _field.Inline; set { _field.Inline = value; } }


public EmbedFieldBuilder() public EmbedFieldBuilder()
{ {
@@ -182,9 +297,35 @@ namespace Discord
{ {
private EmbedAuthor _author; private EmbedAuthor _author;


public string Name { get { return _author.Name; } set { _author.Name = value; } }
public string Url { get { return _author.Url; } set { _author.Url = value; } }
public string IconUrl { get { return _author.IconUrl; } set { _author.IconUrl = value; } }
public const int MaxAuthorNameLength = 256;

public string Name
{
get => _author.Name;
set
{
if (value?.Length > MaxAuthorNameLength) throw new ArgumentException($"Author name length must be less than or equal to {MaxAuthorNameLength}.", nameof(Name));
_author.Name = value;
}
}
public string Url
{
get => _author.Url;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(Url));
_author.Url = value;
}
}
public string IconUrl
{
get => _author.IconUrl;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl));
_author.IconUrl = value;
}
}


public EmbedAuthorBuilder() public EmbedAuthorBuilder()
{ {
@@ -215,8 +356,26 @@ namespace Discord
{ {
private EmbedFooter _footer; private EmbedFooter _footer;


public string Text { get { return _footer.Text; } set { _footer.Text = value; } }
public string IconUrl { get { return _footer.IconUrl; } set { _footer.IconUrl = value; } }
public const int MaxFooterTextLength = 2048;

public string Text
{
get => _footer.Text;
set
{
if (value?.Length > MaxFooterTextLength) throw new ArgumentException($"Footer text length must be less than or equal to {MaxFooterTextLength}.", nameof(Text));
_footer.Text = value;
}
}
public string IconUrl
{
get => _footer.IconUrl;
set
{
if (!value.IsNullOrUri()) throw new ArgumentException("Url must be a well-formed URI", nameof(IconUrl));
_footer.IconUrl = value;
}
}


public EmbedFooterBuilder() public EmbedFooterBuilder()
{ {


+ 2
- 2
src/Discord.Net.Rest/Entities/Users/RestGuildUser.cs View File

@@ -85,8 +85,8 @@ namespace Discord.Rest
else if (args.RoleIds.IsSpecified) else if (args.RoleIds.IsSpecified)
UpdateRoles(args.RoleIds.Value.ToArray()); UpdateRoles(args.RoleIds.Value.ToArray());
} }
public Task KickAsync(RequestOptions options = null)
=> UserHelper.KickAsync(this, Discord, options);
public Task KickAsync(string reason = null, RequestOptions options = null)
=> UserHelper.KickAsync(this, Discord, reason, options);
/// <inheritdoc /> /// <inheritdoc />
public Task AddRoleAsync(IRole role, RequestOptions options = null) public Task AddRoleAsync(IRole role, RequestOptions options = null)
=> AddRolesAsync(new[] { role }, options); => AddRolesAsync(new[] { role }, options);


+ 3
- 5
src/Discord.Net.Rest/Entities/Users/RestUser.cs View File

@@ -54,7 +54,7 @@ namespace Discord.Rest
Update(model); Update(model);
} }


public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null)
public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
=> UserHelper.CreateDMChannelAsync(this, Discord, options); => UserHelper.CreateDMChannelAsync(this, Discord, options);


public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
@@ -64,9 +64,7 @@ namespace Discord.Rest
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";


//IUser //IUser
Task<IDMChannel> IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IDMChannel>(null);
async Task<IDMChannel> IUser.CreateDMChannelAsync(RequestOptions options)
=> await CreateDMChannelAsync(options).ConfigureAwait(false);
async Task<IDMChannel> IUser.GetOrCreateDMChannelAsync(RequestOptions options)
=> await GetOrCreateDMChannelAsync(options);
} }
} }

+ 1
- 1
src/Discord.Net.Rest/Entities/Users/RestWebhookUser.cs View File

@@ -45,7 +45,7 @@ namespace Discord.Rest
GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook; GuildPermissions IGuildUser.GuildPermissions => GuildPermissions.Webhook;


ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue); ChannelPermissions IGuildUser.GetPermissions(IGuildChannel channel) => Permissions.ToChannelPerms(channel, GuildPermissions.Webhook.RawValue);
Task IGuildUser.KickAsync(RequestOptions options)
Task IGuildUser.KickAsync(string reason, RequestOptions options)
{ {
throw new NotSupportedException("Webhook users cannot be kicked."); throw new NotSupportedException("Webhook users cannot be kicked.");
} }


+ 2
- 2
src/Discord.Net.Rest/Entities/Users/UserHelper.cs View File

@@ -53,9 +53,9 @@ namespace Discord.Rest
} }


public static async Task KickAsync(IGuildUser user, BaseDiscordClient client, public static async Task KickAsync(IGuildUser user, BaseDiscordClient client,
RequestOptions options)
string reason, RequestOptions options)
{ {
await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, options).ConfigureAwait(false);
await client.ApiClient.RemoveGuildMemberAsync(user.GuildId, user.Id, reason, options).ConfigureAwait(false);
} }


public static async Task<RestDMChannel> CreateDMChannelAsync(IUser user, BaseDiscordClient client, public static async Task<RestDMChannel> CreateDMChannelAsync(IUser user, BaseDiscordClient client,


+ 23
- 0
src/Discord.Net.Rest/Extensions/EmbedBuilderExtensions.cs View File

@@ -0,0 +1,23 @@
namespace Discord
{
public static class EmbedBuilderExtensions
{
public static EmbedBuilder WithColor(this EmbedBuilder builder, uint rawValue) =>
builder.WithColor(new Color(rawValue));

public static EmbedBuilder WithColor(this EmbedBuilder builder, byte r, byte g, byte b) =>
builder.WithColor(new Color(r, g, b));

public static EmbedBuilder WithColor(this EmbedBuilder builder, int r, int g, int b) =>
builder.WithColor(new Color(r, g, b));

public static EmbedBuilder WithColor(this EmbedBuilder builder, float r, float g, float b) =>
builder.WithColor(new Color(r, g, b));

public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IUser user) =>
builder.WithAuthor($"{user.Username}#{user.Discriminator}", user.GetAvatarUrl());

public static EmbedBuilder WithAuthor(this EmbedBuilder builder, IGuildUser user) =>
builder.WithAuthor($"{user.Nickname ?? user.Username}#{user.Discriminator}", user.GetAvatarUrl());
}
}

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

@@ -62,26 +62,31 @@ namespace Discord.Net.Rest
_cancelToken = cancelToken; _cancelToken = cancelToken;
} }


public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly)
public async Task<RestResponse> SendAsync(string method, string endpoint, CancellationToken cancelToken, bool headerOnly, string reason = null)
{ {
string uri = Path.Combine(_baseUrl, endpoint); string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
}
} }
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly)
public async Task<RestResponse> SendAsync(string method, string endpoint, string json, CancellationToken cancelToken, bool headerOnly, string reason = null)
{ {
string uri = Path.Combine(_baseUrl, endpoint); string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{ {
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json"); restRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false); return await SendInternalAsync(restRequest, cancelToken, headerOnly).ConfigureAwait(false);
} }
} }
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly)
public async Task<RestResponse> SendAsync(string method, string endpoint, IReadOnlyDictionary<string, object> multipartParams, CancellationToken cancelToken, bool headerOnly, string reason = null)
{ {
string uri = Path.Combine(_baseUrl, endpoint); string uri = Path.Combine(_baseUrl, endpoint);
using (var restRequest = new HttpRequestMessage(GetMethod(method), uri)) using (var restRequest = new HttpRequestMessage(GetMethod(method), uri))
{ {
if (reason != null) restRequest.Headers.Add("X-Audit-Log-Reason", Uri.EscapeDataString(reason));
var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture));
if (multipartParams != null) if (multipartParams != null)
{ {


+ 1
- 1
src/Discord.Net.Rest/Net/Queue/Requests/JsonRestRequest.cs View File

@@ -15,7 +15,7 @@ namespace Discord.Net.Queue


public override async Task<RestResponse> SendAsync() public override async Task<RestResponse> SendAsync()
{ {
return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, Json, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false);
} }
} }
} }

+ 1
- 1
src/Discord.Net.Rest/Net/Queue/Requests/MultipartRestRequest.cs View File

@@ -16,7 +16,7 @@ namespace Discord.Net.Queue


public override async Task<RestResponse> SendAsync() public override async Task<RestResponse> SendAsync()
{ {
return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, MultipartParams, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false);
} }
} }
} }

+ 1
- 1
src/Discord.Net.Rest/Net/Queue/Requests/RestRequest.cs View File

@@ -28,7 +28,7 @@ namespace Discord.Net.Queue


public virtual async Task<RestResponse> SendAsync() public virtual async Task<RestResponse> SendAsync()
{ {
return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly).ConfigureAwait(false);
return await Client.SendAsync(Method, Endpoint, Options.CancelToken, Options.HeaderOnly, Options.AuditLogReason).ConfigureAwait(false);
} }
} }
} }

+ 3
- 1
src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs View File

@@ -54,7 +54,9 @@ namespace Discord.Rpc
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


+ 3
- 1
src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs View File

@@ -57,7 +57,9 @@ namespace Discord.Rpc
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


+ 3
- 1
src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs View File

@@ -58,7 +58,9 @@ namespace Discord.Rpc
=> ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options);


public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null) public Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options);
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messages.Select(x => x.Id), options);
public Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null)
=> ChannelHelper.DeleteMessagesAsync(this, Discord, messageIds, options);


public Task TriggerTypingAsync(RequestOptions options = null) public Task TriggerTypingAsync(RequestOptions options = null)
=> ChannelHelper.TriggerTypingAsync(this, Discord, options); => ChannelHelper.TriggerTypingAsync(this, Discord, options);


+ 3
- 5
src/Discord.Net.Rpc/Entities/Users/RpcUser.cs View File

@@ -49,7 +49,7 @@ namespace Discord.Rpc
Username = model.Username.Value; Username = model.Username.Value;
} }


public Task<RestDMChannel> CreateDMChannelAsync(RequestOptions options = null)
public Task<RestDMChannel> GetOrCreateDMChannelAsync(RequestOptions options = null)
=> UserHelper.CreateDMChannelAsync(this, Discord, options); => UserHelper.CreateDMChannelAsync(this, Discord, options);


public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) public string GetAvatarUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
@@ -59,9 +59,7 @@ namespace Discord.Rpc
private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})"; private string DebuggerDisplay => $"{Username}#{Discriminator} ({Id}{(IsBot ? ", Bot" : "")})";


//IUser //IUser
Task<IDMChannel> IUser.GetDMChannelAsync(CacheMode mode, RequestOptions options)
=> Task.FromResult<IDMChannel>(null);
async Task<IDMChannel> IUser.CreateDMChannelAsync(RequestOptions options)
=> await CreateDMChannelAsync(options).ConfigureAwait(false);
async Task<IDMChannel> IUser.GetOrCreateDMChannelAsync(RequestOptions options)
=> await GetOrCreateDMChannelAsync(options);
} }
} }

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

@@ -142,31 +142,31 @@ namespace Discord.Audio


public AudioOutStream CreateOpusStream(int bufferMillis) public AudioOutStream CreateOpusStream(int bufferMillis)
{ {
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream( outputStream, this);
var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc);
return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger);
var outputStream = new OutputStream(ApiClient); //Ignores header
var sodiumEncrypter = new SodiumEncryptStream( outputStream, this); //Passes header
var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes
return new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Generates header
} }
public AudioOutStream CreateDirectOpusStream() public AudioOutStream CreateDirectOpusStream()
{ {
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this);
return new RTPWriteStream(sodiumEncrypter, _ssrc);
var outputStream = new OutputStream(ApiClient); //Ignores header
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header
return new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header (external input), passes
} }
public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis)
public AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate, int bufferMillis, int packetLoss)
{ {
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this);
var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc);
var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger);
return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application);
var outputStream = new OutputStream(ApiClient); //Ignores header
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header
var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes
var bufferedStream = new BufferedWriteStream(rtpWriter, this, bufferMillis, _connection.CancelToken, _audioLogger); //Ignores header, generates header
return new OpusEncodeStream(bufferedStream, bitrate ?? (96 * 1024), application, packetLoss); //Generates header
} }
public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate)
public AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate, int packetLoss)
{ {
var outputStream = new OutputStream(ApiClient);
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this);
var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc);
return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application);
var outputStream = new OutputStream(ApiClient); //Ignores header
var sodiumEncrypter = new SodiumEncryptStream(outputStream, this); //Passes header
var rtpWriter = new RTPWriteStream(sodiumEncrypter, _ssrc); //Consumes header, passes
return new OpusEncodeStream(rtpWriter, bitrate ?? (96 * 1024), application, packetLoss); //Generates header
} }


internal async Task CreateInputStreamAsync(ulong userId) internal async Task CreateInputStreamAsync(ulong userId)
@@ -174,11 +174,11 @@ namespace Discord.Audio
//Assume Thread-safe //Assume Thread-safe
if (!_streams.ContainsKey(userId)) if (!_streams.ContainsKey(userId))
{ {
var readerStream = new InputStream();
var opusDecoder = new OpusDecodeStream(readerStream);
var readerStream = new InputStream(); //Consumes header
var opusDecoder = new OpusDecodeStream(readerStream); //Passes header
//var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger); //var jitterBuffer = new JitterBuffer(opusDecoder, _audioLogger);
var rtpReader = new RTPReadStream(opusDecoder);
var decryptStream = new SodiumDecryptStream(rtpReader, this);
var rtpReader = new RTPReadStream(opusDecoder); //Generates header
var decryptStream = new SodiumDecryptStream(rtpReader, this); //No header
_streams.TryAdd(userId, new StreamPair(readerStream, decryptStream)); _streams.TryAdd(userId, new StreamPair(readerStream, decryptStream));
await _streamCreatedEvent.InvokeAsync(userId, readerStream); await _streamCreatedEvent.InvokeAsync(userId, readerStream);
} }


+ 2
- 2
src/Discord.Net.WebSocket/Audio/Opus/OpusEncoder.cs View File

@@ -17,7 +17,7 @@ namespace Discord.Audio
public AudioApplication Application { get; } public AudioApplication Application { get; }
public int BitRate { get;} public int BitRate { get;}


public OpusEncoder(int bitrate, AudioApplication application)
public OpusEncoder(int bitrate, AudioApplication application, int packetLoss)
{ {
if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate) if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)
throw new ArgumentOutOfRangeException(nameof(bitrate)); throw new ArgumentOutOfRangeException(nameof(bitrate));
@@ -48,7 +48,7 @@ namespace Discord.Audio
_ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error); _ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error);
CheckError(error); CheckError(error);
CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal)); CheckError(EncoderCtl(_ptr, OpusCtl.SetSignal, (int)opusSignal));
CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, 30)); //%
CheckError(EncoderCtl(_ptr, OpusCtl.SetPacketLossPercent, packetLoss)); //%
CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True CheckError(EncoderCtl(_ptr, OpusCtl.SetInbandFEC, 1)); //True
CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate)); CheckError(EncoderCtl(_ptr, OpusCtl.SetBitrate, bitrate));
} }


+ 5
- 2
src/Discord.Net.WebSocket/Audio/Streams/BufferedWriteStream.cs View File

@@ -88,11 +88,12 @@ namespace Discord.Audio.Streams
if (_queuedFrames.TryDequeue(out Frame frame)) if (_queuedFrames.TryDequeue(out Frame frame))
{ {
await _client.SetSpeakingAsync(true).ConfigureAwait(false); await _client.SetSpeakingAsync(true).ConfigureAwait(false);
_next.WriteHeader(seq++, timestamp, false);
_next.WriteHeader(seq, timestamp, false);
await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false); await _next.WriteAsync(frame.Buffer, 0, frame.Bytes).ConfigureAwait(false);
_bufferPool.Enqueue(frame.Buffer); _bufferPool.Enqueue(frame.Buffer);
_queueLock.Release(); _queueLock.Release();
nextTick += _ticksPerFrame; nextTick += _ticksPerFrame;
seq++;
timestamp += OpusEncoder.FrameSamplesPerChannel; timestamp += OpusEncoder.FrameSamplesPerChannel;
_silenceFrames = 0; _silenceFrames = 0;
#if DEBUG #if DEBUG
@@ -105,12 +106,13 @@ namespace Discord.Audio.Streams
{ {
if (_silenceFrames++ < MaxSilenceFrames) if (_silenceFrames++ < MaxSilenceFrames)
{ {
_next.WriteHeader(seq++, timestamp, false);
_next.WriteHeader(seq, timestamp, false);
await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false); await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false);
} }
else else
await _client.SetSpeakingAsync(false).ConfigureAwait(false); await _client.SetSpeakingAsync(false).ConfigureAwait(false);
nextTick += _ticksPerFrame; nextTick += _ticksPerFrame;
seq++;
timestamp += OpusEncoder.FrameSamplesPerChannel; timestamp += OpusEncoder.FrameSamplesPerChannel;
} }
#if DEBUG #if DEBUG
@@ -126,6 +128,7 @@ 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) public override async Task WriteAsync(byte[] data, int offset, int count, CancellationToken cancelToken)
{ {
if (cancelToken.CanBeCanceled) if (cancelToken.CanBeCanceled)


+ 2
- 2
src/Discord.Net.WebSocket/Audio/Streams/JitterBuffer.cs View File

@@ -1,4 +1,4 @@
using Discord.Logging;
/*using Discord.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Threading; using System.Threading;
@@ -243,4 +243,4 @@ namespace Discord.Audio.Streams
return Task.Delay(0); return Task.Delay(0);
} }
} }
}
}*/

+ 9
- 8
src/Discord.Net.WebSocket/Audio/Streams/OpusDecodeStream.cs View File

@@ -25,12 +25,13 @@ namespace Discord.Audio.Streams
public override void WriteHeader(ushort seq, uint timestamp, bool missed) public override void WriteHeader(ushort seq, uint timestamp, bool missed)
{ {
if (_hasHeader) if (_hasHeader)
throw new InvalidOperationException("Header received with no payload");
_nextMissed = missed;
throw new InvalidOperationException("Header received with no payload");
_hasHeader = true; _hasHeader = true;

_nextMissed = missed;
_next.WriteHeader(seq, timestamp, missed); _next.WriteHeader(seq, timestamp, missed);
} }
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{ {
if (!_hasHeader) if (!_hasHeader)
throw new InvalidOperationException("Received payload without an RTP header"); throw new InvalidOperationException("Received payload without an RTP header");
@@ -39,17 +40,17 @@ namespace Discord.Audio.Streams
if (!_nextMissed) if (!_nextMissed)
{ {
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false); count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, false);
await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false);
await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false);
} }
else if (count > 0) else if (count > 0)
{ {
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true);
await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false);
count = _decoder.DecodeFrame(buffer, offset, count, _buffer, 0, true);
await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false);
} }
else else
{ {
count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true);
await _next.WriteAsync(_buffer, 0, count, cancellationToken).ConfigureAwait(false);
count = _decoder.DecodeFrame(null, 0, 0, _buffer, 0, true);
await _next.WriteAsync(_buffer, 0, count, cancelToken).ConfigureAwait(false);
} }
} }




+ 18
- 10
src/Discord.Net.WebSocket/Audio/Streams/OpusEncodeStream.cs View File

@@ -8,20 +8,22 @@ namespace Discord.Audio.Streams
public class OpusEncodeStream : AudioOutStream public class OpusEncodeStream : AudioOutStream
{ {
public const int SampleRate = 48000; public const int SampleRate = 48000;
private readonly AudioStream _next; private readonly AudioStream _next;
private readonly OpusEncoder _encoder; private readonly OpusEncoder _encoder;
private readonly byte[] _buffer; private readonly byte[] _buffer;
private int _partialFramePos; private int _partialFramePos;

public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application)
private ushort _seq;
private uint _timestamp;
public OpusEncodeStream(AudioStream next, int bitrate, AudioApplication application, int packetLoss)
{ {
_next = next; _next = next;
_encoder = new OpusEncoder(bitrate, application);
_encoder = new OpusEncoder(bitrate, application, packetLoss);
_buffer = new byte[OpusConverter.FrameBytes]; _buffer = new byte[OpusConverter.FrameBytes];
} }


public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancelToken)
{ {
//Assume threadsafe //Assume threadsafe
while (count > 0) while (count > 0)
@@ -30,10 +32,13 @@ namespace Discord.Audio.Streams
{ {
//We have enough data and no partial frames. Pass the buffer directly to the encoder //We have enough data and no partial frames. Pass the buffer directly to the encoder
int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0); int encFrameSize = _encoder.EncodeFrame(buffer, offset, _buffer, 0);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false);
_next.WriteHeader(_seq, _timestamp, false);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false);


offset += OpusConverter.FrameBytes; offset += OpusConverter.FrameBytes;
count -= OpusConverter.FrameBytes; count -= OpusConverter.FrameBytes;
_seq++;
_timestamp += OpusConverter.FrameSamplesPerChannel;
} }
else if (_partialFramePos + count >= OpusConverter.FrameBytes) else if (_partialFramePos + count >= OpusConverter.FrameBytes)
{ {
@@ -41,11 +46,14 @@ namespace Discord.Audio.Streams
int partialSize = OpusConverter.FrameBytes - _partialFramePos; int partialSize = OpusConverter.FrameBytes - _partialFramePos;
Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize); Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize);
int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0); int encFrameSize = _encoder.EncodeFrame(_buffer, 0, _buffer, 0);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancellationToken).ConfigureAwait(false);
_next.WriteHeader(_seq, _timestamp, false);
await _next.WriteAsync(_buffer, 0, encFrameSize, cancelToken).ConfigureAwait(false);


offset += partialSize; offset += partialSize;
count -= partialSize; count -= partialSize;
_partialFramePos = 0; _partialFramePos = 0;
_seq++;
_timestamp += OpusConverter.FrameSamplesPerChannel;
} }
else else
{ {
@@ -57,8 +65,8 @@ namespace Discord.Audio.Streams
} }
} }


/*
public override async Task FlushAsync(CancellationToken cancellationToken)
/* //Opus throws memory errors on bad frames
public override async Task FlushAsync(CancellationToken cancelToken)
{ {
try try
{ {
@@ -67,7 +75,7 @@ namespace Discord.Audio.Streams
} }
catch (Exception) { } //Incomplete frame catch (Exception) { } //Incomplete frame
_partialFramePos = 0; _partialFramePos = 0;
await base.FlushAsync(cancellationToken).ConfigureAwait(false);
await base.FlushAsync(cancelToken).ConfigureAwait(false);
}*/ }*/


public override async Task FlushAsync(CancellationToken cancelToken) public override async Task FlushAsync(CancellationToken cancelToken)


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save