Browse Source

Implemented command type readers, parser and service.

tags/1.0-rc
RogueException 9 years ago
parent
commit
f59b6b9004
19 changed files with 775 additions and 42 deletions
  1. +6
    -5
      src/Discord.Net.Commands/Attributes/CommandAttribute.cs
  2. +95
    -9
      src/Discord.Net.Commands/Command.cs
  3. +20
    -0
      src/Discord.Net.Commands/CommandError.cs
  4. +34
    -0
      src/Discord.Net.Commands/CommandParameter.cs
  5. +144
    -0
      src/Discord.Net.Commands/CommandParser.cs
  6. +121
    -8
      src/Discord.Net.Commands/CommandService.cs
  7. +13
    -4
      src/Discord.Net.Commands/Module.cs
  8. +48
    -0
      src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
  9. +17
    -0
      src/Discord.Net.Commands/Readers/GenericTypeReader.cs
  10. +24
    -0
      src/Discord.Net.Commands/Readers/MessageTypeReader.cs
  11. +36
    -0
      src/Discord.Net.Commands/Readers/RoleTypeReader.cs
  12. +9
    -0
      src/Discord.Net.Commands/Readers/TypeReader.cs
  13. +66
    -0
      src/Discord.Net.Commands/Readers/UserTypeReader.cs
  14. +35
    -0
      src/Discord.Net.Commands/Results/ExecuteResult.cs
  15. +9
    -0
      src/Discord.Net.Commands/Results/IResult.cs
  16. +35
    -0
      src/Discord.Net.Commands/Results/ParseResult.cs
  17. +33
    -0
      src/Discord.Net.Commands/Results/SearchResult.cs
  18. +30
    -0
      src/Discord.Net.Commands/Results/TypeReaderResult.cs
  19. +0
    -16
      src/Discord.Net.Commands/SearchResults.cs

+ 6
- 5
src/Discord.Net.Commands/Attributes/CommandAttribute.cs View File

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

+ 95
- 9
src/Discord.Net.Commands/Command.cs View File

@@ -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
- 0
src/Discord.Net.Commands/CommandError.cs View 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
- 0
src/Discord.Net.Commands/CommandParameter.cs View 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
- 0
src/Discord.Net.Commands/CommandParser.cs View 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());
}
}
}

+ 121
- 8
src/Discord.Net.Commands/CommandService.cs View File

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

+ 13
- 4
src/Discord.Net.Commands/Module.cs View File

@@ -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
- 0
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs View 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
- 0
src/Discord.Net.Commands/Readers/GenericTypeReader.cs View 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
- 0
src/Discord.Net.Commands/Readers/MessageTypeReader.cs View 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
- 0
src/Discord.Net.Commands/Readers/RoleTypeReader.cs View 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
- 0
src/Discord.Net.Commands/Readers/TypeReader.cs View 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
- 0
src/Discord.Net.Commands/Readers/UserTypeReader.cs View 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
- 0
src/Discord.Net.Commands/Results/ExecuteResult.cs View 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
- 0
src/Discord.Net.Commands/Results/IResult.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Commands
{
public interface IResult
{
CommandError? Error { get; }
string ErrorReason { get; }
bool IsSuccess { get; }
}
}

+ 35
- 0
src/Discord.Net.Commands/Results/ParseResult.cs View 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
- 0
src/Discord.Net.Commands/Results/SearchResult.cs View 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
- 0
src/Discord.Net.Commands/Results/TypeReaderResult.cs View 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}";
}
}

+ 0
- 16
src/Discord.Net.Commands/SearchResults.cs View File

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

Loading…
Cancel
Save