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">
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>rc3</VersionSuffix>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<Authors>RogueException</Authors>
<PackageTags>discord;discordapp</PackageTags>
<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)
[![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)


+ 1
- 1
appveyor.yml View File

@@ -34,7 +34,7 @@ after_build:
if ($Env:APPVEYOR_REPO_TAG -eq "true") {
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix=""
} 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 }



+ 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
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;
outsource to a service for that.

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

Modules are constructed using Dependency Injection. Any parameters
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

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

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

Finally, pass the map into the `LoadAssembly` method.
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

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.

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

>[!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
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.

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

# Preconditions



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

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

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

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

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

string token = "bot token here";

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

await InstallCommands();

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

await Task.Delay(-1);
}

public async Task InstallCommands()
{
// Hook the MessageReceived Event into our Command Handler
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());
}

public async Task HandleCommand(SocketMessage messageParam)
{
{
// Don't process the command if it was a System Message
var message = messageParam as SocketUserMessage;
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
var context = new CommandContext(client, message);
// 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)
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)
{
// Here, we will inject the Dependency Map with
// Here, we will inject the ServiceProvider with
// 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());
}


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

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

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

// Inherit from PreconditionAttribute
public class RequireOwnerAttribute : PreconditionAttribute
{
// 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
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 (context.User.Id == ownerId)
return PreconditionResult.FromSuccess();


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

@@ -8,7 +8,11 @@ public class Program
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.StartAsync();
@@ -25,7 +29,8 @@ public class Program

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

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

@@ -224,4 +224,4 @@ should be to separate the program (initialization and command handler),
the modules (handle commands), and the services (persistent storage,
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,
// 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,

// 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>.
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.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;
}

@@ -92,16 +99,17 @@ class Program
// and other dependencies that your commands might need.
_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.
// Tip: There's an overload taking in a 'validateScopes' bool to make sure
// you haven't made any mistakes in your dependency graph.
_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.
_client.MessageReceived += HandleCommandAsync;
}
@@ -120,7 +128,7 @@ class Program
// commands to be invoked by mentioning the bot instead.
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);
// 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)

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!**

@@ -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,
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
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
var ffmpeg = CreateStream(path);
var output = ffmpeg.StandardOutput.BaseStream;
var discord = client.CreatePCMStream(AudioApplication.Mixed, 1920);
var discord = client.CreatePCMStream(AudioApplication.Mixed);
await output.CopyToAsync(discord);
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)]
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);
}
}

+ 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)
{
var guildUser = await context.Guild.GetCurrentUserAsync();
IGuildUser guildUser = null;
if (context.Guild != null)
guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false);

if (GuildPermission.HasValue)
{
if (guildUser == null)
return PreconditionResult.FromError("Command must be used in a guild channel");
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)
@@ -65,7 +67,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(guildChannel);

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


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

@@ -52,7 +52,7 @@ namespace Discord.Commands
if (guildUser == null)
return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel"));
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)
@@ -66,7 +66,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(guildChannel);

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


+ 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<ParameterBuilder> _parameters;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases;

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 Summary { get; set; }
@@ -24,6 +25,7 @@ namespace Discord.Commands.Builders

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

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

_preconditions = new List<PreconditionAttribute>();
_parameters = new List<ParameterBuilder>();
_attributes = new List<Attribute>();
_aliases = new List<string>();
}
//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)
{
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
@@ -77,12 +80,17 @@ namespace Discord.Commands.Builders
{
for (int i = 0; i < aliases.Length; i++)
{
var alias = aliases[i] ?? "";
string alias = aliases[i] ?? "";
if (!_aliases.Contains(alias))
_aliases.Add(alias);
}
return this;
}
public CommandBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
{
_preconditions.Add(precondition);
@@ -122,11 +130,11 @@ namespace Discord.Commands.Builders

var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple);
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);
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);


+ 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<ModuleBuilder> _submodules;
private readonly List<PreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases;

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

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

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())
throw new InvalidOperationException("Could not find any valid modules from the given selection");*/
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null);
var subGroups = validTypes.Intersect(topLevelGroups);

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

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

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

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

return result;
}

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

@@ -128,26 +151,35 @@ namespace Discord.Commands
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)
@@ -165,22 +197,34 @@ namespace Discord.Commands

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
{
instance.BeforeExecute();
instance.BeforeExecute(cmd);

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
{
instance.AfterExecute();
instance.AfterExecute(cmd);
(instance as IDisposable)?.Dispose();
}
};
}

