Browse Source

Merge 7b87911351 into 748e92bcda

pull/1127/merge
SleepyBoyy GitHub 7 years ago
parent
commit
207878dbf3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 1370 additions and 1246 deletions
  1. +2
    -2
      samples/01_basic_ping_bot/Program.cs
  2. +11
    -14
      samples/02_commands_framework/Program.cs
  3. +2
    -5
      samples/02_commands_framework/Services/CommandHandlingService.cs
  4. +3
    -1
      samples/02_commands_framework/Services/PictureService.cs
  5. +21
    -18
      src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs
  6. +3
    -6
      src/Discord.Net.Analyzers/SymbolExtensions.cs
  7. +1
    -1
      src/Discord.Net.Commands/AssemblyInfo.cs
  8. +5
    -5
      src/Discord.Net.Commands/Attributes/AliasAttribute.cs
  9. +6
    -5
      src/Discord.Net.Commands/Attributes/CommandAttribute.cs
  10. +1
    -1
      src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs
  11. +6
    -6
      src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
  12. +4
    -3
      src/Discord.Net.Commands/Attributes/GroupAttribute.cs
  13. +3
    -3
      src/Discord.Net.Commands/Attributes/NameAttribute.cs
  14. +5
    -6
      src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
  15. +3
    -2
      src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs
  16. +7
    -5
      src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
  17. +28
    -23
      src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  18. +17
    -15
      src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
  19. +6
    -5
      src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
  20. +6
    -4
      src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
  21. +32
    -26
      src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  22. +5
    -5
      src/Discord.Net.Commands/Attributes/PriorityAttribute.cs
  23. +1
    -1
      src/Discord.Net.Commands/Attributes/RemainderAttribute.cs
  24. +3
    -3
      src/Discord.Net.Commands/Attributes/RemarksAttribute.cs
  25. +3
    -3
      src/Discord.Net.Commands/Attributes/SummaryAttribute.cs
  26. +47
    -35
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  27. +39
    -28
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  28. +43
    -61
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  29. +25
    -22
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  30. +7
    -8
      src/Discord.Net.Commands/CommandContext.cs
  31. +3
    -3
      src/Discord.Net.Commands/CommandException.cs
  32. +9
    -4
      src/Discord.Net.Commands/CommandMatch.cs
  33. +75
    -78
      src/Discord.Net.Commands/CommandParser.cs
  34. +135
    -88
      src/Discord.Net.Commands/CommandService.cs
  35. +9
    -5
      src/Discord.Net.Commands/CommandServiceConfig.cs
  36. +1
    -1
      src/Discord.Net.Commands/EmptyServiceProvider.cs
  37. +5
    -11
      src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs
  38. +14
    -20
      src/Discord.Net.Commands/Extensions/MessageExtensions.cs
  39. +1
    -1
      src/Discord.Net.Commands/IModuleBase.cs
  40. +104
    -95
      src/Discord.Net.Commands/Info/CommandInfo.cs
  41. +25
    -31
      src/Discord.Net.Commands/Info/ModuleInfo.cs
  42. +23
    -19
      src/Discord.Net.Commands/Info/ParameterInfo.cs
  43. +6
    -8
      src/Discord.Net.Commands/Map/CommandMap.cs
  44. +56
    -79
      src/Discord.Net.Commands/Map/CommandMapNode.cs
  45. +22
    -17
      src/Discord.Net.Commands/ModuleBase.cs
  46. +6
    -5
      src/Discord.Net.Commands/PrimitiveParsers.cs
  47. +20
    -18
      src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
  48. +16
    -16
      src/Discord.Net.Commands/Readers/EnumTypeReader.cs
  49. +6
    -8
      src/Discord.Net.Commands/Readers/MessageTypeReader.cs
  50. +7
    -4
      src/Discord.Net.Commands/Readers/NullableTypeReader.cs
  51. +8
    -5
      src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs
  52. +18
    -20
      src/Discord.Net.Commands/Readers/RoleTypeReader.cs
  53. +18
    -19
      src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs
  54. +2
    -1
      src/Discord.Net.Commands/Readers/TypeReader.cs
  55. +30
    -22
      src/Discord.Net.Commands/Readers/UserTypeReader.cs
  56. +5
    -2
      src/Discord.Net.Commands/Results/ExecuteResult.cs
  57. +27
    -19
      src/Discord.Net.Commands/Results/ParseResult.cs
  58. +12
    -8
      src/Discord.Net.Commands/Results/PreconditionGroupResult.cs
  59. +10
    -7
      src/Discord.Net.Commands/Results/PreconditionResult.cs
  60. +5
    -7
      src/Discord.Net.Commands/Results/RuntimeResult.cs
  61. +4
    -1
      src/Discord.Net.Commands/Results/SearchResult.cs
  62. +11
    -3
      src/Discord.Net.Commands/Results/TypeReaderResult.cs
  63. +71
    -82
      src/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs
  64. +19
    -15
      src/Discord.Net.Commands/Utilities/ReflectionUtils.cs
  65. +1
    -1
      src/Discord.Net.Core/AssemblyInfo.cs
  66. +2
    -2
      src/Discord.Net.Core/Audio/AudioApplication.cs
  67. +2
    -2
      src/Discord.Net.Core/Audio/AudioInStream.cs
  68. +3
    -3
      src/Discord.Net.Core/Audio/AudioOutStream.cs
  69. +18
    -24
      src/Discord.Net.Core/Audio/AudioStream.cs
  70. +14
    -8
      src/Discord.Net.Core/Audio/IAudioClient.cs
  71. +1
    -1
      src/Discord.Net.Core/Audio/RTPFrame.cs
  72. +11
    -6
      src/Discord.Net.Core/CDN.cs
  73. +10
    -7
      src/Discord.Net.Core/DiscordConfig.cs
  74. +9
    -6
      src/Discord.Net.Core/Entities/Activities/Game.cs
  75. +5
    -3
      src/Discord.Net.Core/Entities/Activities/GameAsset.cs
  76. +3
    -1
      src/Discord.Net.Core/Entities/Activities/GameParty.cs
  77. +5
    -5
      src/Discord.Net.Core/Entities/Activities/GameSecrets.cs
  78. +4
    -4
      src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs
  79. +6
    -4
      src/Discord.Net.Core/Entities/Activities/RichGame.cs
  80. +6
    -4
      src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs
  81. +5
    -5
      src/Discord.Net.Core/Entities/Activities/StreamingGame.cs
  82. +2
    -8
      src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs
  83. +4
    -9
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs
  84. +5
    -11
      src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs
  85. +9
    -7
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  86. +1
    -2
      src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs
  87. +0
    -6
      src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs
  88. +4
    -3
      src/Discord.Net.Core/Entities/Channels/IChannel.cs
  89. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IDMChannel.cs
  90. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IGroupChannel.cs
  91. +19
    -4
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  92. +24
    -8
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  93. +5
    -3
      src/Discord.Net.Core/Entities/Channels/INestedChannel.cs
  94. +3
    -0
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  95. +1
    -0
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  96. +6
    -5
      src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs
  97. +3
    -2
      src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs
  98. +3
    -2
      src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs
  99. +10
    -11
      src/Discord.Net.Core/Entities/Emotes/Emoji.cs
  100. +36
    -33
      src/Discord.Net.Core/Entities/Emotes/Emote.cs

+ 2
- 2
samples/01_basic_ping_bot/Program.cs View File

@@ -14,13 +14,13 @@ namespace _01_basic_ping_bot
// - Here, under the 02_commands_framework sample // - Here, under the 02_commands_framework sample
// - https://github.com/foxbot/DiscordBotBase - a barebones bot template // - https://github.com/foxbot/DiscordBotBase - a barebones bot template
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
internal class Program
{ {
private DiscordSocketClient _client; private DiscordSocketClient _client;


// Discord.Net heavily utilizes TAP for async, so we create // Discord.Net heavily utilizes TAP for async, so we create
// an asynchronous context from the beginning. // an asynchronous context from the beginning.
static void Main(string[] args)
private static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult(); => new Program().MainAsync().GetAwaiter().GetResult();


public async Task MainAsync() public async Task MainAsync()


+ 11
- 14
samples/02_commands_framework/Program.cs View File

@@ -1,10 +1,10 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord; using Discord;
using Discord.WebSocket;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using _02_commands_framework.Services; using _02_commands_framework.Services;


namespace _02_commands_framework namespace _02_commands_framework
@@ -17,9 +17,9 @@ namespace _02_commands_framework
// - Here, under the 02_commands_framework sample // - Here, under the 02_commands_framework sample
// - https://github.com/foxbot/DiscordBotBase - a barebones bot template // - https://github.com/foxbot/DiscordBotBase - a barebones bot template
// - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
class Program
internal class Program
{ {
static void Main(string[] args)
private static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult(); => new Program().MainAsync().GetAwaiter().GetResult();


public async Task MainAsync() public async Task MainAsync()
@@ -46,15 +46,12 @@ namespace _02_commands_framework
return Task.CompletedTask; return Task.CompletedTask;
} }


private IServiceProvider ConfigureServices()
{
return new ServiceCollection()
.AddSingleton<DiscordSocketClient>()
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
.AddSingleton<HttpClient>()
.AddSingleton<PictureService>()
.BuildServiceProvider();
}
private IServiceProvider ConfigureServices() => new ServiceCollection()
.AddSingleton<DiscordSocketClient>()
.AddSingleton<CommandService>()
.AddSingleton<CommandHandlingService>()
.AddSingleton<HttpClient>()
.AddSingleton<PictureService>()
.BuildServiceProvider();
} }
} }

+ 2
- 5
samples/02_commands_framework/Services/CommandHandlingService.cs View File

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


namespace _02_commands_framework.Services namespace _02_commands_framework.Services
{ {
@@ -23,10 +23,7 @@ namespace _02_commands_framework.Services
_discord.MessageReceived += MessageReceivedAsync; _discord.MessageReceived += MessageReceivedAsync;
} }


public async Task InitializeAsync()
{
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
}
public async Task InitializeAsync() => await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);


public async Task MessageReceivedAsync(SocketMessage rawMessage) public async Task MessageReceivedAsync(SocketMessage rawMessage)
{ {


+ 3
- 1
samples/02_commands_framework/Services/PictureService.cs View File

@@ -9,7 +9,9 @@ namespace _02_commands_framework.Services
private readonly HttpClient _http; private readonly HttpClient _http;


public PictureService(HttpClient http) public PictureService(HttpClient http)
=> _http = http;
{
_http = http;
}


public async Task<Stream> GetCatPictureAsync() public async Task<Stream> GetCatPictureAsync()
{ {


+ 21
- 18
src/Discord.Net.Analyzers/GuildAccessAnalyzer.cs View File

@@ -1,11 +1,11 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using Discord.Commands;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics;
using Discord.Commands;


namespace Discord.Analyzers namespace Discord.Analyzers
{ {
@@ -14,18 +14,25 @@ namespace Discord.Analyzers
{ {
private const string DiagnosticId = "DNET0001"; private const string DiagnosticId = "DNET0001";
private const string Title = "Limit command to Guild contexts."; private const string Title = "Limit command to Guild contexts.";
private const string MessageFormat = "Command method '{0}' is accessing 'Context.Guild' but is not restricted to Guild contexts.";
private const string Description = "Accessing 'Context.Guild' in a command without limiting the command to run only in guilds.";

private const string MessageFormat =
"Command method '{0}' is accessing 'Context.Guild' but is not restricted to Guild contexts.";

private const string Description =
"Accessing 'Context.Guild' in a command without limiting the command to run only in guilds.";

private const string Category = "API Usage"; private const string Category = "API Usage";


private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true, description: Description);
private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
Category, DiagnosticSeverity.Warning, true, Description);

private static readonly Func<AttributeData, bool> AttributeDataPredicate =
a => a.AttributeClass.Name == nameof(RequireContextAttribute);


public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);


public override void Initialize(AnalysisContext context)
{
public override void Initialize(AnalysisContext context) =>
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression);
}


private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
{ {
@@ -53,18 +60,14 @@ namespace Discord.Analyzers


// Is the '[RequireContext]' attribute not applied to either the // Is the '[RequireContext]' attribute not applied to either the
// method or the class, or its argument isn't 'ContextType.Guild'? // method or the class, or its argument isn't 'ContextType.Guild'?
var ctxAttribute = methodAttributes.SingleOrDefault(_attributeDataPredicate)
?? typeSymbol.GetAttributes().SingleOrDefault(_attributeDataPredicate);
var ctxAttribute = methodAttributes.SingleOrDefault(AttributeDataPredicate)
?? typeSymbol.GetAttributes().SingleOrDefault(AttributeDataPredicate);


if (ctxAttribute == null || ctxAttribute.ConstructorArguments.Any(arg => !arg.Value.Equals((int)ContextType.Guild)))
{
// Report the diagnostic
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), methodSymbol.Name);
context.ReportDiagnostic(diagnostic);
}
if (ctxAttribute != null &&
!ctxAttribute.ConstructorArguments.Any(arg => !arg.Value.Equals((int)ContextType.Guild))) return;
// Report the diagnostic
var diagnostic = Diagnostic.Create(Rule, context.Node.GetLocation(), methodSymbol.Name);
context.ReportDiagnostic(diagnostic);
} }

private static readonly Func<AttributeData, bool> _attributeDataPredicate =
(a => a.AttributeClass.Name == nameof(RequireContextAttribute));
} }
} }

+ 3
- 6
src/Discord.Net.Analyzers/SymbolExtensions.cs View File

@@ -1,20 +1,17 @@
using System;
using Discord.Commands;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Discord.Commands;


namespace Discord.Analyzers namespace Discord.Analyzers
{ {
internal static class SymbolExtensions internal static class SymbolExtensions
{ {
private static readonly string _moduleBaseName = typeof(ModuleBase<>).Name;
private static readonly string ModuleBaseName = typeof(ModuleBase<>).Name;


public static bool DerivesFromModuleBase(this ITypeSymbol symbol) public static bool DerivesFromModuleBase(this ITypeSymbol symbol)
{ {
for (var bType = symbol.BaseType; bType != null; bType = bType.BaseType) for (var bType = symbol.BaseType; bType != null; bType = bType.BaseType)
{
if (bType.MetadataName == _moduleBaseName)
if (bType.MetadataName == ModuleBaseName)
return true; return true;
}
return false; return false;
} }
} }


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

@@ -1,3 +1,3 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;


[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]

+ 5
- 5
src/Discord.Net.Commands/Attributes/AliasAttribute.cs View File

@@ -3,16 +3,16 @@ using System;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> Provides aliases for a command. </summary> /// <summary> Provides aliases for a command. </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AliasAttribute : Attribute public class AliasAttribute : Attribute
{ {
/// <summary> The aliases which have been defined for the command. </summary>
public string[] Aliases { get; }

/// <summary> Creates a new <see cref="AliasAttribute"/> with the given aliases. </summary>
/// <summary> Creates a new <see cref="AliasAttribute" /> with the given aliases. </summary>
public AliasAttribute(params string[] aliases) public AliasAttribute(params string[] aliases)
{ {
Aliases = aliases; Aliases = aliases;
} }

/// <summary> The aliases which have been defined for the command. </summary>
public string[] Aliases { get; }
} }
} }

+ 6
- 5
src/Discord.Net.Commands/Attributes/CommandAttribute.cs View File

@@ -2,20 +2,21 @@ using System;


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Method)]
public class CommandAttribute : Attribute public class CommandAttribute : Attribute
{ {
public string Text { get; }
public RunMode RunMode { get; set; } = RunMode.Default;
public bool? IgnoreExtraArgs { get; set; }

public CommandAttribute() public CommandAttribute()
{ {
Text = null; Text = null;
} }

public CommandAttribute(string text) public CommandAttribute(string text)
{ {
Text = text; Text = text;
} }

public string Text { get; }
public RunMode RunMode { get; set; } = RunMode.Default;
public bool? IgnoreExtraArgs { get; set; }
} }
} }

+ 1
- 1
src/Discord.Net.Commands/Attributes/DontAutoLoadAttribute.cs View File

@@ -2,7 +2,7 @@ using System;


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class)]
public class DontAutoLoadAttribute : Attribute public class DontAutoLoadAttribute : Attribute
{ {
} }


+ 6
- 6
src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs View File

@@ -1,9 +1,9 @@
using System; using System;


namespace Discord.Commands {
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DontInjectAttribute : Attribute {
}
namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Property)]
public class DontInjectAttribute : Attribute
{
}
} }

+ 4
- 3
src/Discord.Net.Commands/Attributes/GroupAttribute.cs View File

@@ -2,18 +2,19 @@ using System;


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Class)]
public class GroupAttribute : Attribute public class GroupAttribute : Attribute
{ {
public string Prefix { get; }

public GroupAttribute() public GroupAttribute()
{ {
Prefix = null; Prefix = null;
} }

public GroupAttribute(string prefix) public GroupAttribute(string prefix)
{ {
Prefix = prefix; Prefix = prefix;
} }

public string Prefix { get; }
} }
} }

+ 3
- 3
src/Discord.Net.Commands/Attributes/NameAttribute.cs View File

@@ -3,14 +3,14 @@ using System;
namespace Discord.Commands namespace Discord.Commands
{ {
// Override public name of command/module // Override public name of command/module
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)]
public class NameAttribute : Attribute public class NameAttribute : Attribute
{ {
public string Text { get; }

public NameAttribute(string text) public NameAttribute(string text)
{ {
Text = text; Text = text;
} }

public string Text { get; }
} }
} }

+ 5
- 6
src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs View File

@@ -1,22 +1,21 @@
using System; using System;

using System.Reflection; using System.Reflection;


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter)]
public class OverrideTypeReaderAttribute : Attribute public class OverrideTypeReaderAttribute : Attribute
{ {
private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo();


public Type TypeReader { get; }

public OverrideTypeReaderAttribute(Type overridenTypeReader) public OverrideTypeReaderAttribute(Type overridenTypeReader)
{ {
if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo()))
throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}"); throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}");
TypeReader = overridenTypeReader; TypeReader = overridenTypeReader;
} }
}

public Type TypeReader { get; }
}
} }

+ 3
- 2
src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs View File

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


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)]
public abstract class ParameterPreconditionAttribute : Attribute public abstract class ParameterPreconditionAttribute : Attribute
{ {
public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services);
public abstract Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, ParameterInfo parameter,
object value, IServiceProvider services);
} }
} }

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

@@ -3,16 +3,18 @@ using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)]
public abstract class PreconditionAttribute : Attribute public abstract class PreconditionAttribute : Attribute
{ {
/// <summary> /// <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 langword="null"/>
/// or not at all will require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// 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 langword="null" />
/// or not at all will require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// </summary> /// </summary>
public string Group { get; set; } = null; public string Group { get; set; } = null;


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

+ 28
- 23
src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -4,30 +4,34 @@ using System.Threading.Tasks;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> /// <summary>
/// This attribute requires that the bot has a specified permission in the channel a command is invoked in.
/// This attribute requires that the bot has a specified permission in the channel a command is invoked in.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequireBotPermissionAttribute : PreconditionAttribute public class RequireBotPermissionAttribute : PreconditionAttribute
{ {
public GuildPermission? GuildPermission { get; }
public ChannelPermission? ChannelPermission { get; }

/// <summary> /// <summary>
/// Require that the bot account has a specified GuildPermission
/// Require that the bot account has a specified GuildPermission
/// </summary> /// </summary>
/// <remarks>This precondition will always fail if the command is being invoked in a private channel.</remarks> /// <remarks>This precondition will always fail if the command is being invoked in a private channel.</remarks>
/// <param name="permission">The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <param name="permission">
/// The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the
/// permissions together.
/// </param>
public RequireBotPermissionAttribute(GuildPermission permission) public RequireBotPermissionAttribute(GuildPermission permission)
{ {
GuildPermission = permission; GuildPermission = permission;
ChannelPermission = null; ChannelPermission = null;
} }

/// <summary> /// <summary>
/// Require that the bot account has a specified ChannelPermission.
/// Require that the bot account has a specified ChannelPermission.
/// </summary> /// </summary>
/// <param name="permission">The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <param name="permission">
/// The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing
/// the permissions together.
/// </param>
/// <example> /// <example>
/// <code language="c#">
/// <code language="c#">
/// [Command("permission")] /// [Command("permission")]
/// [RequireBotPermission(ChannelPermission.ManageMessages)] /// [RequireBotPermission(ChannelPermission.ManageMessages)]
/// public async Task Purge() /// public async Task Purge()
@@ -41,7 +45,11 @@ namespace Discord.Commands
GuildPermission = null; GuildPermission = null;
} }


public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
public GuildPermission? GuildPermission { get; }
public ChannelPermission? ChannelPermission { get; }

public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context,
CommandInfo command, IServiceProvider services)
{ {
IGuildUser guildUser = null; IGuildUser guildUser = null;
if (context.Guild != null) if (context.Guild != null)
@@ -55,19 +63,16 @@ namespace Discord.Commands
return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}"); return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}");
} }


if (ChannelPermission.HasValue)
{
ChannelPermissions perms;
if (context.Channel is IGuildChannel guildChannel)
perms = guildUser.GetPermissions(guildChannel);
else
perms = ChannelPermissions.All(context.Channel);

if (!perms.Has(ChannelPermission.Value))
return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}");
}
if (!ChannelPermission.HasValue) return PreconditionResult.FromSuccess();
ChannelPermissions perms;
if (context.Channel is IGuildChannel guildChannel)
perms = guildUser.GetPermissions(guildChannel);
else
perms = ChannelPermissions.All(context.Channel);


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

+ 17
- 15
src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;


namespace Discord.Commands namespace Discord.Commands
{ {
@@ -13,19 +12,20 @@ namespace Discord.Commands
} }


