Implemented command type readers, parser and service.
This commit is contained in:
@@ -6,13 +6,14 @@ namespace Discord.Commands
|
||||
public class CommandAttribute : Attribute
|
||||
{
|
||||
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.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||
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 Description { 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>();
|
||||
if (description != null)
|
||||
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})";
|
||||
}
|
||||
}
|
||||
|
||||
20
src/Discord.Net.Commands/CommandError.cs
Normal file
20
src/Discord.Net.Commands/CommandError.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public enum CommandError
|
||||
{
|
||||
//Search
|
||||
UnknownCommand,
|
||||
|
||||
//Parse
|
||||
ParseFailed,
|
||||
BadArgCount,
|
||||
|
||||
//Parse (Type Reader)
|
||||
CastFailed,
|
||||
ObjectNotFound,
|
||||
MultipleMatches,
|
||||
|
||||
//Execute
|
||||
Exception,
|
||||
}
|
||||
}
|
||||
34
src/Discord.Net.Commands/CommandParameter.cs
Normal file
34
src/Discord.Net.Commands/CommandParameter.cs
Normal file
@@ -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)" : "")}";
|
||||
}
|
||||
}
|
||||
144
src/Discord.Net.Commands/CommandParser.cs
Normal file
144
src/Discord.Net.Commands/CommandParser.cs
Normal file
@@ -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.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
@@ -14,6 +15,7 @@ namespace Discord.Commands
|
||||
private readonly SemaphoreSlim _moduleLock;
|
||||
private readonly ConcurrentDictionary<object, Module> _modules;
|
||||
private readonly ConcurrentDictionary<string, List<Command>> _map;
|
||||
private readonly Dictionary<Type, TypeReader> _typeReaders;
|
||||
|
||||
public IEnumerable<Module> Modules => _modules.Select(x => x.Value);
|
||||
public IEnumerable<Command> Commands => _modules.SelectMany(x => x.Value.Commands);
|
||||
@@ -23,6 +25,113 @@ namespace Discord.Commands
|
||||
_moduleLock = new SemaphoreSlim(1, 1);
|
||||
_modules = new ConcurrentDictionary<object, Module>();
|
||||
_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)
|
||||
@@ -46,7 +155,7 @@ namespace Discord.Commands
|
||||
}
|
||||
private Module LoadInternal(object module, TypeInfo typeInfo)
|
||||
{
|
||||
var loadedModule = new Module(module, typeInfo);
|
||||
var loadedModule = new Module(this, module, typeInfo);
|
||||
_modules[module] = loadedModule;
|
||||
|
||||
foreach (var cmd in loadedModule.Commands)
|
||||
@@ -114,7 +223,7 @@ namespace Discord.Commands
|
||||
}
|
||||
|
||||
//TODO: C#7 Candidate for tuple
|
||||
public SearchResults Search(string input)
|
||||
public SearchResult Search(string input)
|
||||
{
|
||||
string lowerInput = input.ToLowerInvariant();
|
||||
|
||||
@@ -125,21 +234,25 @@ namespace Discord.Commands
|
||||
{
|
||||
endPos = input.IndexOf(' ', startPos);
|
||||
string cmdText = endPos == -1 ? input.Substring(startPos) : input.Substring(startPos, endPos - startPos);
|
||||
startPos = endPos + 1;
|
||||
if (!_map.TryGetValue(cmdText, out group))
|
||||
break;
|
||||
bestGroup = group;
|
||||
if (endPos == -1)
|
||||
{
|
||||
startPos = input.Length;
|
||||
break;
|
||||
}
|
||||
else
|
||||
startPos = endPos + 1;
|
||||
}
|
||||
|
||||
ImmutableArray<Command> cmds;
|
||||
|
||||
if (bestGroup != null)
|
||||
{
|
||||
lock (bestGroup)
|
||||
cmds = bestGroup.ToImmutableArray();
|
||||
return SearchResult.FromSuccess(bestGroup.ToImmutableArray(), input.Substring(startPos));
|
||||
}
|
||||
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.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
[DebuggerDisplay(@"{DebuggerDisplay,nq}")]
|
||||
public class Module
|
||||
{
|
||||
public CommandService Service { get; }
|
||||
public string Name { 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>();
|
||||
SearchClass(parent, commands, typeInfo);
|
||||
SearchClass(instance, commands, typeInfo);
|
||||
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)
|
||||
{
|
||||
var cmdAttr = method.GetCustomAttribute<CommandAttribute>();
|
||||
if (cmdAttr != null)
|
||||
commands.Add(new Command(cmdAttr, method));
|
||||
commands.Add(new Command(this, instance, cmdAttr, method));
|
||||
}
|
||||
foreach (var type in typeInfo.DeclaredNestedTypes)
|
||||
{
|
||||
@@ -29,5 +35,8 @@ namespace Discord.Commands
|
||||
SearchClass(ReflectionUtils.CreateObject(type), commands, type);
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => Name;
|
||||
private string DebuggerDisplay => Name;
|
||||
}
|
||||
}
|
||||
|
||||
48
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
Normal file
48
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Discord.Net.Commands/Readers/GenericTypeReader.cs
Normal file
17
src/Discord.Net.Commands/Readers/GenericTypeReader.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
24
src/Discord.Net.Commands/Readers/MessageTypeReader.cs
Normal file
24
src/Discord.Net.Commands/Readers/MessageTypeReader.cs
Normal file
@@ -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."));
|
||||
}
|
||||
}
|
||||
}
|
||||
36
src/Discord.Net.Commands/Readers/RoleTypeReader.cs
Normal file
36
src/Discord.Net.Commands/Readers/RoleTypeReader.cs
Normal file
@@ -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."));
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Discord.Net.Commands/Readers/TypeReader.cs
Normal file
9
src/Discord.Net.Commands/Readers/TypeReader.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public abstract class TypeReader
|
||||
{
|
||||
public abstract Task<TypeReaderResult> Read(IMessage context, string input);
|
||||
}
|
||||
}
|
||||
66
src/Discord.Net.Commands/Readers/UserTypeReader.cs
Normal file
66
src/Discord.Net.Commands/Readers/UserTypeReader.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/Discord.Net.Commands/Results/ExecuteResult.cs
Normal file
35
src/Discord.Net.Commands/Results/ExecuteResult.cs
Normal file
@@ -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}";
|
||||
}
|
||||
}
|
||||
9
src/Discord.Net.Commands/Results/IResult.cs
Normal file
9
src/Discord.Net.Commands/Results/IResult.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public interface IResult
|
||||
{
|
||||
CommandError? Error { get; }
|
||||
string ErrorReason { get; }
|
||||
bool IsSuccess { get; }
|
||||
}
|
||||
}
|
||||
35
src/Discord.Net.Commands/Results/ParseResult.cs
Normal file
35
src/Discord.Net.Commands/Results/ParseResult.cs
Normal file
@@ -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}";
|
||||
}
|
||||
}
|
||||
33
src/Discord.Net.Commands/Results/SearchResult.cs
Normal file
33
src/Discord.Net.Commands/Results/SearchResult.cs
Normal file
@@ -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}";
|
||||
}
|
||||
}
|
||||
30
src/Discord.Net.Commands/Results/TypeReaderResult.cs
Normal file
30
src/Discord.Net.Commands/Results/TypeReaderResult.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user