builder.Callback = ExecuteCallback;
}

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)
{
// 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)
{
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.IsGenericMethod;
}
}
}
}

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

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

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

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

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

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

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)
DefaultValue = Activator.CreateInstance(type);
@@ -84,6 +87,11 @@ namespace Discord.Commands.Builders
return this;
}

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


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

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

//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)
=> 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);
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);
}
}

+ 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.Threading.Tasks;

@@ -13,7 +14,7 @@ namespace Discord.Commands
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;
StringBuilder argBuilder = new StringBuilder(input.Length);
@@ -110,7 +111,7 @@ namespace Discord.Commands
if (curParam == null)
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)
return ParseResult.FromError(typeReaderResult);

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

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)
return ParseResult.FromError(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<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(CommandServiceConfig config)
@@ -59,6 +59,9 @@ namespace Discord.Commands
foreach (var type in PrimitiveParsers.SupportedTypes)
_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>>();
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>)));
@@ -95,7 +98,7 @@ namespace Discord.Commands
if (_typedModuleDefs.ContainsKey(type))
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))
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);
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)
{
@@ -161,8 +164,7 @@ namespace Discord.Commands
await _moduleLock.WaitAsync().ConfigureAwait(false);
try
{
ModuleInfo module;
if (!_typedModuleDefs.TryRemove(type, out module))
if (!_typedModuleDefs.TryRemove(type, out var module))
return false;

return RemoveModuleInternal(module);
@@ -196,20 +198,18 @@ namespace Discord.Commands
}
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;
}
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 null;
}
internal TypeReader GetDefaultTypeReader(Type type)
{
TypeReader reader;
if (_defaultTypeReaders.TryGetValue(type, out reader))
if (_defaultTypeReaders.TryGetValue(type, out var reader))
return reader;
var typeInfo = type.GetTypeInfo();

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

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

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

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

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

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

Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false;
@@ -68,58 +70,80 @@ namespace Discord.Commands
{
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();
}
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)
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);
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)
return Task.FromResult(ExecuteResult.FromError(parseResult));
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult));

var argList = new object[parseResult.ArgValues.Count];
for (int i = 0; i < parseResult.ArgValues.Count; i++)
{
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;
}
var paramList = new object[parseResult.ParamValues.Count];
for (int i = 0; i < parseResult.ParamValues.Count; i++)
{
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;
}

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;