/// <summary> /// <summary>
/// Require that the command be invoked in a specified context.
/// Require that the command be invoked in a specified context.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequireContextAttribute : PreconditionAttribute public class RequireContextAttribute : PreconditionAttribute
{ {
public ContextType Contexts { get; }

/// <summary> /// <summary>
/// Require that the command be invoked in a specified context.
/// Require that the command be invoked in a specified context.
/// </summary> /// </summary>
/// <param name="contexts">The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together.</param>
/// <param name="contexts">
/// The type of context the command can be invoked in. Multiple contexts can be specified by ORing
/// the contexts together.
/// </param>
/// <example> /// <example>
/// <code language="c#">
/// <code language="c#">
/// [Command("private_only")] /// [Command("private_only")]
/// [RequireContext(ContextType.DM | ContextType.Group)] /// [RequireContext(ContextType.DM | ContextType.Group)]
/// public async Task PrivateOnly() /// public async Task PrivateOnly()
@@ -38,21 +38,23 @@ namespace Discord.Commands
Contexts = contexts; Contexts = contexts;
} }


public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
public ContextType Contexts { get; }

public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command,
IServiceProvider services)
{ {
bool isValid = false;
var isValid = false;


if ((Contexts & ContextType.Guild) != 0) if ((Contexts & ContextType.Guild) != 0)
isValid = isValid || context.Channel is IGuildChannel;
isValid = context.Channel is IGuildChannel;
if ((Contexts & ContextType.DM) != 0) if ((Contexts & ContextType.DM) != 0)
isValid = isValid || context.Channel is IDMChannel; isValid = isValid || context.Channel is IDMChannel;
if ((Contexts & ContextType.Group) != 0) if ((Contexts & ContextType.Group) != 0)
isValid = isValid || context.Channel is IGroupChannel; isValid = isValid || context.Channel is IGroupChannel;


if (isValid)
return Task.FromResult(PreconditionResult.FromSuccess());
else
return Task.FromResult(PreconditionResult.FromError($"Invalid context for command; accepted contexts: {Contexts}"));
return Task.FromResult(isValid
? PreconditionResult.FromSuccess()
: PreconditionResult.FromError($"Invalid context for command; accepted contexts: {Contexts}"));
} }
} }
} }

+ 6
- 5
src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs View File

@@ -4,17 +4,18 @@ using System.Threading.Tasks;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> /// <summary>
/// Require that the command is invoked in a channel marked NSFW
/// Require that the command is invoked in a channel marked NSFW
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequireNsfwAttribute : PreconditionAttribute public class RequireNsfwAttribute : PreconditionAttribute
{ {
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command,
IServiceProvider services)
{ {
if (context.Channel is ITextChannel text && text.IsNsfw) if (context.Channel is ITextChannel text && text.IsNsfw)
return Task.FromResult(PreconditionResult.FromSuccess()); return Task.FromResult(PreconditionResult.FromSuccess());
else
return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel."));
return Task.FromResult(
PreconditionResult.FromError("This command may only be invoked in an NSFW channel."));
} }
} }
} }

+ 6
- 4
src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs View File

@@ -4,13 +4,14 @@ using System.Threading.Tasks;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> /// <summary>
/// Require that the command is invoked by the owner of the bot.
/// Require that the command is invoked by the owner of the bot.
/// </summary> /// </summary>
/// <remarks>This precondition will only work if the bot is a bot account.</remarks> /// <remarks>This precondition will only work if the bot is a bot account.</remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequireOwnerAttribute : PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute
{ {
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)
public override async Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context,
CommandInfo command, IServiceProvider services)
{ {
switch (context.Client.TokenType) switch (context.Client.TokenType)
{ {
@@ -20,7 +21,8 @@ namespace Discord.Commands
return PreconditionResult.FromError("Command can only be run by the owner of the bot"); return PreconditionResult.FromError("Command can only be run by the owner of the bot");
return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();
default: default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
return PreconditionResult.FromError(
$"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
} }
} }
} }


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

@@ -4,30 +4,34 @@ using System.Threading.Tasks;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> /// <summary>
/// This attribute requires that the user invoking the command has a specified permission.
/// This attribute requires that the user invoking the command has a specified permission.
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequireUserPermissionAttribute : PreconditionAttribute public class RequireUserPermissionAttribute : PreconditionAttribute
{ {
public GuildPermission? GuildPermission { get; }
public ChannelPermission? ChannelPermission { get; }

/// <summary> /// <summary>
/// Require that the user invoking the command has a specified GuildPermission
/// Require that the user invoking the command has a specified GuildPermission
/// </summary> /// </summary>
/// <remarks>This precondition will always fail if the command is being invoked in a private channel.</remarks> /// <remarks>This precondition will always fail if the command is being invoked in a private channel.</remarks>
/// <param name="permission">The GuildPermission that the user must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <param name="permission">
/// The GuildPermission that the user must have. Multiple permissions can be specified by ORing
/// the permissions together.
/// </param>
public RequireUserPermissionAttribute(GuildPermission permission) public RequireUserPermissionAttribute(GuildPermission permission)
{ {
GuildPermission = permission; GuildPermission = permission;
ChannelPermission = null; ChannelPermission = null;
} }

/// <summary> /// <summary>
/// Require that the user invoking the command has a specified ChannelPermission.
/// Require that the user invoking the command has a specified ChannelPermission.
/// </summary> /// </summary>
/// <param name="permission">The ChannelPermission that the user must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <param name="permission">
/// The ChannelPermission that the user must have. Multiple permissions can be specified by ORing
/// the permissions together.
/// </param>
/// <example> /// <example>
/// <code language="c#">
/// <code language="c#">
/// [Command("permission")] /// [Command("permission")]
/// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)] /// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)]
/// public async Task HasPermission() /// public async Task HasPermission()
@@ -41,32 +45,34 @@ namespace Discord.Commands
ChannelPermission = permission; ChannelPermission = permission;
GuildPermission = null; GuildPermission = null;
} }
public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services)

public GuildPermission? GuildPermission { get; }
public ChannelPermission? ChannelPermission { get; }

public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo command,
IServiceProvider services)
{ {
var guildUser = context.User as IGuildUser; var guildUser = context.User as IGuildUser;


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


if (ChannelPermission.HasValue)
{
ChannelPermissions perms;
if (context.Channel is IGuildChannel guildChannel)
perms = guildUser.GetPermissions(guildChannel);
else
perms = ChannelPermissions.All(context.Channel);

if (!perms.Has(ChannelPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}"));
}
if (!ChannelPermission.HasValue) return Task.FromResult(PreconditionResult.FromSuccess());
ChannelPermissions perms;
if (context.Channel is IGuildChannel guildChannel)
perms = guildUser.GetPermissions(guildChannel);
else
perms = ChannelPermissions.All(context.Channel);


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

+ 5
- 5
src/Discord.Net.Commands/Attributes/PriorityAttribute.cs View File

@@ -3,16 +3,16 @@ using System;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> Sets priority of commands </summary> /// <summary> Sets priority of commands </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Method)]
public class PriorityAttribute : Attribute public class PriorityAttribute : Attribute
{ {
/// <summary> The priority which has been set for the command </summary>
public int Priority { get; }

/// <summary> Creates a new <see cref="PriorityAttribute"/> with the given priority. </summary>
/// <summary> Creates a new <see cref="PriorityAttribute" /> with the given priority. </summary>
public PriorityAttribute(int priority) public PriorityAttribute(int priority)
{ {
Priority = priority; Priority = priority;
} }

/// <summary> The priority which has been set for the command </summary>
public int Priority { get; }
} }
} }

+ 1
- 1
src/Discord.Net.Commands/Attributes/RemainderAttribute.cs View File

@@ -2,7 +2,7 @@ using System;


namespace Discord.Commands namespace Discord.Commands
{ {
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Parameter)]
public class RemainderAttribute : Attribute public class RemainderAttribute : Attribute
{ {
} }


+ 3
- 3
src/Discord.Net.Commands/Attributes/RemarksAttribute.cs View File

@@ -3,14 +3,14 @@ using System;
namespace Discord.Commands namespace Discord.Commands
{ {
// Extension of the Cosmetic Summary, for Groups, Commands, and Parameters // Extension of the Cosmetic Summary, for Groups, Commands, and Parameters
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RemarksAttribute : Attribute public class RemarksAttribute : Attribute
{ {
public string Text { get; }

public RemarksAttribute(string text) public RemarksAttribute(string text)
{ {
Text = text; Text = text;
} }

public string Text { get; }
} }
} }

+ 3
- 3
src/Discord.Net.Commands/Attributes/SummaryAttribute.cs View File

@@ -3,14 +3,14 @@ using System;
namespace Discord.Commands namespace Discord.Commands
{ {
// Cosmetic Summary, for Groups and Commands // Cosmetic Summary, for Groups and Commands
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter)]
public class SummaryAttribute : Attribute public class SummaryAttribute : Attribute
{ {
public string Text { get; }

public SummaryAttribute(string text) public SummaryAttribute(string text)
{ {
Text = text; Text = text;
} }

public string Text { get; }
} }
} }

+ 47
- 35
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -1,32 +1,16 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic;


namespace Discord.Commands.Builders namespace Discord.Commands.Builders
{ {
public class CommandBuilder public class CommandBuilder
{ {
private readonly List<PreconditionAttribute> _preconditions;
private readonly List<ParameterBuilder> _parameters;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases; private readonly List<string> _aliases;

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

public string Name { get; set; }
public string Summary { get; set; }
public string Remarks { get; set; }
public string PrimaryAlias { get; set; }
public RunMode RunMode { get; set; }
public int Priority { get; set; }
public bool IgnoreExtraArgs { get; set; }

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


//Automatic //Automatic
internal CommandBuilder(ModuleBuilder module) internal CommandBuilder(ModuleBuilder module)
@@ -38,8 +22,10 @@ namespace Discord.Commands.Builders
_attributes = new List<Attribute>(); _attributes = new List<Attribute>();
_aliases = new List<string>(); _aliases = new List<string>();
} }

//User-defined //User-defined
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback)
internal CommandBuilder(ModuleBuilder module, string primaryAlias,
Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback)
: this(module) : this(module)
{ {
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
@@ -50,26 +36,46 @@ namespace Discord.Commands.Builders
_aliases.Add(primaryAlias); _aliases.Add(primaryAlias);
} }


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

public string Name { get; set; }
public string Summary { get; set; }
public string Remarks { get; set; }
public string PrimaryAlias { get; set; }
public RunMode RunMode { get; set; }
public int Priority { get; set; }
public bool IgnoreExtraArgs { get; set; }

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

public CommandBuilder WithName(string name) public CommandBuilder WithName(string name)
{ {
Name = name; Name = name;
return this; return this;
} }

public CommandBuilder WithSummary(string summary) public CommandBuilder WithSummary(string summary)
{ {
Summary = summary; Summary = summary;
return this; return this;
} }

public CommandBuilder WithRemarks(string remarks) public CommandBuilder WithRemarks(string remarks)
{ {
Remarks = remarks; Remarks = remarks;
return this; return this;
} }

public CommandBuilder WithRunMode(RunMode runMode) public CommandBuilder WithRunMode(RunMode runMode)
{ {
RunMode = runMode; RunMode = runMode;
return this; return this;
} }

public CommandBuilder WithPriority(int priority) public CommandBuilder WithPriority(int priority)
{ {
Priority = priority; Priority = priority;
@@ -78,24 +84,28 @@ namespace Discord.Commands.Builders


public CommandBuilder AddAliases(params string[] aliases) public CommandBuilder AddAliases(params string[] aliases)
{ {
for (int i = 0; i < aliases.Length; i++)
foreach (var t in aliases)
{ {
string alias = aliases[i] ?? "";
var alias = t ?? "";
if (!_aliases.Contains(alias)) if (!_aliases.Contains(alias))
_aliases.Add(alias); _aliases.Add(alias);
} }

return this; return this;
} }

public CommandBuilder AddAttributes(params Attribute[] attributes) public CommandBuilder AddAttributes(params Attribute[] attributes)
{ {
_attributes.AddRange(attributes); _attributes.AddRange(attributes);
return this; return this;
} }

public CommandBuilder AddPrecondition(PreconditionAttribute precondition) public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
{ {
_preconditions.Add(precondition); _preconditions.Add(precondition);
return this; return this;
} }

public CommandBuilder AddParameter<T>(string name, Action<ParameterBuilder> createFunc) public CommandBuilder AddParameter<T>(string name, Action<ParameterBuilder> createFunc)
{ {
var param = new ParameterBuilder(this, name, typeof(T)); var param = new ParameterBuilder(this, name, typeof(T));
@@ -103,6 +113,7 @@ namespace Discord.Commands.Builders
_parameters.Add(param); _parameters.Add(param);
return this; return this;
} }

public CommandBuilder AddParameter(string name, Type type, Action<ParameterBuilder> createFunc) public CommandBuilder AddParameter(string name, Type type, Action<ParameterBuilder> createFunc)
{ {
var param = new ParameterBuilder(this, name, type); var param = new ParameterBuilder(this, name, type);
@@ -110,6 +121,7 @@ namespace Discord.Commands.Builders
_parameters.Add(param); _parameters.Add(param);
return this; return this;
} }

internal CommandBuilder AddParameter(Action<ParameterBuilder> createFunc) internal CommandBuilder AddParameter(Action<ParameterBuilder> createFunc)
{ {
var param = new ParameterBuilder(this); var param = new ParameterBuilder(this);
@@ -124,18 +136,18 @@ namespace Discord.Commands.Builders
if (Name == null) if (Name == null)
Name = PrimaryAlias; Name = PrimaryAlias;


if (_parameters.Count > 0)
{
var lastParam = _parameters[_parameters.Count - 1];
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. 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. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}");
}
if (_parameters.Count <= 0) return new CommandInfo(this, info, service);
var lastParam = _parameters[_parameters.Count - 1];
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. 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. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}");


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


+ 39
- 28
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

@@ -7,26 +7,11 @@ namespace Discord.Commands.Builders
{ {
public class ModuleBuilder public class ModuleBuilder
{ {
private readonly List<string> _aliases;
private readonly List<Attribute> _attributes;
private readonly List<CommandBuilder> _commands; private readonly List<CommandBuilder> _commands;
private readonly List<ModuleBuilder> _submodules;
private readonly List<PreconditionAttribute> _preconditions; private readonly List<PreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases;

public CommandService Service { get; }
public ModuleBuilder Parent { get; }
public string Name { get; set; }
public string Summary { get; set; }
public string Remarks { get; set; }
public string Group { get; set; }

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;

internal TypeInfo TypeInfo { get; set; }
private readonly List<ModuleBuilder> _submodules;


//Automatic //Automatic
internal ModuleBuilder(CommandService service, ModuleBuilder parent) internal ModuleBuilder(CommandService service, ModuleBuilder parent)
@@ -40,25 +25,43 @@ namespace Discord.Commands.Builders
_attributes = new List<Attribute>(); _attributes = new List<Attribute>();
_aliases = new List<string>(); _aliases = new List<string>();
} }

//User-defined //User-defined
internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias) internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias)
: this(service, parent) : this(service, parent)
{ {
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));


_aliases = new List<string> { primaryAlias };
_aliases = new List<string> {primaryAlias};
} }


public CommandService Service { get; }
public ModuleBuilder Parent { get; }
public string Name { get; set; }
public string Summary { get; set; }
public string Remarks { get; set; }
public string Group { get; set; }

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;

internal TypeInfo TypeInfo { get; set; }

public ModuleBuilder WithName(string name) public ModuleBuilder WithName(string name)
{ {
Name = name; Name = name;
return this; return this;
} }

public ModuleBuilder WithSummary(string summary) public ModuleBuilder WithSummary(string summary)
{ {
Summary = summary; Summary = summary;
return this; return this;
} }

public ModuleBuilder WithRemarks(string remarks) public ModuleBuilder WithRemarks(string remarks)
{ {
Remarks = remarks; Remarks = remarks;
@@ -67,31 +70,38 @@ namespace Discord.Commands.Builders


public ModuleBuilder AddAliases(params string[] aliases) public ModuleBuilder AddAliases(params string[] aliases)
{ {
for (int i = 0; i < aliases.Length; i++)
foreach (var t in aliases)
{ {
string alias = aliases[i] ?? "";
var alias = t ?? "";
if (!_aliases.Contains(alias)) if (!_aliases.Contains(alias))
_aliases.Add(alias); _aliases.Add(alias);
} }

return this; return this;
} }

public ModuleBuilder AddAttributes(params Attribute[] attributes) public ModuleBuilder AddAttributes(params Attribute[] attributes)
{ {
_attributes.AddRange(attributes); _attributes.AddRange(attributes);
return this; return this;
} }

public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
{ {
_preconditions.Add(precondition); _preconditions.Add(precondition);
return this; return this;
} }
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback, Action<CommandBuilder> createFunc)

public ModuleBuilder AddCommand(string primaryAlias,
Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback,
Action<CommandBuilder> createFunc)
{ {
var builder = new CommandBuilder(this, primaryAlias, callback); var builder = new CommandBuilder(this, primaryAlias, callback);
createFunc(builder); createFunc(builder);
_commands.Add(builder); _commands.Add(builder);
return this; return this;
} }

internal ModuleBuilder AddCommand(Action<CommandBuilder> createFunc) internal ModuleBuilder AddCommand(Action<CommandBuilder> createFunc)
{ {
var builder = new CommandBuilder(this); var builder = new CommandBuilder(this);
@@ -99,6 +109,7 @@ namespace Discord.Commands.Builders
_commands.Add(builder); _commands.Add(builder);
return this; return this;
} }

public ModuleBuilder AddModule(string primaryAlias, Action<ModuleBuilder> createFunc) public ModuleBuilder AddModule(string primaryAlias, Action<ModuleBuilder> createFunc)
{ {
var builder = new ModuleBuilder(Service, this, primaryAlias); var builder = new ModuleBuilder(Service, this, primaryAlias);
@@ -106,6 +117,7 @@ namespace Discord.Commands.Builders
_submodules.Add(builder); _submodules.Add(builder);
return this; return this;
} }

internal ModuleBuilder AddModule(Action<ModuleBuilder> createFunc) internal ModuleBuilder AddModule(Action<ModuleBuilder> createFunc)
{ {
var builder = new ModuleBuilder(Service, this); var builder = new ModuleBuilder(Service, this);
@@ -120,17 +132,16 @@ namespace Discord.Commands.Builders
if (Name == null) if (Name == null)
Name = _aliases[0]; Name = _aliases[0];


if (TypeInfo != null && !TypeInfo.IsAbstract)
{
var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(TypeInfo, service, services);
moduleInstance.OnModuleBuilding(service, this);
}
if (TypeInfo == null || TypeInfo.IsAbstract) return new ModuleInfo(this, service, services, parent);
var moduleInstance = ReflectionUtils.CreateObject<IModuleBase>(TypeInfo, service, services);
moduleInstance.OnModuleBuilding(service, this);


return new ModuleInfo(this, service, services, parent); return new ModuleInfo(this, service, services, parent);
} }


public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services); public ModuleInfo Build(CommandService service, IServiceProvider services) => BuildImpl(service, services);


internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) => BuildImpl(service, services, parent);
internal ModuleInfo Build(CommandService service, IServiceProvider services, ModuleInfo parent) =>
BuildImpl(service, services, parent);
} }
} }

+ 43
- 61
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -1,9 +1,8 @@
using System; using System;
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;

using Discord.Commands.Builders; using Discord.Commands.Builders;


namespace Discord.Commands namespace Discord.Commands
@@ -17,38 +16,37 @@ namespace Discord.Commands
bool IsLoadableModule(TypeInfo info) bool IsLoadableModule(TypeInfo info)
{ {
return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) && return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) &&
info.GetCustomAttribute<DontAutoLoadAttribute>() == null;
info.GetCustomAttribute<DontAutoLoadAttribute>() == null;
} }


var result = new List<TypeInfo>(); var result = new List<TypeInfo>();


foreach (var typeInfo in assembly.DefinedTypes) foreach (var typeInfo in assembly.DefinedTypes)
{
if (typeInfo.IsPublic || typeInfo.IsNestedPublic) if (typeInfo.IsPublic || typeInfo.IsNestedPublic)
{ {
if (IsValidModuleDefinition(typeInfo) && if (IsValidModuleDefinition(typeInfo) &&
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) !typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
{
result.Add(typeInfo); result.Add(typeInfo);
}
} }
else if (IsLoadableModule(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)}.");
}
}
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; return result;
} }




public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, IServiceProvider services, params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services);
public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service, IServiceProvider services)
public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, IServiceProvider services,
params TypeInfo[] validTypes) => BuildAsync(validTypes, service, services);

public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes,
CommandService service, IServiceProvider services)
{ {
/*if (!validTypes.Any()) /*if (!validTypes.Any())
throw new InvalidOperationException("Could not find any valid modules from the given selection");*/ throw new InvalidOperationException("Could not find any valid modules from the given selection");*/


var topLevelGroups = validTypes.Where(x => x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo()));
var topLevelGroups = validTypes.Where(x =>
x.DeclaringType == null || !IsValidModuleDefinition(x.DeclaringType.GetTypeInfo()));


var builtTypes = new List<TypeInfo>(); var builtTypes = new List<TypeInfo>();


@@ -69,22 +67,24 @@ namespace Discord.Commands
result[typeInfo.AsType()] = module.Build(service, services); result[typeInfo.AsType()] = module.Build(service, services);
} }


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


return result; return result;
} }


