Early 1.0 REST Preview
This commit is contained in:
@@ -1,83 +0,0 @@
|
||||
using Discord.Commands.Permissions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
//TODO: Make this more friendly and expose it to be extendable
|
||||
public class Command
|
||||
{
|
||||
private string[] _aliases;
|
||||
internal CommandParameter[] _parameters;
|
||||
private IPermissionChecker[] _checks;
|
||||
private Func<CommandEventArgs, Task> _runFunc;
|
||||
internal readonly Dictionary<string, CommandParameter> _parametersByName;
|
||||
|
||||
public string Text { get; }
|
||||
public string Category { get; internal set; }
|
||||
public bool IsHidden { get; internal set; }
|
||||
public string Description { get; internal set; }
|
||||
|
||||
public IEnumerable<string> Aliases => _aliases;
|
||||
public IEnumerable<CommandParameter> Parameters => _parameters;
|
||||
public CommandParameter this[string name] => _parametersByName[name];
|
||||
|
||||
internal Command(string text)
|
||||
{
|
||||
Text = text;
|
||||
IsHidden = false;
|
||||
_aliases = new string[0];
|
||||
_parameters = new CommandParameter[0];
|
||||
_parametersByName = new Dictionary<string, CommandParameter>();
|
||||
}
|
||||
|
||||
|
||||
internal void SetAliases(string[] aliases)
|
||||
{
|
||||
_aliases = aliases;
|
||||
}
|
||||
internal void SetParameters(CommandParameter[] parameters)
|
||||
{
|
||||
_parametersByName.Clear();
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
parameters[i].Id = i;
|
||||
_parametersByName[parameters[i].Name] = parameters[i];
|
||||
}
|
||||
_parameters = parameters;
|
||||
}
|
||||
internal void SetChecks(IPermissionChecker[] checks)
|
||||
{
|
||||
_checks = checks;
|
||||
}
|
||||
|
||||
internal bool CanRun(User user, ITextChannel channel, out string error)
|
||||
{
|
||||
for (int i = 0; i < _checks.Length; i++)
|
||||
{
|
||||
if (!_checks[i].CanRun(this, user, channel, out error))
|
||||
return false;
|
||||
}
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void SetRunFunc(Func<CommandEventArgs, Task> func)
|
||||
{
|
||||
_runFunc = func;
|
||||
}
|
||||
internal void SetRunFunc(Action<CommandEventArgs> func)
|
||||
{
|
||||
_runFunc = TaskHelper.ToAsync(func);
|
||||
}
|
||||
internal Task Run(CommandEventArgs args)
|
||||
{
|
||||
var task = _runFunc(args);
|
||||
if (task != null)
|
||||
return task;
|
||||
else
|
||||
return TaskHelper.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
using Discord.Commands.Permissions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
//TODO: Make this more friendly and expose it to be extendable
|
||||
public sealed class CommandBuilder
|
||||
{
|
||||
private readonly CommandService _service;
|
||||
private readonly Command _command;
|
||||
private readonly List<CommandParameter> _params;
|
||||
private readonly List<IPermissionChecker> _checks;
|
||||
private readonly List<string> _aliases;
|
||||
private readonly string _prefix;
|
||||
private bool _allowRequiredParams, _areParamsClosed;
|
||||
|
||||
public CommandService Service => _service;
|
||||
|
||||
internal CommandBuilder(CommandService service, string text, string prefix = "", string category = "", IEnumerable<IPermissionChecker> initialChecks = null)
|
||||
{
|
||||
_service = service;
|
||||
_prefix = prefix;
|
||||
|
||||
_command = new Command(AppendPrefix(prefix, text));
|
||||
_command.Category = category;
|
||||
|
||||
if (initialChecks != null)
|
||||
_checks = new List<IPermissionChecker>(initialChecks);
|
||||
else
|
||||
_checks = new List<IPermissionChecker>();
|
||||
|
||||
_params = new List<CommandParameter>();
|
||||
_aliases = new List<string>();
|
||||
|
||||
_allowRequiredParams = true;
|
||||
_areParamsClosed = false;
|
||||
}
|
||||
|
||||
public CommandBuilder Alias(params string[] aliases)
|
||||
{
|
||||
_aliases.AddRange(aliases);
|
||||
return this;
|
||||
}
|
||||
/*public CommandBuilder Category(string category)
|
||||
{
|
||||
_command.Category = category;
|
||||
return this;
|
||||
}*/
|
||||
public CommandBuilder Description(string description)
|
||||
{
|
||||
_command.Description = description;
|
||||
return this;
|
||||
}
|
||||
public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required)
|
||||
{
|
||||
if (_areParamsClosed)
|
||||
throw new Exception($"No parameters may be added after a {nameof(ParameterType.Multiple)} or {nameof(ParameterType.Unparsed)} parameter.");
|
||||
if (!_allowRequiredParams && type == ParameterType.Required)
|
||||
throw new Exception($"{nameof(ParameterType.Required)} parameters may not be added after an optional one");
|
||||
|
||||
_params.Add(new CommandParameter(name, type));
|
||||
|
||||
if (type == ParameterType.Optional)
|
||||
_allowRequiredParams = false;
|
||||
if (type == ParameterType.Multiple || type == ParameterType.Unparsed)
|
||||
_areParamsClosed = true;
|
||||
return this;
|
||||
}
|
||||
public CommandBuilder Hide()
|
||||
{
|
||||
_command.IsHidden = true;
|
||||
return this;
|
||||
}
|
||||
public CommandBuilder AddCheck(IPermissionChecker check)
|
||||
{
|
||||
_checks.Add(check);
|
||||
return this;
|
||||
}
|
||||
public CommandBuilder AddCheck(Func<Command, User, ITextChannel, bool> checkFunc, string errorMsg = null)
|
||||
{
|
||||
_checks.Add(new GenericPermissionChecker(checkFunc, errorMsg));
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Do(Func<CommandEventArgs, Task> func)
|
||||
{
|
||||
_command.SetRunFunc(func);
|
||||
Build();
|
||||
}
|
||||
public void Do(Action<CommandEventArgs> func)
|
||||
{
|
||||
_command.SetRunFunc(func);
|
||||
Build();
|
||||
}
|
||||
private void Build()
|
||||
{
|
||||
_command.SetParameters(_params.ToArray());
|
||||
_command.SetChecks(_checks.ToArray());
|
||||
_command.SetAliases(_aliases.Select(x => AppendPrefix(_prefix, x)).ToArray());
|
||||
_service.AddCommand(_command);
|
||||
}
|
||||
|
||||
internal static string AppendPrefix(string prefix, string cmd)
|
||||
{
|
||||
if (cmd != "")
|
||||
{
|
||||
if (prefix != "")
|
||||
return prefix + ' ' + cmd;
|
||||
else
|
||||
return cmd;
|
||||
}
|
||||
else
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
public class CommandGroupBuilder
|
||||
{
|
||||
private readonly CommandService _service;
|
||||
private readonly string _prefix;
|
||||
private readonly List<IPermissionChecker> _checks;
|
||||
private string _category;
|
||||
|
||||
public CommandService Service => _service;
|
||||
|
||||
internal CommandGroupBuilder(CommandService service, string prefix = "", string category = null, IEnumerable<IPermissionChecker> initialChecks = null)
|
||||
{
|
||||
_service = service;
|
||||
_prefix = prefix;
|
||||
_category = category;
|
||||
if (initialChecks != null)
|
||||
_checks = new List<IPermissionChecker>(initialChecks);
|
||||
else
|
||||
_checks = new List<IPermissionChecker>();
|
||||
}
|
||||
|
||||
public CommandGroupBuilder Category(string category)
|
||||
{
|
||||
_category = category;
|
||||
return this;
|
||||
}
|
||||
public void AddCheck(IPermissionChecker checker)
|
||||
{
|
||||
_checks.Add(checker);
|
||||
}
|
||||
public void AddCheck(Func<Command, User, ITextChannel, bool> checkFunc, string errorMsg = null)
|
||||
{
|
||||
_checks.Add(new GenericPermissionChecker(checkFunc, errorMsg));
|
||||
}
|
||||
|
||||
public CommandGroupBuilder CreateGroup(string cmd, Action<CommandGroupBuilder> config)
|
||||
{
|
||||
config(new CommandGroupBuilder(_service, CommandBuilder.AppendPrefix(_prefix, cmd), _category, _checks));
|
||||
return this;
|
||||
}
|
||||
public CommandBuilder CreateCommand()
|
||||
=> CreateCommand("");
|
||||
public CommandBuilder CreateCommand(string cmd)
|
||||
=> new CommandBuilder(_service, cmd, _prefix, _category, _checks);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput }
|
||||
public class CommandErrorEventArgs : CommandEventArgs
|
||||
{
|
||||
public CommandErrorType ErrorType { get; }
|
||||
public Exception Exception { get; }
|
||||
|
||||
public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex)
|
||||
: base(baseArgs.Message, baseArgs.Command, baseArgs.Args)
|
||||
{
|
||||
Exception = ex;
|
||||
ErrorType = errorType;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public class CommandEventArgs : EventArgs
|
||||
{
|
||||
private readonly string[] _args;
|
||||
|
||||
public Message Message { get; }
|
||||
public Command Command { get; }
|
||||
|
||||
public User User => Message.User;
|
||||
public ITextChannel Channel => Message.Channel;
|
||||
|
||||
public CommandEventArgs(Message message, Command command, string[] args)
|
||||
{
|
||||
Message = message;
|
||||
Command = command;
|
||||
_args = args;
|
||||
}
|
||||
|
||||
public string[] Args => _args;
|
||||
public string GetArg(int index) => _args[index];
|
||||
public string GetArg(string name) => _args[Command[name].Id];
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public static class CommandExtensions
|
||||
{
|
||||
public static DiscordClient UsingCommands(this DiscordClient client, CommandServiceConfig config = null)
|
||||
{
|
||||
client.AddService(new CommandService(config));
|
||||
return client;
|
||||
}
|
||||
public static DiscordClient UsingCommands(this DiscordClient client, Action<CommandServiceConfigBuilder> configFunc = null)
|
||||
{
|
||||
var builder = new CommandServiceConfigBuilder();
|
||||
configFunc(builder);
|
||||
client.AddService(new CommandService(builder));
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
//Represents either a single function, command group, or both
|
||||
internal class CommandMap
|
||||
{
|
||||
private readonly CommandMap _parent;
|
||||
private readonly string _name, _fullName;
|
||||
|
||||
private readonly List<Command> _commands;
|
||||
private readonly Dictionary<string, CommandMap> _items;
|
||||
private bool _isVisible, _hasNonAliases, _hasSubGroups;
|
||||
|
||||
public string Name => _name;
|
||||
public string FullName => _fullName;
|
||||
public bool IsVisible => _isVisible;
|
||||
public bool HasNonAliases => _hasNonAliases;
|
||||
public bool HasSubGroups => _hasSubGroups;
|
||||
public IEnumerable<Command> Commands => _commands;
|
||||
public IEnumerable<CommandMap> SubGroups => _items.Values;
|
||||
|
||||
public CommandMap()
|
||||
{
|
||||
_items = new Dictionary<string, CommandMap>();
|
||||
_commands = new List<Command>();
|
||||
_isVisible = false;
|
||||
_hasNonAliases = false;
|
||||
_hasSubGroups = false;
|
||||
}
|
||||
public CommandMap(CommandMap parent, string name, string fullName)
|
||||
: this()
|
||||
{
|
||||
_parent = parent;
|
||||
_name = name;
|
||||
_fullName = fullName;
|
||||
}
|
||||
|
||||
public CommandMap GetItem(string text)
|
||||
{
|
||||
return GetItem(0, text.Split(' '));
|
||||
}
|
||||
public CommandMap GetItem(int index, string[] parts)
|
||||
{
|
||||
if (index != parts.Length)
|
||||
{
|
||||
string nextPart = parts[index];
|
||||
CommandMap nextGroup;
|
||||
if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup))
|
||||
return nextGroup.GetItem(index + 1, parts);
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public IEnumerable<Command> GetCommands()
|
||||
{
|
||||
if (_commands.Count > 0)
|
||||
return _commands;
|
||||
else if (_parent != null)
|
||||
return _parent.GetCommands();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
public IEnumerable<Command> GetCommands(string text)
|
||||
{
|
||||
return GetCommands(0, text.Split(' '));
|
||||
}
|
||||
public IEnumerable<Command> GetCommands(int index, string[] parts)
|
||||
{
|
||||
if (index != parts.Length)
|
||||
{
|
||||
string nextPart = parts[index];
|
||||
CommandMap nextGroup;
|
||||
if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup))
|
||||
{
|
||||
var cmd = nextGroup.GetCommands(index + 1, parts);
|
||||
if (cmd != null)
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
if (_commands != null)
|
||||
return _commands;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void AddCommand(string text, Command command, bool isAlias)
|
||||
{
|
||||
AddCommand(0, text.Split(' '), command, isAlias);
|
||||
}
|
||||
private void AddCommand(int index, string[] parts, Command command, bool isAlias)
|
||||
{
|
||||
if (!command.IsHidden)
|
||||
_isVisible = true;
|
||||
|
||||
if (index != parts.Length)
|
||||
{
|
||||
CommandMap nextGroup;
|
||||
string name = parts[index].ToLowerInvariant();
|
||||
string fullName = string.Join(" ", parts, 0, index + 1);
|
||||
if (!_items.TryGetValue(name, out nextGroup))
|
||||
{
|
||||
nextGroup = new CommandMap(this, name, fullName);
|
||||
_items.Add(name, nextGroup);
|
||||
_hasSubGroups = true;
|
||||
}
|
||||
nextGroup.AddCommand(index + 1, parts, command, isAlias);
|
||||
}
|
||||
else
|
||||
{
|
||||
_commands.Add(command);
|
||||
if (!isAlias)
|
||||
_hasNonAliases = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanRun(User user, ITextChannel channel, out string error)
|
||||
{
|
||||
error = null;
|
||||
if (_commands.Count > 0)
|
||||
{
|
||||
foreach (var cmd in _commands)
|
||||
{
|
||||
if (cmd.CanRun(user, channel, out error))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (_items.Count > 0)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Value.CanRun(user, channel, out error))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public enum ParameterType
|
||||
{
|
||||
/// <summary> Catches a single required parameter. </summary>
|
||||
Required,
|
||||
/// <summary> Catches a single optional parameter. </summary>
|
||||
Optional,
|
||||
/// <summary> Catches a zero or more optional parameters. </summary>
|
||||
Multiple,
|
||||
/// <summary> Catches all remaining text as a single optional parameter. </summary>
|
||||
Unparsed
|
||||
}
|
||||
public class CommandParameter
|
||||
{
|
||||
public string Name { get; }
|
||||
public int Id { get; internal set; }
|
||||
public ParameterType Type { get; }
|
||||
|
||||
internal CommandParameter(string name, ParameterType type)
|
||||
{
|
||||
Name = name;
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
internal static class CommandParser
|
||||
{
|
||||
private enum ParserPart
|
||||
{
|
||||
None,
|
||||
Parameter,
|
||||
QuotedParameter,
|
||||
DoubleQuotedParameter
|
||||
}
|
||||
|
||||
public static bool ParseCommand(string input, CommandMap map, out IEnumerable<Command> commands, out int endPos)
|
||||
{
|
||||
int startPosition = 0;
|
||||
int endPosition = 0;
|
||||
int inputLength = input.Length;
|
||||
bool isEscaped = false;
|
||||
commands = null;
|
||||
endPos = 0;
|
||||
|
||||
if (input == "")
|
||||
return false;
|
||||
|
||||
while (endPosition < inputLength)
|
||||
{
|
||||
char currentChar = input[endPosition++];
|
||||
if (isEscaped)
|
||||
isEscaped = false;
|
||||
else if (currentChar == '\\')
|
||||
isEscaped = true;
|
||||
|
||||
bool isWhitespace = IsWhiteSpace(currentChar);
|
||||
if ((!isEscaped && isWhitespace) || endPosition >= inputLength)
|
||||
{
|
||||
int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition;
|
||||
string temp = input.Substring(startPosition, length);
|
||||
if (temp == "")
|
||||
startPosition = endPosition;
|
||||
else
|
||||
{
|
||||
var newMap = map.GetItem(temp);
|
||||
if (newMap != null)
|
||||
{
|
||||
map = newMap;
|
||||
endPos = endPosition;
|
||||
}
|
||||
else
|
||||
break;
|
||||
startPosition = endPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
commands = map.GetCommands(); //Work our way backwards to find a command that matches our input
|
||||
return commands != null;
|
||||
}
|
||||
private static bool IsWhiteSpace(char c) => c == ' ' || c == '\n' || c == '\r' || c == '\t';
|
||||
|
||||
//TODO: Check support for escaping
|
||||
public static CommandErrorType? ParseArgs(string input, int startPos, Command command, out string[] args)
|
||||
{
|
||||
ParserPart currentPart = ParserPart.None;
|
||||
int startPosition = startPos;
|
||||
int endPosition = startPos;
|
||||
int inputLength = input.Length;
|
||||
bool isEscaped = false;
|
||||
|
||||
var expectedArgs = command._parameters;
|
||||
List<string> argList = new List<string>();
|
||||
CommandParameter parameter = null;
|
||||
|
||||
args = null;
|
||||
|
||||
if (input == "")
|
||||
return CommandErrorType.InvalidInput;
|
||||
|
||||
while (endPosition < inputLength)
|
||||
{
|
||||
if (startPosition == endPosition && (parameter == null || parameter.Type != ParameterType.Multiple)) //Is first char of a new arg
|
||||
{
|
||||
if (argList.Count >= expectedArgs.Length)
|
||||
return CommandErrorType.BadArgCount; //Too many args
|
||||
parameter = expectedArgs[argList.Count];
|
||||
if (parameter.Type == ParameterType.Unparsed)
|
||||
{
|
||||
argList.Add(input.Substring(startPosition));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char currentChar = input[endPosition++];
|
||||
if (isEscaped)
|
||||
isEscaped = false;
|
||||
else if (currentChar == '\\')
|
||||
isEscaped = true;
|
||||
|
||||
bool isWhitespace = IsWhiteSpace(currentChar);
|
||||
if (endPosition == startPosition + 1 && isWhitespace) //Has no text yet, and is another whitespace
|
||||
{
|
||||
startPosition = endPosition;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (currentPart)
|
||||
{
|
||||
case ParserPart.None:
|
||||
if ((!isEscaped && currentChar == '\"'))
|
||||
{
|
||||
currentPart = ParserPart.DoubleQuotedParameter;
|
||||
startPosition = endPosition;
|
||||
}
|
||||
else if ((!isEscaped && currentChar == '\''))
|
||||
{
|
||||
currentPart = ParserPart.QuotedParameter;
|
||||
startPosition = endPosition;
|
||||
}
|
||||
else if ((!isEscaped && isWhitespace) || endPosition >= inputLength)
|
||||
{
|
||||
int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition;
|
||||
if (length == 0)
|
||||
startPosition = endPosition;
|
||||
else
|
||||
{
|
||||
string temp = input.Substring(startPosition, length);
|
||||
argList.Add(temp);
|
||||
currentPart = ParserPart.None;
|
||||
startPosition = endPosition;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserPart.QuotedParameter:
|
||||
if ((!isEscaped && currentChar == '\''))
|
||||
{
|
||||
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
|
||||
argList.Add(temp);
|
||||
currentPart = ParserPart.None;
|
||||
startPosition = endPosition;
|
||||
}
|
||||
else if (endPosition >= inputLength)
|
||||
return CommandErrorType.InvalidInput;
|
||||
break;
|
||||
case ParserPart.DoubleQuotedParameter:
|
||||
if ((!isEscaped && currentChar == '\"'))
|
||||
{
|
||||
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
|
||||
argList.Add(temp);
|
||||
currentPart = ParserPart.None;
|
||||
startPosition = endPosition;
|
||||
}
|
||||
else if (endPosition >= inputLength)
|
||||
return CommandErrorType.InvalidInput;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//Unclosed quotes
|
||||
if (currentPart == ParserPart.QuotedParameter ||
|
||||
currentPart == ParserPart.DoubleQuotedParameter)
|
||||
return CommandErrorType.InvalidInput;
|
||||
|
||||
//Too few args
|
||||
for (int i = argList.Count; i < expectedArgs.Length; i++)
|
||||
{
|
||||
var param = expectedArgs[i];
|
||||
switch (param.Type)
|
||||
{
|
||||
case ParameterType.Required:
|
||||
return CommandErrorType.BadArgCount;
|
||||
case ParameterType.Optional:
|
||||
case ParameterType.Unparsed:
|
||||
argList.Add("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*if (argList.Count > expectedArgs.Length)
|
||||
{
|
||||
if (expectedArgs.Length == 0 || expectedArgs[expectedArgs.Length - 1].Type != ParameterType.Multiple)
|
||||
return CommandErrorType.BadArgCount;
|
||||
}*/
|
||||
|
||||
args = argList.ToArray();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public partial class CommandService : IService
|
||||
{
|
||||
private readonly List<Command> _allCommands;
|
||||
private readonly Dictionary<string, CommandMap> _categories;
|
||||
private readonly CommandMap _map; //Command map stores all commands by their input text, used for fast resolving and parsing
|
||||
|
||||
public CommandServiceConfig Config { get; }
|
||||
public CommandGroupBuilder Root { get; }
|
||||
public DiscordClient Client { get; private set; }
|
||||
|
||||
//AllCommands store a flattened collection of all commands
|
||||
public IEnumerable<Command> AllCommands => _allCommands;
|
||||
//Groups store all commands by their module, used for more informative help
|
||||
internal IEnumerable<CommandMap> Categories => _categories.Values;
|
||||
|
||||
public event EventHandler<CommandEventArgs> CommandExecuted = delegate { };
|
||||
public event EventHandler<CommandErrorEventArgs> CommandErrored = delegate { };
|
||||
|
||||
private void OnCommand(CommandEventArgs args)
|
||||
=> CommandExecuted(this, args);
|
||||
private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null)
|
||||
=> CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex));
|
||||
|
||||
public CommandService()
|
||||
: this(new CommandServiceConfigBuilder())
|
||||
{
|
||||
}
|
||||
public CommandService(CommandServiceConfigBuilder builder)
|
||||
: this(builder.Build())
|
||||
{
|
||||
if (builder.ExecuteHandler != null)
|
||||
CommandExecuted += builder.ExecuteHandler;
|
||||
if (builder.ErrorHandler != null)
|
||||
CommandErrored += builder.ErrorHandler;
|
||||
}
|
||||
public CommandService(CommandServiceConfig config)
|
||||
{
|
||||
Config = config;
|
||||
|
||||
_allCommands = new List<Command>();
|
||||
_map = new CommandMap();
|
||||
_categories = new Dictionary<string, CommandMap>();
|
||||
Root = new CommandGroupBuilder(this);
|
||||
}
|
||||
|
||||
void IService.Install(DiscordClient client)
|
||||
{
|
||||
Client = client;
|
||||
|
||||
if (Config.HelpMode != HelpMode.Disabled)
|
||||
{
|
||||
CreateCommand("help")
|
||||
.Parameter("command", ParameterType.Multiple)
|
||||
.Hide()
|
||||
.Description("Returns information about commands.")
|
||||
.Do(async e =>
|
||||
{
|
||||
ITextChannel replyChannel = Config.HelpMode == HelpMode.Public ? e.Channel : await e.User.CreatePMChannel().ConfigureAwait(false);
|
||||
if (e.Args.Length > 0) //Show command help
|
||||
{
|
||||
var map = _map.GetItem(string.Join(" ", e.Args));
|
||||
if (map != null)
|
||||
await ShowCommandHelp(map, e.User, e.Channel, replyChannel).ConfigureAwait(false);
|
||||
else
|
||||
await replyChannel.SendMessage("Unable to display help: Unknown command.").ConfigureAwait(false);
|
||||
}
|
||||
else //Show general help
|
||||
await ShowGeneralHelp(e.User, e.Channel, replyChannel).ConfigureAwait(false);
|
||||
});
|
||||
}
|
||||
|
||||
client.MessageReceived += async (s, e) =>
|
||||
{
|
||||
if (_allCommands.Count == 0) return;
|
||||
if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return;
|
||||
|
||||
string msg = e.Message.RawText;
|
||||
if (msg.Length == 0) return;
|
||||
|
||||
string cmdMsg = null;
|
||||
|
||||
//Check for command char
|
||||
if (Config.PrefixChar.HasValue)
|
||||
{
|
||||
if (msg[0] == Config.PrefixChar.Value)
|
||||
cmdMsg = msg.Substring(1);
|
||||
}
|
||||
|
||||
//Check for mention
|
||||
if (cmdMsg == null && Config.AllowMentionPrefix)
|
||||
{
|
||||
string mention = client.CurrentUser.Mention;
|
||||
if (msg.StartsWith(mention) && msg.Length > mention.Length)
|
||||
cmdMsg = msg.Substring(mention.Length + 1);
|
||||
else
|
||||
{
|
||||
mention = $"@{client.CurrentUser.Name}";
|
||||
if (msg.StartsWith(mention) && msg.Length > mention.Length)
|
||||
cmdMsg = msg.Substring(mention.Length + 1);
|
||||
}
|
||||
}
|
||||
|
||||
//Check using custom activator
|
||||
if (cmdMsg == null && Config.CustomPrefixHandler != null)
|
||||
{
|
||||
int index = Config.CustomPrefixHandler(e.Message);
|
||||
if (index >= 0)
|
||||
cmdMsg = msg.Substring(index);
|
||||
}
|
||||
|
||||
if (cmdMsg == null) return;
|
||||
|
||||
//Parse command
|
||||
IEnumerable<Command> commands;
|
||||
int argPos;
|
||||
CommandParser.ParseCommand(cmdMsg, _map, out commands, out argPos);
|
||||
if (commands == null)
|
||||
{
|
||||
CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null);
|
||||
OnCommandError(CommandErrorType.UnknownCommand, errorArgs);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var command in commands)
|
||||
{
|
||||
//Parse arguments
|
||||
string[] args;
|
||||
var error = CommandParser.ParseArgs(cmdMsg, argPos, command, out args);
|
||||
if (error != null)
|
||||
{
|
||||
if (error == CommandErrorType.BadArgCount)
|
||||
continue;
|
||||
else
|
||||
{
|
||||
var errorArgs = new CommandEventArgs(e.Message, command, null);
|
||||
OnCommandError(error.Value, errorArgs);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var eventArgs = new CommandEventArgs(e.Message, command, args);
|
||||
|
||||
// Check permissions
|
||||
string errorText;
|
||||
if (!command.CanRun(eventArgs.User, eventArgs.Channel, out errorText))
|
||||
{
|
||||
OnCommandError(CommandErrorType.BadPermissions, eventArgs, errorText != null ? new Exception(errorText) : null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the command
|
||||
try
|
||||
{
|
||||
OnCommand(eventArgs);
|
||||
await command.Run(eventArgs).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
OnCommandError(CommandErrorType.Exception, eventArgs, ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var errorArgs2 = new CommandEventArgs(e.Message, null, null);
|
||||
OnCommandError(CommandErrorType.BadArgCount, errorArgs2);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Task ShowGeneralHelp(User user, ITextChannel channel, ITextChannel replyChannel = null)
|
||||
{
|
||||
StringBuilder output = new StringBuilder();
|
||||
bool isFirstCategory = true;
|
||||
foreach (var category in _categories)
|
||||
{
|
||||
bool isFirstItem = true;
|
||||
foreach (var group in category.Value.SubGroups)
|
||||
{
|
||||
string error;
|
||||
if (group.IsVisible && (group.HasSubGroups || group.HasNonAliases) && group.CanRun(user, channel, out error))
|
||||
{
|
||||
if (isFirstItem)
|
||||
{
|
||||
isFirstItem = false;
|
||||
//This is called for the first item in each category. If we never get here, we dont bother writing the header for a category type (since it's empty)
|
||||
if (isFirstCategory)
|
||||
{
|
||||
isFirstCategory = false;
|
||||
//Called for the first non-empty category
|
||||
output.AppendLine("These are the commands you can use:");
|
||||
}
|
||||
else
|
||||
output.AppendLine();
|
||||
if (category.Key != "")
|
||||
{
|
||||
output.Append(Format.Bold(category.Key));
|
||||
output.Append(": ");
|
||||
}
|
||||
}
|
||||
else
|
||||
output.Append(", ");
|
||||
output.Append('`');
|
||||
output.Append(group.Name);
|
||||
if (group.HasSubGroups)
|
||||
output.Append("*");
|
||||
output.Append('`');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (output.Length == 0)
|
||||
output.Append("There are no commands you have permission to run.");
|
||||
else
|
||||
output.AppendLine("\n\nRun `help <command>` for more information.");
|
||||
|
||||
return (replyChannel ?? channel).SendMessage(output.ToString());
|
||||
}
|
||||
|
||||
private Task ShowCommandHelp(CommandMap map, User user, ITextChannel channel, ITextChannel replyChannel = null)
|
||||
{
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
IEnumerable<Command> cmds = map.Commands;
|
||||
bool isFirstCmd = true;
|
||||
string error;
|
||||
if (cmds.Any())
|
||||
{
|
||||
foreach (var cmd in cmds)
|
||||
{
|
||||
if (cmd.CanRun(user, channel, out error))
|
||||
{
|
||||
if (isFirstCmd)
|
||||
isFirstCmd = false;
|
||||
else
|
||||
output.AppendLine();
|
||||
ShowCommandHelpInternal(cmd, user, channel, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append('`');
|
||||
output.Append(map.FullName);
|
||||
output.Append("`\n");
|
||||
}
|
||||
|
||||
bool isFirstSubCmd = true;
|
||||
foreach (var subCmd in map.SubGroups.Where(x => x.CanRun(user, channel, out error) && x.IsVisible))
|
||||
{
|
||||
if (isFirstSubCmd)
|
||||
{
|
||||
isFirstSubCmd = false;
|
||||
output.AppendLine("Sub Commands: ");
|
||||
}
|
||||
else
|
||||
output.Append(", ");
|
||||
output.Append('`');
|
||||
output.Append(subCmd.Name);
|
||||
if (subCmd.SubGroups.Any())
|
||||
output.Append("*");
|
||||
output.Append('`');
|
||||
}
|
||||
|
||||
if (isFirstCmd && isFirstSubCmd) //Had no commands and no subcommands
|
||||
{
|
||||
output.Clear();
|
||||
output.AppendLine("There are no commands you have permission to run.");
|
||||
}
|
||||
|
||||
return (replyChannel ?? channel).SendMessage(output.ToString());
|
||||
}
|
||||
public Task ShowCommandHelp(Command command, User user, ITextChannel channel, ITextChannel replyChannel = null)
|
||||
{
|
||||
StringBuilder output = new StringBuilder();
|
||||
string error;
|
||||
if (!command.CanRun(user, channel, out error))
|
||||
output.AppendLine(error ?? "You do not have permission to access this command.");
|
||||
else
|
||||
ShowCommandHelpInternal(command, user, channel, output);
|
||||
return (replyChannel ?? channel).SendMessage(output.ToString());
|
||||
}
|
||||
private void ShowCommandHelpInternal(Command command, User user, ITextChannel channel, StringBuilder output)
|
||||
{
|
||||
output.Append('`');
|
||||
output.Append(command.Text);
|
||||
foreach (var param in command.Parameters)
|
||||
{
|
||||
switch (param.Type)
|
||||
{
|
||||
case ParameterType.Required:
|
||||
output.Append($" <{param.Name}>");
|
||||
break;
|
||||
case ParameterType.Optional:
|
||||
output.Append($" [{param.Name}]");
|
||||
break;
|
||||
case ParameterType.Multiple:
|
||||
output.Append($" [{param.Name}...]");
|
||||
break;
|
||||
case ParameterType.Unparsed:
|
||||
output.Append($" [-]");
|
||||
break;
|
||||
}
|
||||
}
|
||||
output.AppendLine("`");
|
||||
output.AppendLine($"{command.Description ?? "No description."}");
|
||||
|
||||
if (command.Aliases.Any())
|
||||
output.AppendLine($"Aliases: `" + string.Join("`, `", command.Aliases) + '`');
|
||||
}
|
||||
|
||||
public void CreateGroup(string cmd, Action<CommandGroupBuilder> config = null) => Root.CreateGroup(cmd, config);
|
||||
public CommandBuilder CreateCommand(string cmd) => Root.CreateCommand(cmd);
|
||||
|
||||
internal void AddCommand(Command command)
|
||||
{
|
||||
_allCommands.Add(command);
|
||||
|
||||
//Get category
|
||||
CommandMap category;
|
||||
string categoryName = command.Category ?? "";
|
||||
if (!_categories.TryGetValue(categoryName, out category))
|
||||
{
|
||||
category = new CommandMap();
|
||||
_categories.Add(categoryName, category);
|
||||
}
|
||||
|
||||
//Add main command
|
||||
category.AddCommand(command.Text, command, false);
|
||||
_map.AddCommand(command.Text, command, false);
|
||||
|
||||
//Add aliases
|
||||
foreach (var alias in command.Aliases)
|
||||
{
|
||||
category.AddCommand(alias, command, true);
|
||||
_map.AddCommand(alias, command, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public class CommandServiceConfigBuilder
|
||||
{
|
||||
/// <summary> Gets or sets the prefix character used to trigger commands, if ActivationMode has the Char flag set. </summary>
|
||||
public char? PrefixChar { get; set; } = null;
|
||||
/// <summary> Gets or sets whether a message beginning with a mention to the logged-in user should be treated as a command. </summary>
|
||||
public bool AllowMentionPrefix { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Gets or sets a custom function used to detect messages that should be treated as commands.
|
||||
/// This function should a positive one indicating the index of where the in the message's RawText the command begins,
|
||||
/// and a negative value if the message should be ignored.
|
||||
/// </summary>
|
||||
public Func<Message, int> CustomPrefixHandler { get; set; } = null;
|
||||
|
||||
/// <summary> Gets or sets whether a help function should be automatically generated. </summary>
|
||||
public HelpMode HelpMode { get; set; } = HelpMode.Disabled;
|
||||
|
||||
|
||||
/// <summary> Gets or sets a handler that is called on any successful command execution. </summary>
|
||||
public EventHandler<CommandEventArgs> ExecuteHandler { get; set; }
|
||||
/// <summary> Gets or sets a handler that is called on any error during command parsing or execution. </summary>
|
||||
public EventHandler<CommandErrorEventArgs> ErrorHandler { get; set; }
|
||||
|
||||
public CommandServiceConfig Build() => new CommandServiceConfig(this);
|
||||
}
|
||||
public class CommandServiceConfig
|
||||
{
|
||||
public char? PrefixChar { get; }
|
||||
public bool AllowMentionPrefix { get; }
|
||||
public Func<Message, int> CustomPrefixHandler { get; }
|
||||
|
||||
/// <summary> Gets or sets whether a help function should be automatically generated. </summary>
|
||||
public HelpMode HelpMode { get; set; } = HelpMode.Disabled;
|
||||
|
||||
internal CommandServiceConfig(CommandServiceConfigBuilder builder)
|
||||
{
|
||||
PrefixChar = builder.PrefixChar;
|
||||
AllowMentionPrefix = builder.AllowMentionPrefix;
|
||||
CustomPrefixHandler = builder.CustomPrefixHandler;
|
||||
HelpMode = builder.HelpMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>19793545-ef89-48f4-8100-3ebaad0a9141</ProjectGuid>
|
||||
<RootNamespace>Discord.Commands</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<ProduceOutputsOnBuild>True</ProduceOutputsOnBuild>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Discord.Commands.Permissions
|
||||
{
|
||||
internal class GenericPermissionChecker : IPermissionChecker
|
||||
{
|
||||
private readonly Func<Command, User, ITextChannel, bool> _checkFunc;
|
||||
private readonly string _error;
|
||||
|
||||
public GenericPermissionChecker(Func<Command, User, ITextChannel, bool> checkFunc, string error = null)
|
||||
{
|
||||
_checkFunc = checkFunc;
|
||||
_error = error;
|
||||
}
|
||||
|
||||
public bool CanRun(Command command, User user, ITextChannel channel, out string error)
|
||||
{
|
||||
error = _error;
|
||||
return _checkFunc(command, user, channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Discord.Commands
|
||||
{
|
||||
public enum HelpMode
|
||||
{
|
||||
/// <summary> Disable the automatic help command. </summary>
|
||||
Disabled,
|
||||
/// <summary> Use the automatic help command and respond in the channel the command is used. </summary>
|
||||
Public,
|
||||
/// <summary> Use the automatic help command and respond in a private message. </summary>
|
||||
Private
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Discord.Commands.Permissions
|
||||
{
|
||||
public interface IPermissionChecker
|
||||
{
|
||||
bool CanRun(Command command, User user, ITextChannel channel, out string error);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"version": "1.0.0-alpha1",
|
||||
"description": "A Discord.Net extension adding basic command support.",
|
||||
"authors": [ "RogueException" ],
|
||||
"tags": [ "discord", "discordapp" ],
|
||||
"projectUrl": "https://github.com/RogueException/Discord.Net",
|
||||
"licenseUrl": "http://opensource.org/licenses/MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/RogueException/Discord.Net"
|
||||
},
|
||||
"compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ],
|
||||
|
||||
"compilationOptions": {
|
||||
"warningsAsErrors": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"Discord.Net": "1.0.0-alpha1"
|
||||
},
|
||||
"frameworks": {
|
||||
"net45": { },
|
||||
"dotnet5.4": { }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user