| @@ -6,13 +6,14 @@ namespace Discord.Commands | |||||
| public class CommandAttribute : Attribute | public class CommandAttribute : Attribute | ||||
| { | { | ||||
| public string Text { get; } | public string Text { get; } | ||||
| public string Name { get; } | |||||
| public CommandAttribute(string name) : this(name, name) { } | |||||
| public CommandAttribute(string text, string name) | |||||
| public CommandAttribute() | |||||
| { | { | ||||
| Text = text.ToLowerInvariant(); | |||||
| Name = name; | |||||
| Text = null; | |||||
| } | |||||
| public CommandAttribute(string text) | |||||
| { | |||||
| Text = text; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,35 +1,121 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Collections.Immutable; | |||||
| using System.Diagnostics; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public class Command | public class Command | ||||
| { | { | ||||
| private Action<IMessage> _action; | |||||
| private readonly object _instance; | |||||
| private readonly Func<IMessage, IReadOnlyList<object>, Task> _action; | |||||
| public string Name { get; } | public string Name { get; } | ||||
| public string Description { get; } | public string Description { get; } | ||||
| public string Text { get; } | public string Text { get; } | ||||
| public Module Module { get; } | |||||
| public IReadOnlyList<CommandParameter> Parameters { get; } | |||||
| internal Command(CommandAttribute attribute, MethodInfo methodInfo) | |||||
| internal Command(Module module, object instance, CommandAttribute attribute, MethodInfo methodInfo) | |||||
| { | { | ||||
| Module = module; | |||||
| _instance = instance; | |||||
| Name = methodInfo.Name; | |||||
| Text = attribute.Text; | |||||
| var description = methodInfo.GetCustomAttribute<DescriptionAttribute>(); | var description = methodInfo.GetCustomAttribute<DescriptionAttribute>(); | ||||
| if (description != null) | if (description != null) | ||||
| Description = description.Text; | Description = description.Text; | ||||
| Name = attribute.Name; | |||||
| Text = attribute.Text; | |||||
| Parameters = BuildParameters(methodInfo); | |||||
| _action = BuildAction(methodInfo); | |||||
| } | } | ||||
| public void Invoke(IMessage msg) | |||||
| public async Task<ParseResult> Parse(IMessage msg, SearchResult searchResult) | |||||
| { | { | ||||
| _action.Invoke(msg); | |||||
| if (!searchResult.IsSuccess) | |||||
| return ParseResult.FromError(searchResult); | |||||
| return await CommandParser.ParseArgs(this, msg, searchResult.ArgText, 0).ConfigureAwait(false); | |||||
| } | |||||
| public async Task<ExecuteResult> Execute(IMessage msg, ParseResult parseResult) | |||||
| { | |||||
| if (!parseResult.IsSuccess) | |||||
| return ExecuteResult.FromError(parseResult); | |||||
| try | |||||
| { | |||||
| await _action.Invoke(msg, parseResult.Values);//Note: This code may need context | |||||
| return ExecuteResult.FromSuccess(); | |||||
| } | |||||
| catch (Exception ex) | |||||
| { | |||||
| return ExecuteResult.FromError(ex); | |||||
| } | |||||
| } | } | ||||
| private void BuildAction() | |||||
| private IReadOnlyList<CommandParameter> BuildParameters(MethodInfo methodInfo) | |||||
| { | { | ||||
| _action = null; | |||||
| //TODO: Implement | |||||
| var parameters = methodInfo.GetParameters(); | |||||
| var paramBuilder = ImmutableArray.CreateBuilder<CommandParameter>(parameters.Length - 1); | |||||
| for (int i = 0; i < parameters.Length; i++) | |||||
| { | |||||
| var parameter = parameters[i]; | |||||
| var type = parameter.ParameterType; | |||||
| if (i == 0) | |||||
| { | |||||
| if (type != typeof(IMessage)) | |||||
| throw new InvalidOperationException("The first parameter of a command must be IMessage."); | |||||
| else | |||||
| continue; | |||||
| } | |||||
| var typeInfo = type.GetTypeInfo(); | |||||
| if (typeInfo.IsEnum) | |||||
| type = Enum.GetUnderlyingType(type); | |||||
| var reader = Module.Service.GetTypeReader(type); | |||||
| if (reader == null) | |||||
| throw new InvalidOperationException($"This type ({type.FullName}) is not supported."); | |||||
| bool isUnparsed = parameter.GetCustomAttribute<UnparsedAttribute>() != null; | |||||
| if (isUnparsed) | |||||
| { | |||||
| if (type != typeof(string)) | |||||
| throw new InvalidOperationException("Unparsed parameters only support the string type."); | |||||
| else if (i != parameters.Length - 1) | |||||
| throw new InvalidOperationException("Unparsed parameters must be the last parameter in a command."); | |||||
| } | |||||
| string name = parameter.Name; | |||||
| string description = typeInfo.GetCustomAttribute<DescriptionAttribute>()?.Text; | |||||
| bool isOptional = parameter.IsOptional; | |||||
| object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null; | |||||
| paramBuilder.Add(new CommandParameter(name, description, reader, isOptional, isUnparsed, defaultValue)); | |||||
| } | |||||
| return paramBuilder.ToImmutable(); | |||||
| } | |||||
| private Func<IMessage, IReadOnlyList<object>, Task> BuildAction(MethodInfo methodInfo) | |||||
| { | |||||
| //TODO: Temporary reflection hack. Lets build an actual expression tree here. | |||||
| return (msg, args) => | |||||
| { | |||||
| object[] newArgs = new object[args.Count + 1]; | |||||
| newArgs[0] = msg; | |||||
| for (int i = 0; i < args.Count; i++) | |||||
| newArgs[i + 1] = args[i]; | |||||
| var result = methodInfo.Invoke(_instance, newArgs); | |||||
| return result as Task ?? Task.CompletedTask; | |||||
| }; | |||||
| } | } | ||||
| public override string ToString() => Name; | |||||
| private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,20 @@ | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public enum CommandError | |||||
| { | |||||
| //Search | |||||
| UnknownCommand, | |||||
| //Parse | |||||
| ParseFailed, | |||||
| BadArgCount, | |||||
| //Parse (Type Reader) | |||||
| CastFailed, | |||||
| ObjectNotFound, | |||||
| MultipleMatches, | |||||
| //Execute | |||||
| Exception, | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| using System.Diagnostics; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| //TODO: Add support for Multiple | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public class CommandParameter | |||||
| { | |||||
| private readonly TypeReader _reader; | |||||
| public string Name { get; } | |||||
| public string Description { get; } | |||||
| public bool IsOptional { get; } | |||||
| public bool IsUnparsed { get; } | |||||
| internal object DefaultValue { get; } | |||||
| public CommandParameter(string name, string description, TypeReader reader, bool isOptional, bool isUnparsed, object defaultValue) | |||||
| { | |||||
| _reader = reader; | |||||
| IsOptional = isOptional; | |||||
| IsUnparsed = isUnparsed; | |||||
| DefaultValue = defaultValue; | |||||
| } | |||||
| public async Task<TypeReaderResult> Parse(IMessage context, string input) | |||||
| { | |||||
| return await _reader.Read(context, input).ConfigureAwait(false); | |||||
| } | |||||
| public override string ToString() => Name; | |||||
| private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsUnparsed ? " (Unparsed)" : "")}"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,144 @@ | |||||
| using System.Collections.Immutable; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| internal static class CommandParser | |||||
| { | |||||
| private enum ParserPart | |||||
| { | |||||
| None, | |||||
| Parameter, | |||||
| QuotedParameter | |||||
| } | |||||
| //TODO: Check support for escaping | |||||
| public static async Task<ParseResult> ParseArgs(Command command, IMessage context, string input, int startPos) | |||||
| { | |||||
| CommandParameter curParam = null; | |||||
| StringBuilder argBuilder = new StringBuilder(input.Length); | |||||
| int endPos = input.Length; | |||||
| var curPart = ParserPart.None; | |||||
| int lastArgEndPos = int.MinValue; | |||||
| var argList = ImmutableArray.CreateBuilder<object>(); | |||||
| bool isEscaping = false; | |||||
| char c; | |||||
| for (int curPos = startPos; curPos <= endPos; curPos++) | |||||
| { | |||||
| if (curPos < endPos) | |||||
| c = input[curPos]; | |||||
| else | |||||
| c = '\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 == '\\') | |||||
| { | |||||
| isEscaping = true; | |||||
| continue; | |||||
| } | |||||
| //If we're processing an unparsed parameter, ignore all other logic | |||||
| if (curParam != null && curParam.IsUnparsed) | |||||
| { | |||||
| argBuilder.Append(c); | |||||
| continue; | |||||
| } | |||||
| //If we're not currently processing one, are we starting the next argument yet? | |||||
| if (curPart == ParserPart.None) | |||||
| { | |||||
| 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 | |||||
| { | |||||
| curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; | |||||
| if (curParam.IsUnparsed) | |||||
| { | |||||
| argBuilder.Append(c); | |||||
| continue; | |||||
| } | |||||
| if (c == '\"') | |||||
| { | |||||
| curPart = ParserPart.QuotedParameter; | |||||
| continue; | |||||
| } | |||||
| curPart = ParserPart.Parameter; | |||||
| } | |||||
| } | |||||
| //Has this parameter ended yet? | |||||
| string argString = null; | |||||
| if (curPart == ParserPart.Parameter) | |||||
| { | |||||
| if (curPos == endPos || char.IsWhiteSpace(c)) | |||||
| { | |||||
| argString = argBuilder.ToString(); | |||||
| lastArgEndPos = curPos; | |||||
| } | |||||
| else | |||||
| argBuilder.Append(c); | |||||
| } | |||||
| else if (curPart == ParserPart.QuotedParameter) | |||||
| { | |||||
| if (c == '\"') | |||||
| { | |||||
| argString = argBuilder.ToString(); //Remove quotes | |||||
| lastArgEndPos = curPos + 1; | |||||
| } | |||||
| else | |||||
| argBuilder.Append(c); | |||||
| } | |||||
| if (argString != null) | |||||
| { | |||||
| if (curParam == null) | |||||
| return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters."); | |||||
| var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false); | |||||
| if (!typeReaderResult.IsSuccess) | |||||
| return ParseResult.FromError(typeReaderResult); | |||||
| argList.Add(typeReaderResult.Value); | |||||
| curParam = null; | |||||
| curPart = ParserPart.None; | |||||
| argBuilder.Clear(); | |||||
| } | |||||
| } | |||||
| if (curParam != null && curParam.IsUnparsed) | |||||
| argList.Add(argBuilder.ToString()); | |||||
| if (isEscaping) | |||||
| 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"); | |||||
| if (argList.Count < command.Parameters.Count) | |||||
| { | |||||
| for (int i = argList.Count; i < command.Parameters.Count; i++) | |||||
| { | |||||
| var param = command.Parameters[i]; | |||||
| if (!param.IsOptional) | |||||
| return ParseResult.FromError(CommandError.BadArgCount, "The input text has too few parameters."); | |||||
| argList.Add(param.DefaultValue); | |||||
| } | |||||
| } | |||||
| return ParseResult.FromSuccess(argList.ToImmutable()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -2,6 +2,7 @@ | |||||
| using System.Collections.Concurrent; | using System.Collections.Concurrent; | ||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | using System.Collections.Immutable; | ||||
| using System.Globalization; | |||||
| using System.Linq; | using System.Linq; | ||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Threading; | using System.Threading; | ||||
| @@ -14,6 +15,7 @@ namespace Discord.Commands | |||||
| private readonly SemaphoreSlim _moduleLock; | private readonly SemaphoreSlim _moduleLock; | ||||
| private readonly ConcurrentDictionary<object, Module> _modules; | private readonly ConcurrentDictionary<object, Module> _modules; | ||||
| private readonly ConcurrentDictionary<string, List<Command>> _map; | private readonly ConcurrentDictionary<string, List<Command>> _map; | ||||
| private readonly Dictionary<Type, TypeReader> _typeReaders; | |||||
| public IEnumerable<Module> Modules => _modules.Select(x => x.Value); | public IEnumerable<Module> Modules => _modules.Select(x => x.Value); | ||||
| public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands); | public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands); | ||||
| @@ -23,6 +25,113 @@ namespace Discord.Commands | |||||
| _moduleLock = new SemaphoreSlim(1, 1); | _moduleLock = new SemaphoreSlim(1, 1); | ||||
| _modules = new ConcurrentDictionary<object, Module>(); | _modules = new ConcurrentDictionary<object, Module>(); | ||||
| _map = new ConcurrentDictionary<string, List<Command>>(); | _map = new ConcurrentDictionary<string, List<Command>>(); | ||||
| _typeReaders = new Dictionary<Type, TypeReader> | |||||
| { | |||||
| [typeof(string)] = new GenericTypeReader((m, s) => Task.FromResult(TypeReaderResult.FromSuccess(s))), | |||||
| [typeof(byte)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| byte value; | |||||
| if (byte.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Byte")); | |||||
| }), | |||||
| [typeof(sbyte)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| sbyte value; | |||||
| if (sbyte.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse SByte")); | |||||
| }), | |||||
| [typeof(ushort)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| ushort value; | |||||
| if (ushort.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt16")); | |||||
| }), | |||||
| [typeof(short)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| short value; | |||||
| if (short.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int16")); | |||||
| }), | |||||
| [typeof(uint)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| uint value; | |||||
| if (uint.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt32")); | |||||
| }), | |||||
| [typeof(int)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| int value; | |||||
| if (int.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int32")); | |||||
| }), | |||||
| [typeof(ulong)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| ulong value; | |||||
| if (ulong.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse UInt64")); | |||||
| }), | |||||
| [typeof(long)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| long value; | |||||
| if (long.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Int64")); | |||||
| }), | |||||
| [typeof(float)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| float value; | |||||
| if (float.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Single")); | |||||
| }), | |||||
| [typeof(double)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| double value; | |||||
| if (double.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Double")); | |||||
| }), | |||||
| [typeof(decimal)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| decimal value; | |||||
| if (decimal.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Decimal")); | |||||
| }), | |||||
| [typeof(DateTime)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| DateTime value; | |||||
| if (DateTime.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse DateTime")); | |||||
| }), | |||||
| [typeof(DateTimeOffset)] = new GenericTypeReader((m, s) => | |||||
| { | |||||
| DateTimeOffset value; | |||||
| if (DateTimeOffset.TryParse(s, out value)) return Task.FromResult(TypeReaderResult.FromSuccess(value)); | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse DateTimeOffset")); | |||||
| }), | |||||
| [typeof(IMessage)] = new MessageTypeReader(), | |||||
| [typeof(IChannel)] = new ChannelTypeReader<IChannel>(), | |||||
| [typeof(IGuildChannel)] = new ChannelTypeReader<IGuildChannel>(), | |||||
| [typeof(ITextChannel)] = new ChannelTypeReader<ITextChannel>(), | |||||
| [typeof(IVoiceChannel)] = new ChannelTypeReader<IVoiceChannel>(), | |||||
| [typeof(IRole)] = new RoleTypeReader(), | |||||
| [typeof(IUser)] = new UserTypeReader<IUser>(), | |||||
| [typeof(IGuildUser)] = new UserTypeReader<IGuildUser>() | |||||
| }; | |||||
| } | |||||
| public void AddTypeReader<T>(TypeReader reader) | |||||
| { | |||||
| _typeReaders[typeof(T)] = reader; | |||||
| } | |||||
| public void AddTypeReader(Type type, TypeReader reader) | |||||
| { | |||||
| _typeReaders[type] = reader; | |||||
| } | |||||
| internal TypeReader GetTypeReader(Type type) | |||||
| { | |||||
| TypeReader reader; | |||||
| if (_typeReaders.TryGetValue(type, out reader)) | |||||
| return reader; | |||||
| return null; | |||||
| } | } | ||||
| public async Task<Module> Load(object module) | public async Task<Module> Load(object module) | ||||
| @@ -46,7 +155,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| private Module LoadInternal(object module, TypeInfo typeInfo) | private Module LoadInternal(object module, TypeInfo typeInfo) | ||||
| { | { | ||||
| var loadedModule = new Module(module, typeInfo); | |||||
| var loadedModule = new Module(this, module, typeInfo); | |||||
| _modules[module] = loadedModule; | _modules[module] = loadedModule; | ||||
| foreach (var cmd in loadedModule.Commands) | foreach (var cmd in loadedModule.Commands) | ||||
| @@ -114,7 +223,7 @@ namespace Discord.Commands | |||||
| } | } | ||||
| //TODO: C#7 Candidate for tuple | //TODO: C#7 Candidate for tuple | ||||
| public SearchResults Search(string input) | |||||
| public SearchResult Search(string input) | |||||
| { | { | ||||
| string lowerInput = input.ToLowerInvariant(); | string lowerInput = input.ToLowerInvariant(); | ||||
| @@ -125,21 +234,25 @@ namespace Discord.Commands | |||||
| { | { | ||||
| endPos = input.IndexOf(' ', startPos); | endPos = input.IndexOf(' ', startPos); | ||||
| string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); | string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos); | ||||
| startPos = endPos + 1; | |||||
| if (!_map.TryGetValue(cmdText, out group)) | if (!_map.TryGetValue(cmdText, out group)) | ||||
| break; | break; | ||||
| bestGroup = group; | bestGroup = group; | ||||
| if (endPos == -1) | |||||
| { | |||||
| startPos = input.Length; | |||||
| break; | |||||
| } | |||||
| else | |||||
| startPos = endPos + 1; | |||||
| } | } | ||||
| ImmutableArray<Command> cmds; | |||||
| if (bestGroup != null) | if (bestGroup != null) | ||||
| { | { | ||||
| lock (bestGroup) | lock (bestGroup) | ||||
| cmds = bestGroup.ToImmutableArray(); | |||||
| return SearchResult.FromSuccess(bestGroup.ToImmutableArray(), input.Substring(startPos)); | |||||
| } | } | ||||
| else | else | ||||
| cmds = ImmutableArray.Create<Command>(); | |||||
| return new SearchResults(cmds, startPos); | |||||
| return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,27 +1,33 @@ | |||||
| using System.Collections.Generic; | using System.Collections.Generic; | ||||
| using System.Diagnostics; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public class Module | public class Module | ||||
| { | { | ||||
| public CommandService Service { get; } | |||||
| public string Name { get; } | public string Name { get; } | ||||
| public IEnumerable<Command> Commands { get; } | public IEnumerable<Command> Commands { get; } | ||||
| internal Module(object parent, TypeInfo typeInfo) | |||||
| internal Module(CommandService service, object instance, TypeInfo typeInfo) | |||||
| { | { | ||||
| Service = service; | |||||
| Name = typeInfo.Name; | |||||
| List<Command> commands = new List<Command>(); | List<Command> commands = new List<Command>(); | ||||
| SearchClass(parent, commands, typeInfo); | |||||
| SearchClass(instance, commands, typeInfo); | |||||
| Commands = commands; | Commands = commands; | ||||
| } | } | ||||
| private void SearchClass(object parent, List<Command> commands, TypeInfo typeInfo) | |||||
| private void SearchClass(object instance, List<Command> commands, TypeInfo typeInfo) | |||||
| { | { | ||||
| foreach (var method in typeInfo.DeclaredMethods) | foreach (var method in typeInfo.DeclaredMethods) | ||||
| { | { | ||||
| var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | var cmdAttr = method.GetCustomAttribute<CommandAttribute>(); | ||||
| if (cmdAttr != null) | if (cmdAttr != null) | ||||
| commands.Add(new Command(cmdAttr, method)); | |||||
| commands.Add(new Command(this, instance, cmdAttr, method)); | |||||
| } | } | ||||
| foreach (var type in typeInfo.DeclaredNestedTypes) | foreach (var type in typeInfo.DeclaredNestedTypes) | ||||
| { | { | ||||
| @@ -29,5 +35,8 @@ namespace Discord.Commands | |||||
| SearchClass(ReflectionUtils.CreateObject(type), commands, type); | SearchClass(ReflectionUtils.CreateObject(type), commands, type); | ||||
| } | } | ||||
| } | } | ||||
| public override string ToString() => Name; | |||||
| private string DebuggerDisplay => Name; | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,48 @@ | |||||
| using System; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| internal class ChannelTypeReader<T> : TypeReader | |||||
| where T : class, IChannel | |||||
| { | |||||
| public override async Task<TypeReaderResult> Read(IMessage context, string input) | |||||
| { | |||||
| IGuildChannel guildChannel = context.Channel as IGuildChannel; | |||||
| IChannel result = null; | |||||
| if (guildChannel != null) | |||||
| { | |||||
| //By Id | |||||
| ulong id; | |||||
| if (MentionUtils.TryParseChannel(input, out id) || ulong.TryParse(input, out id)) | |||||
| { | |||||
| var channel = await guildChannel.Guild.GetChannelAsync(id).ConfigureAwait(false); | |||||
| if (channel != null) | |||||
| result = channel; | |||||
| } | |||||
| //By Name | |||||
| if (result == null) | |||||
| { | |||||
| var channels = await guildChannel.Guild.GetChannelsAsync().ConfigureAwait(false); | |||||
| var filteredChannels = channels.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); | |||||
| if (filteredChannels.Length > 1) | |||||
| return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple channels found."); | |||||
| else if (filteredChannels.Length == 1) | |||||
| result = filteredChannels[0]; | |||||
| } | |||||
| } | |||||
| if (result == null) | |||||
| return TypeReaderResult.FromError(CommandError.ObjectNotFound, "Channel not found."); | |||||
| T castResult = result as T; | |||||
| if (castResult == null) | |||||
| return TypeReaderResult.FromError(CommandError.CastFailed, $"Channel is not a {typeof(T).Name}."); | |||||
| else | |||||
| return TypeReaderResult.FromSuccess(castResult); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,17 @@ | |||||
| using System; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| internal class GenericTypeReader : TypeReader | |||||
| { | |||||
| private readonly Func<IMessage, string, Task<TypeReaderResult>> _action; | |||||
| public GenericTypeReader(Func<IMessage, string, Task<TypeReaderResult>> action) | |||||
| { | |||||
| _action = action; | |||||
| } | |||||
| public override Task<TypeReaderResult> Read(IMessage context, string input) => _action(context, input); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| using System.Globalization; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| internal class MessageTypeReader : TypeReader | |||||
| { | |||||
| public override Task<TypeReaderResult> Read(IMessage context, string input) | |||||
| { | |||||
| //By Id | |||||
| ulong id; | |||||
| if (ulong.TryParse(input, NumberStyles.None, CultureInfo.InvariantCulture, out id)) | |||||
| { | |||||
| var msg = context.Channel.GetCachedMessage(id); | |||||
| if (msg == null) | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Message not found.")); | |||||
| else | |||||
| return Task.FromResult(TypeReaderResult.FromSuccess(msg)); | |||||
| } | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, "Failed to parse Message Id.")); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,36 @@ | |||||
| using System; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| internal class RoleTypeReader : TypeReader | |||||
| { | |||||
| public override Task<TypeReaderResult> Read(IMessage context, string input) | |||||
| { | |||||
| IGuildChannel guildChannel = context.Channel as IGuildChannel; | |||||
| if (guildChannel != null) | |||||
| { | |||||
| //By Id | |||||
| ulong id; | |||||
| if (MentionUtils.TryParseRole(input, out id) || ulong.TryParse(input, out id)) | |||||
| { | |||||
| var channel = guildChannel.Guild.GetRole(id); | |||||
| if (channel != null) | |||||
| return Task.FromResult(TypeReaderResult.FromSuccess(channel)); | |||||
| } | |||||
| //By Name | |||||
| var roles = guildChannel.Guild.Roles; | |||||
| var filteredRoles = roles.Where(x => string.Equals(input, x.Name, StringComparison.OrdinalIgnoreCase)).ToArray(); | |||||
| if (filteredRoles.Length > 1) | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple roles found.")); | |||||
| else if (filteredRoles.Length == 1) | |||||
| return Task.FromResult(TypeReaderResult.FromSuccess(filteredRoles[0])); | |||||
| } | |||||
| return Task.FromResult(TypeReaderResult.FromError(CommandError.ObjectNotFound, "Role not found.")); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public abstract class TypeReader | |||||
| { | |||||
| public abstract Task<TypeReaderResult> Read(IMessage context, string input); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,66 @@ | |||||
| using System; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| internal class UserTypeReader<T> : TypeReader | |||||
| where T : class, IUser | |||||
| { | |||||
| public override async Task<TypeReaderResult> Read(IMessage context, string input) | |||||
| { | |||||
| IGuildChannel guildChannel = context.Channel as IGuildChannel; | |||||
| IUser result = null; | |||||
| if (guildChannel != null) | |||||
| { | |||||
| //By Id | |||||
| ulong id; | |||||
| if (MentionUtils.TryParseUser(input, out id) || ulong.TryParse(input, out id)) | |||||
| { | |||||
| var user = await guildChannel.Guild.GetUserAsync(id).ConfigureAwait(false); | |||||
| if (user != null) | |||||
| result = user; | |||||
| } | |||||
| //By Username + Discriminator | |||||
| if (result == null) | |||||
| { | |||||
| int index = input.LastIndexOf('#'); | |||||
| if (index >= 0) | |||||
| { | |||||
| string username = input.Substring(0, index); | |||||
| ushort discriminator; | |||||
| if (ushort.TryParse(input.Substring(index + 1), out discriminator)) | |||||
| { | |||||
| var users = await guildChannel.Guild.GetUsersAsync().ConfigureAwait(false); | |||||
| result = users.Where(x => | |||||
| x.DiscriminatorValue == discriminator && | |||||
| string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); | |||||
| } | |||||
| } | |||||
| } | |||||
| //By Username | |||||
| if (result == null) | |||||
| { | |||||
| var users = await guildChannel.Guild.GetUsersAsync().ConfigureAwait(false); | |||||
| var filteredUsers = users.Where(x => string.Equals(input, x.Username, StringComparison.OrdinalIgnoreCase)).ToArray(); | |||||
| if (filteredUsers.Length > 1) | |||||
| return TypeReaderResult.FromError(CommandError.MultipleMatches, "Multiple users found."); | |||||
| else if (filteredUsers.Length == 1) | |||||
| result = filteredUsers[0]; | |||||
| } | |||||
| } | |||||
| if (result == null) | |||||
| return TypeReaderResult.FromError(CommandError.ObjectNotFound, "User not found."); | |||||
| T castResult = result as T; | |||||
| if (castResult == null) | |||||
| return TypeReaderResult.FromError(CommandError.CastFailed, $"User is not a {typeof(T).Name}."); | |||||
| else | |||||
| return TypeReaderResult.FromSuccess(castResult); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| using System; | |||||
| using System.Diagnostics; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public struct ExecuteResult : IResult | |||||
| { | |||||
| public Exception Exception { get; } | |||||
| public CommandError? Error { get; } | |||||
| public string ErrorReason { get; } | |||||
| public bool IsSuccess => !Error.HasValue; | |||||
| private ExecuteResult(Exception exception, CommandError? error, string errorReason) | |||||
| { | |||||
| Exception = exception; | |||||
| Error = error; | |||||
| ErrorReason = errorReason; | |||||
| } | |||||
| internal static ExecuteResult FromSuccess() | |||||
| => new ExecuteResult(null, null, null); | |||||
| internal static ExecuteResult FromError(CommandError error, string reason) | |||||
| => new ExecuteResult(null, error, reason); | |||||
| internal static ExecuteResult FromError(Exception ex) | |||||
| => new ExecuteResult(ex, CommandError.Exception, ex.Message); | |||||
| internal static ExecuteResult FromError(ParseResult result) | |||||
| => new ExecuteResult(null, result.Error, result.ErrorReason); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public interface IResult | |||||
| { | |||||
| CommandError? Error { get; } | |||||
| string ErrorReason { get; } | |||||
| bool IsSuccess { get; } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public struct ParseResult : IResult | |||||
| { | |||||
| public IReadOnlyList<object> Values { get; } | |||||
| public CommandError? Error { get; } | |||||
| public string ErrorReason { get; } | |||||
| public bool IsSuccess => !Error.HasValue; | |||||
| private ParseResult(IReadOnlyList<object> values, CommandError? error, string errorReason) | |||||
| { | |||||
| Values = values; | |||||
| Error = error; | |||||
| ErrorReason = errorReason; | |||||
| } | |||||
| internal static ParseResult FromSuccess(IReadOnlyList<object> values) | |||||
| => new ParseResult(values, null, null); | |||||
| internal static ParseResult FromError(CommandError error, string reason) | |||||
| => new ParseResult(null, error, reason); | |||||
| internal static ParseResult FromError(SearchResult result) | |||||
| => new ParseResult(null, result.Error, result.ErrorReason); | |||||
| internal static ParseResult FromError(TypeReaderResult result) | |||||
| => new ParseResult(null, result.Error, result.ErrorReason); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? $"Success ({Values.Count} Values)" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,33 @@ | |||||
| using System.Collections.Generic; | |||||
| using System.Diagnostics; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public struct SearchResult : IResult | |||||
| { | |||||
| public IReadOnlyList<Command> Commands { get; } | |||||
| public string ArgText { get; } | |||||
| public CommandError? Error { get; } | |||||
| public string ErrorReason { get; } | |||||
| public bool IsSuccess => !Error.HasValue; | |||||
| private SearchResult(IReadOnlyList<Command> commands, string argText, CommandError? error, string errorReason) | |||||
| { | |||||
| Commands = commands; | |||||
| ArgText = argText; | |||||
| Error = error; | |||||
| ErrorReason = errorReason; | |||||
| } | |||||
| internal static SearchResult FromSuccess(IReadOnlyList<Command> commands, string argText) | |||||
| => new SearchResult(commands, argText, null, null); | |||||
| internal static SearchResult FromError(CommandError error, string reason) | |||||
| => new SearchResult(null, null, error, reason); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? $"Success ({Commands.Count} Results)" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| using System.Diagnostics; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| [DebuggerDisplay(@"{DebuggerDisplay,nq}")] | |||||
| public struct TypeReaderResult : IResult | |||||
| { | |||||
| public object Value { get; } | |||||
| public CommandError? Error { get; } | |||||
| public string ErrorReason { get; } | |||||
| public bool IsSuccess => !Error.HasValue; | |||||
| private TypeReaderResult(object value, CommandError? error, string errorReason) | |||||
| { | |||||
| Value = value; | |||||
| Error = error; | |||||
| ErrorReason = errorReason; | |||||
| } | |||||
| public static TypeReaderResult FromSuccess(object value) | |||||
| => new TypeReaderResult(value, null, null); | |||||
| public static TypeReaderResult FromError(CommandError error, string reason) | |||||
| => new TypeReaderResult(null, error, reason); | |||||
| public override string ToString() => IsSuccess ? "Success" : $"{Error}: {ErrorReason}"; | |||||
| private string DebuggerDisplay => IsSuccess ? $"Success ({Value})" : $"{Error}: {ErrorReason}"; | |||||
| } | |||||
| } | |||||
| @@ -1,16 +0,0 @@ | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public struct SearchResults | |||||
| { | |||||
| IReadOnlyList<Command> Commands { get; } | |||||
| int ArgsPos { get; } | |||||
| public SearchResults(IReadOnlyList<Command> commands, int argsPos) | |||||
| { | |||||
| Commands = commands; | |||||
| ArgsPos = argsPos; | |||||
| } | |||||
| } | |||||
| } | |||||