private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes, List<TypeInfo> builtTypes, CommandService service, IServiceProvider services)
private static void BuildSubTypes(ModuleBuilder builder, IEnumerable<TypeInfo> subTypes,
List<TypeInfo> builtTypes, CommandService service, IServiceProvider services)
{ {
foreach (var typeInfo in subTypes) foreach (var typeInfo in subTypes)
{ {
if (!IsValidModuleDefinition(typeInfo)) if (!IsValidModuleDefinition(typeInfo))
continue; continue;
if (builtTypes.Contains(typeInfo)) if (builtTypes.Contains(typeInfo))
continue; continue;
builder.AddModule((module) =>
builder.AddModule(module =>
{ {
BuildModule(module, typeInfo, service, services); BuildModule(module, typeInfo, service, services);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service, services);
@@ -94,13 +94,13 @@ namespace Discord.Commands
} }
} }


private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service, IServiceProvider services)
private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service,
IServiceProvider services)
{ {
var attributes = typeInfo.GetCustomAttributes(); var attributes = typeInfo.GetCustomAttributes();
builder.TypeInfo = typeInfo; builder.TypeInfo = typeInfo;


foreach (var attribute in attributes) foreach (var attribute in attributes)
{
switch (attribute) switch (attribute)
{ {
case NameAttribute name: case NameAttribute name:
@@ -127,7 +127,6 @@ namespace Discord.Commands
builder.AddAttributes(attribute); builder.AddAttributes(attribute);
break; break;
} }
}


//Check for unspecified info //Check for unspecified info
if (builder.Aliases.Count == 0) if (builder.Aliases.Count == 0)
@@ -138,20 +137,15 @@ namespace Discord.Commands
var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x));


foreach (var method in validCommands) foreach (var method in validCommands)
{
builder.AddCommand((command) =>
{
BuildCommand(command, typeInfo, method, service, services);
});
}
builder.AddCommand(command => { BuildCommand(command, typeInfo, method, service, services); });
} }


private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service, IServiceProvider serviceprovider)
private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method,
CommandService service, IServiceProvider serviceprovider)
{ {
var attributes = method.GetCustomAttributes(); var attributes = method.GetCustomAttributes();
foreach (var attribute in attributes) foreach (var attribute in attributes)
{
switch (attribute) switch (attribute)
{ {
case CommandAttribute command: case CommandAttribute command:
@@ -182,7 +176,6 @@ namespace Discord.Commands
builder.AddAttributes(attribute); builder.AddAttributes(attribute);
break; break;
} }
}


if (builder.Name == null) if (builder.Name == null)
builder.Name = method.Name; builder.Name = method.Name;
@@ -190,16 +183,15 @@ namespace Discord.Commands
var parameters = method.GetParameters(); var parameters = method.GetParameters();
int pos = 0, count = parameters.Length; int pos = 0, count = parameters.Length;
foreach (var paramInfo in parameters) foreach (var paramInfo in parameters)
{
builder.AddParameter((parameter) =>
builder.AddParameter(parameter =>
{ {
BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider);
}); });
}


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


async Task<IResult> ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd)
async Task<IResult> ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services,
CommandInfo cmd)
{ {
var instance = createInstance(services); var instance = createInstance(services);
instance.SetContext(context); instance.SetContext(context);
@@ -210,9 +202,7 @@ namespace Discord.Commands


var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); var task = method.Invoke(instance, args) as Task ?? Task.Delay(0);
if (task is Task<RuntimeResult> resultTask) if (task is Task<RuntimeResult> resultTask)
{
return await resultTask.ConfigureAwait(false); return await resultTask.ConfigureAwait(false);
}
else else
{ {
await task.ConfigureAwait(false); await task.ConfigureAwait(false);
@@ -229,7 +219,8 @@ namespace Discord.Commands
builder.Callback = ExecuteCallback; builder.Callback = ExecuteCallback;
} }


private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service, IServiceProvider services)
private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo,
int position, int count, CommandService service, IServiceProvider services)
{ {
var attributes = paramInfo.GetCustomAttributes(); var attributes = paramInfo.GetCustomAttributes();
var paramType = paramInfo.ParameterType; var paramType = paramInfo.ParameterType;
@@ -240,7 +231,6 @@ namespace Discord.Commands
builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null;


foreach (var attribute in attributes) foreach (var attribute in attributes)
{
switch (attribute) switch (attribute)
{ {
case SummaryAttribute summary: case SummaryAttribute summary:
@@ -261,7 +251,8 @@ namespace Discord.Commands
break; break;
case RemainderAttribute _: case RemainderAttribute _:
if (position != count - 1) 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}");
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; builder.IsRemainder = true;
break; break;
@@ -269,26 +260,22 @@ namespace Discord.Commands
builder.AddAttributes(attribute); builder.AddAttributes(attribute);
break; break;
} }
}


builder.ParameterType = paramType; builder.ParameterType = paramType;


if (builder.TypeReader == null) if (builder.TypeReader == null)
{
builder.TypeReader = service.GetDefaultTypeReader(paramType) builder.TypeReader = service.GetDefaultTypeReader(paramType)
?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value;
}
?? service.GetTypeReaders(paramType)?.FirstOrDefault().Value;
} }


private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType, IServiceProvider services)
private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType,
IServiceProvider services)
{ {
var readers = service.GetTypeReaders(paramType); var readers = service.GetTypeReaders(paramType);
TypeReader reader = null;
TypeReader reader;
if (readers != null) if (readers != null)
{
if (readers.TryGetValue(typeReaderType, out reader)) if (readers.TryGetValue(typeReaderType, out reader))
return reader; return reader;
}


//We dont have a cached type reader, create one //We dont have a cached type reader, create one
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services); reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services);
@@ -297,19 +284,14 @@ namespace Discord.Commands
return reader; return reader;
} }


private static bool IsValidModuleDefinition(TypeInfo typeInfo)
{
return _moduleTypeInfo.IsAssignableFrom(typeInfo) &&
!typeInfo.IsAbstract &&
!typeInfo.ContainsGenericParameters;
}
private static bool IsValidModuleDefinition(TypeInfo typeInfo) => _moduleTypeInfo.IsAssignableFrom(typeInfo) &&
!typeInfo.IsAbstract &&
!typeInfo.ContainsGenericParameters;


private static bool IsValidCommandDefinition(MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(CommandAttribute)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod;
}
private static bool IsValidCommandDefinition(MethodInfo methodInfo) =>
methodInfo.IsDefined(typeof(CommandAttribute)) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod;
} }
} }

+ 25
- 22
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -1,29 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;


using System.Collections.Generic;

namespace Discord.Commands.Builders namespace Discord.Commands.Builders
{ {
public class ParameterBuilder public class ParameterBuilder
{ {
private readonly List<ParameterPreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes; private readonly List<Attribute> _attributes;

public CommandBuilder Command { get; }
public string Name { get; internal set; }
public Type ParameterType { get; internal set; }

public TypeReader TypeReader { get; set; }
public bool IsOptional { get; set; }
public bool IsRemainder { get; set; }
public bool IsMultiple { get; set; }
public object DefaultValue { get; set; }
public string Summary { get; set; }

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


//Automatic //Automatic
internal ParameterBuilder(CommandBuilder command) internal ParameterBuilder(CommandBuilder command)
@@ -33,6 +18,7 @@ namespace Discord.Commands.Builders


Command = command; Command = command;
} }

//User-defined //User-defined
internal ParameterBuilder(CommandBuilder command, string name, Type type) internal ParameterBuilder(CommandBuilder command, string name, Type type)
: this(command) : this(command)
@@ -43,6 +29,20 @@ namespace Discord.Commands.Builders
SetType(type); SetType(type);
} }


public CommandBuilder Command { get; }
public string Name { get; internal set; }
public Type ParameterType { get; internal set; }

public TypeReader TypeReader { get; set; }
public bool IsOptional { get; set; }
public bool IsRemainder { get; set; }
public bool IsMultiple { get; set; }
public object DefaultValue { get; set; }
public string Summary { get; set; }

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

internal void SetType(Type type) internal void SetType(Type type)
{ {
TypeReader = GetReader(type); TypeReader = GetReader(type);
@@ -57,10 +57,7 @@ namespace Discord.Commands.Builders
private TypeReader GetReader(Type type) private TypeReader GetReader(Type type)
{ {
var readers = Command.Module.Service.GetTypeReaders(type); var readers = Command.Module.Service.GetTypeReaders(type);
if (readers != null)
return readers.FirstOrDefault().Value;
else
return Command.Module.Service.GetDefaultTypeReader(type);
return readers != null ? readers.FirstOrDefault().Value : Command.Module.Service.GetDefaultTypeReader(type);
} }


public ParameterBuilder WithSummary(string summary) public ParameterBuilder WithSummary(string summary)
@@ -68,21 +65,25 @@ namespace Discord.Commands.Builders
Summary = summary; Summary = summary;
return this; return this;
} }

public ParameterBuilder WithDefault(object defaultValue) public ParameterBuilder WithDefault(object defaultValue)
{ {
DefaultValue = defaultValue; DefaultValue = defaultValue;
return this; return this;
} }

public ParameterBuilder WithIsOptional(bool isOptional) public ParameterBuilder WithIsOptional(bool isOptional)
{ {
IsOptional = isOptional; IsOptional = isOptional;
return this; return this;
} }

public ParameterBuilder WithIsRemainder(bool isRemainder) public ParameterBuilder WithIsRemainder(bool isRemainder)
{ {
IsRemainder = isRemainder; IsRemainder = isRemainder;
return this; return this;
} }

public ParameterBuilder WithIsMultiple(bool isMultiple) public ParameterBuilder WithIsMultiple(bool isMultiple)
{ {
IsMultiple = isMultiple; IsMultiple = isMultiple;
@@ -94,6 +95,7 @@ namespace Discord.Commands.Builders
_attributes.AddRange(attributes); _attributes.AddRange(attributes);
return this; return this;
} }

public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition)
{ {
_preconditions.Add(precondition); _preconditions.Add(precondition);
@@ -103,7 +105,8 @@ namespace Discord.Commands.Builders
internal ParameterInfo Build(CommandInfo info) internal ParameterInfo Build(CommandInfo info)
{ {
if ((TypeReader ?? (TypeReader = GetReader(ParameterType))) == null) if ((TypeReader ?? (TypeReader = GetReader(ParameterType))) == null)
throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified");
throw new InvalidOperationException(
$"No type reader found for type {ParameterType.Name}, one must be specified");


return new ParameterInfo(this, info, Command.Module.Service); return new ParameterInfo(this, info, Command.Module.Service);
} }


+ 7
- 8
src/Discord.Net.Commands/CommandContext.cs View File

@@ -2,14 +2,6 @@
{ {
public class CommandContext : ICommandContext public class CommandContext : ICommandContext
{ {
public IDiscordClient Client { get; }
public IGuild Guild { get; }
public IMessageChannel Channel { get; }
public IUser User { get; }
public IUserMessage Message { get; }

public bool IsPrivate => Channel is IPrivateChannel;
public CommandContext(IDiscordClient client, IUserMessage msg) public CommandContext(IDiscordClient client, IUserMessage msg)
{ {
Client = client; Client = client;
@@ -18,5 +10,12 @@
User = msg.Author; User = msg.Author;
Message = msg; Message = msg;
} }

public bool IsPrivate => Channel is IPrivateChannel;
public IDiscordClient Client { get; }
public IGuild Guild { get; }
public IMessageChannel Channel { get; }
public IUser User { get; }
public IUserMessage Message { get; }
} }
} }

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

@@ -4,14 +4,14 @@ namespace Discord.Commands
{ {
public class CommandException : Exception public class CommandException : Exception
{ {
public CommandInfo Command { get; }
public ICommandContext Context { get; }

public CommandException(CommandInfo command, ICommandContext context, Exception ex) public CommandException(CommandInfo command, ICommandContext context, Exception ex)
: base($"Error occurred executing {command.GetLogText(context)}.", ex) : base($"Error occurred executing {command.GetLogText(context)}.", ex)
{ {
Command = command; Command = command;
Context = context; Context = context;
} }

public CommandInfo Command { get; }
public ICommandContext Context { get; }
} }
} }

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

@@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;


namespace Discord.Commands namespace Discord.Commands
{ {
@@ -16,12 +15,18 @@ namespace Discord.Commands
Alias = alias; Alias = alias;
} }


public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context,
IServiceProvider services = null)
=> Command.CheckPreconditionsAsync(context, services); => Command.CheckPreconditionsAsync(context, services);
public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)

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

public Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList,
IEnumerable<object> paramList, IServiceProvider services)
=> Command.ExecuteAsync(context, argList, paramList, services); => Command.ExecuteAsync(context, argList, paramList, services);

public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
=> Command.ExecuteAsync(context, parseResult, services); => Command.ExecuteAsync(context, parseResult, services);
} }


+ 75
- 78
src/Discord.Net.Commands/CommandParser.cs View File

@@ -8,22 +8,18 @@ namespace Discord.Commands
{ {
internal static class CommandParser internal static class CommandParser
{ {
private enum ParserPart
{
None,
Parameter,
QuotedParameter
}
public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context, bool ignoreExtraArgs, IServiceProvider services, string input, int startPos, IReadOnlyDictionary<char, char> aliasMap)
public static async Task<ParseResult> ParseArgsAsync(CommandInfo command, ICommandContext context,
bool ignoreExtraArgs, IServiceProvider services, string input, int startPos,
IReadOnlyDictionary<char, char> aliasMap)
{ {
ParameterInfo curParam = null; ParameterInfo curParam = null;
StringBuilder argBuilder = new StringBuilder(input.Length);
int endPos = input.Length;
var argBuilder = new StringBuilder(input.Length);
var endPos = input.Length;
var curPart = ParserPart.None; var curPart = ParserPart.None;
int lastArgEndPos = int.MinValue;
var lastArgEndPos = int.MinValue;
var argList = ImmutableArray.CreateBuilder<TypeReaderResult>(); var argList = ImmutableArray.CreateBuilder<TypeReaderResult>();
var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>(); var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>();
bool isEscaping = false;
var isEscaping = false;
char c, matchQuote = '\0'; char c, matchQuote = '\0';


// local helper functions // local helper functions
@@ -46,23 +42,19 @@ namespace Discord.Commands
return '\"'; return '\"';
} }


for (int curPos = startPos; curPos <= endPos; curPos++)
for (var curPos = startPos; curPos <= endPos; curPos++)
{ {
if (curPos < endPos)
c = input[curPos];
else
c = '\0';
c = curPos < endPos ? input[curPos] : '\0';


//If this character is escaped, skip it //If this character is escaped, skip it
if (isEscaping) if (isEscaping)
{
if (curPos != endPos) if (curPos != endPos)
{ {
argBuilder.Append(c); argBuilder.Append(c);
isEscaping = false; isEscaping = false;
continue; continue;
} }
}
//Are we escaping the next character? //Are we escaping the next character?
if (c == '\\' && (curParam == null || !curParam.IsRemainder)) if (c == '\\' && (curParam == null || !curParam.IsRemainder))
{ {
@@ -82,98 +74,96 @@ namespace Discord.Commands
{ {
if (char.IsWhiteSpace(c) || curPos == endPos) if (char.IsWhiteSpace(c) || curPos == endPos)
continue; //Skip whitespace between arguments continue; //Skip whitespace between arguments
else if (curPos == lastArgEndPos)
return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments.");
else
if (curPos == lastArgEndPos)
return ParseResult.FromError(CommandError.ParseFailed,
"There must be at least one character of whitespace between arguments.");
if (curParam == null)
curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null;

if (curParam != null && curParam.IsRemainder)
{ {
if (curParam == null)
curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null;

if (curParam != null && curParam.IsRemainder)
{
argBuilder.Append(c);
continue;
}
if (IsOpenQuote(aliasMap, c))
{
curPart = ParserPart.QuotedParameter;
matchQuote = GetMatch(aliasMap, c);
continue;
}
curPart = ParserPart.Parameter;
argBuilder.Append(c);
continue;
}

if (IsOpenQuote(aliasMap, c))
{
curPart = ParserPart.QuotedParameter;
matchQuote = GetMatch(aliasMap, c);
continue;
} }

curPart = ParserPart.Parameter;
} }


//Has this parameter ended yet? //Has this parameter ended yet?
string argString = null; string argString = null;
if (curPart == ParserPart.Parameter)
switch (curPart)
{ {
if (curPos == endPos || char.IsWhiteSpace(c))
{
case ParserPart.Parameter when curPos == endPos || char.IsWhiteSpace(c):
argString = argBuilder.ToString(); argString = argBuilder.ToString();
lastArgEndPos = curPos; lastArgEndPos = curPos;
}
else
break;
case ParserPart.Parameter:
argBuilder.Append(c); argBuilder.Append(c);
}
else if (curPart == ParserPart.QuotedParameter)
{
if (c == matchQuote)
{
break;
case ParserPart.QuotedParameter when c == matchQuote:
argString = argBuilder.ToString(); //Remove quotes argString = argBuilder.ToString(); //Remove quotes
lastArgEndPos = curPos + 1; lastArgEndPos = curPos + 1;
}
else
break;
case ParserPart.QuotedParameter:
argBuilder.Append(c); argBuilder.Append(c);
break;
} }
if (argString != null)

if (argString == null) continue;
if (curParam == null)
{ {
if (curParam == null)
{
if (command.IgnoreExtraArgs)
break;
else
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");
}
if (command.IgnoreExtraArgs)
break;
return ParseResult.FromError(CommandError.BadArgCount,
"The input text has too many parameters.");
}


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


if (curParam.IsMultiple)
{
paramList.Add(typeReaderResult);
if (curParam.IsMultiple)
{
paramList.Add(typeReaderResult);


curPart = ParserPart.None;
}
else
{
argList.Add(typeReaderResult);
curPart = ParserPart.None;
}
else
{
argList.Add(typeReaderResult);


curParam = null;
curPart = ParserPart.None;
}
argBuilder.Clear();
curParam = null;
curPart = ParserPart.None;
} }

argBuilder.Clear();
} }


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


if (isEscaping) if (isEscaping)
return ParseResult.FromError(CommandError.ParseFailed, "Input text may not end on an incomplete escape.");
return ParseResult.FromError(CommandError.ParseFailed,
"Input text may not end on an incomplete escape.");
if (curPart == ParserPart.QuotedParameter) if (curPart == ParserPart.QuotedParameter)
return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete"); return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete");
//Add missing optionals //Add missing optionals
for (int i = argList.Count; i < command.Parameters.Count; i++)
for (var i = argList.Count; i < command.Parameters.Count; i++)
{ {
var param = command.Parameters[i]; var param = command.Parameters[i];
if (param.IsMultiple) if (param.IsMultiple)
@@ -182,8 +172,15 @@ namespace Discord.Commands
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters.");
argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue)); argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue));
} }
return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable()); return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable());
} }

private enum ParserPart
{
None,
Parameter,
QuotedParameter
}
} }
} }

+ 135
- 88
src/Discord.Net.Commands/CommandService.cs View File

@@ -13,40 +13,39 @@ namespace Discord.Commands
{ {
public class CommandService public class CommandService
{ {
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();
internal readonly bool CaseSensitive, _throwOnError, _ignoreExtraArgs;
internal readonly Logger _cmdLogger;


public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted { add { _commandExecutedEvent.Add(value); } remove { _commandExecutedEvent.Remove(value); } }
internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent = new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>();
internal readonly AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>> _commandExecutedEvent =
new AsyncEvent<Func<CommandInfo, ICommandContext, IResult, Task>>();


private readonly SemaphoreSlim _moduleLock;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
internal readonly RunMode _defaultRunMode;
private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders; private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders;
private readonly ImmutableList<Tuple<Type, Type>> _entityTypeReaders; //TODO: Candidate for C#7 Tuple private readonly ImmutableList<Tuple<Type, Type>> _entityTypeReaders; //TODO: Candidate for C#7 Tuple
private readonly HashSet<ModuleInfo> _moduleDefs;
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();
internal readonly LogManager _logManager;
private readonly CommandMap _map; private readonly CommandMap _map;
private readonly HashSet<ModuleInfo> _moduleDefs;


internal readonly bool _caseSensitive, _throwOnError, _ignoreExtraArgs;
internal readonly char _separatorChar;
internal readonly RunMode _defaultRunMode;
internal readonly Logger _cmdLogger;
internal readonly LogManager _logManager;
private readonly SemaphoreSlim _moduleLock;
internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap; internal readonly IReadOnlyDictionary<char, char> _quotationMarkAliasMap;
internal readonly char _separatorChar;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;


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 CommandService() : this(new CommandServiceConfig())
{
}


public CommandService() : this(new CommandServiceConfig()) { }
public CommandService(CommandServiceConfig config) public CommandService(CommandServiceConfig config)
{ {
_caseSensitive = config.CaseSensitiveCommands;
CaseSensitive = config.CaseSensitiveCommands;
_throwOnError = config.ThrowOnError; _throwOnError = config.ThrowOnError;
_ignoreExtraArgs = config.IgnoreExtraArgs; _ignoreExtraArgs = config.IgnoreExtraArgs;
_separatorChar = config.SeparatorChar; _separatorChar = config.SeparatorChar;
_defaultRunMode = config.DefaultRunMode; _defaultRunMode = config.DefaultRunMode;
_quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary();
_quotationMarkAliasMap =
(config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary();
if (_defaultRunMode == RunMode.Default) if (_defaultRunMode == RunMode.Default)
throw new InvalidOperationException("The default run mode cannot be set to Default."); throw new InvalidOperationException("The default run mode cannot be set to Default.");


@@ -64,7 +63,8 @@ namespace Discord.Commands
foreach (var type in PrimitiveParsers.SupportedTypes) foreach (var type in PrimitiveParsers.SupportedTypes)
{ {
_defaultTypeReaders[type] = PrimitiveTypeReader.Create(type); _defaultTypeReaders[type] = PrimitiveTypeReader.Create(type);
_defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] = NullableTypeReader.Create(type, _defaultTypeReaders[type]);
_defaultTypeReaders[typeof(Nullable<>).MakeGenericType(type)] =
NullableTypeReader.Create(type, _defaultTypeReaders[type]);
} }


var tsreader = new TimeSpanTypeReader(); var tsreader = new TimeSpanTypeReader();
@@ -72,7 +72,11 @@ namespace Discord.Commands
_defaultTypeReaders[typeof(TimeSpan?)] = NullableTypeReader.Create(typeof(TimeSpan), tsreader); _defaultTypeReaders[typeof(TimeSpan?)] = NullableTypeReader.Create(typeof(TimeSpan), tsreader);


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


var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>(); var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>();
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>))); entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>)));
@@ -82,6 +86,27 @@ namespace Discord.Commands
_entityTypeReaders = entityTypeReaders.ToImmutable(); _entityTypeReaders = entityTypeReaders.ToImmutable();
} }


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 event Func<LogMessage, Task> Log
{
add => _logEvent.Add(value);
remove => _logEvent.Remove(value);
}

