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