@@ -130,7 +154,7 @@ namespace Discord.Commands
for (int position = 0; position < Parameters.Count; position++)
{
var parameter = Parameters[position];
var argument = args[position];
object argument = args[position];
var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false);
if (!result.IsSuccess)
return ExecuteResult.FromError(result);
@@ -139,10 +163,9 @@ namespace Discord.Commands
switch (RunMode)
{
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
var t2 = Task.Run(async () =>
var t2 = Task.Run(async () =>
{
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);
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)
{
@@ -178,8 +215,13 @@ namespace Discord.Commands
else
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)
@@ -190,7 +232,7 @@ namespace Discord.Commands
argCount--;

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

internal string GetLogText(ICommandContext context)
{
{
if (context.Guild != null)
return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}";
else
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.Collections.Generic;
using System.Collections.Immutable;
@@ -16,6 +17,7 @@ namespace Discord.Commands
public IReadOnlyList<string> Aliases { get; }
public IReadOnlyList<CommandInfo> Commands { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }
public IReadOnlyList<ModuleInfo> Submodules { get; }
public ModuleInfo Parent { get; }
public bool IsSubmodule => Parent != null;
@@ -32,6 +34,7 @@ namespace Discord.Commands
Aliases = BuildAliases(builder, service).ToImmutableArray();
Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray();
Preconditions = BuildPreconditions(builder).ToImmutableArray();
Attributes = BuildAttributes(builder).ToImmutableArray();

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

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 IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }

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

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

_reader = builder.TypeReader;
}
@@ -54,9 +56,10 @@ namespace Discord.Commands
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;


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

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)
{
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(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.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();
}



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

@@ -9,7 +9,7 @@ namespace Discord.Commands
internal class ChannelTypeReader<T> : TypeReader
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)
{


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

@@ -44,12 +44,11 @@ namespace Discord.Commands
_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;

if (_tryParse(input, out baseValue))
if (_tryParse(input, out T baseValue))
{
if (_enumsByValue.TryGetValue(baseValue, out 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;

namespace Discord.Commands
@@ -6,15 +7,14 @@ namespace Discord.Commands
internal class MessageTypeReader<T> : TypeReader
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;

//By Id (1.0)
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);
}



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

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

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


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

@@ -9,7 +9,7 @@ namespace Discord.Commands
internal class RoleTypeReader<T> : TypeReader
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;



+ 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
{
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
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>();
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)
{
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 &&
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
{
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
public struct PreconditionResult : IResult
public class PreconditionResult : IResult
{
public CommandError? Error { get; }
public string ErrorReason { get; }

public bool IsSuccess => !Error.HasValue;

private PreconditionResult(CommandError? error, string errorReason)
protected PreconditionResult(CommandError? error, string errorReason)
{
Error = error;
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)
{
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);
}
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 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)
{
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>
AudioOutStream CreateDirectOpusStream();
/// <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>
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>
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null);
/// <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>
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
{
// 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>
/// Creates a unciode emoji.
/// Creates a unicode emoji.
/// </summary>
/// <param name="unicode">The pure UTF-8 encoding of an emoji</param>
public Emoji(string unicode)
@@ -15,9 +23,17 @@
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;
}

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

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

public override string ToString() => Name;
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);
/// <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>
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>
/// <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>
Task RemoveBanAsync(IUser user, RequestOptions options = null);
/// <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>
Medium = 2,
/// <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.Collections.Immutable;
using System.Diagnostics;
using System.Linq;

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

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

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


+ 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
{


+ 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
{
@@ -19,6 +20,6 @@ namespace Discord
}

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
{


+ 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
{
@@ -19,6 +20,6 @@ namespace Discord
}

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
{
@@ -17,6 +18,6 @@ namespace Discord
}

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
{
string Url { get; }
string Type { get; }
string Title { get; }
string Description { get; }
EmbedType Type { get; }
DateTimeOffset? Timestamp { get; }
Color? Color { 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>
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>
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);

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

/// <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 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 RetryMode? RetryMode { get; 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 string BucketId { get; set; }


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

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

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


+ 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
{


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

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

namespace Discord.API


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

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

namespace Discord.API


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

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

namespace Discord.API


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

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

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
{
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 SemaphoreSlim _stateLock;
private readonly RestClientProvider RestClientProvider;
private readonly RestClientProvider _restClientProvider;

protected bool _isDisposed;
private CancellationTokenSource _loginCancelToken;
@@ -48,7 +48,7 @@ namespace Discord.API
public DiscordRestApiClient(RestClientProvider restClientProvider, string userAgent, RetryMode defaultRetryMode = RetryMode.AlwaysRetry,
JsonSerializer serializer = null)
{
RestClientProvider = restClientProvider;
_restClientProvider = restClientProvider;
UserAgent = userAgent;
DefaultRetryMode = defaultRetryMode;
_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)
{
RestClient = RestClientProvider(baseUrl);
RestClient = _restClientProvider(baseUrl);
RestClient.SetHeader("accept", "*/*");
RestClient.SetHeader("user-agent", UserAgent);
RestClient.SetHeader("authorization", GetPrefixedToken(AuthTokenType, AuthToken));
@@ -189,7 +189,7 @@ namespace Discord.API
options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId;
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);
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.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);
return DeserializeJson<TResponse>(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false));
}
@@ -803,7 +803,8 @@ namespace Discord.API
options = RequestOptions.CreateOrClone(options);

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)
{
@@ -980,14 +981,15 @@ namespace Discord.API
Expression<Func<string>> endpoint = () => $"guilds/{guildId}/members?limit={limit}&after={afterUserId}";
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(userId, 0, nameof(userId));
options = RequestOptions.CreateOrClone(options);

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


+ 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 model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false);
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)
{
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);

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

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

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

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)
=> 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,
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);
}
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)
=> 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)
=> 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;

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()
{
_embed = new Embed("rich");
_embed = new Embed(EmbedType.Rich);
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 EmbedFooterBuilder Footer { get; set; }
@@ -30,8 +77,10 @@ namespace Discord
get => _fields;
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;
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)
{
Footer = footer;
@@ -100,6 +160,16 @@ namespace Discord
Footer = footer;
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)
{
@@ -107,7 +177,7 @@ namespace Discord
.WithIsInline(false)
.WithName(name)
.WithValue(value);
Fields.Add(field);
AddField(field);
return this;
}
public EmbedBuilder AddInlineField(string name, object value)
@@ -116,11 +186,16 @@ namespace Discord
.WithIsInline(true)
.WithName(name)
.WithValue(value);
Fields.Add(field);
AddField(field);
return this;
}
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);
return this;
}
@@ -128,7 +203,18 @@ namespace Discord
{
var field = new EmbedFieldBuilder();
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;
}

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

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