public event Func<CommandInfo, ICommandContext, IResult, Task> CommandExecuted
{
add => _commandExecutedEvent.Add(value);
remove => _commandExecutedEvent.Remove(value);
}

//Modules //Modules
public async Task<ModuleInfo> CreateModuleAsync(string primaryAlias, Action<ModuleBuilder> buildFunc) public async Task<ModuleInfo> CreateModuleAsync(string primaryAlias, Action<ModuleBuilder> buildFunc)
{ {
@@ -102,12 +127,13 @@ namespace Discord.Commands
} }


/// <summary> /// <summary>
/// Add a command module from a type
/// Add a command module from a type
/// </summary> /// </summary>
/// <typeparam name="T">The type of module</typeparam> /// <typeparam name="T">The type of module</typeparam>
/// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param> /// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param>
/// <returns>A built module</returns> /// <returns>A built module</returns>
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services); public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services);

public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services) public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services)
{ {
services = services ?? EmptyServiceProvider.Instance; services = services ?? EmptyServiceProvider.Instance;
@@ -118,14 +144,13 @@ namespace Discord.Commands
var typeInfo = type.GetTypeInfo(); var typeInfo = type.GetTypeInfo();


if (_typedModuleDefs.ContainsKey(type)) if (_typedModuleDefs.ContainsKey(type))
throw new ArgumentException($"This module has already been added.");

var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false)).FirstOrDefault();
throw new ArgumentException("This module has already been added.");


if (module.Value == default(ModuleInfo))
throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?");
var module = (await ModuleClassBuilder.BuildAsync(this, services, typeInfo).ConfigureAwait(false))
.FirstOrDefault();


_typedModuleDefs[module.Key] = module.Value;
_typedModuleDefs[module.Key] = module.Value ?? throw new InvalidOperationException(
$"Could not build the module {type.FullName}, did you pass an invalid type?");


return LoadModuleInternal(module.Value); return LoadModuleInternal(module.Value);
} }
@@ -134,8 +159,9 @@ namespace Discord.Commands
_moduleLock.Release(); _moduleLock.Release();
} }
} }

/// <summary> /// <summary>
/// Add command modules from an assembly
/// Add command modules from an assembly
/// </summary> /// </summary>
/// <param name="assembly">The assembly containing command modules</param> /// <param name="assembly">The assembly containing command modules</param>
/// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param> /// <param name="services">An IServiceProvider for your dependency injection solution, if using one - otherwise, pass null</param>
@@ -163,6 +189,7 @@ namespace Discord.Commands
_moduleLock.Release(); _moduleLock.Release();
} }
} }

private ModuleInfo LoadModuleInternal(ModuleInfo module) private ModuleInfo LoadModuleInternal(ModuleInfo module)
{ {
_moduleDefs.Add(module); _moduleDefs.Add(module);
@@ -188,22 +215,22 @@ namespace Discord.Commands
_moduleLock.Release(); _moduleLock.Release();
} }
} }

public Task<bool> RemoveModuleAsync<T>() => RemoveModuleAsync(typeof(T)); public Task<bool> RemoveModuleAsync<T>() => RemoveModuleAsync(typeof(T));

public async Task<bool> RemoveModuleAsync(Type type) public async Task<bool> RemoveModuleAsync(Type type)
{ {
await _moduleLock.WaitAsync().ConfigureAwait(false); await _moduleLock.WaitAsync().ConfigureAwait(false);
try try
{ {
if (!_typedModuleDefs.TryRemove(type, out var module))
return false;

return RemoveModuleInternal(module);
return _typedModuleDefs.TryRemove(type, out var module) && RemoveModuleInternal(module);
} }
finally finally
{ {
_moduleLock.Release(); _moduleLock.Release();
} }
} }

private bool RemoveModuleInternal(ModuleInfo module) private bool RemoveModuleInternal(ModuleInfo module)
{ {
if (!_moduleDefs.Remove(module)) if (!_moduleDefs.Remove(module))
@@ -212,60 +239,73 @@ namespace Discord.Commands
foreach (var cmd in module.Commands) foreach (var cmd in module.Commands)
_map.RemoveCommand(cmd); _map.RemoveCommand(cmd);


foreach (var submodule in module.Submodules)
{
RemoveModuleInternal(submodule);
}
foreach (var submodule in module.Submodules) RemoveModuleInternal(submodule);


return true; return true;
} }


//Type Readers //Type Readers
/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added.
/// If a default <see cref="TypeReader"/> exists for <typeparamref name="T"/>, a warning will be logged and the default <see cref="TypeReader"/> will be replaced.
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object type.
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a <see cref="NullableTypeReader{T}" /> will also be
/// added.
/// If a default <see cref="TypeReader" /> exists for <typeparamref name="T" />, a warning will be logged and the
/// default <see cref="TypeReader" /> will be replaced.
/// </summary> /// </summary>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader" />.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
public void AddTypeReader<T>(TypeReader reader) public void AddTypeReader<T>(TypeReader reader)
=> AddTypeReader(typeof(T), reader); => AddTypeReader(typeof(T), reader);

/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added.
/// If a default <see cref="TypeReader"/> exists for <paramref name="type"/>, a warning will be logged and the default <see cref="TypeReader"/> will be replaced.
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object type.
/// If <paramref name="type" /> is a <see cref="ValueType" />, a <see cref="NullableTypeReader{T}" /> for the value
/// type will also be added.
/// If a default <see cref="TypeReader" /> exists for <paramref name="type" />, a warning will be logged and the
/// default <see cref="TypeReader" /> will be replaced.
/// </summary> /// </summary>
/// <param name="type">A <see cref="Type"/> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
/// <param name="type">A <see cref="Type" /> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
public void AddTypeReader(Type type, TypeReader reader) public void AddTypeReader(Type type, TypeReader reader)
{ {
if (_defaultTypeReaders.ContainsKey(type)) if (_defaultTypeReaders.ContainsKey(type))
_ = _cmdLogger.WarningAsync($"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." +
$"To suppress this message, use AddTypeReader<T>(reader, true).");
_ = _cmdLogger.WarningAsync(
$"The default TypeReader for {type.FullName} was replaced by {reader.GetType().FullName}." +
"To suppress this message, use AddTypeReader<T>(reader, true).");
AddTypeReader(type, reader, true); AddTypeReader(type, reader, true);
} }

/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <typeparamref name="T"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> will also be added.
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object type.
/// If <typeparamref name="T" /> is a <see cref="ValueType" />, a <see cref="NullableTypeReader{T}" /> will also be
/// added.
/// </summary> /// </summary>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader"/>.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
/// <param name="replaceDefault">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <typeparamref name="T"/> if one exists.</param>
/// <typeparam name="T">The object type to be read by the <see cref="TypeReader" />.</typeparam>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
/// <param name="replaceDefault">
/// If <paramref name="reader" /> should replace the default <see cref="TypeReader" /> for
/// <typeparamref name="T" /> if one exists.
/// </param>
public void AddTypeReader<T>(TypeReader reader, bool replaceDefault) public void AddTypeReader<T>(TypeReader reader, bool replaceDefault)
=> AddTypeReader(typeof(T), reader, replaceDefault); => AddTypeReader(typeof(T), reader, replaceDefault);

/// <summary> /// <summary>
/// Adds a custom <see cref="TypeReader"/> to this <see cref="CommandService"/> for the supplied object type.
/// If <paramref name="type"/> is a <see cref="ValueType"/>, a <see cref="NullableTypeReader{T}"/> for the value type will also be added.
/// Adds a custom <see cref="TypeReader" /> to this <see cref="CommandService" /> for the supplied object type.
/// If <paramref name="type" /> is a <see cref="ValueType" />, a <see cref="NullableTypeReader{T}" /> for the value
/// type will also be added.
/// </summary> /// </summary>
/// <param name="type">A <see cref="Type"/> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader"/> to be added.</param>
/// <param name="replaceDefault">If <paramref name="reader"/> should replace the default <see cref="TypeReader"/> for <paramref name="type"/> if one exists.</param>
/// <param name="type">A <see cref="Type" /> instance for the type to be read.</param>
/// <param name="reader">An instance of the <see cref="TypeReader" /> to be added.</param>
/// <param name="replaceDefault">
/// If <paramref name="reader" /> should replace the default <see cref="TypeReader" /> for
/// <paramref name="type" /> if one exists.
/// </param>
public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault) public void AddTypeReader(Type type, TypeReader reader, bool replaceDefault)
{ {
if (replaceDefault && HasDefaultTypeReader(type)) if (replaceDefault && HasDefaultTypeReader(type))
{ {
_defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); _defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader);
if (type.GetTypeInfo().IsValueType)
if (!type.GetTypeInfo().IsValueType) return;
{ {
var nullableType = typeof(Nullable<>).MakeGenericType(type); var nullableType = typeof(Nullable<>).MakeGenericType(type);
var nullableReader = NullableTypeReader.Create(type, reader); var nullableReader = NullableTypeReader.Create(type, reader);
@@ -281,28 +321,29 @@ namespace Discord.Commands
AddNullableTypeReader(type, reader); AddNullableTypeReader(type, reader);
} }
} }

internal bool HasDefaultTypeReader(Type type) internal bool HasDefaultTypeReader(Type type)
{ {
if (_defaultTypeReaders.ContainsKey(type)) if (_defaultTypeReaders.ContainsKey(type))
return true; return true;


var typeInfo = type.GetTypeInfo(); var typeInfo = type.GetTypeInfo();
if (typeInfo.IsEnum)
return true;
return _entityTypeReaders.Any(x => type == x.Item1 || typeInfo.ImplementedInterfaces.Contains(x.Item2));
return typeInfo.IsEnum || _entityTypeReaders.Any(x => type == x.Item1 || typeInfo.ImplementedInterfaces.Contains(x.Item2));
} }

internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader) internal void AddNullableTypeReader(Type valueType, TypeReader valueTypeReader)
{ {
var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType), x => new ConcurrentDictionary<Type, TypeReader>());
var readers = _typeReaders.GetOrAdd(typeof(Nullable<>).MakeGenericType(valueType),
x => new ConcurrentDictionary<Type, TypeReader>());
var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader); var nullableReader = NullableTypeReader.Create(valueType, valueTypeReader);
readers[nullableReader.GetType()] = nullableReader; readers[nullableReader.GetType()] = nullableReader;
} }

internal IDictionary<Type, TypeReader> GetTypeReaders(Type type) internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
{ {
if (_typeReaders.TryGetValue(type, out var definedTypeReaders))
return definedTypeReaders;
return null;
return _typeReaders.TryGetValue(type, out var definedTypeReaders) ? definedTypeReaders : null;
} }

internal TypeReader GetDefaultTypeReader(Type type) internal TypeReader GetDefaultTypeReader(Type type)
{ {
if (_defaultTypeReaders.TryGetValue(type, out var reader)) if (_defaultTypeReaders.TryGetValue(type, out var reader))
@@ -318,37 +359,39 @@ namespace Discord.Commands
} }


//Is this an entity? //Is this an entity?
for (int i = 0; i < _entityTypeReaders.Count; i++)
{
if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1))
foreach (var t in _entityTypeReaders)
if (type == t.Item1 ||
typeInfo.ImplementedInterfaces.Contains(t.Item1))
{ {
reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader;
reader = Activator.CreateInstance(t.Item2.MakeGenericType(type)) as TypeReader;
_defaultTypeReaders[type] = reader; _defaultTypeReaders[type] = reader;
return reader; return reader;
} }
}
return null; return null;
} }


//Execution //Execution
public SearchResult Search(ICommandContext context, int argPos) public SearchResult Search(ICommandContext context, int argPos)
=> Search(context.Message.Content.Substring(argPos)); => Search(context.Message.Content.Substring(argPos));

public SearchResult Search(ICommandContext context, string input) public SearchResult Search(ICommandContext context, string input)
=> Search(input); => Search(input);

public SearchResult Search(string input) public SearchResult Search(string input)
{ {
string searchInput = _caseSensitive ? input : input.ToLowerInvariant();
var searchInput = CaseSensitive ? input : input.ToLowerInvariant();
var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray(); var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray();


if (matches.Length > 0)
return SearchResult.FromSuccess(input, matches);
else
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.");
return matches.Length > 0 ? SearchResult.FromSuccess(input, matches) : SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.");
} }


public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services,
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling); => ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling);
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)

public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services,
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
{ {
services = services ?? EmptyServiceProvider.Instance; services = services ?? EmptyServiceProvider.Instance;


@@ -360,9 +403,8 @@ namespace Discord.Commands
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();


foreach (var match in commands) foreach (var match in commands)
{
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
}
preconditionResults[match] =
await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);


var successfulPreconditions = preconditionResults var successfulPreconditions = preconditionResults
.Where(x => x.Value.IsSuccess) .Where(x => x.Value.IsSuccess)
@@ -382,16 +424,18 @@ namespace Discord.Commands
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions) foreach (var pair in successfulPreconditions)
{ {
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services)
.ConfigureAwait(false);


if (parseResult.Error == CommandError.MultipleMatches) if (parseResult.Error == CommandError.MultipleMatches)
{ {
IReadOnlyList<TypeReaderValue> argList, paramList;
switch (multiMatchHandling) switch (multiMatchHandling)
{ {
case MultiMatchHandling.Best: 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();
IReadOnlyList<TypeReaderValue> argList = parseResult.ArgValues
.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
IReadOnlyList<TypeReaderValue> paramList = parseResult.ParamValues
.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
parseResult = ParseResult.FromSuccess(argList, paramList); parseResult = ParseResult.FromSuccess(argList, paramList);
break; break;
} }
@@ -407,8 +451,11 @@ namespace Discord.Commands


if (match.Command.Parameters.Count > 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;
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; argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count; paramValuesScore = paramValuesSum / match.Command.Parameters.Count;


+ 9
- 5
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -1,15 +1,17 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;


namespace Discord.Commands namespace Discord.Commands
{ {
public class CommandServiceConfig public class CommandServiceConfig
{ {
/// <summary> Gets or sets the default RunMode commands should have, if one is not specified on the Command attribute or builder. </summary>
/// <summary>
/// Gets or sets the default RunMode commands should have, if one is not specified on the Command attribute or
/// builder.
/// </summary>
public RunMode DefaultRunMode { get; set; } = RunMode.Sync; public RunMode DefaultRunMode { get; set; } = RunMode.Sync;


public char SeparatorChar { get; set; } = ' '; public char SeparatorChar { get; set; } = ' ';
/// <summary> Determines whether commands should be case-sensitive. </summary> /// <summary> Determines whether commands should be case-sensitive. </summary>
public bool CaseSensitiveCommands { get; set; } = false; public bool CaseSensitiveCommands { get; set; } = false;


@@ -19,8 +21,10 @@ namespace Discord.Commands
/// <summary> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary> /// <summary> Determines whether RunMode.Sync commands should push exceptions up to the caller. </summary>
public bool ThrowOnError { get; set; } = true; public bool ThrowOnError { get; set; } = true;


/// <summary> Collection of aliases that can wrap strings for command parsing.
/// represents the opening quotation mark and the value is the corresponding closing mark.</summary>
/// <summary>
/// Collection of aliases that can wrap strings for command parsing.
/// represents the opening quotation mark and the value is the corresponding closing mark.
/// </summary>
public Dictionary<char, char> QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap; public Dictionary<char, char> QuotationMarkAliasMap { get; set; } = QuotationAliasUtils.GetDefaultAliasMap;


/// <summary> Determines whether extra parameters should be ignored. </summary> /// <summary> Determines whether extra parameters should be ignored. </summary>


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

@@ -5,7 +5,7 @@ namespace Discord.Commands
internal class EmptyServiceProvider : IServiceProvider internal class EmptyServiceProvider : IServiceProvider
{ {
public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider(); public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider();
public object GetService(Type serviceType) => null; public object GetService(Type serviceType) => null;
} }
} }

+ 5
- 11
src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;


namespace Discord.Commands namespace Discord.Commands
{ {
@@ -8,15 +9,8 @@ namespace Discord.Commands
public static IEnumerable<TResult> Permutate<TFirst, TSecond, TResult>( public static IEnumerable<TResult> Permutate<TFirst, TSecond, TResult>(
this IEnumerable<TFirst> set, this IEnumerable<TFirst> set,
IEnumerable<TSecond> others, IEnumerable<TSecond> others,
Func<TFirst, TSecond, TResult> func)
{
foreach (TFirst elem in set)
{
foreach (TSecond elem2 in others)
{
yield return func(elem, elem2);
}
}
}
Func<TFirst, TSecond, TResult> func) => from elem in set
from elem2 in others
select func(elem, elem2);
} }
}
}

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

@@ -7,40 +7,34 @@ namespace Discord.Commands
public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos)
{ {
var text = msg.Content; var text = msg.Content;
if (text.Length > 0 && text[0] == c)
{
argPos = 1;
return true;
}
return false;
if (text.Length <= 0 || text[0] != c) return false;
argPos = 1;
return true;
} }
public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal)

public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos,
StringComparison comparisonType = StringComparison.Ordinal)
{ {
var text = msg.Content; var text = msg.Content;
if (text.StartsWith(str, comparisonType))
{
argPos = str.Length;
return true;
}
return false;
if (!text.StartsWith(str, comparisonType)) return false;
argPos = str.Length;
return true;
} }

public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos) public static bool HasMentionPrefix(this IUserMessage msg, IUser user, ref int argPos)
{ {
var text = msg.Content; var text = msg.Content;
if (text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; if (text.Length <= 3 || text[0] != '<' || text[1] != '@') return false;


int endPos = text.IndexOf('>');
var endPos = text.IndexOf('>');
if (endPos == -1) return false; if (endPos == -1) return false;
if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> " if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> "


ulong userId; ulong userId;
if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out userId)) return false; if (!MentionUtils.TryParseUser(text.Substring(0, endPos + 1), out userId)) return false;
if (userId == user.Id)
{
argPos = endPos + 2;
return true;
}
return false;
if (userId != user.Id) return false;
argPos = endPos + 2;
return true;
} }
} }
} }

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

@@ -7,7 +7,7 @@ namespace Discord.Commands
void SetContext(ICommandContext context); void SetContext(ICommandContext context);


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


void OnModuleBuilding(CommandService commandService, ModuleBuilder builder); void OnModuleBuilding(CommandService commandService, ModuleBuilder builder);


+ 104
- 95
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -1,39 +1,28 @@
using Discord.Commands.Builders;
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord.Commands.Builders;


namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay("{Name,nq}")]
[DebuggerDisplay("{" + nameof(Name) + ",nq}")]
public class CommandInfo public class CommandInfo
{ {
private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));
private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>();
private static readonly MethodInfo _convertParamsMethod =
typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));


private readonly CommandService _commandService;
private readonly Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> _action;
private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters =
new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>();


public ModuleInfo Module { get; }
public string Name { get; }
public string Summary { get; }
public string Remarks { get; }
public int Priority { get; }
public bool HasVarArgs { get; }
public bool IgnoreExtraArgs { get; }
public RunMode RunMode { get; }
private readonly Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> _action;


public IReadOnlyList<string> Aliases { get; }
public IReadOnlyList<ParameterInfo> Parameters { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }
private readonly CommandService _commandService;


internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service)
{ {
@@ -43,7 +32,7 @@ namespace Discord.Commands
Summary = builder.Summary; Summary = builder.Summary;
Remarks = builder.Remarks; Remarks = builder.Remarks;


RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode);
RunMode = builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode;
Priority = builder.Priority; Priority = builder.Priority;


Aliases = module.Aliases Aliases = module.Aliases
@@ -51,52 +40,66 @@ namespace Discord.Commands
{ {
if (first == "") if (first == "")
return second; return second;
else if (second == "")
if (second == "")
return first; return first;
else
return first + service._separatorChar + second;
return first + service._separatorChar + second;
}) })
.Select(x => service._caseSensitive ? x : x.ToLowerInvariant())
.Select(x => service.CaseSensitive ? x : x.ToLowerInvariant())
.ToImmutableArray(); .ToImmutableArray();


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


Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray();
HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false;
HasVarArgs = builder.Parameters.Count > 0 && builder.Parameters[builder.Parameters.Count - 1].IsMultiple;
IgnoreExtraArgs = builder.IgnoreExtraArgs; IgnoreExtraArgs = builder.IgnoreExtraArgs;


_action = builder.Callback; _action = builder.Callback;
_commandService = service; _commandService = service;
} }


public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
public ModuleInfo Module { get; }
public string Name { get; }
public string Summary { get; }
public string Remarks { get; }
public int Priority { get; }
public bool HasVarArgs { get; }
public bool IgnoreExtraArgs { get; }
public RunMode RunMode { get; }

public IReadOnlyList<string> Aliases { get; }
public IReadOnlyList<ParameterInfo> Parameters { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<Attribute> Attributes { get; }

public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context,
IServiceProvider services = null)
{ {
services = services ?? EmptyServiceProvider.Instance; services = services ?? EmptyServiceProvider.Instance;


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


if (!results.Any(p => p.IsSuccess)) if (!results.Any(p => p.IsSuccess))
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results);
return PreconditionGroupResult.FromError(
$"{type} precondition group {preconditionGroup.Key} failed.", results);
} }
}
return PreconditionGroupResult.FromSuccess(); return PreconditionGroupResult.FromSuccess();
} }


@@ -105,13 +108,11 @@ namespace Discord.Commands
return moduleResult; return moduleResult;


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

return PreconditionResult.FromSuccess();
return !commandResult.IsSuccess ? commandResult : PreconditionResult.FromSuccess();
} }


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


