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