return _embed;
}
public static implicit operator Embed(EmbedBuilder builder) => builder?.Build();
@@ -149,9 +241,32 @@ namespace Discord
{
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()
{
@@ -182,9 +297,35 @@ namespace Discord
{
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()
{
@@ -215,8 +356,26 @@ namespace Discord
{
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()
{


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

@@ -85,8 +85,8 @@ namespace Discord.Rest
else if (args.RoleIds.IsSpecified)
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 />
public Task AddRoleAsync(IRole role, RequestOptions options = null)
=> 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);
}

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

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" : "")})";

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

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.");
}


+ 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,
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,


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

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

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

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

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

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

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" : "")})";

//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)
{
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()
{
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)
@@ -174,11 +174,11 @@ namespace Discord.Audio
//Assume Thread-safe
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 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));
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 int BitRate { get;}

public OpusEncoder(int bitrate, AudioApplication application)
public OpusEncoder(int bitrate, AudioApplication application, int packetLoss)
{
if (bitrate < 1 || bitrate > DiscordVoiceAPIClient.MaxBitrate)
throw new ArgumentOutOfRangeException(nameof(bitrate));
@@ -48,7 +48,7 @@ namespace Discord.Audio
_ptr = CreateEncoder(SamplingRate, Channels, (int)opusApplication, out var error);
CheckError(error);
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.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))
{
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);
_bufferPool.Enqueue(frame.Buffer);
_queueLock.Release();
nextTick += _ticksPerFrame;
seq++;
timestamp += OpusEncoder.FrameSamplesPerChannel;
_silenceFrames = 0;
#if DEBUG
@@ -105,12 +106,13 @@ namespace Discord.Audio.Streams
{
if (_silenceFrames++ < MaxSilenceFrames)
{
_next.WriteHeader(seq++, timestamp, false);
_next.WriteHeader(seq, timestamp, false);
await _next.WriteAsync(_silenceFrame, 0, _silenceFrame.Length).ConfigureAwait(false);
}
else
await _client.SetSpeakingAsync(false).ConfigureAwait(false);
nextTick += _ticksPerFrame;
seq++;
timestamp += OpusEncoder.FrameSamplesPerChannel;
}
#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)
{
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.Collections.Concurrent;
using System.Threading;
@@ -243,4 +243,4 @@ namespace Discord.Audio.Streams
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)
{
if (_hasHeader)
throw new InvalidOperationException("Header received with no payload");
_nextMissed = missed;
throw new InvalidOperationException("Header received with no payload");
_hasHeader = true;

_nextMissed = 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)
throw new InvalidOperationException("Received payload without an RTP header");
@@ -39,17 +40,17 @@ namespace Discord.Audio.Streams
if (!_nextMissed)
{
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)
{
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
{
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 const int SampleRate = 48000;
private readonly AudioStream _next;
private readonly OpusEncoder _encoder;
private readonly byte[] _buffer;
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;
_encoder = new OpusEncoder(bitrate, application);
_encoder = new OpusEncoder(bitrate, application, packetLoss);
_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
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
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;
count -= OpusConverter.FrameBytes;
_seq++;
_timestamp += OpusConverter.FrameSamplesPerChannel;
}
else if (_partialFramePos + count >= OpusConverter.FrameBytes)
{
@@ -41,11 +46,14 @@ namespace Discord.Audio.Streams
int partialSize = OpusConverter.FrameBytes - _partialFramePos;
Buffer.BlockCopy(buffer, offset, _buffer, _partialFramePos, partialSize);
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;
count -= partialSize;
_partialFramePos = 0;
_seq++;
_timestamp += OpusConverter.FrameSamplesPerChannel;
}
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
{
@@ -67,7 +75,7 @@ namespace Discord.Audio.Streams
}
catch (Exception) { } //Incomplete frame
_partialFramePos = 0;
await base.FlushAsync(cancellationToken).ConfigureAwait(false);
await base.FlushAsync(cancelToken).ConfigureAwait(false);
}*/

public override async Task FlushAsync(CancellationToken cancelToken)


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

Loading…
Cancel
Save