@@ -120,9 +121,10 @@ namespace Discord.Commands
if (preconditionResult != null && !preconditionResult.IsSuccess) if (preconditionResult != null && !preconditionResult.IsSuccess)
return ParseResult.FromError(preconditionResult); return ParseResult.FromError(preconditionResult);


string input = searchResult.Text.Substring(startIndex);
var input = searchResult.Text.Substring(startIndex);


return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input, 0, _commandService._quotationMarkAliasMap).ConfigureAwait(false);
return await CommandParser.ParseArgsAsync(this, context, _commandService._ignoreExtraArgs, services, input,
0, _commandService._quotationMarkAliasMap).ConfigureAwait(false);
} }


public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
@@ -131,7 +133,7 @@ namespace Discord.Commands
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); return Task.FromResult((IResult)ExecuteResult.FromError(parseResult));


var argList = new object[parseResult.ArgValues.Count]; var argList = new object[parseResult.ArgValues.Count];
for (int i = 0; i < parseResult.ArgValues.Count; i++)
for (var i = 0; i < parseResult.ArgValues.Count; i++)
{ {
if (!parseResult.ArgValues[i].IsSuccess) if (!parseResult.ArgValues[i].IsSuccess)
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i]));
@@ -139,7 +141,7 @@ namespace Discord.Commands
} }


var paramList = new object[parseResult.ParamValues.Count]; var paramList = new object[parseResult.ParamValues.Count];
for (int i = 0; i < parseResult.ParamValues.Count; i++)
for (var i = 0; i < parseResult.ParamValues.Count; i++)
{ {
if (!parseResult.ParamValues[i].IsSuccess) if (!parseResult.ParamValues[i].IsSuccess)
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i]));
@@ -148,19 +150,22 @@ namespace Discord.Commands


return ExecuteAsync(context, argList, paramList, services); return ExecuteAsync(context, argList, paramList, services);
} }
public async Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)

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


try try
{ {
object[] args = GenerateArgs(argList, paramList);
var args = GenerateArgs(argList, paramList);


for (int position = 0; position < Parameters.Count; position++)
for (var position = 0; position < Parameters.Count; position++)
{ {
var parameter = Parameters[position]; var parameter = Parameters[position];
object argument = args[position];
var result = await parameter.CheckPreconditionsAsync(context, argument, services).ConfigureAwait(false);
var argument = args[position];
var result = await parameter.CheckPreconditionsAsync(context, argument, services)
.ConfigureAwait(false);
if (!result.IsSuccess) if (!result.IsSuccess)
return ExecuteResult.FromError(result); return ExecuteResult.FromError(result);
} }
@@ -176,6 +181,7 @@ namespace Discord.Commands
}); });
break; break;
} }

return ExecuteResult.FromSuccess(); return ExecuteResult.FromSuccess();
} }
catch (Exception ex) catch (Exception ex)
@@ -184,30 +190,39 @@ namespace Discord.Commands
} }
} }


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


var executeResult = ExecuteResult.FromSuccess(); var executeResult = ExecuteResult.FromSuccess();
@@ -221,13 +236,11 @@ namespace Discord.Commands


var wrappedEx = new CommandException(this, context, ex); var wrappedEx = new CommandException(this, context, ex);
await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false); await Module.Service._cmdLogger.ErrorAsync(wrappedEx).ConfigureAwait(false);
if (Module.Service._throwOnError)
{
if (ex == originalEx)
throw;
else
ExceptionDispatchInfo.Capture(ex).Throw();
}
if (!Module.Service._throwOnError) return ExecuteResult.FromError(CommandError.Exception, ex.Message);
if (ex == originalEx)
throw;
else
ExceptionDispatchInfo.Capture(ex).Throw();


return ExecuteResult.FromError(CommandError.Exception, ex.Message); return ExecuteResult.FromError(CommandError.Exception, ex.Message);
} }
@@ -239,30 +252,30 @@ namespace Discord.Commands


private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList) private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList)
{ {
int argCount = Parameters.Count;
var argCount = Parameters.Count;
var array = new object[Parameters.Count]; var array = new object[Parameters.Count];
if (HasVarArgs) if (HasVarArgs)
argCount--; argCount--;


int i = 0;
foreach (object arg in argList)
var i = 0;
foreach (var arg in argList)
{ {
if (i == argCount) if (i == argCount)
throw new InvalidOperationException("Command was invoked with too many parameters"); throw new InvalidOperationException("Command was invoked with too many parameters");
array[i++] = arg; array[i++] = arg;
} }

if (i < argCount) if (i < argCount)
throw new InvalidOperationException("Command was invoked with too few parameters"); throw new InvalidOperationException("Command was invoked with too few parameters");


if (HasVarArgs)
if (!HasVarArgs) return array;
var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t =>
{ {
var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t =>
{
var method = _convertParamsMethod.MakeGenericMethod(t);
return (Func<IEnumerable<object>, object>)method.CreateDelegate(typeof(Func<IEnumerable<object>, object>));
});
array[i] = func(paramsList);
}
var method = _convertParamsMethod.MakeGenericMethod(t);
return (Func<IEnumerable<object>, object>)method.CreateDelegate(
typeof(Func<IEnumerable<object>, object>));
});
array[i] = func(paramsList);


return array; return array;
} }
@@ -270,12 +283,8 @@ namespace Discord.Commands
private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList) private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList)
=> paramsList.Cast<T>().ToArray(); => 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}";
}
internal string GetLogText(ICommandContext context) => context.Guild != null
? $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"
: $"\"{Name}\" for {context.User} in {context.Channel}";
} }
} }

+ 25
- 31
src/Discord.Net.Commands/Info/ModuleInfo.cs View File

@@ -1,28 +1,15 @@
using System; using System;
using System.Linq;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq;
using Discord.Commands.Builders; using Discord.Commands.Builders;


namespace Discord.Commands namespace Discord.Commands
{ {
public class ModuleInfo public class ModuleInfo
{ {
public CommandService Service { get; }
public string Name { get; }
public string Summary { get; }
public string Remarks { get; }
public string Group { get; }

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;

internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services, ModuleInfo parent = null)
internal ModuleInfo(ModuleBuilder builder, CommandService service, IServiceProvider services,
ModuleInfo parent = null)
{ {
Service = service; Service = service;


@@ -40,6 +27,20 @@ namespace Discord.Commands
Submodules = BuildSubmodules(builder, service, services).ToImmutableArray(); Submodules = BuildSubmodules(builder, service, services).ToImmutableArray();
} }


public CommandService Service { get; }
public string Name { get; }
public string Summary { get; }
public string Remarks { get; }
public string Group { get; }

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;

private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service) private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service)
{ {
var result = builder.Aliases.ToList(); var result = builder.Aliases.ToList();
@@ -57,31 +58,24 @@ namespace Discord.Commands
{ {
if (first == "") if (first == "")
return second; return second;
else if (second == "")
if (second == "")
return first; return first;
else
return first + service._separatorChar + second;
return first + service._separatorChar + second;
}).ToList(); }).ToList();
} }


return result; return result;
} }


private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services)
{
var result = new List<ModuleInfo>();
private IEnumerable<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service,
IServiceProvider services) =>
parent.Modules.Select(submodule => submodule.Build(service, services, this)).ToList();


foreach (var submodule in parent.Modules)
result.Add(submodule.Build(service, services, this));

return result;
}

private static List<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder)
private static IEnumerable<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder)
{ {
var result = new List<PreconditionAttribute>(); var result = new List<PreconditionAttribute>();


ModuleBuilder parent = builder;
var parent = builder;
while (parent != null) while (parent != null)
{ {
result.AddRange(parent.Preconditions); result.AddRange(parent.Preconditions);
@@ -95,7 +89,7 @@ namespace Discord.Commands
{ {
var result = new List<Attribute>(); var result = new List<Attribute>();


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


+ 23
- 19
src/Discord.Net.Commands/Info/ParameterInfo.cs View File

@@ -1,9 +1,8 @@
using Discord.Commands.Builders;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord.Commands.Builders;


namespace Discord.Commands namespace Discord.Commands
{ {
@@ -11,18 +10,6 @@ namespace Discord.Commands
{ {
private readonly TypeReader _reader; private readonly TypeReader _reader;


public CommandInfo Command { get; }
public string Name { get; }
public string Summary { get; }
public bool IsOptional { get; }
public bool IsRemainder { get; }
public bool IsMultiple { get; }
public Type Type { get; }
public object DefaultValue { get; }

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

internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service)
{ {
Command = command; Command = command;
@@ -42,13 +29,30 @@ namespace Discord.Commands
_reader = builder.TypeReader; _reader = builder.TypeReader;
} }


public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, object arg, IServiceProvider services = null)
public CommandInfo Command { get; }
public string Name { get; }
public string Summary { get; }
public bool IsOptional { get; }
public bool IsRemainder { get; }
public bool IsMultiple { get; }
public Type Type { get; }
public object DefaultValue { get; }

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

private string DebuggerDisplay =>
$"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}";

public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, object arg,
IServiceProvider services = null)
{ {
services = services ?? EmptyServiceProvider.Instance; services = services ?? EmptyServiceProvider.Instance;


foreach (var precondition in Preconditions) foreach (var precondition in Preconditions)
{ {
var result = await precondition.CheckPermissionsAsync(context, this, arg, services).ConfigureAwait(false);
var result = await precondition.CheckPermissionsAsync(context, this, arg, services)
.ConfigureAwait(false);
if (!result.IsSuccess) if (!result.IsSuccess)
return result; return result;
} }
@@ -56,13 +60,13 @@ namespace Discord.Commands
return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();
} }


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


public override string ToString() => Name; public override string ToString() => Name;
private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}";
} }
}
}

+ 6
- 8
src/Discord.Net.Commands/Map/CommandMap.cs View File

@@ -4,9 +4,9 @@ namespace Discord.Commands
{ {
internal class CommandMap internal class CommandMap
{ {
private readonly CommandService _service;
private static readonly string[] _blankAliases = {""};
private readonly CommandMapNode _root; private readonly CommandMapNode _root;
private static readonly string[] _blankAliases = new[] { "" };
private readonly CommandService _service;


public CommandMap(CommandService service) public CommandMap(CommandService service)
{ {
@@ -16,18 +16,16 @@ namespace Discord.Commands


public void AddCommand(CommandInfo command) public void AddCommand(CommandInfo command)
{ {
foreach (string text in command.Aliases)
foreach (var text in command.Aliases)
_root.AddCommand(_service, text, 0, command); _root.AddCommand(_service, text, 0, command);
} }

public void RemoveCommand(CommandInfo command) public void RemoveCommand(CommandInfo command)
{ {
foreach (string text in command.Aliases)
foreach (var text in command.Aliases)
_root.RemoveCommand(_service, text, 0, command); _root.RemoveCommand(_service, text, 0, command);
} }


public IEnumerable<CommandMatch> GetCommands(string text)
{
return _root.GetCommands(_service, text, 0, text != "");
}
public IEnumerable<CommandMatch> GetCommands(string text) => _root.GetCommands(_service, text, 0, text != "");
} }
} }

+ 56
- 79
src/Discord.Net.Commands/Map/CommandMapNode.cs View File

@@ -7,15 +7,13 @@ namespace Discord.Commands
{ {
internal class CommandMapNode internal class CommandMapNode
{ {
private static readonly char[] _whitespaceChars = new[] { ' ', '\r', '\n' };
private static readonly char[] _whitespaceChars = {' ', '\r', '\n'};
private readonly object _lockObj = new object();
private readonly string _name;


private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; private readonly ConcurrentDictionary<string, CommandMapNode> _nodes;
private readonly string _name;
private readonly object _lockObj = new object();
private ImmutableArray<CommandInfo> _commands; private ImmutableArray<CommandInfo> _commands;


public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0;

public CommandMapNode(string name) public CommandMapNode(string name)
{ {
_name = name; _name = name;
@@ -23,113 +21,92 @@ namespace Discord.Commands
_commands = ImmutableArray.Create<CommandInfo>(); _commands = ImmutableArray.Create<CommandInfo>();
} }


public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0;

public void AddCommand(CommandService service, string text, int index, CommandInfo command) public void AddCommand(CommandService service, string text, int index, CommandInfo command)
{ {
int nextSegment = NextSegment(text, index, service._separatorChar);
string name;
var nextSegment = NextSegment(text, index, service._separatorChar);


lock (_lockObj) lock (_lockObj)
{
if (text == "")
switch (text)
{ {
if (_name == "")
case "" when _name == "":
throw new InvalidOperationException("Cannot add commands to the root node."); throw new InvalidOperationException("Cannot add commands to the root node.");
_commands = _commands.Add(command);
}
else
{
if (nextSegment == -1)
name = text.Substring(index);
else
name = text.Substring(index, nextSegment - index);
string fullName = _name == "" ? name : _name + service._separatorChar + name;
var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName));
nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
case "":
_commands = _commands.Add(command);
break;
default:
var name = nextSegment == -1
? text.Substring(index)
: text.Substring(index, nextSegment - index);
var fullName = _name == "" ? name : _name + service._separatorChar + name;
var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName));
nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
break;
} }
}
} }

public void RemoveCommand(CommandService service, string text, int index, CommandInfo command) public void RemoveCommand(CommandService service, string text, int index, CommandInfo command)
{ {
int nextSegment = NextSegment(text, index, service._separatorChar);
string name;
var nextSegment = NextSegment(text, index, service._separatorChar);


lock (_lockObj) lock (_lockObj)
{
if (text == "") if (text == "")
_commands = _commands.Remove(command); _commands = _commands.Remove(command);
else else
{ {
if (nextSegment == -1)
name = text.Substring(index);
else
name = text.Substring(index, nextSegment - index);

CommandMapNode nextNode;
if (_nodes.TryGetValue(name, out nextNode))
{
nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
if (nextNode.IsEmpty)
_nodes.TryRemove(name, out nextNode);
}
var name = nextSegment == -1 ? text.Substring(index) : text.Substring(index, nextSegment - index);

if (!_nodes.TryGetValue(name, out var nextNode)) return;
nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
if (nextNode.IsEmpty)
_nodes.TryRemove(name, out nextNode);
} }
}
} }


public IEnumerable<CommandMatch> GetCommands(CommandService service, string text, int index, bool visitChildren = true)
public IEnumerable<CommandMatch> GetCommands(CommandService service, string text, int index,
bool visitChildren = true)
{ {
var commands = _commands; var commands = _commands;
for (int i = 0; i < commands.Length; i++)
for (var i = 0; i < commands.Length; i++)
yield return new CommandMatch(_commands[i], _name); yield return new CommandMatch(_commands[i], _name);


if (visitChildren)
{
string name;
CommandMapNode nextNode;

//Search for next segment
int nextSegment = NextSegment(text, index, service._separatorChar);
if (nextSegment == -1)
name = text.Substring(index);
else
name = text.Substring(index, nextSegment - index);
if (_nodes.TryGetValue(name, out nextNode))
{
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true))
yield return cmd;
}
if (!visitChildren) yield break;
//Search for next segment
var nextSegment = NextSegment(text, index, service._separatorChar);
var name = nextSegment == -1 ? text.Substring(index) : text.Substring(index, nextSegment - index);
if (_nodes.TryGetValue(name, out var nextNode))
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1))
yield return cmd;


//Check if this is the last command segment before args
nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar);
if (nextSegment != -1)
{
name = text.Substring(index, nextSegment - index);
if (_nodes.TryGetValue(name, out nextNode))
{
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false))
yield return cmd;
}
}
//Check if this is the last command segment before args
nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar);
if (nextSegment == -1) yield break;
{
name = text.Substring(index, nextSegment - index);
if (!_nodes.TryGetValue(name, out nextNode)) yield break;
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text,
nextSegment + 1, false))
yield return cmd;
} }
} }


private static int NextSegment(string text, int startIndex, char separator)
{
return text.IndexOf(separator, startIndex);
}
private static int NextSegment(string text, int startIndex, char separator) =>
text.IndexOf(separator, startIndex);

private static int NextSegment(string text, int startIndex, char[] separators, char except) private static int NextSegment(string text, int startIndex, char[] separators, char except)
{ {
int lowest = int.MaxValue;
for (int i = 0; i < separators.Length; i++)
{
if (separators[i] != except)
var lowest = int.MaxValue;
foreach (var t in separators)
if (t != except)
{ {
int index = text.IndexOf(separators[i], startIndex);
var index = text.IndexOf(t, startIndex);
if (index != -1 && index < lowest) if (index != -1 && index < lowest)
lowest = index; lowest = index;
} }
}
return (lowest != int.MaxValue) ? lowest : -1;
return lowest != int.MaxValue ? lowest : -1;
} }
} }
} }

+ 22
- 17
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -4,23 +4,38 @@ using Discord.Commands.Builders;


namespace Discord.Commands namespace Discord.Commands
{ {
public abstract class ModuleBase : ModuleBase<ICommandContext> { }
public abstract class ModuleBase : ModuleBase<ICommandContext>
{
}


public abstract class ModuleBase<T> : IModuleBase public abstract class ModuleBase<T> : IModuleBase
where T : class, ICommandContext where T : class, ICommandContext
{ {
public T Context { get; private set; } public T Context { get; private set; }


//IModuleBase
void IModuleBase.SetContext(ICommandContext context)
{
var newValue = context as T;
Context = newValue ?? throw new InvalidOperationException(
$"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}");
}

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

void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) =>
OnModuleBuilding(commandService, builder);

/// <summary> /// <summary>
/// Sends a message to the source channel
/// Sends a message to the source channel
/// </summary> /// </summary>
/// <param name="message">Contents of the message; optional only if <paramref name="embed"/> is specified</param>
/// <param name="message">Contents of the message; optional only if <paramref name="embed" /> is specified</param>
/// <param name="isTTS">Specifies if Discord should read this message aloud using TTS</param> /// <param name="isTTS">Specifies if Discord should read this message aloud using TTS</param>
/// <param name="embed">An embed to be displayed alongside the message</param> /// <param name="embed">An embed to be displayed alongside the message</param>
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false, Embed embed = null, RequestOptions options = null)
{
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);
}
protected virtual async Task<IUserMessage> ReplyAsync(string message = null, bool isTTS = false,
Embed embed = null, RequestOptions options = null) => await Context.Channel
.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);


protected virtual void BeforeExecute(CommandInfo command) protected virtual void BeforeExecute(CommandInfo command)
{ {
@@ -33,15 +48,5 @@ namespace Discord.Commands
protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder) protected virtual void OnModuleBuilding(CommandService commandService, ModuleBuilder builder)
{ {
} }

//IModuleBase
void IModuleBase.SetContext(ICommandContext context)
{
var newValue = context as T;
Context = newValue ?? throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}");
}
void IModuleBase.BeforeExecute(CommandInfo command) => BeforeExecute(command);
void IModuleBase.AfterExecute(CommandInfo command) => AfterExecute(command);
void IModuleBase.OnModuleBuilding(CommandService commandService, ModuleBuilder builder) => OnModuleBuilding(commandService, builder);
} }
} }

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

@@ -8,11 +8,12 @@ namespace Discord.Commands


internal static class PrimitiveParsers internal static class PrimitiveParsers
{ {
private static readonly Lazy<IReadOnlyDictionary<Type, Delegate>> _parsers = new Lazy<IReadOnlyDictionary<Type, Delegate>>(CreateParsers);
private static readonly Lazy<IReadOnlyDictionary<Type, Delegate>> Parsers =
new Lazy<IReadOnlyDictionary<Type, Delegate>>(CreateParsers);


public static IEnumerable<Type> SupportedTypes = _parsers.Value.Keys;
public static IEnumerable<Type> SupportedTypes = Parsers.Value.Keys;


static IReadOnlyDictionary<Type, Delegate> CreateParsers()
private static IReadOnlyDictionary<Type, Delegate> CreateParsers()
{ {
var parserBuilder = ImmutableDictionary.CreateBuilder<Type, Delegate>(); var parserBuilder = ImmutableDictionary.CreateBuilder<Type, Delegate>();
parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse; parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse;
@@ -34,7 +35,7 @@ namespace Discord.Commands
return parserBuilder.ToImmutable(); return parserBuilder.ToImmutable();
} }


public static TryParseDelegate<T> Get<T>() => (TryParseDelegate<T>)_parsers.Value[typeof(T)];
public static Delegate Get(Type type) => _parsers.Value[type];
public static TryParseDelegate<T> Get<T>() => (TryParseDelegate<T>)Parsers.Value[typeof(T)];
public static Delegate Get(Type type) => Parsers.Value[type];
} }
} }

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

