@@ -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() | ||||
@@ -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(); | |||||
} | } | ||||
} | } |
@@ -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) | ||||
{ | { | ||||
@@ -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() | ||||
{ | { | ||||
@@ -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)); | |||||
} | } | ||||
} | } |
@@ -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,3 +1,3 @@ | |||||
using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] |
@@ -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; } | |||||
} | } | ||||
} | } |
@@ -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; } | |||||
} | } | ||||
} | } |
@@ -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 | ||||
{ | { | ||||
} | } | ||||
@@ -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 | |||||
{ | |||||
} | |||||
} | } |
@@ -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,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; } | |||||
} | } | ||||
} | } |
@@ -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,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); | |||||
} | } | ||||
} | } |
@@ -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 && 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 && 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); | |||||
} | } | ||||
} | } |
@@ -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,14 @@ 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(); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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,21 @@ 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}")); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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.")); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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)}."); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -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,32 @@ 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()); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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; } | |||||
} | } | ||||
} | } |
@@ -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,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,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; } | |||||
} | } | ||||
} | } |
@@ -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); | ||||
} | } | ||||
@@ -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); | |||||
} | } | ||||
} | } |
@@ -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; | |||||
} | } | ||||
} | } |
@@ -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); | ||||
} | } | ||||
@@ -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; } | |||||
} | } | ||||
} | } |
@@ -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; } | |||||
} | } | ||||
} | } |
@@ -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); | ||||
} | } | ||||
@@ -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 | |||||
} | |||||
} | } | ||||
} | } |
@@ -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; | ||||
@@ -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> | ||||
@@ -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; | ||||
} | } | ||||
} | } |
@@ -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 | ||||
{ | { | ||||
@@ -10,13 +11,7 @@ namespace Discord.Commands | |||||
IEnumerable<TSecond> others, | IEnumerable<TSecond> others, | ||||
Func<TFirst, TSecond, TResult> func) | Func<TFirst, TSecond, TResult> func) | ||||
{ | { | ||||
foreach (TFirst elem in set) | |||||
{ | |||||
foreach (TSecond elem2 in others) | |||||
{ | |||||
yield return func(elem, elem2); | |||||
} | |||||
} | |||||
return from elem in set from elem2 in others select func(elem, elem2); | |||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -7,40 +7,37 @@ 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; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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); | ||||
@@ -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,36 @@ 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) | |||||
switch (task) | |||||
{ | { | ||||
var result = await execTask.ConfigureAwait(false); | |||||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); | |||||
return result; | |||||
} | |||||
else | |||||
{ | |||||
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 +233,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 +249,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; | ||||
} | } | ||||
@@ -272,10 +282,7 @@ namespace Discord.Commands | |||||
internal string GetLogText(ICommandContext context) | 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}"; | |||||
return context.Guild != null ? $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}" : $"\"{Name}\" for {context.User} in {context.Channel}"; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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,26 @@ 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) | |||||
private IEnumerable<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, | |||||
IServiceProvider services) | |||||
{ | { | ||||
var result = new List<ModuleInfo>(); | |||||
foreach (var submodule in parent.Modules) | |||||
result.Add(submodule.Build(service, services, this)); | |||||
return result; | |||||
return parent.Modules.Select(submodule => submodule.Build(service, services, this)).ToList(); | |||||
} | } | ||||
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 +91,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); | ||||
@@ -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)" : "")}"; | |||||
} | } | ||||
} | |||||
} |
@@ -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 != ""); | |||||
} | } | ||||
} | } |
@@ -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,90 @@ 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; | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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); | |||||
} | } | ||||
} | } |
@@ -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]; | |||||
} | } | ||||
} | } |
@@ -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."); | ||||
} | } | ||||
@@ -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}")); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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."); | ||||
} | } | ||||
@@ -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); | ||||
} | } | ||||
@@ -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}")); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -9,31 +9,27 @@ 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) | ||||
@@ -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")); | ||||
} | |||||
} | } | ||||
} | } |
@@ -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); | |||||
} | } | ||||
} | } |
@@ -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) | ||||
@@ -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}"; | ||||
} | } | ||||
@@ -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,53 @@ 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.Any(t => t.Values.Count > 1)) | |||||
{ | { | ||||
if (argValues[i].Values.Count > 1) | |||||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); | |||||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, | |||||
"Multiple matches found."); | |||||
} | } | ||||
for (int i = 0; i < paramValues.Count; i++) | |||||
if (paramValues.Any(t => t.Values.Count > 1)) | |||||
{ | { | ||||
if (paramValues[i].Values.Count > 1) | |||||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); | |||||
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}"; | |||||
} | } | ||||
} | } |
@@ -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}"; | |||||
} | } | ||||
} | } |
@@ -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}"; | |||||
} | } | ||||
} | } |
@@ -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,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); | ||||
@@ -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}"; | |||||
} | } | ||||
} | } |
@@ -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> { | |||||
{'\"', '\"' }, | |||||
{'«', '»' }, | |||||
{'‘', '’' }, | |||||
{'“', '”' }, | |||||
{'„', '‟' }, | |||||
{'‹', '›' }, | |||||
{'‚', '‛' }, | |||||
{'《', '》' }, | |||||
{'〈', '〉' }, | |||||
{'「', '」' }, | |||||
{'『', '』' }, | |||||
{'〝', '〞' }, | |||||
{'﹁', '﹂' }, | |||||
{'﹃', '﹄' }, | |||||
{'"', '"' }, | |||||
{''', ''' }, | |||||
{'「', '」' }, | |||||
{'(', ')' }, | |||||
{'༺', '༻' }, | |||||
{'༼', '༽' }, | |||||
{'᚛', '᚜' }, | |||||
{'⁅', '⁆' }, | |||||
{'⌈', '⌉' }, | |||||
{'⌊', '⌋' }, | |||||
{'❨', '❩' }, | |||||
{'❪', '❫' }, | |||||
{'❬', '❭' }, | |||||
{'❮', '❯' }, | |||||
{'❰', '❱' }, | |||||
{'❲', '❳' }, | |||||
{'❴', '❵' }, | |||||
{'⟅', '⟆' }, | |||||
{'⟦', '⟧' }, | |||||
{'⟨', '⟩' }, | |||||
{'⟪', '⟫' }, | |||||
{'⟬', '⟭' }, | |||||
{'⟮', '⟯' }, | |||||
{'⦃', '⦄' }, | |||||
{'⦅', '⦆' }, | |||||
{'⦇', '⦈' }, | |||||
{'⦉', '⦊' }, | |||||
{'⦋', '⦌' }, | |||||
{'⦍', '⦎' }, | |||||
{'⦏', '⦐' }, | |||||
{'⦑', '⦒' }, | |||||
{'⦓', '⦔' }, | |||||
{'⦕', '⦖' }, | |||||
{'⦗', '⦘' }, | |||||
{'⧘', '⧙' }, | |||||
{'⧚', '⧛' }, | |||||
{'⧼', '⧽' }, | |||||
{'⸂', '⸃' }, | |||||
{'⸄', '⸅' }, | |||||
{'⸉', '⸊' }, | |||||
{'⸌', '⸍' }, | |||||
{'⸜', '⸝' }, | |||||
{'⸠', '⸡' }, | |||||
{'⸢', '⸣' }, | |||||
{'⸤', '⸥' }, | |||||
{'⸦', '⸧' }, | |||||
{'⸨', '⸩' }, | |||||
{'【', '】'}, | |||||
{'〔', '〕' }, | |||||
{'〖', '〗' }, | |||||
{'〘', '〙' }, | |||||
{'〚', '〛' } | |||||
}; | |||||
} | |||||
} | |||||
{'\"', '\"'}, | |||||
{'«', '»'}, | |||||
{'‘', '’'}, | |||||
{'“', '”'}, | |||||
{'„', '‟'}, | |||||
{'‹', '›'}, | |||||
{'‚', '‛'}, | |||||
{'《', '》'}, | |||||
{'〈', '〉'}, | |||||
{'「', '」'}, | |||||
{'『', '』'}, | |||||
{'〝', '〞'}, | |||||
{'﹁', '﹂'}, | |||||
{'﹃', '﹄'}, | |||||
{'"', '"'}, | |||||
{''', '''}, | |||||
{'「', '」'}, | |||||
{'(', ')'}, | |||||
{'༺', '༻'}, | |||||
{'༼', '༽'}, | |||||
{'᚛', '᚜'}, | |||||
{'⁅', '⁆'}, | |||||
{'⌈', '⌉'}, | |||||
{'⌊', '⌋'}, | |||||
{'❨', '❩'}, | |||||
{'❪', '❫'}, | |||||
{'❬', '❭'}, | |||||
{'❮', '❯'}, | |||||
{'❰', '❱'}, | |||||
{'❲', '❳'}, | |||||
{'❴', '❵'}, | |||||
{'⟅', '⟆'}, | |||||
{'⟦', '⟧'}, | |||||
{'⟨', '⟩'}, | |||||
{'⟪', '⟫'}, | |||||
{'⟬', '⟭'}, | |||||
{'⟮', '⟯'}, | |||||
{'⦃', '⦄'}, | |||||
{'⦅', '⦆'}, | |||||
{'⦇', '⦈'}, | |||||
{'⦉', '⦊'}, | |||||
{'⦋', '⦌'}, | |||||
{'⦍', '⦎'}, | |||||
{'⦏', '⦐'}, | |||||
{'⦑', '⦒'}, | |||||
{'⦓', '⦔'}, | |||||
{'⦕', '⦖'}, | |||||
{'⦗', '⦘'}, | |||||
{'⧘', '⧙'}, | |||||
{'⧚', '⧛'}, | |||||
{'⧼', '⧽'}, | |||||
{'⸂', '⸃'}, | |||||
{'⸄', '⸅'}, | |||||
{'⸉', '⸊'}, | |||||
{'⸌', '⸍'}, | |||||
{'⸜', '⸝'}, | |||||
{'⸠', '⸡'}, | |||||
{'⸢', '⸣'}, | |||||
{'⸤', '⸥'}, | |||||
{'⸦', '⸧'}, | |||||
{'⸨', '⸩'}, | |||||
{'【', '】'}, | |||||
{'〔', '〕'}, | |||||
{'〖', '〗'}, | |||||
{'〘', '〙'}, | |||||
{'〚', '〛'} | |||||
}; | |||||
} | } | ||||
} | } |
@@ -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,35 @@ 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."); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -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")] |
@@ -1,9 +1,9 @@ | |||||
namespace Discord.Audio | namespace Discord.Audio | ||||
{ | { | ||||
public enum AudioApplication : int | |||||
public enum AudioApplication | |||||
{ | { | ||||
Voice, | Voice, | ||||
Music, | Music, | ||||
Mixed | Mixed | ||||
} | } | ||||
} | |||||
} |
@@ -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(); | |||||
} | } | ||||
} | } |
@@ -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(); | |||||
} | } | ||||
} | } |
@@ -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(); | |||||
} | } | ||||
} | } |
@@ -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); | ||||
} | } | ||||
@@ -15,4 +15,4 @@ namespace Discord.Audio | |||||
Missed = missed; | Missed = missed; | ||||
} | } | ||||
} | } | ||||
} | |||||
} |
@@ -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}"; | ||||
@@ -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; | ||||
@@ -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; | |||||
} | } | ||||
} | } |
@@ -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; | ||||
} | } | ||||
@@ -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; } | ||||
@@ -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,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; } | |||||
} | } | ||||
} | |||||
} |
@@ -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; | |||||
} | } | ||||
} | } |
@@ -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)"; | |||||
} | } | ||||
} | } |
@@ -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; | |||||
} | } | ||||
} | |||||
} |
@@ -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 | ||||
{ | { | ||||
@@ -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 | ||||
{ } | |||||
{ | |||||
} | |||||
} | } |
@@ -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; } | ||||
} | } | ||||
@@ -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,6 +1,5 @@ | |||||
using Discord.Audio; | |||||
using System; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Discord.Audio; | |||||
namespace Discord | namespace Discord | ||||
{ | { | ||||
@@ -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 | ||||
@@ -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); | ||||
} | } | ||||
@@ -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); | ||||
} | } | ||||
} | |||||
} |
@@ -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); | ||||
} | } | ||||
} | |||||
} |
@@ -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); | |||||
} | } | ||||
} | } |
@@ -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); | ||||
} | } | ||||
} | } |
@@ -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); | |||||
} | } | ||||
} | } |
@@ -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); | ||||
} | } | ||||
@@ -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; } | ||||
@@ -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; } | |||||
} | } | ||||
} | } |
@@ -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; } | ||||
} | } | ||||
@@ -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; } | ||||
} | } | ||||
@@ -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) | ||||
{ | { | ||||
@@ -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, <:dab:277855270321782784></param> | /// <param name="text">The raw encoding of an emote; for example, <:dab:277855270321782784></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,25 @@ 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; | |||||
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; | |||||
int splitIndex = text.IndexOf(':', startIndex); | |||||
if (splitIndex == -1) | |||||
return false; | |||||
var splitIndex = text.IndexOf(':', startIndex); | |||||
if (splitIndex == -1) | |||||
return false; | |||||
if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) | |||||
return false; | |||||
if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, | |||||
CultureInfo.InvariantCulture, out var id)) | |||||
return false; | |||||
string name = text.Substring(startIndex, splitIndex - startIndex); | |||||
result = new Emote(id, name, animated); | |||||
return true; | |||||
} | |||||
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}>"; | ||||
} | } | ||||
} | } |