@@ -14,13 +14,13 @@ namespace _01_basic_ping_bot | |||
// - Here, under the 02_commands_framework sample | |||
// - 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 | |||
class Program | |||
internal class Program | |||
{ | |||
private DiscordSocketClient _client; | |||
// Discord.Net heavily utilizes TAP for async, so we create | |||
// an asynchronous context from the beginning. | |||
static void Main(string[] args) | |||
private static void Main(string[] args) | |||
=> new Program().MainAsync().GetAwaiter().GetResult(); | |||
public async Task MainAsync() | |||
@@ -1,10 +1,10 @@ | |||
using System; | |||
using System.Net.Http; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Discord; | |||
using Discord.WebSocket; | |||
using Discord.Commands; | |||
using Discord.WebSocket; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using _02_commands_framework.Services; | |||
namespace _02_commands_framework | |||
@@ -17,9 +17,9 @@ namespace _02_commands_framework | |||
// - Here, under the 02_commands_framework sample | |||
// - 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 | |||
class Program | |||
internal class Program | |||
{ | |||
static void Main(string[] args) | |||
private static void Main(string[] args) | |||
=> new Program().MainAsync().GetAwaiter().GetResult(); | |||
public async Task MainAsync() | |||
@@ -46,15 +46,12 @@ namespace _02_commands_framework | |||
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.Reflection; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Discord; | |||
using Discord.Commands; | |||
using Discord.WebSocket; | |||
using Microsoft.Extensions.DependencyInjection; | |||
namespace _02_commands_framework.Services | |||
{ | |||
@@ -23,10 +23,7 @@ namespace _02_commands_framework.Services | |||
_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) | |||
{ | |||
@@ -9,7 +9,9 @@ namespace _02_commands_framework.Services | |||
private readonly HttpClient _http; | |||
public PictureService(HttpClient http) | |||
=> _http = http; | |||
{ | |||
_http = http; | |||
} | |||
public async Task<Stream> GetCatPictureAsync() | |||
{ | |||
@@ -1,11 +1,11 @@ | |||
using System; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
using Discord.Commands; | |||
using Microsoft.CodeAnalysis; | |||
using Microsoft.CodeAnalysis.CSharp; | |||
using Microsoft.CodeAnalysis.CSharp.Syntax; | |||
using Microsoft.CodeAnalysis.Diagnostics; | |||
using Discord.Commands; | |||
namespace Discord.Analyzers | |||
{ | |||
@@ -14,18 +14,25 @@ namespace Discord.Analyzers | |||
{ | |||
private const string DiagnosticId = "DNET0001"; | |||
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 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 void Initialize(AnalysisContext context) | |||
{ | |||
public override void Initialize(AnalysisContext context) => | |||
context.RegisterSyntaxNodeAction(AnalyzeMemberAccess, SyntaxKind.SimpleMemberAccessExpression); | |||
} | |||
private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context) | |||
{ | |||
@@ -53,18 +60,14 @@ namespace Discord.Analyzers | |||
// Is the '[RequireContext]' attribute not applied to either the | |||
// 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 Discord.Commands; | |||
namespace Discord.Analyzers | |||
{ | |||
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) | |||
{ | |||
for (var bType = symbol.BaseType; bType != null; bType = bType.BaseType) | |||
{ | |||
if (bType.MetadataName == _moduleBaseName) | |||
if (bType.MetadataName == ModuleBaseName) | |||
return true; | |||
} | |||
return false; | |||
} | |||
} | |||
@@ -1,3 +1,3 @@ | |||
using System.Runtime.CompilerServices; | |||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] |
@@ -3,16 +3,16 @@ using System; | |||
namespace Discord.Commands | |||
{ | |||
/// <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 | |||
{ | |||
/// <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) | |||
{ | |||
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 | |||
{ | |||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Method)] | |||
public class CommandAttribute : Attribute | |||
{ | |||
public string Text { get; } | |||
public RunMode RunMode { get; set; } = RunMode.Default; | |||
public bool? IgnoreExtraArgs { get; set; } | |||
public CommandAttribute() | |||
{ | |||
Text = null; | |||
} | |||
public CommandAttribute(string 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 | |||
{ | |||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Class)] | |||
public class DontAutoLoadAttribute : Attribute | |||
{ | |||
} | |||
@@ -1,9 +1,9 @@ | |||
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 | |||
{ | |||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Class)] | |||
public class GroupAttribute : Attribute | |||
{ | |||
public string Prefix { get; } | |||
public GroupAttribute() | |||
{ | |||
Prefix = null; | |||
} | |||
public GroupAttribute(string prefix) | |||
{ | |||
Prefix = prefix; | |||
} | |||
public string Prefix { get; } | |||
} | |||
} |
@@ -3,14 +3,14 @@ using System; | |||
namespace Discord.Commands | |||
{ | |||
// 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 string Text { get; } | |||
public NameAttribute(string text) | |||
{ | |||
Text = text; | |||
} | |||
public string Text { get; } | |||
} | |||
} |
@@ -1,22 +1,21 @@ | |||
using System; | |||
using System.Reflection; | |||
namespace Discord.Commands | |||
{ | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Parameter)] | |||
public class OverrideTypeReaderAttribute : Attribute | |||
{ | |||
private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo(); | |||
public Type TypeReader { get; } | |||
public OverrideTypeReaderAttribute(Type overridenTypeReader) | |||
{ | |||
if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo())) | |||
throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}"); | |||
TypeReader = overridenTypeReader; | |||
} | |||
} | |||
public Type TypeReader { get; } | |||
} | |||
} |
@@ -3,9 +3,10 @@ using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] | |||
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 | |||
{ | |||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true)] | |||
public abstract class PreconditionAttribute : Attribute | |||
{ | |||
/// <summary> | |||
/// Specify a group that this precondition belongs to. Preconditions of the same group require only one | |||
/// of the preconditions to pass in order to be successful (A || B). Specifying <see cref="Group"/> = <see 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> | |||
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 | |||
{ | |||
/// <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> | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] | |||
public class RequireBotPermissionAttribute : PreconditionAttribute | |||
{ | |||
public GuildPermission? GuildPermission { get; } | |||
public ChannelPermission? ChannelPermission { get; } | |||
/// <summary> | |||
/// Require that the bot account has a specified GuildPermission | |||
/// Require that the bot account has a specified GuildPermission | |||
/// </summary> | |||
/// <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) | |||
{ | |||
GuildPermission = permission; | |||
ChannelPermission = null; | |||
} | |||
/// <summary> | |||
/// Require that the bot account has a specified ChannelPermission. | |||
/// Require that the bot account has a specified ChannelPermission. | |||
/// </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> | |||
/// <code language="c#"> | |||
/// <code language="c#"> | |||
/// [Command("permission")] | |||
/// [RequireBotPermission(ChannelPermission.ManageMessages)] | |||
/// public async Task Purge() | |||
@@ -41,7 +45,11 @@ namespace Discord.Commands | |||
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; | |||
if (context.Guild != null) | |||
@@ -55,19 +63,16 @@ namespace Discord.Commands | |||
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.Threading.Tasks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
namespace Discord.Commands | |||
{ | |||
@@ -13,19 +12,20 @@ namespace Discord.Commands | |||
} | |||
/// <summary> | |||
/// Require that the command be invoked in a specified context. | |||
/// Require that the command be invoked in a specified context. | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] | |||
public class RequireContextAttribute : PreconditionAttribute | |||
{ | |||
public ContextType Contexts { get; } | |||
/// <summary> | |||
/// Require that the command be invoked in a specified context. | |||
/// Require that the command be invoked in a specified context. | |||
/// </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> | |||
/// <code language="c#"> | |||
/// <code language="c#"> | |||
/// [Command("private_only")] | |||
/// [RequireContext(ContextType.DM | ContextType.Group)] | |||
/// public async Task PrivateOnly() | |||
@@ -38,21 +38,23 @@ namespace Discord.Commands | |||
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) | |||
isValid = isValid || context.Channel is IGuildChannel; | |||
isValid = context.Channel is IGuildChannel; | |||
if ((Contexts & ContextType.DM) != 0) | |||
isValid = isValid || context.Channel is IDMChannel; | |||
if ((Contexts & ContextType.Group) != 0) | |||
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 | |||
{ | |||
/// <summary> | |||
/// Require that the command is invoked in a channel marked NSFW | |||
/// Require that the command is invoked in a channel marked NSFW | |||
/// </summary> | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] | |||
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) | |||
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 | |||
{ | |||
/// <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> | |||
/// <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 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) | |||
{ | |||
@@ -20,7 +21,8 @@ namespace Discord.Commands | |||
return PreconditionResult.FromError("Command can only be run by the owner of the bot"); | |||
return PreconditionResult.FromSuccess(); | |||
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 | |||
{ | |||
/// <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> | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] | |||
public class RequireUserPermissionAttribute : PreconditionAttribute | |||
{ | |||
public GuildPermission? GuildPermission { get; } | |||
public ChannelPermission? ChannelPermission { get; } | |||
/// <summary> | |||
/// Require that the user invoking the command has a specified GuildPermission | |||
/// Require that the user invoking the command has a specified GuildPermission | |||
/// </summary> | |||
/// <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) | |||
{ | |||
GuildPermission = permission; | |||
ChannelPermission = null; | |||
} | |||
/// <summary> | |||
/// Require that the user invoking the command has a specified ChannelPermission. | |||
/// Require that the user invoking the command has a specified ChannelPermission. | |||
/// </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> | |||
/// <code language="c#"> | |||
/// <code language="c#"> | |||
/// [Command("permission")] | |||
/// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)] | |||
/// public async Task HasPermission() | |||
@@ -41,32 +45,34 @@ namespace Discord.Commands | |||
ChannelPermission = permission; | |||
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; | |||
if (GuildPermission.HasValue) | |||
{ | |||
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)) | |||
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 | |||
{ | |||
/// <summary> Sets priority of commands </summary> | |||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Method)] | |||
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) | |||
{ | |||
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 | |||
{ | |||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)] | |||
[AttributeUsage(AttributeTargets.Parameter)] | |||
public class RemainderAttribute : Attribute | |||
{ | |||
} | |||
@@ -3,14 +3,14 @@ using System; | |||
namespace Discord.Commands | |||
{ | |||
// 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 string Text { get; } | |||
public RemarksAttribute(string text) | |||
{ | |||
Text = text; | |||
} | |||
public string Text { get; } | |||
} | |||
} |
@@ -3,14 +3,14 @@ using System; | |||
namespace Discord.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 string Text { get; } | |||
public SummaryAttribute(string text) | |||
{ | |||
Text = text; | |||
} | |||
public string Text { get; } | |||
} | |||
} |
@@ -1,32 +1,16 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Threading.Tasks; | |||
using System.Collections.Generic; | |||
namespace Discord.Commands.Builders | |||
{ | |||
public class CommandBuilder | |||
{ | |||
private readonly List<PreconditionAttribute> _preconditions; | |||
private readonly List<ParameterBuilder> _parameters; | |||
private readonly List<Attribute> _attributes; | |||
private readonly List<string> _aliases; | |||
public ModuleBuilder Module { get; } | |||
internal Func<ICommandContext, object[], IServiceProvider, 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 | |||
internal CommandBuilder(ModuleBuilder module) | |||
@@ -38,8 +22,10 @@ namespace Discord.Commands.Builders | |||
_attributes = new List<Attribute>(); | |||
_aliases = new List<string>(); | |||
} | |||
//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) | |||
{ | |||
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); | |||
@@ -50,26 +36,46 @@ namespace Discord.Commands.Builders | |||
_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) | |||
{ | |||
Name = name; | |||
return this; | |||
} | |||
public CommandBuilder WithSummary(string summary) | |||
{ | |||
Summary = summary; | |||
return this; | |||
} | |||
public CommandBuilder WithRemarks(string remarks) | |||
{ | |||
Remarks = remarks; | |||
return this; | |||
} | |||
public CommandBuilder WithRunMode(RunMode runMode) | |||
{ | |||
RunMode = runMode; | |||
return this; | |||
} | |||
public CommandBuilder WithPriority(int priority) | |||
{ | |||
Priority = priority; | |||
@@ -78,24 +84,28 @@ namespace Discord.Commands.Builders | |||
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)) | |||
_aliases.Add(alias); | |||
} | |||
return this; | |||
} | |||
public CommandBuilder AddAttributes(params Attribute[] attributes) | |||
{ | |||
_attributes.AddRange(attributes); | |||
return this; | |||
} | |||
public CommandBuilder AddPrecondition(PreconditionAttribute precondition) | |||
{ | |||
_preconditions.Add(precondition); | |||
return this; | |||
} | |||
public CommandBuilder AddParameter<T>(string name, Action<ParameterBuilder> createFunc) | |||
{ | |||
var param = new ParameterBuilder(this, name, typeof(T)); | |||
@@ -103,6 +113,7 @@ namespace Discord.Commands.Builders | |||
_parameters.Add(param); | |||
return this; | |||
} | |||
public CommandBuilder AddParameter(string name, Type type, Action<ParameterBuilder> createFunc) | |||
{ | |||
var param = new ParameterBuilder(this, name, type); | |||
@@ -110,6 +121,7 @@ namespace Discord.Commands.Builders | |||
_parameters.Add(param); | |||
return this; | |||
} | |||
internal CommandBuilder AddParameter(Action<ParameterBuilder> createFunc) | |||
{ | |||
var param = new ParameterBuilder(this); | |||
@@ -124,18 +136,18 @@ namespace Discord.Commands.Builders | |||
if (Name == null) | |||
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); | |||
} | |||
@@ -7,26 +7,11 @@ namespace Discord.Commands.Builders | |||
{ | |||
public class ModuleBuilder | |||
{ | |||
private readonly List<string> _aliases; | |||
private readonly List<Attribute> _attributes; | |||
private readonly List<CommandBuilder> _commands; | |||
private readonly List<ModuleBuilder> _submodules; | |||
private readonly List<PreconditionAttribute> _preconditions; | |||
private readonly List<Attribute> _attributes; | |||
private readonly List<string> _aliases; | |||
public CommandService Service { get; } | |||
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 | |||
internal ModuleBuilder(CommandService service, ModuleBuilder parent) | |||
@@ -40,25 +25,43 @@ namespace Discord.Commands.Builders | |||
_attributes = new List<Attribute>(); | |||
_aliases = new List<string>(); | |||
} | |||
//User-defined | |||
internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias) | |||
: this(service, parent) | |||
{ | |||
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) | |||
{ | |||
Name = name; | |||
return this; | |||
} | |||
public ModuleBuilder WithSummary(string summary) | |||
{ | |||
Summary = summary; | |||
return this; | |||
} | |||
public ModuleBuilder WithRemarks(string remarks) | |||
{ | |||
Remarks = remarks; | |||
@@ -67,31 +70,38 @@ namespace Discord.Commands.Builders | |||
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)) | |||
_aliases.Add(alias); | |||
} | |||
return this; | |||
} | |||
public ModuleBuilder AddAttributes(params Attribute[] attributes) | |||
{ | |||
_attributes.AddRange(attributes); | |||
return this; | |||
} | |||
public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) | |||
{ | |||
_preconditions.Add(precondition); | |||
return this; | |||
} | |||
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, 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); | |||
createFunc(builder); | |||
_commands.Add(builder); | |||
return this; | |||
} | |||
internal ModuleBuilder AddCommand(Action<CommandBuilder> createFunc) | |||
{ | |||
var builder = new CommandBuilder(this); | |||
@@ -99,6 +109,7 @@ namespace Discord.Commands.Builders | |||
_commands.Add(builder); | |||
return this; | |||
} | |||
public ModuleBuilder AddModule(string primaryAlias, Action<ModuleBuilder> createFunc) | |||
{ | |||
var builder = new ModuleBuilder(Service, this, primaryAlias); | |||
@@ -106,6 +117,7 @@ namespace Discord.Commands.Builders | |||
_submodules.Add(builder); | |||
return this; | |||
} | |||
internal ModuleBuilder AddModule(Action<ModuleBuilder> createFunc) | |||
{ | |||
var builder = new ModuleBuilder(Service, this); | |||
@@ -120,17 +132,16 @@ namespace Discord.Commands.Builders | |||
if (Name == null) | |||
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); | |||
} | |||
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.Linq; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Threading.Tasks; | |||
using Discord.Commands.Builders; | |||
namespace Discord.Commands | |||
@@ -17,38 +16,37 @@ namespace Discord.Commands | |||
bool IsLoadableModule(TypeInfo info) | |||
{ | |||
return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) && | |||
info.GetCustomAttribute<DontAutoLoadAttribute>() == null; | |||
info.GetCustomAttribute<DontAutoLoadAttribute>() == null; | |||
} | |||
var result = new List<TypeInfo>(); | |||
foreach (var typeInfo in assembly.DefinedTypes) | |||
{ | |||
if (typeInfo.IsPublic || typeInfo.IsNestedPublic) | |||
{ | |||
if (IsValidModuleDefinition(typeInfo) && | |||
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) | |||
{ | |||
result.Add(typeInfo); | |||
} | |||
} | |||
else if (IsLoadableModule(typeInfo)) | |||
{ | |||
await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}."); | |||
} | |||
} | |||
await service._cmdLogger.WarningAsync( | |||
$"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}."); | |||
return result; | |||
} | |||
public static 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()) | |||
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>(); | |||
@@ -69,22 +67,24 @@ namespace Discord.Commands | |||
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; | |||
} | |||
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) | |||
{ | |||
if (!IsValidModuleDefinition(typeInfo)) | |||
continue; | |||
if (builtTypes.Contains(typeInfo)) | |||
continue; | |||
builder.AddModule((module) => | |||
builder.AddModule(module => | |||
{ | |||
BuildModule(module, typeInfo, 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(); | |||
builder.TypeInfo = typeInfo; | |||
foreach (var attribute in attributes) | |||
{ | |||
switch (attribute) | |||
{ | |||
case NameAttribute name: | |||
@@ -127,7 +127,6 @@ namespace Discord.Commands | |||
builder.AddAttributes(attribute); | |||
break; | |||
} | |||
} | |||
//Check for unspecified info | |||
if (builder.Aliases.Count == 0) | |||
@@ -138,20 +137,15 @@ namespace Discord.Commands | |||
var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); | |||
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(); | |||
foreach (var attribute in attributes) | |||
{ | |||
switch (attribute) | |||
{ | |||
case CommandAttribute command: | |||
@@ -182,7 +176,6 @@ namespace Discord.Commands | |||
builder.AddAttributes(attribute); | |||
break; | |||
} | |||
} | |||
if (builder.Name == null) | |||
builder.Name = method.Name; | |||
@@ -190,16 +183,15 @@ namespace Discord.Commands | |||
var parameters = method.GetParameters(); | |||
int pos = 0, count = parameters.Length; | |||
foreach (var paramInfo in parameters) | |||
{ | |||
builder.AddParameter((parameter) => | |||
builder.AddParameter(parameter => | |||
{ | |||
BuildParameter(parameter, paramInfo, pos++, count, service, serviceprovider); | |||
}); | |||
} | |||
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); | |||
instance.SetContext(context); | |||
@@ -210,9 +202,7 @@ namespace Discord.Commands | |||
var task = method.Invoke(instance, args) as Task ?? Task.Delay(0); | |||
if (task is Task<RuntimeResult> resultTask) | |||
{ | |||
return await resultTask.ConfigureAwait(false); | |||
} | |||
else | |||
{ | |||
await task.ConfigureAwait(false); | |||
@@ -229,7 +219,8 @@ namespace Discord.Commands | |||
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 paramType = paramInfo.ParameterType; | |||
@@ -240,7 +231,6 @@ namespace Discord.Commands | |||
builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; | |||
foreach (var attribute in attributes) | |||
{ | |||
switch (attribute) | |||
{ | |||
case SummaryAttribute summary: | |||
@@ -261,7 +251,8 @@ namespace Discord.Commands | |||
break; | |||
case RemainderAttribute _: | |||
if (position != count - 1) | |||
throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); | |||
throw new InvalidOperationException( | |||
$"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}"); | |||
builder.IsRemainder = true; | |||
break; | |||
@@ -269,26 +260,22 @@ namespace Discord.Commands | |||
builder.AddAttributes(attribute); | |||
break; | |||
} | |||
} | |||
builder.ParameterType = paramType; | |||
if (builder.TypeReader == null) | |||
{ | |||
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); | |||
TypeReader reader = null; | |||
TypeReader reader; | |||
if (readers != null) | |||
{ | |||
if (readers.TryGetValue(typeReaderType, out reader)) | |||
return reader; | |||
} | |||
//We dont have a cached type reader, create one | |||
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, services); | |||
@@ -297,19 +284,14 @@ namespace Discord.Commands | |||
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.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Collections.Generic; | |||
namespace Discord.Commands.Builders | |||
{ | |||
public class ParameterBuilder | |||
{ | |||
private readonly List<ParameterPreconditionAttribute> _preconditions; | |||
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 | |||
internal ParameterBuilder(CommandBuilder command) | |||
@@ -33,6 +18,7 @@ namespace Discord.Commands.Builders | |||
Command = command; | |||
} | |||
//User-defined | |||
internal ParameterBuilder(CommandBuilder command, string name, Type type) | |||
: this(command) | |||
@@ -43,6 +29,20 @@ namespace Discord.Commands.Builders | |||
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) | |||
{ | |||
TypeReader = GetReader(type); | |||
@@ -57,10 +57,7 @@ namespace Discord.Commands.Builders | |||
private TypeReader GetReader(Type 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) | |||
@@ -68,21 +65,25 @@ namespace Discord.Commands.Builders | |||
Summary = summary; | |||
return this; | |||
} | |||
public ParameterBuilder WithDefault(object defaultValue) | |||
{ | |||
DefaultValue = defaultValue; | |||
return this; | |||
} | |||
public ParameterBuilder WithIsOptional(bool isOptional) | |||
{ | |||
IsOptional = isOptional; | |||
return this; | |||
} | |||
public ParameterBuilder WithIsRemainder(bool isRemainder) | |||
{ | |||
IsRemainder = isRemainder; | |||
return this; | |||
} | |||
public ParameterBuilder WithIsMultiple(bool isMultiple) | |||
{ | |||
IsMultiple = isMultiple; | |||
@@ -94,6 +95,7 @@ namespace Discord.Commands.Builders | |||
_attributes.AddRange(attributes); | |||
return this; | |||
} | |||
public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition) | |||
{ | |||
_preconditions.Add(precondition); | |||
@@ -103,7 +105,8 @@ namespace Discord.Commands.Builders | |||
internal ParameterInfo Build(CommandInfo info) | |||
{ | |||
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); | |||
} | |||
@@ -2,14 +2,6 @@ | |||
{ | |||
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) | |||
{ | |||
Client = client; | |||
@@ -18,5 +10,12 @@ | |||
User = msg.Author; | |||
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 CommandInfo Command { get; } | |||
public ICommandContext Context { get; } | |||
public CommandException(CommandInfo command, ICommandContext context, Exception ex) | |||
: base($"Error occurred executing {command.GetLogText(context)}.", ex) | |||
{ | |||
Command = command; | |||
Context = context; | |||
} | |||
public CommandInfo Command { get; } | |||
public ICommandContext Context { get; } | |||
} | |||
} |
@@ -1,7 +1,6 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
namespace Discord.Commands | |||
{ | |||
@@ -16,12 +15,18 @@ namespace Discord.Commands | |||
Alias = alias; | |||
} | |||
public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null) | |||
public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, | |||
IServiceProvider services = null) | |||
=> 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); | |||
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); | |||
public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services) | |||
=> Command.ExecuteAsync(context, parseResult, services); | |||
} | |||
@@ -8,22 +8,18 @@ namespace Discord.Commands | |||
{ | |||
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; | |||
StringBuilder argBuilder = new StringBuilder(input.Length); | |||
int endPos = input.Length; | |||
var argBuilder = new StringBuilder(input.Length); | |||
var endPos = input.Length; | |||
var curPart = ParserPart.None; | |||
int lastArgEndPos = int.MinValue; | |||
var lastArgEndPos = int.MinValue; | |||
var argList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | |||
var paramList = ImmutableArray.CreateBuilder<TypeReaderResult>(); | |||
bool isEscaping = false; | |||
var isEscaping = false; | |||
char c, matchQuote = '\0'; | |||
// local helper functions | |||
@@ -46,23 +42,19 @@ namespace Discord.Commands | |||
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 (isEscaping) | |||
{ | |||
if (curPos != endPos) | |||
{ | |||
argBuilder.Append(c); | |||
isEscaping = false; | |||
continue; | |||
} | |||
} | |||
//Are we escaping the next character? | |||
if (c == '\\' && (curParam == null || !curParam.IsRemainder)) | |||
{ | |||
@@ -82,98 +74,96 @@ namespace Discord.Commands | |||
{ | |||
if (char.IsWhiteSpace(c) || curPos == endPos) | |||
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? | |||
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(); | |||
lastArgEndPos = curPos; | |||
} | |||
else | |||
break; | |||
case ParserPart.Parameter: | |||
argBuilder.Append(c); | |||
} | |||
else if (curPart == ParserPart.QuotedParameter) | |||
{ | |||
if (c == matchQuote) | |||
{ | |||
break; | |||
case ParserPart.QuotedParameter when c == matchQuote: | |||
argString = argBuilder.ToString(); //Remove quotes | |||
lastArgEndPos = curPos + 1; | |||
} | |||
else | |||
break; | |||
case ParserPart.QuotedParameter: | |||
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) | |||
{ | |||
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) | |||
return ParseResult.FromError(typeReaderResult); | |||
argList.Add(typeReaderResult); | |||
} | |||
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) | |||
return ParseResult.FromError(CommandError.ParseFailed, "A quoted parameter is incomplete"); | |||
//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]; | |||
if (param.IsMultiple) | |||
@@ -182,8 +172,15 @@ namespace Discord.Commands | |||
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); | |||
argList.Add(TypeReaderResult.FromSuccess(param.DefaultValue)); | |||
} | |||
return ParseResult.FromSuccess(argList.ToImmutable(), paramList.ToImmutable()); | |||
} | |||
private enum ParserPart | |||
{ | |||
None, | |||
Parameter, | |||
QuotedParameter | |||
} | |||
} | |||
} |
@@ -13,40 +13,39 @@ namespace Discord.Commands | |||
{ | |||
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 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 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 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) | |||
{ | |||
_caseSensitive = config.CaseSensitiveCommands; | |||
CaseSensitive = config.CaseSensitiveCommands; | |||
_throwOnError = config.ThrowOnError; | |||
_ignoreExtraArgs = config.IgnoreExtraArgs; | |||
_separatorChar = config.SeparatorChar; | |||
_defaultRunMode = config.DefaultRunMode; | |||
_quotationMarkAliasMap = (config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary(); | |||
_quotationMarkAliasMap = | |||
(config.QuotationMarkAliasMap ?? new Dictionary<char, char>()).ToImmutableDictionary(); | |||
if (_defaultRunMode == RunMode.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) | |||
{ | |||
_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(); | |||
@@ -72,7 +72,11 @@ namespace Discord.Commands | |||
_defaultTypeReaders[typeof(TimeSpan?)] = NullableTypeReader.Create(typeof(TimeSpan), tsreader); | |||
_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>>(); | |||
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>))); | |||
@@ -82,6 +86,27 @@ namespace Discord.Commands | |||
_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 | |||
public async Task<ModuleInfo> CreateModuleAsync(string primaryAlias, Action<ModuleBuilder> buildFunc) | |||
{ | |||
@@ -102,12 +127,13 @@ namespace Discord.Commands | |||
} | |||
/// <summary> | |||
/// Add a command module from a type | |||
/// Add a command module from a type | |||
/// </summary> | |||
/// <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> | |||
/// <returns>A built module</returns> | |||
public Task<ModuleInfo> AddModuleAsync<T>(IServiceProvider services) => AddModuleAsync(typeof(T), services); | |||
public async Task<ModuleInfo> AddModuleAsync(Type type, IServiceProvider services) | |||
{ | |||
services = services ?? EmptyServiceProvider.Instance; | |||
@@ -118,14 +144,13 @@ namespace Discord.Commands | |||
var typeInfo = type.GetTypeInfo(); | |||
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); | |||
} | |||
@@ -134,8 +159,9 @@ namespace Discord.Commands | |||
_moduleLock.Release(); | |||
} | |||
} | |||
/// <summary> | |||
/// Add command modules from an assembly | |||
/// Add command modules from an assembly | |||
/// </summary> | |||
/// <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> | |||
@@ -163,6 +189,7 @@ namespace Discord.Commands | |||
_moduleLock.Release(); | |||
} | |||
} | |||
private ModuleInfo LoadModuleInternal(ModuleInfo module) | |||
{ | |||
_moduleDefs.Add(module); | |||
@@ -188,22 +215,22 @@ namespace Discord.Commands | |||
_moduleLock.Release(); | |||
} | |||
} | |||
public Task<bool> RemoveModuleAsync<T>() => RemoveModuleAsync(typeof(T)); | |||
public async Task<bool> RemoveModuleAsync(Type type) | |||
{ | |||
await _moduleLock.WaitAsync().ConfigureAwait(false); | |||
try | |||
{ | |||
if (!_typedModuleDefs.TryRemove(type, out var module)) | |||
return false; | |||
return RemoveModuleInternal(module); | |||
return _typedModuleDefs.TryRemove(type, out var module) && RemoveModuleInternal(module); | |||
} | |||
finally | |||
{ | |||
_moduleLock.Release(); | |||
} | |||
} | |||
private bool RemoveModuleInternal(ModuleInfo module) | |||
{ | |||
if (!_moduleDefs.Remove(module)) | |||
@@ -212,60 +239,73 @@ namespace Discord.Commands | |||
foreach (var cmd in module.Commands) | |||
_map.RemoveCommand(cmd); | |||
foreach (var submodule in module.Submodules) | |||
{ | |||
RemoveModuleInternal(submodule); | |||
} | |||
foreach (var submodule in module.Submodules) RemoveModuleInternal(submodule); | |||
return true; | |||
} | |||
//Type Readers | |||
/// <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> | |||
/// <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) | |||
=> AddTypeReader(typeof(T), reader); | |||
/// <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> | |||
/// <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) | |||
{ | |||
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); | |||
} | |||
/// <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> | |||
/// <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) | |||
=> AddTypeReader(typeof(T), reader, replaceDefault); | |||
/// <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> | |||
/// <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) | |||
{ | |||
if (replaceDefault && HasDefaultTypeReader(type)) | |||
{ | |||
_defaultTypeReaders.AddOrUpdate(type, reader, (k, v) => reader); | |||
if (type.GetTypeInfo().IsValueType) | |||
if (!type.GetTypeInfo().IsValueType) return; | |||
{ | |||
var nullableType = typeof(Nullable<>).MakeGenericType(type); | |||
var nullableReader = NullableTypeReader.Create(type, reader); | |||
@@ -281,28 +321,29 @@ namespace Discord.Commands | |||
AddNullableTypeReader(type, reader); | |||
} | |||
} | |||
internal bool HasDefaultTypeReader(Type type) | |||
{ | |||
if (_defaultTypeReaders.ContainsKey(type)) | |||
return true; | |||
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) | |||
{ | |||
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); | |||
readers[nullableReader.GetType()] = nullableReader; | |||
} | |||
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) | |||
{ | |||
if (_defaultTypeReaders.TryGetValue(type, out var reader)) | |||
@@ -318,37 +359,39 @@ namespace Discord.Commands | |||
} | |||
//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; | |||
return reader; | |||
} | |||
} | |||
return null; | |||
} | |||
//Execution | |||
public SearchResult Search(ICommandContext context, int argPos) | |||
=> Search(context.Message.Content.Substring(argPos)); | |||
public SearchResult Search(ICommandContext context, string input) | |||
=> Search(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(); | |||
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); | |||
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; | |||
@@ -360,9 +403,8 @@ namespace Discord.Commands | |||
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>(); | |||
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 | |||
.Where(x => x.Value.IsSuccess) | |||
@@ -382,16 +424,18 @@ namespace Discord.Commands | |||
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>(); | |||
foreach (var pair in successfulPreconditions) | |||
{ | |||
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false); | |||
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services) | |||
.ConfigureAwait(false); | |||
if (parseResult.Error == CommandError.MultipleMatches) | |||
{ | |||
IReadOnlyList<TypeReaderValue> argList, paramList; | |||
switch (multiMatchHandling) | |||
{ | |||
case MultiMatchHandling.Best: | |||
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray(); | |||
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); | |||
break; | |||
} | |||
@@ -407,8 +451,11 @@ namespace Discord.Commands | |||
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; | |||
paramValuesScore = paramValuesSum / match.Command.Parameters.Count; | |||
@@ -1,15 +1,17 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Discord.Commands | |||
{ | |||
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 char SeparatorChar { get; set; } = ' '; | |||
/// <summary> Determines whether commands should be case-sensitive. </summary> | |||
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> | |||
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; | |||
/// <summary> Determines whether extra parameters should be ignored. </summary> | |||
@@ -5,7 +5,7 @@ namespace Discord.Commands | |||
internal class EmptyServiceProvider : IServiceProvider | |||
{ | |||
public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider(); | |||
public object GetService(Type serviceType) => null; | |||
} | |||
} |
@@ -1,5 +1,6 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
namespace Discord.Commands | |||
{ | |||
@@ -8,15 +9,8 @@ namespace Discord.Commands | |||
public static IEnumerable<TResult> Permutate<TFirst, TSecond, TResult>( | |||
this IEnumerable<TFirst> set, | |||
IEnumerable<TSecond> others, | |||
Func<TFirst, TSecond, TResult> func) | |||
{ | |||
foreach (TFirst elem in set) | |||
{ | |||
foreach (TSecond elem2 in others) | |||
{ | |||
yield return func(elem, elem2); | |||
} | |||
} | |||
} | |||
Func<TFirst, TSecond, TResult> func) => from elem in set | |||
from elem2 in others | |||
select func(elem, elem2); | |||
} | |||
} | |||
} |
@@ -7,40 +7,34 @@ namespace Discord.Commands | |||
public static bool HasCharPrefix(this IUserMessage msg, char c, ref int argPos) | |||
{ | |||
var text = msg.Content; | |||
if (text.Length > 0 && text[0] == c) | |||
{ | |||
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; | |||
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) | |||
{ | |||
var text = msg.Content; | |||
if (text.Length <= 3 || text[0] != '<' || text[1] != '@') return false; | |||
int endPos = text.IndexOf('>'); | |||
var endPos = text.IndexOf('>'); | |||
if (endPos == -1) return false; | |||
if (text.Length < endPos + 2 || text[endPos + 1] != ' ') return false; //Must end in "> " | |||
ulong userId; | |||
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 BeforeExecute(CommandInfo command); | |||
void AfterExecute(CommandInfo command); | |||
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.Immutable; | |||
using System.Collections.Concurrent; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Runtime.ExceptionServices; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Discord.Commands.Builders; | |||
namespace Discord.Commands | |||
{ | |||
[DebuggerDisplay("{Name,nq}")] | |||
[DebuggerDisplay("{" + nameof(Name) + ",nq}")] | |||
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) | |||
{ | |||
@@ -43,7 +32,7 @@ namespace Discord.Commands | |||
Summary = builder.Summary; | |||
Remarks = builder.Remarks; | |||
RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); | |||
RunMode = builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode; | |||
Priority = builder.Priority; | |||
Aliases = module.Aliases | |||
@@ -51,52 +40,66 @@ namespace Discord.Commands | |||
{ | |||
if (first == "") | |||
return second; | |||
else if (second == "") | |||
if (second == "") | |||
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(); | |||
Preconditions = builder.Preconditions.ToImmutableArray(); | |||
Attributes = builder.Attributes.ToImmutableArray(); | |||
Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); | |||
HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; | |||
HasVarArgs = builder.Parameters.Count > 0 && builder.Parameters[builder.Parameters.Count - 1].IsMultiple; | |||
IgnoreExtraArgs = builder.IgnoreExtraArgs; | |||
_action = builder.Callback; | |||
_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; | |||
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) | |||
{ | |||
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) | |||
return result; | |||
} | |||
} | |||
else | |||
{ | |||
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)) | |||
return PreconditionGroupResult.FromError($"{type} precondition group {preconditionGroup.Key} failed.", results); | |||
return PreconditionGroupResult.FromError( | |||
$"{type} precondition group {preconditionGroup.Key} failed.", results); | |||
} | |||
} | |||
return PreconditionGroupResult.FromSuccess(); | |||
} | |||
@@ -105,13 +108,11 @@ namespace Discord.Commands | |||
return moduleResult; | |||
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; | |||
@@ -120,9 +121,10 @@ namespace Discord.Commands | |||
if (preconditionResult != null && !preconditionResult.IsSuccess) | |||
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) | |||
@@ -131,7 +133,7 @@ namespace Discord.Commands | |||
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult)); | |||
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) | |||
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ArgValues[i])); | |||
@@ -139,7 +141,7 @@ namespace Discord.Commands | |||
} | |||
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) | |||
return Task.FromResult((IResult)ExecuteResult.FromError(parseResult.ParamValues[i])); | |||
@@ -148,19 +150,22 @@ namespace Discord.Commands | |||
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; | |||
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]; | |||
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) | |||
return ExecuteResult.FromError(result); | |||
} | |||
@@ -176,6 +181,7 @@ namespace Discord.Commands | |||
}); | |||
break; | |||
} | |||
return ExecuteResult.FromSuccess(); | |||
} | |||
catch (Exception ex) | |||
@@ -184,30 +190,39 @@ namespace Discord.Commands | |||
} | |||
} | |||
private async Task<IResult> ExecuteInternalAsync(ICommandContext context, object[] args, IServiceProvider services) | |||
private async Task<IResult> ExecuteInternalAsync(ICommandContext context, object[] args, | |||
IServiceProvider services) | |||
{ | |||
await Module.Service._cmdLogger.DebugAsync($"Executing {GetLogText(context)}").ConfigureAwait(false); | |||
try | |||
{ | |||
var task = _action(context, args, services, this); | |||
if (task is Task<IResult> resultTask) | |||
{ | |||
var result = await resultTask.ConfigureAwait(false); | |||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); | |||
if (result is RuntimeResult execResult) | |||
return execResult; | |||
} | |||
else if (task is Task<ExecuteResult> execTask) | |||
{ | |||
var result = await execTask.ConfigureAwait(false); | |||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); | |||
return result; | |||
} | |||
else | |||
switch (task) | |||
{ | |||
await task.ConfigureAwait(false); | |||
var result = ExecuteResult.FromSuccess(); | |||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result).ConfigureAwait(false); | |||
case Task<IResult> resultTask: | |||
{ | |||
var result = await resultTask.ConfigureAwait(false); | |||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result) | |||
.ConfigureAwait(false); | |||
if (result is RuntimeResult execResult) | |||
return execResult; | |||
break; | |||
} | |||
case Task<ExecuteResult> execTask: | |||
{ | |||
var result = await execTask.ConfigureAwait(false); | |||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result) | |||
.ConfigureAwait(false); | |||
return result; | |||
} | |||
default: | |||
{ | |||
await task.ConfigureAwait(false); | |||
var result = ExecuteResult.FromSuccess(); | |||
await Module.Service._commandExecutedEvent.InvokeAsync(this, context, result) | |||
.ConfigureAwait(false); | |||
break; | |||
} | |||
} | |||
var executeResult = ExecuteResult.FromSuccess(); | |||
@@ -221,13 +236,11 @@ namespace Discord.Commands | |||
var wrappedEx = new CommandException(this, context, ex); | |||
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); | |||
} | |||
@@ -239,30 +252,30 @@ namespace Discord.Commands | |||
private object[] GenerateArgs(IEnumerable<object> argList, IEnumerable<object> paramsList) | |||
{ | |||
int argCount = Parameters.Count; | |||
var argCount = Parameters.Count; | |||
var array = new object[Parameters.Count]; | |||
if (HasVarArgs) | |||
argCount--; | |||
int i = 0; | |||
foreach (object arg in argList) | |||
var i = 0; | |||
foreach (var arg in argList) | |||
{ | |||
if (i == argCount) | |||
throw new InvalidOperationException("Command was invoked with too many parameters"); | |||
array[i++] = arg; | |||
} | |||
if (i < argCount) | |||
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; | |||
} | |||
@@ -270,12 +283,8 @@ namespace Discord.Commands | |||
private static T[] ConvertParamsList<T>(IEnumerable<object> paramsList) | |||
=> paramsList.Cast<T>().ToArray(); | |||
internal string GetLogText(ICommandContext context) | |||
{ | |||
if (context.Guild != null) | |||
return $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}"; | |||
else | |||
return $"\"{Name}\" for {context.User} in {context.Channel}"; | |||
} | |||
internal string GetLogText(ICommandContext context) => context.Guild != null | |||
? $"\"{Name}\" for {context.User} in {context.Guild}/{context.Channel}" | |||
: $"\"{Name}\" for {context.User} in {context.Channel}"; | |||
} | |||
} |
@@ -1,28 +1,15 @@ | |||
using System; | |||
using System.Linq; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Linq; | |||
using Discord.Commands.Builders; | |||
namespace Discord.Commands | |||
{ | |||
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; | |||
@@ -40,6 +27,20 @@ namespace Discord.Commands | |||
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) | |||
{ | |||
var result = builder.Aliases.ToList(); | |||
@@ -57,31 +58,24 @@ namespace Discord.Commands | |||
{ | |||
if (first == "") | |||
return second; | |||
else if (second == "") | |||
if (second == "") | |||
return first; | |||
else | |||
return first + service._separatorChar + second; | |||
return first + service._separatorChar + second; | |||
}).ToList(); | |||
} | |||
return result; | |||
} | |||
private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, IServiceProvider services) | |||
{ | |||
var result = new List<ModuleInfo>(); | |||
private IEnumerable<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service, | |||
IServiceProvider services) => | |||
parent.Modules.Select(submodule => submodule.Build(service, services, this)).ToList(); | |||
foreach (var submodule in parent.Modules) | |||
result.Add(submodule.Build(service, services, this)); | |||
return result; | |||
} | |||
private static List<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder) | |||
private static IEnumerable<PreconditionAttribute> BuildPreconditions(ModuleBuilder builder) | |||
{ | |||
var result = new List<PreconditionAttribute>(); | |||
ModuleBuilder parent = builder; | |||
var parent = builder; | |||
while (parent != null) | |||
{ | |||
result.AddRange(parent.Preconditions); | |||
@@ -95,7 +89,7 @@ namespace Discord.Commands | |||
{ | |||
var result = new List<Attribute>(); | |||
ModuleBuilder parent = builder; | |||
var parent = builder; | |||
while (parent != null) | |||
{ | |||
result.AddRange(parent.Attributes); | |||
@@ -1,9 +1,8 @@ | |||
using Discord.Commands.Builders; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Collections.Immutable; | |||
using System.Threading.Tasks; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Discord.Commands.Builders; | |||
namespace Discord.Commands | |||
{ | |||
@@ -11,18 +10,6 @@ namespace Discord.Commands | |||
{ | |||
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) | |||
{ | |||
Command = command; | |||
@@ -42,13 +29,30 @@ namespace Discord.Commands | |||
_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; | |||
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) | |||
return result; | |||
} | |||
@@ -56,13 +60,13 @@ namespace Discord.Commands | |||
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; | |||
return await _reader.ReadAsync(context, input, services).ConfigureAwait(false); | |||
} | |||
public override string ToString() => Name; | |||
private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; | |||
} | |||
} | |||
} |
@@ -4,9 +4,9 @@ namespace Discord.Commands | |||
{ | |||
internal class CommandMap | |||
{ | |||
private readonly CommandService _service; | |||
private static readonly string[] _blankAliases = {""}; | |||
private readonly CommandMapNode _root; | |||
private static readonly string[] _blankAliases = new[] { "" }; | |||
private readonly CommandService _service; | |||
public CommandMap(CommandService service) | |||
{ | |||
@@ -16,18 +16,16 @@ namespace Discord.Commands | |||
public void AddCommand(CommandInfo command) | |||
{ | |||
foreach (string text in command.Aliases) | |||
foreach (var text in command.Aliases) | |||
_root.AddCommand(_service, text, 0, command); | |||
} | |||
public void RemoveCommand(CommandInfo command) | |||
{ | |||
foreach (string text in command.Aliases) | |||
foreach (var text in command.Aliases) | |||
_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 | |||
{ | |||
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 string _name; | |||
private readonly object _lockObj = new object(); | |||
private ImmutableArray<CommandInfo> _commands; | |||
public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; | |||
public CommandMapNode(string name) | |||
{ | |||
_name = name; | |||
@@ -23,113 +21,92 @@ namespace Discord.Commands | |||
_commands = ImmutableArray.Create<CommandInfo>(); | |||
} | |||
public bool IsEmpty => _commands.Length == 0 && _nodes.Count == 0; | |||
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) | |||
{ | |||
if (text == "") | |||
switch (text) | |||
{ | |||
if (_name == "") | |||
case "" when _name == "": | |||
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) | |||
{ | |||
int nextSegment = NextSegment(text, index, service._separatorChar); | |||
string name; | |||
var nextSegment = NextSegment(text, index, service._separatorChar); | |||
lock (_lockObj) | |||
{ | |||
if (text == "") | |||
_commands = _commands.Remove(command); | |||
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; | |||
for (int i = 0; i < commands.Length; i++) | |||
for (var i = 0; i < commands.Length; i++) | |||
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) | |||
{ | |||
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) | |||
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 | |||
{ | |||
public abstract class ModuleBase : ModuleBase<ICommandContext> { } | |||
public abstract class ModuleBase : ModuleBase<ICommandContext> | |||
{ | |||
} | |||
public abstract class ModuleBase<T> : IModuleBase | |||
where T : class, ICommandContext | |||
{ | |||
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> | |||
/// Sends a message to the source channel | |||
/// Sends a message to the source channel | |||
/// </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="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) | |||
{ | |||
@@ -33,15 +48,5 @@ namespace Discord.Commands | |||
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 | |||
{ | |||
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>(); | |||
parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse; | |||
@@ -34,7 +35,7 @@ namespace Discord.Commands | |||
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 | |||
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."); | |||
} | |||
@@ -11,9 +11,10 @@ namespace Discord.Commands | |||
{ | |||
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 Type _enumType; | |||
private readonly TryParseDelegate<T> _tryParse; | |||
public EnumTypeReader(Type type, TryParseDelegate<T> parser) | |||
{ | |||
_enumType = type; | |||
@@ -33,7 +34,7 @@ namespace Discord.Commands | |||
var byValueBuilder = ImmutableDictionary.CreateBuilder<T, object>(); | |||
foreach (var v in Enum.GetNames(_enumType)) | |||
{ | |||
{ | |||
var parsedValue = Enum.Parse(_enumType, v); | |||
byNameBuilder.Add(v.ToLower(), parsedValue); | |||
if (!byValueBuilder.ContainsKey((T)parsedValue)) | |||
@@ -44,24 +45,23 @@ namespace Discord.Commands | |||
_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; | |||
if (_tryParse(input, out T baseValue)) | |||
if (_tryParse(input, out var baseValue)) | |||
{ | |||
if (_enumsByValue.TryGetValue(baseValue, out 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 | |||
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) | |||
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."); | |||
} | |||
@@ -9,8 +9,9 @@ namespace Discord.Commands | |||
{ | |||
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; | |||
} | |||
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 await _baseTypeReader.ReadAsync(context, input, services); | |||
} | |||
@@ -14,12 +14,13 @@ namespace Discord.Commands | |||
internal class PrimitiveTypeReader<T> : TypeReader | |||
{ | |||
private readonly TryParseDelegate<T> _tryParse; | |||
private readonly float _score; | |||
private readonly TryParseDelegate<T> _tryParse; | |||
public PrimitiveTypeReader() | |||
: this(PrimitiveParsers.Get<T>(), 1) | |||
{ } | |||
{ | |||
} | |||
public PrimitiveTypeReader(TryParseDelegate<T> tryParse, float score) | |||
{ | |||
@@ -30,11 +31,13 @@ namespace Discord.Commands | |||
_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.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,29 @@ namespace Discord.Commands | |||
public class RoleTypeReader<T> : TypeReader | |||
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) | |||
@@ -6,30 +6,29 @@ namespace Discord.Commands | |||
{ | |||
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'", //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.FromError(CommandError.ParseFailed, "Failed to parse TimeSpan")); | |||
} | |||
} | |||
} |
@@ -5,6 +5,7 @@ namespace Discord.Commands | |||
{ | |||
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 | |||
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>(); | |||
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>(); | |||
ulong id; | |||
if (context.Guild != null) | |||
guildUsers = await context.Guild.GetUsersAsync(CacheMode.CacheOnly).ConfigureAwait(false); | |||
//By Mention (1.0) | |||
if (MentionUtils.TryParseUser(input, out id)) | |||
if (MentionUtils.TryParseUser(input, out var id)) | |||
{ | |||
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 | |||
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) | |||
if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||
{ | |||
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 | |||
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) | |||
int index = input.LastIndexOf('#'); | |||
var index = input.LastIndexOf('#'); | |||
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 && | |||
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); | |||
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); | |||
} | |||
} | |||
@@ -59,9 +65,11 @@ namespace Discord.Commands | |||
{ | |||
await channelUsers | |||
.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); | |||
} | |||
@@ -69,15 +77,15 @@ namespace Discord.Commands | |||
{ | |||
await channelUsers | |||
.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) | |||
@@ -3,7 +3,7 @@ using System.Diagnostics; | |||
namespace Discord.Commands | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public struct ExecuteResult : IResult | |||
{ | |||
public Exception Exception { get; } | |||
@@ -22,13 +22,16 @@ namespace Discord.Commands | |||
public static ExecuteResult FromSuccess() | |||
=> new ExecuteResult(null, null, null); | |||
public static ExecuteResult FromError(CommandError error, string reason) | |||
=> new ExecuteResult(null, error, reason); | |||
public static ExecuteResult FromError(Exception ex) | |||
=> new ExecuteResult(ex, CommandError.Exception, ex.Message); | |||
public static ExecuteResult FromError(IResult result) | |||
=> new ExecuteResult(null, result.Error, result.ErrorReason); | |||
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
} | |||
@@ -1,10 +1,11 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Diagnostics; | |||
using System.Linq; | |||
namespace Discord.Commands | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public struct ParseResult : IResult | |||
{ | |||
public IReadOnlyList<TypeReaderResult> ArgValues { get; } | |||
@@ -15,7 +16,8 @@ namespace Discord.Commands | |||
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; | |||
ParamValues = paramValues; | |||
@@ -23,43 +25,49 @@ namespace Discord.Commands | |||
ErrorReason = errorReason; | |||
} | |||
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues) | |||
public static ParseResult FromSuccess(IReadOnlyList<TypeReaderResult> argValues, | |||
IReadOnlyList<TypeReaderResult> paramValues) | |||
{ | |||
for (int i = 0; i < argValues.Count; i++) | |||
{ | |||
if (argValues[i].Values.Count > 1) | |||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); | |||
} | |||
for (int i = 0; i < paramValues.Count; i++) | |||
{ | |||
if (paramValues[i].Values.Count > 1) | |||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, "Multiple matches found."); | |||
} | |||
if (argValues.Any(t => t.Values.Count > 1)) | |||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, | |||
"Multiple matches found."); | |||
if (paramValues.Any(t => t.Values.Count > 1)) | |||
return new ParseResult(argValues, paramValues, CommandError.MultipleMatches, | |||
"Multiple matches found."); | |||
return new ParseResult(argValues, paramValues, null, null); | |||
} | |||
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]; | |||
for (int i = 0; i < argValues.Count; i++) | |||
for (var i = 0; i < argValues.Count; 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]; | |||
for (int i = 0; i < paramValues.Count; i++) | |||
for (var i = 0; i < paramValues.Count; i++) | |||
paramList[i] = TypeReaderResult.FromSuccess(paramValues[i]); | |||
} | |||
return new ParseResult(argList, paramList, null, null); | |||
} | |||
public static ParseResult FromError(CommandError error, string reason) | |||
=> new ParseResult(null, null, error, reason); | |||
public static ParseResult FromError(Exception ex) | |||
=> FromError(CommandError.Exception, ex.Message); | |||
public static ParseResult FromError(IResult result) | |||
=> new ParseResult(null, null, result.Error, result.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 | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
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) | |||
{ | |||
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); | |||
public static PreconditionGroupResult FromError(string reason, ICollection<PreconditionResult> 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); | |||
public static new PreconditionGroupResult FromError(IResult result) //needed? | |||
public new static PreconditionGroupResult FromError(IResult result) //needed? | |||
=> new PreconditionGroupResult(result.Error, result.ErrorReason, null); | |||
public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||
} | |||
} |
@@ -3,30 +3,33 @@ using System.Diagnostics; | |||
namespace Discord.Commands | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public class PreconditionResult : IResult | |||
{ | |||
public CommandError? Error { get; } | |||
public string ErrorReason { get; } | |||
public bool IsSuccess => !Error.HasValue; | |||
protected PreconditionResult(CommandError? error, string errorReason) | |||
{ | |||
Error = error; | |||
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() | |||
=> new PreconditionResult(null, null); | |||
public static PreconditionResult FromError(string reason) | |||
=> new PreconditionResult(CommandError.UnmetPrecondition, reason); | |||
public static PreconditionResult FromError(Exception ex) | |||
=> new PreconditionResult(CommandError.Exception, ex.Message); | |||
public static PreconditionResult FromError(IResult result) | |||
=> new PreconditionResult(result.Error, result.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 | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public abstract class RuntimeResult : IResult | |||
{ | |||
protected RuntimeResult(CommandError? error, string reason) | |||
@@ -14,14 +11,15 @@ namespace Discord.Commands | |||
Reason = reason; | |||
} | |||
public CommandError? Error { get; } | |||
public string Reason { get; } | |||
private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; | |||
public CommandError? Error { get; } | |||
public bool IsSuccess => !Error.HasValue; | |||
string IResult.ErrorReason => Reason; | |||
public override string ToString() => Reason ?? (IsSuccess ? "Successful" : "Unsuccessful"); | |||
private string DebuggerDisplay => IsSuccess ? $"Success: {Reason ?? "No Reason"}" : $"{Error}: {Reason}"; | |||
} | |||
} |
@@ -4,7 +4,7 @@ using System.Diagnostics; | |||
namespace Discord.Commands | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public struct SearchResult : IResult | |||
{ | |||
public string Text { get; } | |||
@@ -25,10 +25,13 @@ namespace Discord.Commands | |||
public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands) | |||
=> new SearchResult(text, commands, null, null); | |||
public static SearchResult FromError(CommandError error, string reason) | |||
=> new SearchResult(null, null, error, reason); | |||
public static SearchResult FromError(Exception ex) | |||
=> FromError(CommandError.Exception, ex.Message); | |||
public static SearchResult FromError(IResult result) | |||
=> new SearchResult(null, null, result.Error, result.ErrorReason); | |||
@@ -6,7 +6,7 @@ using System.Linq; | |||
namespace Discord.Commands | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public struct TypeReaderValue | |||
{ | |||
public object Value { get; } | |||
@@ -22,7 +22,7 @@ namespace Discord.Commands | |||
private string DebuggerDisplay => $"[{Value}, {Math.Round(Score, 2)}]"; | |||
} | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public struct TypeReaderResult : IResult | |||
{ | |||
public IReadOnlyCollection<TypeReaderValue> Values { get; } | |||
@@ -31,6 +31,7 @@ namespace Discord.Commands | |||
public string ErrorReason { get; } | |||
public bool IsSuccess => !Error.HasValue; | |||
public object BestMatch => IsSuccess | |||
? (Values.Count == 1 ? Values.Single().Value : Values.OrderByDescending(v => v.Score).First().Value) | |||
: throw new InvalidOperationException("TypeReaderResult was not successful."); | |||
@@ -44,18 +45,25 @@ namespace Discord.Commands | |||
public static TypeReaderResult FromSuccess(object value) | |||
=> new TypeReaderResult(ImmutableArray.Create(new TypeReaderValue(value, 1.0f)), null, null); | |||
public static TypeReaderResult FromSuccess(TypeReaderValue value) | |||
=> new TypeReaderResult(ImmutableArray.Create(value), null, null); | |||
public static TypeReaderResult FromSuccess(IReadOnlyCollection<TypeReaderValue> values) | |||
=> new TypeReaderResult(values, null, null); | |||
public static TypeReaderResult FromError(CommandError error, string reason) | |||
=> new TypeReaderResult(null, error, reason); | |||
public static TypeReaderResult FromError(Exception ex) | |||
=> FromError(CommandError.Exception, ex.Message); | |||
public static TypeReaderResult FromError(IResult result) | |||
=> new TypeReaderResult(null, result.Error, result.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.Text; | |||
using System.Globalization; | |||
namespace Discord.Commands | |||
{ | |||
/// <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> | |||
internal static class QuotationAliasUtils | |||
{ | |||
/// <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> | |||
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.Linq; | |||
using System.Reflection; | |||
using Microsoft.Extensions.DependencyInjection; | |||
namespace Discord.Commands | |||
{ | |||
@@ -12,24 +11,26 @@ namespace Discord.Commands | |||
internal static T CreateObject<T>(TypeInfo typeInfo, CommandService commands, IServiceProvider services = null) | |||
=> CreateBuilder<T>(typeInfo, commands)(services); | |||
internal static Func<IServiceProvider, T> CreateBuilder<T>(TypeInfo typeInfo, CommandService commands) | |||
{ | |||
var constructor = GetConstructor(typeInfo); | |||
var parameters = constructor.GetParameters(); | |||
var properties = GetProperties(typeInfo); | |||
return (services) => | |||
return services => | |||
{ | |||
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); | |||
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)); | |||
return obj; | |||
}; | |||
} | |||
private static T InvokeConstructor<T>(ConstructorInfo constructor, object[] args, TypeInfo ownerType) | |||
{ | |||
try | |||
@@ -47,34 +48,37 @@ namespace Discord.Commands | |||
var constructors = ownerType.DeclaredConstructors.Where(x => !x.IsStatic).ToArray(); | |||
if (constructors.Length == 0) | |||
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}\""); | |||
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) | |||
{ | |||
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(); | |||
} | |||
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)) | |||
return commands; | |||
if (memberType == typeof(IServiceProvider) || memberType == services.GetType()) | |||
return services; | |||
var service = services?.GetService(memberType); | |||
var service = services.GetService(memberType); | |||
if (service != null) | |||
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.Webhook")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Commands")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] | |||
[assembly: InternalsVisibleTo("Discord.Net.Tests")] |
@@ -1,9 +1,9 @@ | |||
namespace Discord.Audio | |||
{ | |||
public enum AudioApplication : int | |||
public enum AudioApplication | |||
{ | |||
Voice, | |||
Music, | |||
Mixed | |||
} | |||
} | |||
} |
@@ -9,11 +9,11 @@ namespace Discord.Audio | |||
public abstract int AvailableFrames { get; } | |||
public override bool CanRead => true; | |||
public override bool CanWrite => true; | |||
public override bool CanWrite => true; | |||
public abstract Task<RTPFrame> ReadFrameAsync(CancellationToken cancelToken); | |||
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 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 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 | |||
{ | |||
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 | |||
{ | |||
/// <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<Exception, Task> Disconnected; | |||
event Func<int, int, Task> LatencyUpdated; | |||
@@ -13,22 +22,19 @@ namespace Discord.Audio | |||
event Func<ulong, Task> StreamDestroyed; | |||
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 SetSpeakingAsync(bool value); | |||
/// <summary>Creates a new outgoing stream accepting Opus-encoded data.</summary> | |||
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> | |||
AudioOutStream CreateDirectOpusStream(); | |||
/// <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> | |||
AudioOutStream CreateDirectPCMStream(AudioApplication application, int? bitrate = null, int packetLoss = 30); | |||
} | |||
@@ -15,4 +15,4 @@ namespace Discord.Audio | |||
Missed = missed; | |||
} | |||
} | |||
} | |||
} |
@@ -6,34 +6,39 @@ namespace Discord | |||
{ | |||
public static string GetApplicationIconUrl(ulong appId, string iconId) | |||
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; | |||
public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, ImageFormat format) | |||
{ | |||
if (avatarId == null) | |||
return null; | |||
string extension = FormatToExtension(format, avatarId); | |||
var extension = FormatToExtension(format, avatarId); | |||
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) | |||
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; | |||
public static string GetGuildSplashUrl(ulong guildId, string splashId) | |||
=> splashId != null ? $"{DiscordConfig.CDNUrl}splashes/{guildId}/{splashId}.jpg" : null; | |||
public static string GetChannelIconUrl(ulong channelId, string iconId) | |||
=> iconId != null ? $"{DiscordConfig.CDNUrl}channel-icons/{channelId}/{iconId}.jpg" : null; | |||
public static string GetEmojiUrl(ulong emojiId, bool animated) | |||
=> $"{DiscordConfig.CDNUrl}emojis/{emojiId}.{(animated ? "gif" : "png")}"; | |||
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}"; | |||
} | |||
public static string GetSpotifyAlbumArtUrl(string albumArtId) | |||
=> $"https://i.scdn.co/image/{albumArtId}"; | |||
public static string GetSpotifyDirectUrl(string trackId) | |||
=> $"https://open.spotify.com/track/{trackId}"; | |||
@@ -6,13 +6,6 @@ namespace Discord | |||
{ | |||
public const int APIVersion = 6; | |||
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 InviteUrl = "https://discord.gg/"; | |||
@@ -23,6 +16,16 @@ namespace Discord | |||
public const int MaxGuildsPerBatch = 100; | |||
public const int MaxUserReactionsPerBatch = 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> | |||
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry; | |||
@@ -2,20 +2,23 @@ using System.Diagnostics; | |||
namespace Discord | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
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) | |||
{ | |||
Name = name; | |||
Type = type; | |||
} | |||
public override string ToString() => 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 | |||
{ | |||
internal GameAsset() { } | |||
internal GameAsset() | |||
{ | |||
} | |||
internal ulong? ApplicationId { get; set; } | |||
public string Text { get; internal set; } | |||
public string ImageId { get; internal set; } | |||
public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) | |||
=> ApplicationId.HasValue ? CDN.GetRichAssetUrl(ApplicationId.Value, ImageId, size, format) : null; | |||
} | |||
@@ -2,7 +2,9 @@ namespace Discord | |||
{ | |||
public class GameParty | |||
{ | |||
internal GameParty() { } | |||
internal GameParty() | |||
{ | |||
} | |||
public string Id { get; internal set; } | |||
public long Members { get; internal set; } | |||
@@ -2,15 +2,15 @@ | |||
{ | |||
public class GameSecrets | |||
{ | |||
public string Match { get; } | |||
public string Join { get; } | |||
public string Spectate { get; } | |||
internal GameSecrets(string match, string join, string spectate) | |||
{ | |||
Match = match; | |||
Join = join; | |||
Spectate = spectate; | |||
} | |||
public string Match { get; } | |||
public string Join { get; } | |||
public string Spectate { get; } | |||
} | |||
} | |||
} |
@@ -4,13 +4,13 @@ namespace Discord | |||
{ | |||
public class GameTimestamps | |||
{ | |||
public DateTimeOffset? Start { get; } | |||
public DateTimeOffset? End { get; } | |||
internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) | |||
{ | |||
Start = start; | |||
End = end; | |||
} | |||
public DateTimeOffset? Start { get; } | |||
public DateTimeOffset? End { get; } | |||
} | |||
} | |||
} |
@@ -2,10 +2,12 @@ using System.Diagnostics; | |||
namespace Discord | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public class RichGame : Game | |||
{ | |||
internal RichGame() { } | |||
internal RichGame() | |||
{ | |||
} | |||
public string Details { get; internal set; } | |||
public string State { get; internal set; } | |||
@@ -15,8 +17,8 @@ namespace Discord | |||
public GameParty Party { get; internal set; } | |||
public GameSecrets Secrets { get; internal set; } | |||
public GameTimestamps Timestamps { get; internal set; } | |||
public override string ToString() => Name; | |||
private string DebuggerDisplay => $"{Name} (Rich)"; | |||
public override string ToString() => Name; | |||
} | |||
} |
@@ -4,9 +4,13 @@ using System.Diagnostics; | |||
namespace Discord | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public class SpotifyGame : Game | |||
{ | |||
internal SpotifyGame() | |||
{ | |||
} | |||
public IReadOnlyCollection<string> Artists { get; internal set; } | |||
public string AlbumTitle { get; internal set; } | |||
public string TrackTitle { get; internal set; } | |||
@@ -17,10 +21,8 @@ namespace Discord | |||
public string AlbumArtUrl { 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})"; | |||
private string DebuggerDisplay => $"{Name} (Spotify)"; | |||
} | |||
} |
@@ -2,11 +2,9 @@ | |||
namespace Discord | |||
{ | |||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||
[DebuggerDisplay(@"{" + nameof(DebuggerDisplay) + @",nq}")] | |||
public class StreamingGame : Game | |||
{ | |||
public string Url { get; internal set; } | |||
public StreamingGame(string name, string url) | |||
{ | |||
Name = name; | |||
@@ -14,7 +12,9 @@ namespace Discord | |||
Type = ActivityType.Streaming; | |||
} | |||
public override string ToString() => Name; | |||
public string Url { get; internal set; } | |||
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> | |||
/// The action type within a <see cref="IAuditLogEntry"/> | |||
/// The action type within a <see cref="IAuditLogEntry" /> | |||
/// </summary> | |||
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> | |||
/// Represents data applied to an <see cref="IAuditLogEntry"/> | |||
/// Represents data applied to an <see cref="IAuditLogEntry" /> | |||
/// </summary> | |||
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 | |||
{ | |||
/// <summary> | |||
/// Represents an entry in an audit log | |||
/// Represents an entry in an audit log | |||
/// </summary> | |||
public interface IAuditLogEntry : ISnowflakeEntity | |||
{ | |||
/// <summary> | |||
/// The action which occured to create this entry | |||
/// The action which occured to create this entry | |||
/// </summary> | |||
ActionType Action { get; } | |||
/// <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> | |||
IAuditLogData Data { get; } | |||
/// <summary> | |||
/// The user responsible for causing the changes | |||
/// The user responsible for causing the changes | |||
/// </summary> | |||
IUser User { get; } | |||
/// <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> | |||
string Reason { get; } | |||
} | |||
@@ -1,10 +1,10 @@ | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// Modify an IGuildChannel with the specified changes. | |||
/// Modify an IGuildChannel with the specified changes. | |||
/// </summary> | |||
/// <example> | |||
/// <code language="c#"> | |||
/// <code language="c#"> | |||
/// await (Context.Channel as ITextChannel)?.ModifyAsync(x => | |||
/// { | |||
/// x.Name = "do-not-enter"; | |||
@@ -14,20 +14,22 @@ | |||
public class GuildChannelProperties | |||
{ | |||
/// <summary> | |||
/// Set the channel to this name | |||
/// Set the channel to this name | |||
/// </summary> | |||
/// <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> | |||
/// <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; } | |||
/// <summary> | |||
/// Move the channel to the following position. This is 0-based! | |||
/// Move the channel to the following position. This is 0-based! | |||
/// </summary> | |||
public Optional<int> Position { get; set; } | |||
/// <summary> | |||
/// Sets the category for this channel | |||
/// Sets the category for this channel | |||
/// </summary> | |||
public Optional<ulong?> CategoryId { get; set; } | |||
} | |||
@@ -1,6 +1,5 @@ | |||
using Discord.Audio; | |||
using System; | |||
using System.Threading.Tasks; | |||
using Discord.Audio; | |||
namespace Discord | |||
{ | |||
@@ -1,9 +1,3 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
public interface ICategoryChannel : IGuildChannel | |||
@@ -7,10 +7,11 @@ namespace Discord | |||
{ | |||
/// <summary> Gets the name of this channel. </summary> | |||
string Name { get; } | |||
/// <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> | |||
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> | |||
Task CloseAsync(RequestOptions options = null); | |||
} | |||
} | |||
} |
@@ -7,4 +7,4 @@ namespace Discord | |||
/// <summary> Leaves this group. </summary> | |||
Task LeaveAsync(RequestOptions options = null); | |||
} | |||
} | |||
} |
@@ -11,16 +11,23 @@ namespace Discord | |||
/// <summary> Gets the guild this channel is a member of. </summary> | |||
IGuild Guild { get; } | |||
/// <summary> Gets the id of the guild this channel is a member of. </summary> | |||
ulong GuildId { get; } | |||
/// <summary> Gets a collection of permission overwrites for this channel. </summary> | |||
IReadOnlyCollection<Overwrite> PermissionOverwrites { get; } | |||
/// <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="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> | |||
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> | |||
OverwritePermissions? GetPermissionOverwrite(IRole role); | |||
/// <summary> Gets the permission overwrite for a specific user, or null if one does not exist. </summary> | |||
OverwritePermissions? GetPermissionOverwrite(IUser user); | |||
/// <summary> Removes the permission overwrite for the given role, if one exists. </summary> | |||
Task RemovePermissionOverwriteAsync(IRole role, RequestOptions options = null); | |||
/// <summary> Removes the permission overwrite for the given user, if one exists. </summary> | |||
Task RemovePermissionOverwriteAsync(IUser user, RequestOptions options = null); | |||
/// <summary> Adds or updates the permission overwrite for the given role. </summary> | |||
Task AddPermissionOverwriteAsync(IRole role, OverwritePermissions permissions, RequestOptions options = null); | |||
/// <summary> Adds or updates the permission overwrite for the given user. </summary> | |||
Task AddPermissionOverwriteAsync(IUser user, OverwritePermissions permissions, RequestOptions options = null); | |||
/// <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> | |||
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 | |||
{ | |||
/// <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> | |||
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> | |||
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> | |||
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> | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, | |||
IAsyncEnumerable<IReadOnlyCollection<IMessage>> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, | |||
CacheMode mode = CacheMode.AllowDownload, RequestOptions options = null); | |||
/// <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); | |||
/// <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); | |||
/// <summary> Gets a collection of pinned messages in this channel. </summary> | |||
Task<IReadOnlyCollection<IMessage>> GetPinnedMessagesAsync(RequestOptions options = null); | |||
/// <summary> Deletes a message based on the message ID in this channel. </summary> | |||
Task DeleteMessageAsync(ulong messageId, RequestOptions options = null); | |||
/// <summary> Deletes a message based on the provided message in this channel. </summary> | |||
Task DeleteMessageAsync(IMessage message, RequestOptions options = null); | |||
/// <summary> Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. </summary> | |||
Task TriggerTypingAsync(RequestOptions options = null); | |||
/// <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); | |||
} | |||
} |
@@ -3,14 +3,16 @@ using System.Threading.Tasks; | |||
namespace Discord | |||
{ | |||
/// <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> | |||
public interface INestedChannel : IGuildChannel | |||
{ | |||
/// <summary> Gets the parentid (category) of this channel in the guild's channel list. </summary> | |||
ulong? CategoryId { get; } | |||
/// <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> | |||
Task DeleteMessagesAsync(IEnumerable<IMessage> messages, RequestOptions options = null); | |||
/// <summary> Bulk deletes multiple messages. </summary> | |||
Task DeleteMessagesAsync(IEnumerable<ulong> messageIds, RequestOptions options = null); | |||
@@ -23,8 +24,10 @@ namespace Discord | |||
/// <summary> Creates a webhook in this text channel. </summary> | |||
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> | |||
Task<IWebhook> GetWebhookAsync(ulong id, RequestOptions options = null); | |||
/// <summary> Gets the webhooks for this text channel. </summary> | |||
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> | |||
int Bitrate { get; } | |||
/// <summary> Gets the max amount of users allowed to be connected to this channel at one time. </summary> | |||
int? UserLimit { get; } | |||
@@ -2,15 +2,16 @@ | |||
{ | |||
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) | |||
{ | |||
Id = id; | |||
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 | |||
{ | |||
/// <summary> | |||
/// What the topic of the channel should be set to. | |||
/// What the topic of the channel should be set to. | |||
/// </summary> | |||
public Optional<string> Topic { get; set; } | |||
/// <summary> | |||
/// Should this channel be flagged as NSFW? | |||
/// Should this channel be flagged as NSFW? | |||
/// </summary> | |||
public Optional<bool> IsNsfw { get; set; } | |||
} | |||
@@ -4,11 +4,12 @@ | |||
public class VoiceChannelProperties : GuildChannelProperties | |||
{ | |||
/// <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> | |||
public Optional<int> Bitrate { get; set; } | |||
/// <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> | |||
public Optional<int?> UserLimit { get; set; } | |||
} | |||
@@ -1,27 +1,26 @@ | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// A unicode emoji | |||
/// A unicode emoji | |||
/// </summary> | |||
public class Emoji : IEmote | |||
{ | |||
// TODO: need to constrain this to unicode-only emojis somehow | |||
/// <summary> | |||
/// The unicode representation of this emote. | |||
/// </summary> | |||
public string Name { get; } | |||
public override string ToString() => Name; | |||
/// <summary> | |||
/// Creates a unicode emoji. | |||
/// Creates a unicode emoji. | |||
/// </summary> | |||
/// <param name="unicode">The pure UTF-8 encoding of an emoji</param> | |||
public Emoji(string 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) | |||
{ | |||
@@ -4,31 +4,37 @@ using System.Globalization; | |||
namespace Discord | |||
{ | |||
/// <summary> | |||
/// A custom image-based emote | |||
/// A custom image-based emote | |||
/// </summary> | |||
public class Emote : IEmote, ISnowflakeEntity | |||
{ | |||
internal Emote(ulong id, string name, bool animated) | |||
{ | |||
Id = id; | |||
Name = name; | |||
Animated = animated; | |||
} | |||
/// <summary> | |||
/// The display name (tooltip) of this emote | |||
/// Is this emote animated? | |||
/// </summary> | |||
public string Name { get; } | |||
public bool Animated { get; } | |||
public string Url => CDN.GetEmojiUrl(Id, Animated); | |||
private string DebuggerDisplay => $"{Name} ({Id})"; | |||
/// <summary> | |||
/// The ID of this emote | |||
/// The display name (tooltip) of this emote | |||
/// </summary> | |||
public ulong Id { get; } | |||
public string Name { get; } | |||
/// <summary> | |||
/// Is this emote animated? | |||
/// The ID of this emote | |||
/// </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) | |||
{ | |||
@@ -50,13 +56,13 @@ namespace Discord | |||
} | |||
/// <summary> | |||
/// Parse an Emote from its raw format | |||
/// Parse an Emote from its raw format | |||
/// </summary> | |||
/// <param name="text">The raw encoding of an emote; for example, <:dab:277855270321782784></param> | |||
/// <returns>An emote</returns> | |||
public static Emote Parse(string text) | |||
{ | |||
if (TryParse(text, out Emote result)) | |||
if (TryParse(text, out var result)) | |||
return result; | |||
throw new ArgumentException("Invalid emote format", nameof(text)); | |||
} | |||
@@ -64,27 +70,24 @@ namespace Discord | |||
public static bool TryParse(string text, out Emote result) | |||
{ | |||
result = null; | |||
if (text.Length >= 4 && text[0] == '<' && (text[1] == ':' || (text[1] == 'a' && text[2] == ':')) && text[text.Length - 1] == '>') | |||
{ | |||
bool animated = text[1] == 'a'; | |||
int startIndex = animated ? 3 : 2; | |||
int splitIndex = text.IndexOf(':', startIndex); | |||
if (splitIndex == -1) | |||
return false; | |||
if (text.Length < 4 || text[0] != '<' || text[1] != ':' && (text[1] != 'a' || text[2] != ':') || | |||
text[text.Length - 1] != '>') return false; | |||
var animated = text[1] == 'a'; | |||
var startIndex = animated ? 3 : 2; | |||
if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, CultureInfo.InvariantCulture, out ulong id)) | |||
return false; | |||
var splitIndex = text.IndexOf(':', startIndex); | |||
if (splitIndex == -1) | |||
return false; | |||
string name = text.Substring(startIndex, splitIndex - startIndex); | |||
result = new Emote(id, name, animated); | |||
return true; | |||
} | |||
return false; | |||
if (!ulong.TryParse(text.Substring(splitIndex + 1, text.Length - splitIndex - 2), NumberStyles.None, | |||
CultureInfo.InvariantCulture, out var id)) | |||
return false; | |||
var name = text.Substring(startIndex, splitIndex - startIndex); | |||
result = new Emote(id, name, animated); | |||
return true; | |||
} | |||
private string DebuggerDisplay => $"{Name} ({Id})"; | |||
public override string ToString() => $"<{(Animated ? "a" : "")}:{Name}:{Id}>"; | |||
} | |||
} |