@@ -9,29 +9,31 @@ namespace Discord.Commands
public class ChannelTypeReader<T> : TypeReader public class ChannelTypeReader<T> : TypeReader
where T : class, IChannel where T : class, IChannel
{ {
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input,
IServiceProvider services)
{ {
if (context.Guild != null)
{
var results = new Dictionary<ulong, TypeReaderValue>();
var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false);
ulong id;
if (context.Guild == null)
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found.");
var results = new Dictionary<ulong, TypeReaderValue>();
var channels = await context.Guild.GetChannelsAsync(CacheMode.CacheOnly).ConfigureAwait(false);


//By Mention (1.0)
if (MentionUtils.TryParseChannel(input, out id))
AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
//By Mention (1.0)
if (MentionUtils.TryParseChannel(input, out var id))
AddResult(results,
await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);


//By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
AddResult(results, await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
//By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
AddResult(results,
await context.Guild.GetChannelAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);


//By Name (0.7-0.8)
foreach (var channel in channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f);
//By Name (0.7-0.8)
foreach (var channel in channels.Where(x =>
string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
AddResult(results, channel as T, channel.Name == input ? 0.80f : 0.70f);


if (results.Count > 0)
return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection());
}
if (results.Count > 0)
return TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection());


return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found.");
} }


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

@@ -11,9 +11,10 @@ namespace Discord.Commands
{ {
public static TypeReader GetReader(Type type) public static TypeReader GetReader(Type type)
{ {
Type baseType = Enum.GetUnderlyingType(type);
var constructor = typeof(EnumTypeReader<>).MakeGenericType(baseType).GetTypeInfo().DeclaredConstructors.First();
return (TypeReader)constructor.Invoke(new object[] { type, PrimitiveParsers.Get(baseType) });
var baseType = Enum.GetUnderlyingType(type);
var constructor = typeof(EnumTypeReader<>).MakeGenericType(baseType).GetTypeInfo().DeclaredConstructors
.First();
return (TypeReader)constructor.Invoke(new object[] {type, PrimitiveParsers.Get(baseType)});
} }
} }


@@ -23,7 +24,7 @@ namespace Discord.Commands
private readonly IReadOnlyDictionary<T, object> _enumsByValue; private readonly IReadOnlyDictionary<T, object> _enumsByValue;
private readonly Type _enumType; private readonly Type _enumType;
private readonly TryParseDelegate<T> _tryParse; private readonly TryParseDelegate<T> _tryParse;
public EnumTypeReader(Type type, TryParseDelegate<T> parser) public EnumTypeReader(Type type, TryParseDelegate<T> parser)
{ {
_enumType = type; _enumType = type;
@@ -33,7 +34,7 @@ namespace Discord.Commands
var byValueBuilder = ImmutableDictionary.CreateBuilder<T, object>(); var byValueBuilder = ImmutableDictionary.CreateBuilder<T, object>();


foreach (var v in Enum.GetNames(_enumType)) foreach (var v in Enum.GetNames(_enumType))
{
{
var parsedValue = Enum.Parse(_enumType, v); var parsedValue = Enum.Parse(_enumType, v);
byNameBuilder.Add(v.ToLower(), parsedValue); byNameBuilder.Add(v.ToLower(), parsedValue);
if (!byValueBuilder.ContainsKey((T)parsedValue)) if (!byValueBuilder.ContainsKey((T)parsedValue))
@@ -44,24 +45,23 @@ namespace Discord.Commands
_enumsByValue = byValueBuilder.ToImmutable(); _enumsByValue = byValueBuilder.ToImmutable();
} }


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


if (_tryParse(input, out T baseValue))
if (_tryParse(input, out var baseValue))
{ {
if (_enumsByValue.TryGetValue(baseValue, out enumValue)) if (_enumsByValue.TryGetValue(baseValue, out enumValue))
return Task.FromResult(TypeReaderResult.FromSuccess(enumValue)); return Task.FromResult(TypeReaderResult.FromSuccess(enumValue));
else
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}"));
}
else
{
if (_enumsByName.TryGetValue(input.ToLower(), out enumValue))
return Task.FromResult(TypeReaderResult.FromSuccess(enumValue));
else
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Value is not a {_enumType.Name}"));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed,
$"Value is not a {_enumType.Name}"));
} }

if (_enumsByName.TryGetValue(input.ToLower(), out enumValue))
return Task.FromResult(TypeReaderResult.FromSuccess(enumValue));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed,
$"Value is not a {_enumType.Name}"));
} }
} }
} }

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

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

//By Id (1.0) //By Id (1.0)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
{
if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg)
return TypeReaderResult.FromSuccess(msg);
}
if (!ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out var id))
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.");
if (await context.Channel.GetMessageAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) is T msg)
return TypeReaderResult.FromSuccess(msg);


return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found."); return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.");
} }


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

@@ -9,8 +9,9 @@ namespace Discord.Commands
{ {
public static TypeReader Create(Type type, TypeReader reader) public static TypeReader Create(Type type, TypeReader reader)
{ {
var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors.First();
return (TypeReader)constructor.Invoke(new object[] { reader });
var constructor = typeof(NullableTypeReader<>).MakeGenericType(type).GetTypeInfo().DeclaredConstructors
.First();
return (TypeReader)constructor.Invoke(new object[] {reader});
} }
} }


@@ -24,9 +25,11 @@ namespace Discord.Commands
_baseTypeReader = baseTypeReader; _baseTypeReader = baseTypeReader;
} }


public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input,
IServiceProvider services)
{ {
if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) || string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase))
if (string.Equals(input, "null", StringComparison.OrdinalIgnoreCase) ||
string.Equals(input, "nothing", StringComparison.OrdinalIgnoreCase))
return TypeReaderResult.FromSuccess(new T?()); return TypeReaderResult.FromSuccess(new T?());
return await _baseTypeReader.ReadAsync(context, input, services); return await _baseTypeReader.ReadAsync(context, input, services);
} }


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

@@ -14,12 +14,13 @@ namespace Discord.Commands


internal class PrimitiveTypeReader<T> : TypeReader internal class PrimitiveTypeReader<T> : TypeReader
{ {
private readonly TryParseDelegate<T> _tryParse;
private readonly float _score; private readonly float _score;
private readonly TryParseDelegate<T> _tryParse;


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


public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score) public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score)
{ {
@@ -30,11 +31,13 @@ namespace Discord.Commands
_score = score; _score = score;
} }


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

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

@@ -9,31 +9,29 @@ namespace Discord.Commands
public class RoleTypeReader<T> : TypeReader public class RoleTypeReader<T> : TypeReader
where T : class, IRole where T : class, IRole
{ {
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input,
IServiceProvider services)
{ {
ulong id;
if (context.Guild == null)
return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found."));
var results = new Dictionary<ulong, TypeReaderValue>();
var roles = context.Guild.Roles;


if (context.Guild != null)
{
var results = new Dictionary<ulong, TypeReaderValue>();
var roles = context.Guild.Roles;
//By Mention (1.0)
if (MentionUtils.TryParseRole(input, out var id))
AddResult(results, context.Guild.GetRole(id) as T, 1.00f);


//By Mention (1.0)
if (MentionUtils.TryParseRole(input, out id))
AddResult(results, context.Guild.GetRole(id) as T, 1.00f);
//By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
AddResult(results, context.Guild.GetRole(id) as T, 0.90f);


//By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
AddResult(results, context.Guild.GetRole(id) as T, 0.90f);
//By Name (0.7-0.8)
foreach (var role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f);


//By Name (0.7-0.8)
foreach (var role in roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)))
AddResult(results, role as T, role.Name == input ? 0.80f : 0.70f);

if (results.Count > 0)
return Task.FromResult(TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection()));
}
return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found."));
return Task.FromResult(results.Count > 0
? TypeReaderResult.FromSuccess(results.Values.ToReadOnlyCollection())
: TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found."));
} }


private void AddResult(Dictionary<ulong, TypeReaderValue> results, T role, float score) private void AddResult(Dictionary<ulong, TypeReaderValue> results, T role, float score)


+ 18
- 19
src/Discord.Net.Commands/Readers/TimeSpanTypeReader.cs View File

@@ -6,30 +6,29 @@ namespace Discord.Commands
{ {
internal class TimeSpanTypeReader : TypeReader internal class TimeSpanTypeReader : TypeReader
{ {
private static readonly string[] _formats = new[]
private static readonly string[] _formats =
{ {
"%d'd'%h'h'%m'm'%s's'", //4d3h2m1s "%d'd'%h'h'%m'm'%s's'", //4d3h2m1s
"%d'd'%h'h'%m'm'", //4d3h2m
"%d'd'%h'h'%s's'", //4d3h 1s
"%d'd'%h'h'", //4d3h
"%d'd'%m'm'%s's'", //4d 2m1s
"%d'd'%m'm'", //4d 2m
"%d'd'%s's'", //4d 1s
"%d'd'", //4d
"%h'h'%m'm'%s's'", // 3h2m1s
"%h'h'%m'm'", // 3h2m
"%h'h'%s's'", // 3h 1s
"%h'h'", // 3h
"%m'm'%s's'", // 2m1s
"%m'm'", // 2m
"%s's'", // 1s
"%d'd'%h'h'%m'm'", //4d3h2m
"%d'd'%h'h'%s's'", //4d3h 1s
"%d'd'%h'h'", //4d3h
"%d'd'%m'm'%s's'", //4d 2m1s
"%d'd'%m'm'", //4d 2m
"%d'd'%s's'", //4d 1s
"%d'd'", //4d
"%h'h'%m'm'%s's'", // 3h2m1s
"%h'h'%m'm'", // 3h2m
"%h'h'%s's'", // 3h 1s
"%h'h'", // 3h
"%m'm'%s's'", // 2m1s
"%m'm'", // 2m
"%s's'" // 1s
}; };


public override Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
{
return (TimeSpan.TryParseExact(input.ToLowerInvariant(), _formats, CultureInfo.InvariantCulture, out var timeSpan))
public override Task<TypeReaderResult>
ReadAsync(ICommandContext context, string input, IServiceProvider services) =>
TimeSpan.TryParseExact(input.ToLowerInvariant(), _formats, CultureInfo.InvariantCulture, out var timeSpan)
? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan)) ? Task.FromResult(TypeReaderResult.FromSuccess(timeSpan))
: Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); : Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan"));
}
} }
} }

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

@@ -5,6 +5,7 @@ namespace Discord.Commands
{ {
public abstract class TypeReader public abstract class TypeReader
{ {
public abstract Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services);
public abstract Task<TypeReaderResult> ReadAsync(ICommandContext context, string input,
IServiceProvider services);
} }
} }

+ 30
- 22
src/Discord.Net.Commands/Readers/UserTypeReader.cs View File

@@ -10,47 +10,53 @@ namespace Discord.Commands
public class UserTypeReader<T> : TypeReader public class UserTypeReader<T> : TypeReader
where T : class, IUser where T : class, IUser
{ {
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input, IServiceProvider services)
public override async Task<TypeReaderResult> ReadAsync(ICommandContext context, string input,
IServiceProvider services)
{ {
var results = new Dictionary<ulong, TypeReaderValue>(); var results = new Dictionary<ulong, TypeReaderValue>();
IAsyncEnumerable<IUser> channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better
var channelUsers = context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten(); // it's better
IReadOnlyCollection<IGuildUser> guildUsers = ImmutableArray.Create<IGuildUser>(); IReadOnlyCollection<IGuildUser> guildUsers = ImmutableArray.Create<IGuildUser>();
ulong id;


if (context.Guild != null) if (context.Guild != null)
guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false); guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false);


//By Mention (1.0) //By Mention (1.0)
if (MentionUtils.TryParseUser(input, out id))
if (MentionUtils.TryParseUser(input, out var id))
{ {
if (context.Guild != null) if (context.Guild != null)
AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
AddResult(results,
await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
else else
AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
AddResult(results,
await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 1.00f);
} }


//By Id (0.9) //By Id (0.9)
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id))
{ {
if (context.Guild != null) if (context.Guild != null)
AddResult(results, await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
AddResult(results,
await context.Guild.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
else else
AddResult(results, await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
AddResult(results,
await context.Channel.GetUserAsync(id, CacheMode.CacheOnly).ConfigureAwait(false) as T, 0.90f);
} }


//By Username + Discriminator (0.7-0.85) //By Username + Discriminator (0.7-0.85)
int index = input.LastIndexOf('#');
var index = input.LastIndexOf('#');
if (index >= 0) if (index >= 0)
{ {
string username = input.Substring(0, index);
if (ushort.TryParse(input.Substring(index + 1), out ushort discriminator))
var username = input.Substring(0, index);
if (ushort.TryParse(input.Substring(index + 1), out var discriminator))
{ {
var channelUser = await channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && var channelUser = await channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
string.Equals(username, x.Username,
StringComparison.OrdinalIgnoreCase));
AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f); AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f);


var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator && var guildUser = guildUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
string.Equals(username, x.Username,
StringComparison.OrdinalIgnoreCase));
AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f); AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f);
} }
} }
@@ -59,9 +65,11 @@ namespace Discord.Commands
{ {
await channelUsers await channelUsers
.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)) .Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase))
.ForEachAsync(channelUser => AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f));
foreach (var guildUser in guildUsers.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)))
.ForEachAsync(channelUser =>
AddResult(results, channelUser as T, channelUser.Username == input ? 0.65f : 0.55f));

foreach (var guildUser in guildUsers.Where(x =>
string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)))
AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f); AddResult(results, guildUser as T, guildUser.Username == input ? 0.60f : 0.50f);
} }


@@ -69,15 +77,15 @@ namespace Discord.Commands
{ {
await channelUsers await channelUsers
.Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase)) .Where(x => string.Equals(input, (x as IGuildUser)?.Nickname, StringComparison.OrdinalIgnoreCase))
.ForEachAsync(channelUser => AddResult(results, channelUser as T, (channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f));
.ForEachAsync(channelUser => AddResult(results, channelUser as T,
(channelUser as IGuildUser).Nickname == input ? 0.65f : 0.55f));


foreach (var guildUser in guildUsers.Where(x => string.Equals(input, (x as IGuildUser).Nickname, StringComparison.OrdinalIgnoreCase)))
AddResult(results, guildUser as T, (guildUser as IGuildUser).Nickname == input ? 0.60f : 0.50f);
foreach (var guildUser in guildUsers.Where(x =>
string.Equals(input, x.Nickname, StringComparison.OrdinalIgnoreCase)))
AddResult(results, guildUser as T, guildUser.Nickname == input ? 0.60f : 0.50f);
} }


if (results.Count > 0)
return TypeReaderResult.FromSuccess(results.Values.ToImmutableArray());
return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found.");
return results.Count > 0 ? TypeReaderResult.FromSuccess(results.Values.ToImmutableArray()) : TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found.");
} }


private void AddResult(Dictionary<ulong, TypeReaderValue> results, T user, float score) private void AddResult(Dictionary<ulong, TypeReaderValue> results, T user, float score)


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

@@ -3,7 +3,7 @@ using System.Diagnostics;


namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public struct ExecuteResult : IResult public struct ExecuteResult : IResult
{ {
public Exception Exception { get; } public Exception Exception { get; }
@@ -22,13 +22,16 @@ namespace Discord.Commands


public static ExecuteResult FromSuccess() public static ExecuteResult FromSuccess()
=> new ExecuteResult(null, null, null); => new ExecuteResult(null, null, null);

public static ExecuteResult FromError(CommandError error, string reason) public static ExecuteResult FromError(CommandError error, string reason)
=> new ExecuteResult(null, error, reason); => new ExecuteResult(null, error, reason);

public static ExecuteResult FromError(Exception ex) public static ExecuteResult FromError(Exception ex)
=> new ExecuteResult(ex, CommandError.Exception, ex.Message); => new ExecuteResult(ex, CommandError.Exception, ex.Message);

public static ExecuteResult FromError(IResult result) public static ExecuteResult FromError(IResult result)
=> new ExecuteResult(null, result.Error, result.ErrorReason); => new ExecuteResult(null, result.Error, result.ErrorReason);
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
} }


+ 27
- 19
src/Discord.Net.Commands/Results/ParseResult.cs View File

@@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;


namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public struct ParseResult : IResult public struct ParseResult : IResult
{ {
public IReadOnlyList<TypeReaderResult> ArgValues { get; } public IReadOnlyList<TypeReaderResult> ArgValues { get; }
@@ -15,7 +16,8 @@ namespace Discord.Commands


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


private ParseResult(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues, CommandError? error, string errorReason)
private ParseResult(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues,
CommandError? error, string errorReason)
{ {
ArgValues = argValues; ArgValues = argValues;
ParamValues = paramValues; ParamValues = paramValues;
@@ -23,43 +25,49 @@ namespace Discord.Commands
ErrorReason = errorReason; ErrorReason = errorReason;
} }


public static ParseResult FromSuccess(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues)
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderResult> argValues,
IReadOnlyList<TypeReaderResult> paramValues)
{ {
for (int i = 0; i < argValues.Count; i++)
{
if (argValues[i].Values.Count > 1)
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.");
}
for (int i = 0; i < paramValues.Count; i++)
{
if (paramValues[i].Values.Count > 1)
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found.");
}
if (argValues.Any(t => t.Values.Count > 1))
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches,
"Multiple matches found.");
if (paramValues.Any(t => t.Values.Count > 1))
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches,
"Multiple matches found.");

return new ParseResult(argValues, paramValues, null, null); return new ParseResult(argValues, paramValues, null, null);
} }
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderValue> argValues, IReadOnlyList<TypeReaderValue> paramValues)

public static ParseResult FromSuccess(IReadOnlyList<TypeReaderValue> argValues,
IReadOnlyList<TypeReaderValue> paramValues)
{ {
var argList = new TypeReaderResult[argValues.Count]; var argList = new TypeReaderResult[argValues.Count];
for (int i = 0; i < argValues.Count; i++)
for (var i = 0; i < argValues.Count; i++)
argList[i] = TypeReaderResult.FromSuccess(argValues[i]); argList[i] = TypeReaderResult.FromSuccess(argValues[i]);
TypeReaderResult[] paramList = null;
if (paramValues != null)
TypeReaderResult[] paramList;
if (paramValues == null) return new ParseResult(argList, null, null, null);
{ {
paramList = new TypeReaderResult[paramValues.Count]; paramList = new TypeReaderResult[paramValues.Count];
for (int i = 0; i < paramValues.Count; i++)
for (var i = 0; i < paramValues.Count; i++)
paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]); paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]);
} }

return new ParseResult(argList, paramList, null, null); return new ParseResult(argList, paramList, null, null);
} }


public static ParseResult FromError(CommandError error, string reason) public static ParseResult FromError(CommandError error, string reason)
=> new ParseResult(null, null, error, reason); => new ParseResult(null, null, error, reason);

public static ParseResult FromError(Exception ex) public static ParseResult FromError(Exception ex)
=> FromError(CommandError.Exception, ex.Message); => FromError(CommandError.Exception, ex.Message);

public static ParseResult FromError(IResult result) public static ParseResult FromError(IResult result)
=> new ParseResult(null, null, result.Error, result.ErrorReason); => new ParseResult(null, null, result.Error, result.ErrorReason);


public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
private string DebuggerDisplay => IsSuccess ? $"Success ({ArgValues.Count}{(ParamValues.Count > 0 ? $" +{ParamValues.Count} Values" : "")})" : $"{Error}: {ErrorReason}";

private string DebuggerDisplay => IsSuccess
? $"Success ({ArgValues.Count}{(ParamValues.Count > 0 ? $" +{ParamValues.Count} Values" : "")})"
: $"{Error}: {ErrorReason}";
} }
} }

+ 12
- 8
src/Discord.Net.Commands/Results/PreconditionGroupResult.cs View File

@@ -4,27 +4,31 @@ using System.Diagnostics;


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

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


public static new PreconditionGroupResult FromSuccess()
public IReadOnlyCollection<PreconditionResult> PreconditionResults { get; }
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";

public new static PreconditionGroupResult FromSuccess()
=> new PreconditionGroupResult(null, null, null); => new PreconditionGroupResult(null, null, null);

public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> preconditions) public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> preconditions)
=> new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions); => new PreconditionGroupResult(CommandError.UnmetPrecondition, reason, preconditions);
public static new PreconditionGroupResult FromError(Exception ex)

public new static PreconditionGroupResult FromError(Exception ex)
=> new PreconditionGroupResult(CommandError.Exception, ex.Message, null); => new PreconditionGroupResult(CommandError.Exception, ex.Message, null);
public static new PreconditionGroupResult FromError(IResult result) //needed?

public new static PreconditionGroupResult FromError(IResult result) //needed?
=> new PreconditionGroupResult(result.Error, result.ErrorReason, null); => new PreconditionGroupResult(result.Error, result.ErrorReason, null);


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

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

@@ -3,30 +3,33 @@ using System.Diagnostics;


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

public bool IsSuccess => !Error.HasValue;

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


private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}";
public CommandError? Error { get; }
public string ErrorReason { get; }

public bool IsSuccess => !Error.HasValue;

public static PreconditionResult FromSuccess() public static PreconditionResult FromSuccess()
=> new PreconditionResult(null, null); => new PreconditionResult(null, null);

public static PreconditionResult FromError(string reason) public static PreconditionResult FromError(string reason)
=> new PreconditionResult(CommandError.UnmetPrecondition, reason); => new PreconditionResult(CommandError.UnmetPrecondition, reason);

public static PreconditionResult FromError(Exception ex) public static PreconditionResult FromError(Exception ex)
=> new PreconditionResult(CommandError.Exception, ex.Message); => new PreconditionResult(CommandError.Exception, ex.Message);

public static PreconditionResult FromError(IResult result) public static PreconditionResult FromError(IResult result)
=> new PreconditionResult(result.Error, result.ErrorReason); => new PreconditionResult(result.Error, result.ErrorReason);


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

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

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Diagnostics;


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


public CommandError? Error { get; }
public string Reason { get; } public string Reason { get; }
private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}";

public CommandError? Error { get; }


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


string IResult.ErrorReason => Reason; string IResult.ErrorReason => Reason;


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

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

@@ -4,7 +4,7 @@ using System.Diagnostics;


namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public struct SearchResult : IResult public struct SearchResult : IResult
{ {
public string Text { get; } public string Text { get; }
@@ -25,10 +25,13 @@ namespace Discord.Commands


public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands) public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands)
=> new SearchResult(text, commands, null, null); => new SearchResult(text, commands, null, null);

public static SearchResult FromError(CommandError error, string reason) public static SearchResult FromError(CommandError error, string reason)
=> new SearchResult(null, null, error, reason); => new SearchResult(null, null, error, reason);

public static SearchResult FromError(Exception ex) public static SearchResult FromError(Exception ex)
=> FromError(CommandError.Exception, ex.Message); => FromError(CommandError.Exception, ex.Message);

public static SearchResult FromError(IResult result) public static SearchResult FromError(IResult result)
=> new SearchResult(null, null, result.Error, result.ErrorReason); => new SearchResult(null, null, result.Error, result.ErrorReason);




+ 11
- 3
src/Discord.Net.Commands/Results/TypeReaderResult.cs View File

@@ -6,7 +6,7 @@ using System.Linq;


namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public struct TypeReaderValue public struct TypeReaderValue
{ {
public object Value { get; } public object Value { get; }
@@ -22,7 +22,7 @@ namespace Discord.Commands
private string DebuggerDisplay => $"[{Value}, {Math.Round(Score, 2)}]"; private string DebuggerDisplay => $"[{Value}, {Math.Round(Score, 2)}]";
} }


[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public struct TypeReaderResult : IResult public struct TypeReaderResult : IResult
{ {
public IReadOnlyCollection<TypeReaderValue> Values { get; } public IReadOnlyCollection<TypeReaderValue> Values { get; }
@@ -31,6 +31,7 @@ namespace Discord.Commands
public string ErrorReason { get; } public string ErrorReason { get; }


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

public object BestMatch => IsSuccess public object BestMatch => IsSuccess
? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value) ? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value)
: throw new InvalidOperationException("TypeReaderResult was not successful."); : throw new InvalidOperationException("TypeReaderResult was not successful.");
@@ -44,18 +45,25 @@ namespace Discord.Commands


public static TypeReaderResult FromSuccess(object value) public static TypeReaderResult FromSuccess(object value)
=> new TypeReaderResult(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null); => new TypeReaderResult(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null);

public static TypeReaderResult FromSuccess(TypeReaderValue value) public static TypeReaderResult FromSuccess(TypeReaderValue value)
=> new TypeReaderResult(ImmutableArray.Create(value), null, null); => new TypeReaderResult(ImmutableArray.Create(value), null, null);

public static TypeReaderResult FromSuccess(IReadOnlyCollection<TypeReaderValue> values) public static TypeReaderResult FromSuccess(IReadOnlyCollection<TypeReaderValue> values)
=> new TypeReaderResult(values, null, null); => new TypeReaderResult(values, null, null);

public static TypeReaderResult FromError(CommandError error, string reason) public static TypeReaderResult FromError(CommandError error, string reason)
=> new TypeReaderResult(null, error, reason); => new TypeReaderResult(null, error, reason);

public static TypeReaderResult FromError(Exception ex) public static TypeReaderResult FromError(Exception ex)
=> FromError(CommandError.Exception, ex.Message); => FromError(CommandError.Exception, ex.Message);

public static TypeReaderResult FromError(IResult result) public static TypeReaderResult FromError(IResult result)
=> new TypeReaderResult(null, result.Error, result.ErrorReason); => new TypeReaderResult(null, result.Error, result.ErrorReason);


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

private string DebuggerDisplay =>
IsSuccess ? $"Success ({string.Join(", ", Values)})" : $"{Error}: {ErrorReason}";
} }
} }

+ 71
- 82
src/Discord.Net.Commands/Utilities/QuotationAliasUtils.cs View File

@@ -1,95 +1,84 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
using System.Globalization;


namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> /// <summary>
/// Utility methods for generating matching pairs of unicode quotation marks for CommandServiceConfig
/// Utility methods for generating matching pairs of unicode quotation marks for CommandServiceConfig
/// </summary> /// </summary>
internal static class QuotationAliasUtils internal static class QuotationAliasUtils
{ {
/// <summary> /// <summary>
/// Generates an IEnumerable of characters representing open-close pairs of
/// quotation punctuation.
/// Generates an IEnumerable of characters representing open-close pairs of
/// quotation punctuation.
/// </summary> /// </summary>
internal static Dictionary<char, char> GetDefaultAliasMap
internal static Dictionary<char, char> GetDefaultAliasMap => new Dictionary<char, char>
{ {
get
{
// Output of a gist provided by https://gist.github.com/ufcpp
// https://gist.github.com/ufcpp/5b2cf9a9bf7d0b8743714a0b88f7edc5
// This was not used for the implementation because of incompatibility with netstandard1.1
return new Dictionary<char, char> {
{'\"', '\"' },
{'«', '»' },
{'‘', '’' },
{'“', '”' },
{'„', '‟' },
{'‹', '›' },
{'‚', '‛' },
{'《', '》' },
{'〈', '〉' },
{'「', '」' },
{'『', '』' },
{'〝', '〞' },
{'﹁', '﹂' },
{'﹃', '﹄' },
{'"', '"' },
{''', ''' },
{'「', '」' },
{'(', ')' },
{'༺', '༻' },
{'༼', '༽' },
{'᚛', '᚜' },
{'⁅', '⁆' },
{'⌈', '⌉' },
{'⌊', '⌋' },
{'❨', '❩' },
{'❪', '❫' },
{'❬', '❭' },
{'❮', '❯' },
{'❰', '❱' },
{'❲', '❳' },
{'❴', '❵' },
{'⟅', '⟆' },
{'⟦', '⟧' },
{'⟨', '⟩' },
{'⟪', '⟫' },
{'⟬', '⟭' },
{'⟮', '⟯' },
{'⦃', '⦄' },
{'⦅', '⦆' },
{'⦇', '⦈' },
{'⦉', '⦊' },
{'⦋', '⦌' },
{'⦍', '⦎' },
{'⦏', '⦐' },
{'⦑', '⦒' },
{'⦓', '⦔' },
{'⦕', '⦖' },
{'⦗', '⦘' },
{'⧘', '⧙' },
{'⧚', '⧛' },
{'⧼', '⧽' },
{'⸂', '⸃' },
{'⸄', '⸅' },
{'⸉', '⸊' },
{'⸌', '⸍' },
{'⸜', '⸝' },
{'⸠', '⸡' },
{'⸢', '⸣' },
{'⸤', '⸥' },
{'⸦', '⸧' },
{'⸨', '⸩' },
{'【', '】'},
{'〔', '〕' },
{'〖', '〗' },
{'〘', '〙' },
{'〚', '〛' }
};
}
}
{'\"', '\"'},
{'«', '»'},
{'‘', '’'},
{'“', '”'},
{'„', '‟'},
{'‹', '›'},
{'‚', '‛'},
{'《', '》'},
{'〈', '〉'},
{'「', '」'},
{'『', '』'},
{'〝', '〞'},
{'﹁', '﹂'},
{'﹃', '﹄'},
{'"', '"'},
{''', '''},
{'「', '」'},
{'(', ')'},
{'༺', '༻'},
{'༼', '༽'},
{'᚛', '᚜'},
{'⁅', '⁆'},
{'⌈', '⌉'},
{'⌊', '⌋'},
{'❨', '❩'},
{'❪', '❫'},
{'❬', '❭'},
{'❮', '❯'},
{'❰', '❱'},
{'❲', '❳'},
{'❴', '❵'},
{'⟅', '⟆'},
{'⟦', '⟧'},
{'⟨', '⟩'},
{'⟪', '⟫'},
{'⟬', '⟭'},
{'⟮', '⟯'},
{'⦃', '⦄'},
{'⦅', '⦆'},
{'⦇', '⦈'},
{'⦉', '⦊'},
{'⦋', '⦌'},
{'⦍', '⦎'},
{'⦏', '⦐'},
{'⦑', '⦒'},
{'⦓', '⦔'},
{'⦕', '⦖'},
{'⦗', '⦘'},
{'⧘', '⧙'},
{'⧚', '⧛'},
{'⧼', '⧽'},
{'⸂', '⸃'},
{'⸄', '⸅'},
{'⸉', '⸊'},
{'⸌', '⸍'},
{'⸜', '⸝'},
{'⸠', '⸡'},
{'⸢', '⸣'},
{'⸤', '⸥'},
{'⸦', '⸧'},
{'⸨', '⸩'},
{'【', '】'},
{'〔', '〕'},
{'〖', '〗'},
{'〘', '〙'},
{'〚', '〛'}
};
} }
} }

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

@@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.DependencyInjection;


namespace Discord.Commands namespace Discord.Commands
{ {
@@ -12,24 +11,26 @@ namespace Discord.Commands


internal static T CreateObject<T>(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null) internal static T CreateObject<T>(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null)
=> CreateBuilder<T>(typeInfo, commands)(services); => CreateBuilder<T>(typeInfo, commands)(services);

internal static Func<IServiceProvider, T> CreateBuilder<T>(TypeInfo typeInfo, CommandService commands) internal static Func<IServiceProvider, T> CreateBuilder<T>(TypeInfo typeInfo, CommandService commands)
{ {
var constructor = GetConstructor(typeInfo); var constructor = GetConstructor(typeInfo);
var parameters = constructor.GetParameters(); var parameters = constructor.GetParameters();
var properties = GetProperties(typeInfo); var properties = GetProperties(typeInfo);


return (services) =>
return services =>
{ {
var args = new object[parameters.Length]; var args = new object[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
for (var i = 0; i < parameters.Length; i++)
args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo); args[i] = GetMember(commands, services, parameters[i].ParameterType, typeInfo);
var obj = InvokeConstructor<T>(constructor, args, typeInfo); var obj = InvokeConstructor<T>(constructor, args, typeInfo);


foreach(var property in properties)
foreach (var property in properties)
property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo)); property.SetValue(obj, GetMember(commands, services, property.PropertyType, typeInfo));
return obj; return obj;
}; };
} }

private static T InvokeConstructor<T>(ConstructorInfo constructor, object[] args, TypeInfo ownerType) private static T InvokeConstructor<T>(ConstructorInfo constructor, object[] args, TypeInfo ownerType)
{ {
try try
@@ -47,34 +48,37 @@ namespace Discord.Commands
var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray();
if (constructors.Length == 0) if (constructors.Length == 0)
throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\""); throw new InvalidOperationException($"No constructor found for \"{ownerType.FullName}\"");
else if (constructors.Length > 1)
if (constructors.Length > 1)
throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\""); throw new InvalidOperationException($"Multiple constructors found for \"{ownerType.FullName}\"");
return constructors[0]; return constructors[0];
} }
private static System.Reflection.PropertyInfo[] GetProperties(TypeInfo ownerType)

private static PropertyInfo[] GetProperties(TypeInfo ownerType)
{ {
var result = new List<System.Reflection.PropertyInfo>();
var result = new List<PropertyInfo>();
while (ownerType != _objectTypeInfo) while (ownerType != _objectTypeInfo)
{ {
foreach (var prop in ownerType.DeclaredProperties)
{
if (prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true && prop.GetCustomAttribute<DontInjectAttribute>() == null)
result.Add(prop);
}
result.AddRange(ownerType.DeclaredProperties.Where(prop =>
prop.SetMethod?.IsStatic == false && prop.SetMethod?.IsPublic == true &&
prop.GetCustomAttribute<DontInjectAttribute>() == null));
ownerType = ownerType.BaseType.GetTypeInfo(); ownerType = ownerType.BaseType.GetTypeInfo();
} }

return result.ToArray(); return result.ToArray();
} }
private static object GetMember(CommandService commands, IServiceProvider services, Type memberType, TypeInfo ownerType)

private static object GetMember(CommandService commands, IServiceProvider services, Type memberType,
TypeInfo ownerType)
{ {
if (memberType == typeof(CommandService)) if (memberType == typeof(CommandService))
return commands; return commands;
if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) if (memberType == typeof(IServiceProvider) || memberType == services.GetType())
return services; return services;
var service = services?.GetService(memberType);
var service = services.GetService(memberType);
if (service != null) if (service != null)
return service; return service;
throw new InvalidOperationException($"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found.");
throw new InvalidOperationException(
$"Failed to create \"{ownerType.FullName}\", dependency \"{memberType.Name}\" was not found.");
} }
} }
} }

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

@@ -6,4 +6,4 @@
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")]
[assembly: InternalsVisibleTo("Discord.Net.Webhook")] [assembly: InternalsVisibleTo("Discord.Net.Webhook")]
[assembly: InternalsVisibleTo("Discord.Net.Commands")] [assembly: InternalsVisibleTo("Discord.Net.Commands")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]
[assembly: InternalsVisibleTo("Discord.Net.Tests")]

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

@@ -1,9 +1,9 @@
namespace Discord.Audio namespace Discord.Audio
{ {
public enum AudioApplication : int
public enum AudioApplication
{ {
Voice, Voice,
Music, Music,
Mixed Mixed
} }
}
}

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

@@ -9,11 +9,11 @@ namespace Discord.Audio
public abstract int AvailableFrames { get; } public abstract int AvailableFrames { get; }


public override bool CanRead => true; public override bool CanRead => true;
public override bool CanWrite => true;
public override bool CanWrite => true;


public abstract Task<RTPFrame> ReadFrameAsync(CancellationToken cancelToken); public abstract Task<RTPFrame> ReadFrameAsync(CancellationToken cancelToken);
public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame); public abstract bool TryReadFrame(CancellationToken cancelToken, out RTPFrame frame);


public override Task FlushAsync(CancellationToken cancelToken) { throw new NotSupportedException(); }
public override Task FlushAsync(CancellationToken cancelToken) => throw new NotSupportedException();
} }
} }

+ 3
- 3
src/Discord.Net.Core/Audio/AudioOutStream.cs View File

@@ -7,8 +7,8 @@ namespace Discord.Audio
{ {
public override bool CanWrite => true; public override bool CanWrite => true;


public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
} }
} }

+ 18
- 24
src/Discord.Net.Core/Audio/AudioStream.cs View File

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


public virtual void WriteHeader(ushort seq, uint timestamp, bool missed)
{
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();
}
public override void Flush()
{
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public void Clear()
{
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public override long Length => throw new NotSupportedException();


public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); }

public override long Length { get { throw new NotSupportedException(); } }
public override long Position public override long Position
{ {
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
get => throw new NotSupportedException();
set => throw new NotSupportedException();
} }


public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
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();

public override void Flush() => FlushAsync(CancellationToken.None).GetAwaiter().GetResult();

public void Clear() => ClearAsync(CancellationToken.None).GetAwaiter().GetResult();

public virtual Task ClearAsync(CancellationToken cancellationToken) => Task.Delay(0);

public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
} }
} }

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

@@ -5,6 +5,15 @@ namespace Discord.Audio
{ {
public interface IAudioClient : IDisposable public interface IAudioClient : IDisposable
{ {
/// <summary> Gets the current connection state of this client. </summary>
ConnectionState ConnectionState { get; }

/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. </summary>
int Latency { get; }

/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }

event Func<Task> Connected; event Func<Task> Connected;
event Func<Exception, Task> Disconnected; event Func<Exception, Task> Disconnected;
event Func<int, int, Task> LatencyUpdated; event Func<int, int, Task> LatencyUpdated;
@@ -13,22 +22,19 @@ namespace Discord.Audio
event Func<ulong, Task> StreamDestroyed; event Func<ulong, Task> StreamDestroyed;
event Func<ulong, bool, Task> SpeakingUpdated; event Func<ulong, bool, Task> SpeakingUpdated;


/// <summary> Gets the current connection state of this client. </summary>
ConnectionState ConnectionState { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice websocket server. </summary>
int Latency { get; }
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the voice UDP server. </summary>
int UdpLatency { get; }

Task StopAsync(); Task StopAsync();
Task SetSpeakingAsync(bool value); Task SetSpeakingAsync(bool value);


/// <summary>Creates a new outgoing stream accepting Opus-encoded data.</summary> /// <summary>Creates a new outgoing stream accepting Opus-encoded data.</summary>
AudioOutStream CreateOpusStream(int bufferMillis = 1000); AudioOutStream CreateOpusStream(int bufferMillis = 1000);

/// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary> /// <summary>Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.</summary>
AudioOutStream CreateDirectOpusStream(); AudioOutStream CreateDirectOpusStream();

/// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary> /// <summary>Creates a new outgoing stream accepting PCM (raw) data.</summary>
AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000, int packetLoss = 30);
AudioOutStream CreatePCMStream(AudioApplication application, int? bitrate = null, int bufferMillis = 1000,
int packetLoss = 30);

/// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary> /// <summary>Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.</summary>
AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30);
} }


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

@@ -15,4 +15,4 @@ namespace Discord.Audio
Missed = missed; Missed = missed;
} }
} }
}
}

+ 11
- 6
src/Discord.Net.Core/CDN.cs View File

@@ -6,34 +6,39 @@ namespace Discord
{ {
public static string GetApplicationIconUrl(ulong appId, string iconId) public static string GetApplicationIconUrl(ulong appId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;

public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format)
{ {
if (avatarId == null) if (avatarId == null)
return null; return null;
string extension = FormatToExtension(format, avatarId);
var extension = FormatToExtension(format, avatarId);
return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}"; return $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{extension}?size={size}";
} }
public static string GetDefaultUserAvatarUrl(ushort discriminator)
{
return $"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png";
}
public static string GetDefaultUserAvatarUrl(ushort discriminator) =>
$"{DiscordConfig.CDNUrl}embed/avatars/{discriminator % 5}.png";
public static string GetGuildIconUrl(ulong guildId, string iconId) public static string GetGuildIconUrl(ulong guildId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;

public static string GetGuildSplashUrl(ulong guildId, string splashId) public static string GetGuildSplashUrl(ulong guildId, string splashId)
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; => splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null;

public static string GetChannelIconUrl(ulong channelId, string iconId) public static string GetChannelIconUrl(ulong channelId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; => iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null;

public static string GetEmojiUrl(ulong emojiId, bool animated) public static string GetEmojiUrl(ulong emojiId, bool animated)
=> $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}";


public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format)
{ {
string extension = FormatToExtension(format, "");
var extension = FormatToExtension(format, "");
return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}";
} }


public static string GetSpotifyAlbumArtUrl(string albumArtId) public static string GetSpotifyAlbumArtUrl(string albumArtId)
=> $"https://i.scdn.co/image/{albumArtId}"; => $"https://i.scdn.co/image/{albumArtId}";

public static string GetSpotifyDirectUrl(string trackId) public static string GetSpotifyDirectUrl(string trackId)
=> $"https://open.spotify.com/track/{trackId}"; => $"https://open.spotify.com/track/{trackId}";




+ 10
- 7
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -6,13 +6,6 @@ namespace Discord
{ {
public const int APIVersion = 6; public const int APIVersion = 6;
public const int VoiceAPIVersion = 3; public const int VoiceAPIVersion = 3;
public static string Version { get; } =
typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ??
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
"Unknown";

public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/";
public const string CDNUrl = "https://cdn.discordapp.com/"; public const string CDNUrl = "https://cdn.discordapp.com/";
public const string InviteUrl = "https://discord.gg/"; public const string InviteUrl = "https://discord.gg/";


@@ -23,6 +16,16 @@ namespace Discord
public const int MaxGuildsPerBatch = 100; public const int MaxGuildsPerBatch = 100;
public const int MaxUserReactionsPerBatch = 100; public const int MaxUserReactionsPerBatch = 100;
public const int MaxAuditLogEntriesPerBatch = 100; public const int MaxAuditLogEntriesPerBatch = 100;
public static readonly string APIUrl = $"https://discordapp.com/api/v{APIVersion}/";

public static string Version { get; } =
typeof(DiscordConfig).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ??
typeof(DiscordConfig).GetTypeInfo().Assembly.GetName().Version.ToString(3) ??
"Unknown";

public static string UserAgent { get; } =
$"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})";


/// <summary> Gets or sets how a request should act in the case of an error, by default. </summary> /// <summary> Gets or sets how a request should act in the case of an error, by default. </summary>
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;


+ 9
- 6
src/Discord.Net.Core/Entities/Activities/Game.cs View File

@@ -2,20 +2,23 @@ using System.Diagnostics;


namespace Discord namespace Discord
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public class Game : IActivity public class Game : IActivity
{ {
public string Name { get; internal set; }
public ActivityType Type { get; internal set; }
internal Game()
{
}


internal Game() { }
public Game(string name, ActivityType type = ActivityType.Playing) public Game(string name, ActivityType type = ActivityType.Playing)
{ {
Name = name; Name = name;
Type = type; Type = type;
} }
public override string ToString() => Name;

private string DebuggerDisplay => Name; private string DebuggerDisplay => Name;
public string Name { get; internal set; }
public ActivityType Type { get; internal set; }

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

+ 5
- 3
src/Discord.Net.Core/Entities/Activities/GameAsset.cs View File

@@ -2,13 +2,15 @@ namespace Discord
{ {
public class GameAsset public class GameAsset
{ {
internal GameAsset() { }
internal GameAsset()
{
}


internal ulong? ApplicationId { get; set; } internal ulong? ApplicationId { get; set; }
public string Text { get; internal set; } public string Text { get; internal set; }
public string ImageId { get; internal set; } public string ImageId { get; internal set; }
public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128)
=> ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; => ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null;
} }


+ 3
- 1
src/Discord.Net.Core/Entities/Activities/GameParty.cs View File

@@ -2,7 +2,9 @@ namespace Discord
{ {
public class GameParty public class GameParty
{ {
internal GameParty() { }
internal GameParty()
{
}


public string Id { get; internal set; } public string Id { get; internal set; }
public long Members { get; internal set; } public long Members { get; internal set; }


+ 5
- 5
src/Discord.Net.Core/Entities/Activities/GameSecrets.cs View File

@@ -2,15 +2,15 @@
{ {
public class GameSecrets public class GameSecrets
{ {
public string Match { get; }
public string Join { get; }
public string Spectate { get; }

internal GameSecrets(string match, string join, string spectate) internal GameSecrets(string match, string join, string spectate)
{ {
Match = match; Match = match;
Join = join; Join = join;
Spectate = spectate; Spectate = spectate;
} }

public string Match { get; }
public string Join { get; }
public string Spectate { get; }
} }
}
}

+ 4
- 4
src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs View File

@@ -4,13 +4,13 @@ namespace Discord
{ {
public class GameTimestamps public class GameTimestamps
{ {
public DateTimeOffset? Start { get; }
public DateTimeOffset? End { get; }

internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end)
{ {
Start = start; Start = start;
End = end; End = end;
} }

public DateTimeOffset? Start { get; }
public DateTimeOffset? End { get; }
} }
}
}

+ 6
- 4
src/Discord.Net.Core/Entities/Activities/RichGame.cs View File

@@ -2,10 +2,12 @@ using System.Diagnostics;


namespace Discord namespace Discord
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public class RichGame : Game public class RichGame : Game
{ {
internal RichGame() { }
internal RichGame()
{
}


public string Details { get; internal set; } public string Details { get; internal set; }
public string State { get; internal set; } public string State { get; internal set; }
@@ -15,8 +17,8 @@ namespace Discord
public GameParty Party { get; internal set; } public GameParty Party { get; internal set; }
public GameSecrets Secrets { get; internal set; } public GameSecrets Secrets { get; internal set; }
public GameTimestamps Timestamps { get; internal set; } public GameTimestamps Timestamps { get; internal set; }
public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} (Rich)"; private string DebuggerDisplay => $"{Name} (Rich)";

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

+ 6
- 4
src/Discord.Net.Core/Entities/Activities/SpotifyGame.cs View File

@@ -4,9 +4,13 @@ using System.Diagnostics;


namespace Discord namespace Discord
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public class SpotifyGame : Game public class SpotifyGame : Game
{ {
internal SpotifyGame()
{
}

public IReadOnlyCollection<string> Artists { get; internal set; } public IReadOnlyCollection<string> Artists { get; internal set; }
public string AlbumTitle { get; internal set; } public string AlbumTitle { get; internal set; }
public string TrackTitle { get; internal set; } public string TrackTitle { get; internal set; }
@@ -17,10 +21,8 @@ namespace Discord


public string AlbumArtUrl { get; internal set; } public string AlbumArtUrl { get; internal set; }
public string TrackUrl { get; internal set; } public string TrackUrl { get; internal set; }

internal SpotifyGame() { }
private string DebuggerDisplay => $"{Name} (Spotify)";


public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})"; public override string ToString() => $"{string.Join(", ", Artists)} - {TrackTitle} ({Duration})";
private string DebuggerDisplay => $"{Name} (Spotify)";
} }
} }

+ 5
- 5
src/Discord.Net.Core/Entities/Activities/StreamingGame.cs View File

@@ -2,11 +2,9 @@


namespace Discord namespace Discord
{ {
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")]
public class StreamingGame : Game public class StreamingGame : Game
{ {
public string Url { get; internal set; }

public StreamingGame(string name, string url) public StreamingGame(string name, string url)
{ {
Name = name; Name = name;
@@ -14,7 +12,9 @@ namespace Discord
Type = ActivityType.Streaming; Type = ActivityType.Streaming;
} }


public override string ToString() => Name;
public string Url { get; internal set; }
private string DebuggerDisplay => $"{Name} ({Url})"; private string DebuggerDisplay => $"{Name} ({Url})";

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

+ 2
- 8
src/Discord.Net.Core/Entities/AuditLogs/ActionType.cs View File

@@ -1,13 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
namespace Discord
{ {
/// <summary> /// <summary>
/// The action type within a <see cref="IAuditLogEntry"/>
/// The action type within a <see cref="IAuditLogEntry" />
/// </summary> /// </summary>
public enum ActionType public enum ActionType
{ {


+ 4
- 9
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogData.cs View File

@@ -1,14 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord
namespace Discord
{ {
/// <summary> /// <summary>
/// Represents data applied to an <see cref="IAuditLogEntry"/>
/// Represents data applied to an <see cref="IAuditLogEntry" />
/// </summary> /// </summary>
public interface IAuditLogData public interface IAuditLogData
{ }
{
}
} }

+ 5
- 11
src/Discord.Net.Core/Entities/AuditLogs/IAuditLogEntry.cs View File

@@ -1,33 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Represents an entry in an audit log
/// Represents an entry in an audit log
/// </summary> /// </summary>
public interface IAuditLogEntry : ISnowflakeEntity public interface IAuditLogEntry : ISnowflakeEntity
{ {
/// <summary> /// <summary>
/// The action which occured to create this entry
/// The action which occured to create this entry
/// </summary> /// </summary>
ActionType Action { get; } ActionType Action { get; }


/// <summary> /// <summary>
/// The data for this entry. May be <see cref="null"/> if no data was available.
/// The data for this entry. May be <see cref="null" /> if no data was available.
/// </summary> /// </summary>
IAuditLogData Data { get; } IAuditLogData Data { get; }


/// <summary> /// <summary>
/// The user responsible for causing the changes
/// The user responsible for causing the changes
/// </summary> /// </summary>
IUser User { get; } IUser User { get; }


/// <summary> /// <summary>
/// The reason behind the change. May be <see cref="null"/> if no reason was provided.
/// The reason behind the change. May be <see cref="null" /> if no reason was provided.
/// </summary> /// </summary>
string Reason { get; } string Reason { get; }
} }


+ 9
- 7
src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs View File

@@ -1,10 +1,10 @@
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// Modify an IGuildChannel with the specified changes.
/// Modify an IGuildChannel with the specified changes.
/// </summary> /// </summary>
/// <example> /// <example>
/// <code language="c#">
/// <code language="c#">
/// await (Context.Channel as ITextChannel)?.ModifyAsync(x => /// await (Context.Channel as ITextChannel)?.ModifyAsync(x =>
/// { /// {
/// x.Name = "do-not-enter"; /// x.Name = "do-not-enter";
@@ -14,20 +14,22 @@
public class GuildChannelProperties public class GuildChannelProperties
{ {
/// <summary> /// <summary>
/// Set the channel to this name
/// Set the channel to this name
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes.
/// It must match the following RegEx: [a-z0-9-_]{2,100}
/// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes.
/// It must match the following RegEx: [a-z0-9-_]{2,100}
/// </remarks> /// </remarks>
/// <exception cref="Net.HttpException">A BadRequest will be thrown if the name does not match the above RegEx.</exception> /// <exception cref="Net.HttpException">A BadRequest will be thrown if the name does not match the above RegEx.</exception>
public Optional<string> Name { get; set; } public Optional<string> Name { get; set; }

/// <summary> /// <summary>
/// Move the channel to the following position. This is 0-based!
/// Move the channel to the following position. This is 0-based!
/// </summary> /// </summary>
public Optional<int> Position { get; set; } public Optional<int> Position { get; set; }

/// <summary> /// <summary>
/// Sets the category for this channel
/// Sets the category for this channel
/// </summary> /// </summary>
public Optional<ulong?> CategoryId { get; set; } public Optional<ulong?> CategoryId { get; set; }
} }


+ 1
- 2
src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs View File

@@ -1,6 +1,5 @@
using Discord.Audio;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Discord.Audio;


namespace Discord namespace Discord
{ {


+ 0
- 6
src/Discord.Net.Core/Entities/Channels/ICategoryChannel.cs View File

@@ -1,9 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord namespace Discord
{ {
public interface ICategoryChannel : IGuildChannel public interface ICategoryChannel : IGuildChannel


+ 4
- 3
src/Discord.Net.Core/Entities/Channels/IChannel.cs View File

@@ -7,10 +7,11 @@ namespace Discord
{ {
/// <summary> Gets the name of this channel. </summary> /// <summary> Gets the name of this channel. </summary>
string Name { get; } string Name { get; }
/// <summary> Gets a collection of all users in this channel. </summary> /// <summary> Gets a collection of all users in this channel. </summary>
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
IAsyncEnumerable<IReadOnlyCollection<IUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload,
RequestOptions options = null);

/// <summary> Gets a user in this channel with the provided id. </summary> /// <summary> Gets a user in this channel with the provided id. </summary>
Task<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); Task<IUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
} }


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

@@ -10,4 +10,4 @@ namespace Discord
/// <summary> Closes this private channel, removing it from your channel list. </summary> /// <summary> Closes this private channel, removing it from your channel list. </summary>
Task CloseAsync(RequestOptions options = null); Task CloseAsync(RequestOptions options = null);
} }
}
}

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

@@ -7,4 +7,4 @@ namespace Discord
/// <summary> Leaves this group. </summary> /// <summary> Leaves this group. </summary>
Task LeaveAsync(RequestOptions options = null); Task LeaveAsync(RequestOptions options = null);
} }
}
}

+ 19
- 4
src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs View File

@@ -11,16 +11,23 @@ namespace Discord


/// <summary> Gets the guild this channel is a member of. </summary> /// <summary> Gets the guild this channel is a member of. </summary>
IGuild Guild { get; } IGuild Guild { get; }

/// <summary> Gets the id of the guild this channel is a member of. </summary> /// <summary> Gets the id of the guild this channel is a member of. </summary>
ulong GuildId { get; } ulong GuildId { get; }

/// <summary> Gets a collection of permission overwrites for this channel. </summary> /// <summary> Gets a collection of permission overwrites for this channel. </summary>
IReadOnlyCollection<Overwrite> PermissionOverwrites { get; } IReadOnlyCollection<Overwrite> PermissionOverwrites { get; }


/// <summary> Creates a new invite to this channel. </summary> /// <summary> Creates a new invite to this channel. </summary>
/// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param> /// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param>
Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null);
/// <param name="isTemporary">
/// If true, a user accepting this invite will be kicked from the guild after closing their
/// client.
/// </param>
Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 86400, int? maxUses = default(int?),
bool isTemporary = false, bool isUnique = false, RequestOptions options = null);

/// <summary> Returns a collection of all invites to this channel. </summary> /// <summary> Returns a collection of all invites to this channel. </summary>
Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null); Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null);


@@ -29,20 +36,28 @@ namespace Discord


/// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary> /// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary>
OverwritePermissions? GetPermissionOverwrite(IRole role); OverwritePermissions? GetPermissionOverwrite(IRole role);

/// <summary> Gets the permission overwrite for a specific user, or null if one does not exist. </summary> /// <summary> Gets the permission overwrite for a specific user, or null if one does not exist. </summary>
OverwritePermissions? GetPermissionOverwrite(IUser user); OverwritePermissions? GetPermissionOverwrite(IUser user);

/// <summary> Removes the permission overwrite for the given role, if one exists. </summary> /// <summary> Removes the permission overwrite for the given role, if one exists. </summary>
Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null);

/// <summary> Removes the permission overwrite for the given user, if one exists. </summary> /// <summary> Removes the permission overwrite for the given user, if one exists. </summary>
Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null);

/// <summary> Adds or updates the permission overwrite for the given role. </summary> /// <summary> Adds or updates the permission overwrite for the given role. </summary>
Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null);

/// <summary> Adds or updates the permission overwrite for the given user. </summary> /// <summary> Adds or updates the permission overwrite for the given user. </summary>
Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null);


/// <summary> Gets a collection of all users in this channel. </summary> /// <summary> Gets a collection of all users in this channel. </summary>
new IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
new IAsyncEnumerable<IReadOnlyCollection<IGuildUser>> GetUsersAsync(CacheMode mode = CacheMode.AllowDownload,
RequestOptions options = null);

/// <summary> Gets a user in this channel with the provided id.</summary> /// <summary> Gets a user in this channel with the provided id.</summary>
new Task<IGuildUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
new Task<IGuildUser> GetUserAsync(ulong id, CacheMode mode = CacheMode.AllowDownload,
RequestOptions options = null);
} }
} }

+ 24
- 8
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -8,35 +8,51 @@ namespace Discord
public interface IMessageChannel : IChannel public interface IMessageChannel : IChannel
{ {
/// <summary> Sends a message to this message channel. </summary> /// <summary> Sends a message to this message channel. </summary>
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
Task<IUserMessage> SendMessageAsync(string text = null, bool isTTS = false, Embed embed = null,
RequestOptions options = null);

/// <summary> Sends a file to this text channel, with an optional caption. </summary> /// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, Embed embed = null,
RequestOptions options = null);


/// <summary> Sends a file to this text channel, with an optional caption. </summary> /// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, Embed embed = null, RequestOptions options = null);
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false,
Embed embed = null, RequestOptions options = null);


/// <summary> Gets a message from this message channel with the given id, or null if not found. </summary> /// <summary> Gets a message from this message channel with the given id, or null if not found. </summary>
Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
Task<IMessage> GetMessageAsync(ulong id, CacheMode mode = CacheMode.AllowDownload,
RequestOptions options = null);

/// <summary> Gets the last N messages from this message channel. </summary> /// <summary> Gets the last N messages from this message channel. </summary>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch,
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary> Gets a collection of messages in this channel. </summary> /// <summary> Gets a collection of messages in this channel. </summary>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch,
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(ulong fromMessageId, Direction dir,
int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary> Gets a collection of messages in this channel. </summary> /// <summary> Gets a collection of messages in this channel. </summary>
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch,
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(IMessage fromMessage, Direction dir,
int limit = DiscordConfig.MaxMessagesPerBatch,
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);

/// <summary> Gets a collection of pinned messages in this channel. </summary> /// <summary> Gets a collection of pinned messages in this channel. </summary>
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null); Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null);


/// <summary> Deletes a message based on the message ID in this channel. </summary> /// <summary> Deletes a message based on the message ID in this channel. </summary>
Task DeleteMessageAsync(ulong messageId, RequestOptions options = null); Task DeleteMessageAsync(ulong messageId, RequestOptions options = null);

/// <summary> Deletes a message based on the provided message in this channel. </summary> /// <summary> Deletes a message based on the provided message in this channel. </summary>
Task DeleteMessageAsync(IMessage message, RequestOptions options = null); Task DeleteMessageAsync(IMessage message, RequestOptions options = null);


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

/// <summary>
/// Continuously broadcasts the "user is typing" message to all users in this channel until the returned object
/// is disposed.
/// </summary>
IDisposable EnterTypingState(RequestOptions options = null); IDisposable EnterTypingState(RequestOptions options = null);
} }
} }

+ 5
- 3
src/Discord.Net.Core/Entities/Channels/INestedChannel.cs View File

@@ -3,14 +3,16 @@ using System.Threading.Tasks;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// A type of guild channel that can be nested within a category.
/// Contains a CategoryId that is set to the parent category, if it is set.
/// A type of guild channel that can be nested within a category.
/// Contains a CategoryId that is set to the parent category, if it is set.
/// </summary> /// </summary>
public interface INestedChannel : IGuildChannel public interface INestedChannel : IGuildChannel
{ {
/// <summary> Gets the parentid (category) of this channel in the guild's channel list. </summary> /// <summary> Gets the parentid (category) of this channel in the guild's channel list. </summary>
ulong? CategoryId { get; } ulong? CategoryId { get; }

/// <summary> Gets the parent channel (category) of this channel, if it is set. If unset, returns null.</summary> /// <summary> Gets the parent channel (category) of this channel, if it is set. If unset, returns null.</summary>
Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null);
Task<ICategoryChannel> GetCategoryAsync(CacheMode mode = CacheMode.AllowDownload,
RequestOptions options = null);
} }
} }

+ 3
- 0
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -15,6 +15,7 @@ namespace Discord


/// <summary> Bulk deletes multiple messages. </summary> /// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null); Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null);

/// <summary> Bulk deletes multiple messages. </summary> /// <summary> Bulk deletes multiple messages. </summary>
Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null); Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null);


@@ -23,8 +24,10 @@ namespace Discord


/// <summary> Creates a webhook in this text channel. </summary> /// <summary> Creates a webhook in this text channel. </summary>
Task<IWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null); Task<IWebhook> CreateWebhookAsync(string name, Stream avatar = null, RequestOptions options = null);

/// <summary> Gets the webhook in this text channel with the provided id, or null if not found. </summary> /// <summary> Gets the webhook in this text channel with the provided id, or null if not found. </summary>
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null); Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null);

/// <summary> Gets the webhooks for this text channel. </summary> /// <summary> Gets the webhooks for this text channel. </summary>
Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null); Task<IReadOnlyCollection<IWebhook>> GetWebhooksAsync(RequestOptions options = null);
} }


+ 1
- 0
src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs View File

@@ -7,6 +7,7 @@ namespace Discord
{ {
/// <summary> Gets the bitrate, in bits per second, clients in this voice channel are requested to use. </summary> /// <summary> Gets the bitrate, in bits per second, clients in this voice channel are requested to use. </summary>
int Bitrate { get; } int Bitrate { get; }

/// <summary> Gets the max amount of users allowed to be connected to this channel at one time. </summary> /// <summary> Gets the max amount of users allowed to be connected to this channel at one time. </summary>
int? UserLimit { get; } int? UserLimit { get; }




+ 6
- 5
src/Discord.Net.Core/Entities/Channels/ReorderChannelProperties.cs View File

@@ -2,15 +2,16 @@
{ {
public class ReorderChannelProperties public class ReorderChannelProperties
{ {
/// <summary>The id of the channel to apply this position to.</summary>
public ulong Id { get; }
/// <summary>The new zero-based position of this channel. </summary>
public int Position { get; }

public ReorderChannelProperties(ulong id, int position) public ReorderChannelProperties(ulong id, int position)
{ {
Id = id; Id = id;
Position = position; Position = position;
} }

/// <summary>The id of the channel to apply this position to.</summary>
public ulong Id { get; }

/// <summary>The new zero-based position of this channel. </summary>
public int Position { get; }
} }
} }

+ 3
- 2
src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs View File

@@ -4,11 +4,12 @@
public class TextChannelProperties : GuildChannelProperties public class TextChannelProperties : GuildChannelProperties
{ {
/// <summary> /// <summary>
/// What the topic of the channel should be set to.
/// What the topic of the channel should be set to.
/// </summary> /// </summary>
public Optional<string> Topic { get; set; } public Optional<string> Topic { get; set; }

/// <summary> /// <summary>
/// Should this channel be flagged as NSFW?
/// Should this channel be flagged as NSFW?
/// </summary> /// </summary>
public Optional<bool> IsNsfw { get; set; } public Optional<bool> IsNsfw { get; set; }
} }


+ 3
- 2
src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs View File

@@ -4,11 +4,12 @@
public class VoiceChannelProperties : GuildChannelProperties public class VoiceChannelProperties : GuildChannelProperties
{ {
/// <summary> /// <summary>
/// The bitrate of the voice connections in this channel. Must be greater than 8000
/// The bitrate of the voice connections in this channel. Must be greater than 8000
/// </summary> /// </summary>
public Optional<int> Bitrate { get; set; } public Optional<int> Bitrate { get; set; }

/// <summary> /// <summary>
/// The maximum number of users that can be present in a channel.
/// The maximum number of users that can be present in a channel.
/// </summary> /// </summary>
public Optional<int?> UserLimit { get; set; } public Optional<int?> UserLimit { get; set; }
} }


+ 10
- 11
src/Discord.Net.Core/Entities/Emotes/Emoji.cs View File

@@ -1,27 +1,26 @@
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// A unicode emoji
/// A unicode emoji
/// </summary> /// </summary>
public class Emoji : IEmote 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> /// <summary>
/// Creates a unicode emoji.
/// Creates a unicode emoji.
/// </summary> /// </summary>
/// <param name="unicode">The pure UTF-8 encoding of an emoji</param> /// <param name="unicode">The pure UTF-8 encoding of an emoji</param>
public Emoji(string unicode) public Emoji(string unicode)
{ {
Name = unicode; Name = unicode;
} }
// 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;


public override bool Equals(object other) public override bool Equals(object other)
{ {


+ 36
- 33
src/Discord.Net.Core/Entities/Emotes/Emote.cs View File

@@ -4,31 +4,37 @@ using System.Globalization;
namespace Discord namespace Discord
{ {
/// <summary> /// <summary>
/// A custom image-based emote
/// A custom image-based emote
/// </summary> /// </summary>
public class Emote : IEmote, ISnowflakeEntity public class Emote : IEmote, ISnowflakeEntity
{ {
internal Emote(ulong id, string name, bool animated)
{
Id = id;
Name = name;
Animated = animated;
}

/// <summary> /// <summary>
/// The display name (tooltip) of this emote
/// Is this emote animated?
/// </summary> /// </summary>
public string Name { get; }
public bool Animated { get; }

public string Url => CDN.GetEmojiUrl(Id, Animated);

private string DebuggerDisplay => $"{Name} ({Id})";

/// <summary> /// <summary>
/// The ID of this emote
/// The display name (tooltip) of this emote
/// </summary> /// </summary>
public ulong Id { get; }
public string Name { get; }

/// <summary> /// <summary>
/// Is this emote animated?
/// The ID of this emote
/// </summary> /// </summary>
public bool Animated { get; }
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);
public string Url => CDN.GetEmojiUrl(Id, Animated);
public ulong Id { get; }


internal Emote(ulong id, string name, bool animated)
{
Id = id;
Name = name;
Animated = animated;
}
public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id);


public override bool Equals(object other) public override bool Equals(object other)
{ {
@@ -50,13 +56,13 @@ namespace Discord
} }


/// <summary> /// <summary>
/// Parse an Emote from its raw format
/// Parse an Emote from its raw format
/// </summary> /// </summary>
/// <param name="text">The raw encoding of an emote; for example, &lt;:dab:277855270321782784&gt;</param> /// <param name="text">The raw encoding of an emote; for example, &lt;:dab:277855270321782784&gt;</param>
/// <returns>An emote</returns> /// <returns>An emote</returns>
public static Emote Parse(string text) public static Emote Parse(string text)
{ {
if (TryParse(text, out Emote result))
if (TryParse(text, out var result))
return result; return result;
throw new ArgumentException("Invalid emote format", nameof(text)); throw new ArgumentException("Invalid emote format", nameof(text));
} }
@@ -64,27 +70,24 @@ namespace Discord
public static bool TryParse(string text, out Emote result) public static bool TryParse(string text, out Emote result)
{ {
result = null; result = null;
if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>')
{
bool animated = text[1] == 'a';
int startIndex = animated ? 3 : 2;

int splitIndex = text.IndexOf(':', startIndex);
if (splitIndex == -1)
return false;
if (text.Length < 4 || text[0] != '<' || text[1] != ':' && (text[1] != 'a' || text[2] != ':') ||
text[text.Length - 1] != '>') return false;
var animated = text[1] == 'a';
var startIndex = animated ? 3 : 2;


if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id))
return false;
var splitIndex = text.IndexOf(':', startIndex);
if (splitIndex == -1)
return false;


string name = text.Substring(startIndex, splitIndex - startIndex);
result = new Emote(id, name, animated);
return true;
}
return false;
if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None,
CultureInfo.InvariantCulture, out var id))
return false;


var name = text.Substring(startIndex, splitIndex - startIndex);
result = new Emote(id, name, animated);
return true;
} }


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

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

Loading…
Cancel
Save