diff --git a/README.md b/README.md
index 7136d32dc..845f996e8 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Discord.Net v0.8.0-beta1
+# Discord.Net v0.8.1-Beta
An unofficial .Net API Wrapper for the Discord client (http://discordapp.com).
Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx).
diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
index 69f042ac3..3a77ea6ed 100644
--- a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
+++ b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj
@@ -7,7 +7,7 @@
{1B5603B4-6F8F-4289-B945-7BAAE523D740}
Library
Properties
- Discord
+ Discord.Commands
Discord.Net.Commands
512
v4.5
@@ -43,14 +43,23 @@
CommandBuilder.cs
+
+ CommandExtensions.cs
+
+
+ CommandMap.cs
+
CommandParser.cs
-
- CommandsPlugin.cs
+
+ CommandService.cs
+
+
+ CommandService.Events.cs
-
- CommandsPlugin.Events.cs
+
+ CommandServiceConfig.cs
diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs
index f8728f4c6..15ad98b95 100644
--- a/src/Discord.Net.Commands/Command.cs
+++ b/src/Discord.Net.Commands/Command.cs
@@ -1,21 +1,111 @@
using System;
+using System.Collections.Generic;
using System.Threading.Tasks;
namespace Discord.Commands
{
+ public enum ParameterType
+ {
+ /// Catches a single required parameter.
+ Required,
+ /// Catches a single optional parameter.
+ Optional,
+ /// Catches a zero or more optional parameters.
+ Multiple,
+ /// Catches all remaining text as a single optional parameter.
+ Unparsed
+ }
+ public sealed class CommandParameter
+ {
+ public string Name { get; }
+ public ParameterType Type { get; }
+
+ public CommandParameter(string name, ParameterType type)
+ {
+ Name = name;
+ Type = type;
+ }
+ }
+
public sealed class Command
{
public string Text { get; }
- public int? MinArgs { get; internal set; }
- public int? MaxArgs { get; internal set; }
- public int MinPerms { get; internal set; }
- internal readonly string[] Parts;
- internal Func Handler;
+ public int? MinArgs { get; private set; }
+ public int? MaxArgs { get; private set; }
+ public int MinPermissions { get; internal set; }
+ public bool IsHidden { get; internal set; }
+ public string Description { get; internal set; }
+
+ public IEnumerable Aliases => _aliases;
+ private string[] _aliases;
+
+ public IEnumerable Parameters => _parameters;
+ internal CommandParameter[] _parameters;
+
+ private Func _handler;
internal Command(string text)
{
Text = text;
- Parts = text.ToLowerInvariant().Split(' ');
+ IsHidden = false;
+ _aliases = new string[0];
+ _parameters = new CommandParameter[0];
+ }
+
+ internal void SetAliases(string[] aliases)
+ {
+ _aliases = aliases;
+ }
+
+ internal void SetParameters(CommandParameter[] parameters)
+ {
+ _parameters = parameters;
+ if (parameters != null)
+ {
+ if (parameters.Length == 0)
+ {
+ MinArgs = 0;
+ MaxArgs = 0;
+ }
+ else
+ {
+ if (parameters[parameters.Length - 1].Type == ParameterType.Multiple)
+ MaxArgs = null;
+ else
+ MaxArgs = parameters.Length;
+
+ int? optionalStart = null;
+ for (int i = parameters.Length - 1; i >= 0; i--)
+ {
+ if (parameters[i].Type == ParameterType.Optional)
+ optionalStart = i;
+ else
+ break;
+ }
+ if (optionalStart == null)
+ MinArgs = MaxArgs;
+ else
+ MinArgs = optionalStart.Value;
+ }
+ }
+ }
+
+ internal void SetHandler(Func func)
+ {
+ _handler = func;
+ }
+ internal void SetHandler(Action func)
+ {
+ _handler = e => { func(e); return TaskHelper.CompletedTask; };
+ }
+
+ internal Task Run(CommandEventArgs args)
+ {
+ var task = _handler(args);
+ if (task != null)
+ return task;
+ else
+ return TaskHelper.CompletedTask;
}
}
}
diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs
index 0db5a388f..99a8ae97c 100644
--- a/src/Discord.Net.Commands/CommandBuilder.cs
+++ b/src/Discord.Net.Commands/CommandBuilder.cs
@@ -1,79 +1,111 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Threading.Tasks;
namespace Discord.Commands
{
public sealed class CommandBuilder
{
+ private readonly CommandService _service;
private readonly Command _command;
- public CommandBuilder(Command command)
+ private List _params;
+ private bool _allowRequired, _isClosed;
+ private string _prefix;
+
+ public CommandBuilder(CommandService service, Command command, string prefix)
{
+ _service = service;
_command = command;
- }
-
- public CommandBuilder ArgsEqual(int argCount)
- {
- _command.MinArgs = argCount;
- _command.MaxArgs = argCount;
- return this;
- }
- public CommandBuilder ArgsAtLeast(int minArgCount)
- {
- _command.MinArgs = minArgCount;
- _command.MaxArgs = null;
- return this;
- }
- public CommandBuilder ArgsAtMost(int maxArgCount)
+ _params = new List();
+ _prefix = prefix;
+ _allowRequired = true;
+ _isClosed = false;
+ }
+
+ public CommandBuilder Alias(params string[] aliases)
{
- _command.MinArgs = null;
- _command.MaxArgs = maxArgCount;
+ aliases = aliases.Select(x => AppendPrefix(_prefix, x)).ToArray();
+ _command.SetAliases(aliases);
return this;
}
- public CommandBuilder ArgsBetween(int minArgCount, int maxArgCount)
+ public CommandBuilder Info(string description)
{
- _command.MinArgs = minArgCount;
- _command.MaxArgs = maxArgCount;
+ _command.Description = description;
return this;
}
- public CommandBuilder NoArgs()
+ public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required)
{
- _command.MinArgs = 0;
- _command.MaxArgs = 0;
+ if (_isClosed)
+ throw new Exception($"No parameters may be added after a {nameof(ParameterType.Multiple)} or {nameof(ParameterType.Unparsed)} parameter.");
+ if (!_allowRequired && 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)
+ _allowRequired = false;
+ if (type == ParameterType.Multiple || type == ParameterType.Unparsed)
+ _isClosed = true;
return this;
}
- public CommandBuilder AnyArgs()
+ public CommandBuilder Hide()
{
- _command.MinArgs = null;
- _command.MaxArgs = null;
+ _command.IsHidden = true;
return this;
}
public CommandBuilder MinPermissions(int level)
{
- _command.MinPerms = level;
+ _command.MinPermissions = level;
return this;
}
- public CommandBuilder Do(Func func)
+ public void Do(Func func)
{
- _command.Handler = func;
- return this;
+ _command.SetHandler(func);
+ Build();
}
- public CommandBuilder Do(Action func)
+ public void Do(Action func)
{
- _command.Handler = e => { func(e); return TaskHelper.CompletedTask; };
- return this;
+ _command.SetHandler(func);
+ Build();
+ }
+ private void Build()
+ {
+ _command.SetParameters(_params.ToArray());
+ foreach (var alias in _command.Aliases)
+ _service.Map.AddCommand(alias, _command);
+ _service.AddCommand(_command);
+ }
+
+ internal static string AppendPrefix(string prefix, string cmd)
+ {
+ if (cmd != "")
+ {
+ if (prefix != "")
+ return prefix + ' ' + cmd;
+ else
+ return cmd;
+ }
+ else
+ {
+ if (prefix != "")
+ return prefix;
+ else
+ throw new ArgumentOutOfRangeException(nameof(cmd));
+ }
}
}
public sealed class CommandGroupBuilder
{
- private readonly CommandsPlugin _plugin;
+ internal readonly CommandService _service;
private readonly string _prefix;
private int _defaultMinPermissions;
- internal CommandGroupBuilder(CommandsPlugin plugin, string prefix, int defaultMinPermissions)
+ internal CommandGroupBuilder(CommandService service, string prefix, int defaultMinPermissions)
{
- _plugin = plugin;
+ _service = service;
_prefix = prefix;
_defaultMinPermissions = defaultMinPermissions;
}
@@ -85,32 +117,16 @@ namespace Discord.Commands
public CommandGroupBuilder CreateCommandGroup(string cmd, Action config = null)
{
- config(new CommandGroupBuilder(_plugin, _prefix + ' ' + cmd, _defaultMinPermissions));
+ config(new CommandGroupBuilder(_service, _prefix + ' ' + cmd, _defaultMinPermissions));
return this;
}
public CommandBuilder CreateCommand()
=> CreateCommand("");
public CommandBuilder CreateCommand(string cmd)
{
- string text;
- if (cmd != "")
- {
- if (_prefix != "")
- text = _prefix + ' ' + cmd;
- else
- text = cmd;
- }
- else
- {
- if (_prefix != "")
- text = _prefix;
- else
- throw new ArgumentOutOfRangeException(nameof(cmd));
- }
- var command = new Command(text);
- command.MinPerms = _defaultMinPermissions;
- _plugin.AddCommand(command);
- return new CommandBuilder(command);
+ var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd));
+ command.MinPermissions = _defaultMinPermissions;
+ return new CommandBuilder(_service, command, _prefix);
}
}
}
diff --git a/src/Discord.Net.Commands/CommandExtensions.cs b/src/Discord.Net.Commands/CommandExtensions.cs
new file mode 100644
index 000000000..e6d7a0ee3
--- /dev/null
+++ b/src/Discord.Net.Commands/CommandExtensions.cs
@@ -0,0 +1,8 @@
+namespace Discord.Commands
+{
+ public static class CommandExtensions
+ {
+ public static CommandService Commands(this DiscordClient client)
+ => client.GetService();
+ }
+}
diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs
new file mode 100644
index 000000000..d066a8910
--- /dev/null
+++ b/src/Discord.Net.Commands/CommandMap.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Discord.Commands
+{
+ internal class CommandMap
+ {
+ private CommandMap _parent;
+ private Command _command;
+ private readonly Dictionary _subCommands;
+
+ public Command Command => _command;
+ public IEnumerable SubCommands => _subCommands.Select(x => x.Value.Command).Where(x => x != null);
+
+ public CommandMap(CommandMap parent)
+ {
+ _parent = parent;
+ _subCommands = new Dictionary();
+ }
+
+ public CommandMap GetMap(string text)
+ {
+ return GetMap(0, text.Split(' '));
+ }
+ public CommandMap GetMap(int index, string[] parts)
+ {
+ if (index != parts.Length)
+ {
+ string nextPart = parts[index];
+ CommandMap nextGroup;
+ if (_subCommands.TryGetValue(nextPart, out nextGroup))
+ return nextGroup.GetMap(index + 1, parts);
+ else
+ return null;
+ }
+ return this;
+ }
+
+ public Command GetCommand()
+ {
+ if (_command != null)
+ return _command;
+ else if (_parent != null)
+ return _parent.GetCommand();
+ else
+ return null;
+ }
+ public Command GetCommand(string text)
+ {
+ return GetCommand(0, text.Split(' '));
+ }
+ public Command GetCommand(int index, string[] parts)
+ {
+ if (index != parts.Length)
+ {
+ string nextPart = parts[index];
+ CommandMap nextGroup;
+ if (_subCommands.TryGetValue(nextPart, out nextGroup))
+ {
+ var cmd = nextGroup.GetCommand(index + 1, parts);
+ if (cmd != null)
+ return cmd;
+ }
+ }
+
+ if (_command != null)
+ return _command;
+ return null;
+ }
+
+ public void AddCommand(string text, Command command)
+ {
+ AddCommand(0, text.Split(' '), command);
+ }
+ public void AddCommand(int index, string[] parts, Command command)
+ {
+ if (index != parts.Length)
+ {
+ string nextPart = parts[index];
+ CommandMap nextGroup;
+ if (!_subCommands.TryGetValue(nextPart, out nextGroup))
+ {
+ nextGroup = new CommandMap(this);
+ _subCommands.Add(nextPart, nextGroup);
+ }
+ nextGroup.AddCommand(index + 1, parts, command);
+ }
+ else
+ {
+ if (_command != null)
+ throw new InvalidOperationException("A command has already been added with this path.");
+ _command = command;
+ }
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs
index 76a4b4006..90b00e977 100644
--- a/src/Discord.Net.Commands/CommandParser.cs
+++ b/src/Discord.Net.Commands/CommandParser.cs
@@ -2,51 +2,24 @@
namespace Discord.Commands
{
- public class CommandPart
- {
- public string Value { get; }
- public int Index { get; }
-
- internal CommandPart(string value, int index)
- {
- Value = value;
- Index = index;
- }
- }
-
- //TODO: Check support for escaping
- public static class CommandParser
+ internal static class CommandParser
{
private enum CommandParserPart
{
None,
- CommandName,
Parameter,
QuotedParameter,
DoubleQuotedParameter
}
- public static bool Parse(string input, out string command, out CommandPart[] args)
- {
- return Parse(input, out command, out args, true);
- }
- public static bool ParseArgs(string input, out CommandPart[] args)
- {
- string ignored;
- return Parse(input, out ignored, out args, false);
- }
-
- private static bool Parse(string input, out string command, out CommandPart[] args, bool parseCommand)
+ public static bool ParseCommand(string input, CommandMap map, out Command command, out int endPos)
{
- CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None;
int startPosition = 0;
int endPosition = 0;
- int inputLength = input.Length;
+ int inputLength = input.Length;
bool isEscaped = false;
- List argList = new List();
-
command = null;
- args = null;
+ endPos = 0;
if (input == "")
return false;
@@ -59,23 +32,71 @@ namespace Discord.Commands
else if (currentChar == '\\')
isEscaped = true;
- switch (currentPart)
+ if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
{
- case CommandParserPart.CommandName:
- if ((!isEscaped && currentChar == ' ') || endPosition >= inputLength)
+ int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
+ string temp = input.Substring(startPosition, length);
+ if (temp == "")
+ startPosition = endPosition;
+ else
+ {
+ var newMap = map.GetMap(temp);
+ if (newMap != null)
{
- int length = (currentChar == ' ' ? endPosition - 1 : endPosition) - startPosition;
- string temp = input.Substring(startPosition, length);
- if (temp == "")
- startPosition = endPosition;
- else
- {
- currentPart = CommandParserPart.None;
- command = temp;
- startPosition = endPosition;
- }
- }
+ map = newMap;
+ endPos = endPosition;
+ }
+ else
+ break;
+ startPosition = endPosition;
+ }
+ }
+ }
+ command = map.GetCommand(); //Work our way backwards to find a command that matches our input
+ return command != null;
+ }
+
+ //TODO: Check support for escaping
+ public static CommandErrorType? ParseArgs(string input, int startPos, Command command, out string[] args)
+ {
+ CommandParserPart currentPart = CommandParserPart.None;
+ int startPosition = startPos;
+ int endPosition = startPos;
+ int inputLength = input.Length;
+ bool isEscaped = false;
+
+ var expectedArgs = command._parameters;
+ List argList = new List();
+ 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 == command.MaxArgs)
+ return CommandErrorType.BadArgCount;
+
+ parameter = command._parameters[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;
+
+ switch (currentPart)
+ {
case CommandParserPart.None:
if ((!isEscaped && currentChar == '\"'))
{
@@ -96,7 +117,7 @@ namespace Discord.Commands
else
{
currentPart = CommandParserPart.None;
- argList.Add(new CommandPart(temp, startPosition));
+ argList.Add(temp);
startPosition = endPosition;
}
}
@@ -106,31 +127,36 @@ namespace Discord.Commands
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
currentPart = CommandParserPart.None;
- argList.Add(new CommandPart(temp, startPosition));
+ argList.Add(temp);
startPosition = endPosition;
}
else if (endPosition >= inputLength)
- return false;
+ return CommandErrorType.InvalidInput;
break;
case CommandParserPart.DoubleQuotedParameter:
if ((!isEscaped && currentChar == '\"'))
{
string temp = input.Substring(startPosition, endPosition - startPosition - 1);
currentPart = CommandParserPart.None;
- argList.Add(new CommandPart(temp, startPosition));
+ argList.Add(temp);
startPosition = endPosition;
}
else if (endPosition >= inputLength)
- return false;
+ return CommandErrorType.InvalidInput;
break;
}
}
- if (parseCommand && (command == null || command == ""))
- return false;
+ if (argList.Count < command.MinArgs)
+ {
+ /*if (command._parameters[command._parameters.Length - 1].Type == ParameterType.Unparsed)
+ argList.Add("");
+ else*/
+ return CommandErrorType.BadArgCount;
+ }
args = argList.ToArray();
- return true;
+ return null;
}
}
}
diff --git a/src/Discord.Net.Commands/CommandService.Events.cs b/src/Discord.Net.Commands/CommandService.Events.cs
new file mode 100644
index 000000000..92a3d429c
--- /dev/null
+++ b/src/Discord.Net.Commands/CommandService.Events.cs
@@ -0,0 +1,54 @@
+using System;
+
+namespace Discord.Commands
+{
+ public class CommandEventArgs
+ {
+ public Message Message { get; }
+ public Command Command { get; }
+ public int? UserPermissions { get; }
+ public string[] Args { get; }
+
+ public User User => Message.User;
+ public Channel Channel => Message.Channel;
+ public Server Server => Message.Channel.Server;
+
+ public CommandEventArgs(Message message, Command command, int? userPermissions, string[] args)
+ {
+ Message = message;
+ Command = command;
+ UserPermissions = userPermissions;
+ Args = args;
+ }
+ }
+
+ 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.UserPermissions, baseArgs.Args)
+ {
+ Exception = ex;
+ ErrorType = errorType;
+ }
+ }
+
+ public partial class CommandService
+ {
+ public event EventHandler RanCommand;
+ private void RaiseRanCommand(CommandEventArgs args)
+ {
+ if (RanCommand != null)
+ RanCommand(this, args);
+ }
+ public event EventHandler CommandError;
+ private void RaiseCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null)
+ {
+ if (CommandError != null)
+ CommandError(this, new CommandErrorEventArgs(errorType, args, ex));
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs
new file mode 100644
index 000000000..efc84d7b2
--- /dev/null
+++ b/src/Discord.Net.Commands/CommandService.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Discord.Commands
+{
+ /// A Discord.Net client with extensions for handling common bot operations like text commands.
+ public partial class CommandService : IService
+ {
+ private DiscordClient _client;
+
+ CommandServiceConfig Config { get; }
+ public IEnumerable Commands => _commands;
+ private readonly List _commands;
+
+ internal CommandMap Map => _map;
+ private readonly CommandMap _map;
+
+ public CommandService(CommandServiceConfig config)
+ {
+ Config = config;
+ _commands = new List();
+ _map = new CommandMap(null);
+ }
+
+ void IService.Install(DiscordClient client)
+ {
+ _client = client;
+ Config.Lock();
+
+ if (Config.HelpMode != HelpMode.Disable)
+ {
+ CreateCommand("help")
+ .Parameter("command", ParameterType.Multiple)
+ .Hide()
+ .Info("Returns information about commands.")
+ .Do(async e =>
+ {
+ Channel channel = Config.HelpMode == HelpMode.Public ? e.Channel : await client.CreatePMChannel(e.User);
+ if (e.Args.Length > 0) //Show command help
+ {
+ var cmd = _map.GetCommand(string.Join(" ", e.Args));
+ if (cmd != null)
+ await ShowHelp(cmd, e.User, channel);
+ else
+ await client.SendMessage(channel, "Unable to display help: unknown command.");
+ }
+ else //Show general help
+ await ShowHelp(e.User, channel);
+ });
+ }
+
+ client.MessageReceived += async (s, e) =>
+ {
+ if (_commands.Count == 0) return;
+ if (e.Message.IsAuthor) return;
+
+ string msg = e.Message.Text;
+ if (msg.Length == 0) return;
+
+ //Check for command char if one is provided
+ var chars = Config.CommandChars;
+ if (chars.Length > 0)
+ {
+ if (!chars.Contains(msg[0]))
+ return;
+ msg = msg.Substring(1);
+ }
+
+ //Parse command
+ Command command;
+ int argPos;
+ CommandParser.ParseCommand(msg, _map, out command, out argPos);
+ if (command == null)
+ {
+ CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null);
+ RaiseCommandError(CommandErrorType.UnknownCommand, errorArgs);
+ return;
+ }
+ else
+ {
+ int userPermissions = Config.PermissionResolver?.Invoke(e.Message.User) ?? 0;
+
+ //Parse arguments
+ string[] args;
+ var error = CommandParser.ParseArgs(msg, argPos, command, out args);
+ if (error != null)
+ {
+ var errorArgs = new CommandEventArgs(e.Message, command, userPermissions, null);
+ RaiseCommandError(error.Value, errorArgs);
+ return;
+ }
+
+ var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args);
+
+ // Check permissions
+ if (userPermissions < command.MinPermissions)
+ {
+ RaiseCommandError(CommandErrorType.BadPermissions, eventArgs);
+ return;
+ }
+
+ // Run the command
+ try
+ {
+ RaiseRanCommand(eventArgs);
+ await command.Run(eventArgs).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ RaiseCommandError(CommandErrorType.Exception, eventArgs, ex);
+ }
+ }
+ };
+ }
+
+ public Task ShowHelp(User user, Channel channel)
+ {
+ int permissions = Config.PermissionResolver(user);
+
+ StringBuilder output = new StringBuilder();
+ output.AppendLine("These are the commands you can use:");
+ output.Append(string.Join(", ", _commands
+ .Where(x => permissions >= x.MinPermissions && !x.IsHidden)
+ .Select(x => '`' + x.Text + '`')));
+
+ var chars = Config.CommandChars;
+ if (chars.Length > 0)
+ {
+ if (chars.Length == 1)
+ output.AppendLine($"\nYou can use `{chars[0]}` to call a command.");
+ else
+ output.AppendLine($"\nYou can use `{string.Join(" ", chars.Take(chars.Length - 1))}` or `{chars.Last()}` to call a command.");
+ }
+
+ output.AppendLine("`help ` can tell you more about how to use a command.");
+
+ return _client.SendMessage(channel, output.ToString());
+ }
+
+ public Task ShowHelp(Command command, User user, Channel channel)
+ {
+ StringBuilder output = new StringBuilder();
+
+ output.Append($"`{command.Text}`");
+
+ if (command.MinArgs != null && command.MaxArgs != null)
+ {
+ if (command.MinArgs == command.MaxArgs)
+ {
+ if (command.MaxArgs != 0)
+ output.Append($" {command.MinArgs.ToString()} Args");
+ }
+ else
+ output.Append($" {command.MinArgs.ToString()} - {command.MaxArgs.ToString()} Args");
+ }
+ else if (command.MinArgs != null && command.MaxArgs == null)
+ output.Append($" ≥{command.MinArgs.ToString()} Args");
+ else if (command.MinArgs == null && command.MaxArgs != null)
+ output.Append($" ≤{command.MaxArgs.ToString()} Args");
+
+ output.Append($": {command.Description ?? "No description set for this command."}");
+
+ return _client.SendMessage(channel, output.ToString());
+ }
+
+ public void CreateCommandGroup(string cmd, Action config = null)
+ => config(new CommandGroupBuilder(this, cmd, 0));
+ public CommandBuilder CreateCommand(string cmd)
+ {
+ var command = new Command(cmd);
+ _commands.Add(command);
+ return new CommandBuilder(this, command, "");
+ }
+
+ internal void AddCommand(Command command)
+ {
+ _commands.Add(command);
+ _map.AddCommand(command.Text, command);
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs
new file mode 100644
index 000000000..f2903d861
--- /dev/null
+++ b/src/Discord.Net.Commands/CommandServiceConfig.cs
@@ -0,0 +1,49 @@
+using System;
+
+namespace Discord.Commands
+{
+ public enum HelpMode
+ {
+ /// Disable the automatic help command.
+ Disable,
+ /// Use the automatic help command and respond in the channel the command is used.
+ Public,
+ /// Use the automatic help command and respond in a private message.
+ Private
+ }
+ public class CommandServiceConfig
+ {
+ public Func PermissionResolver { get { return _permissionsResolver; } set { SetValue(ref _permissionsResolver, value); } }
+ private Func _permissionsResolver;
+
+ public char? CommandChar
+ {
+ get
+ {
+ return _commandChars.Length > 0 ? _commandChars[0] : (char?)null;
+ }
+ set
+ {
+ if (value != null)
+ CommandChars = new char[] { value.Value };
+ else
+ CommandChars = new char[0];
+ }
+ }
+ public char[] CommandChars { get { return _commandChars; } set { SetValue(ref _commandChars, value); } }
+ private char[] _commandChars = new char[] { '!' };
+
+ public HelpMode HelpMode { get { return _helpMode; } set { SetValue(ref _helpMode, value); } }
+ private HelpMode _helpMode = HelpMode.Disable;
+
+ //Lock
+ protected bool _isLocked;
+ internal void Lock() { _isLocked = true; }
+ protected void SetValue(ref T storage, T value)
+ {
+ if (_isLocked)
+ throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created.");
+ storage = value;
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs
deleted file mode 100644
index 5901aa2a7..000000000
--- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-
-namespace Discord.Commands
-{
- public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } }
- public class CommandEventArgs
- {
- public Message Message { get; }
- public Command Command { get; }
- public string CommandText { get; }
- public string ArgText { get; }
- public int? Permissions { get; }
- public string[] Args { get; }
-
- public User User => Message.User;
- public Channel Channel => Message.Channel;
- public Server Server => Message.Channel.Server;
-
- public CommandEventArgs(Message message, Command command, string commandText, string argText, int? permissions, string[] args)
- {
- Message = message;
- Command = command;
- CommandText = commandText;
- ArgText = argText;
- Permissions = permissions;
- Args = args;
- }
- }
- public class CommandErrorEventArgs : CommandEventArgs
- {
- public Exception Exception { get; }
-
- public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex)
- : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args)
- {
- Exception = ex;
- }
- }
- public partial class CommandsPlugin
- {
- public event EventHandler RanCommand;
- private void RaiseRanCommand(CommandEventArgs args)
- {
- if (RanCommand != null)
- RanCommand(this, args);
- }
- public event EventHandler UnknownCommand;
- private void RaiseUnknownCommand(CommandEventArgs args)
- {
- if (UnknownCommand != null)
- UnknownCommand(this, args);
- }
- public event EventHandler CommandError;
- private void RaiseCommandError(CommandEventArgs args, Exception ex)
- {
- if (CommandError != null)
- CommandError(this, new CommandErrorEventArgs(args, ex));
- }
- }
-}
diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs
deleted file mode 100644
index 44abce8f4..000000000
--- a/src/Discord.Net.Commands/CommandsPlugin.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace Discord.Commands
-{
- /// A Discord.Net client with extensions for handling common bot operations like text commands.
- public partial class CommandsPlugin
- {
- private readonly DiscordClient _client;
- private List _commands;
- private Func _getPermissions;
-
- public IEnumerable Commands => _commands;
-
- public char CommandChar { get; set; }
- public bool UseCommandChar { get; set; }
- public bool RequireCommandCharInPublic { get; set; }
- public bool RequireCommandCharInPrivate { get; set; }
-
- public CommandsPlugin(DiscordClient client, Func getPermissions = null)
- {
- _client = client;
- _getPermissions = getPermissions;
- _commands = new List();
-
- CommandChar = '/';
- UseCommandChar = false;
- RequireCommandCharInPublic = true;
- RequireCommandCharInPrivate = true;
-
- client.MessageReceived += async (s, e) =>
- {
- //If commands aren't being used, don't bother processing them
- if (_commands.Count == 0)
- return;
-
- //Ignore messages from ourselves
- if (e.Message.User == client.CurrentUser)
- return;
-
- //Check for the command character
- string msg = e.Message.Text;
- if (UseCommandChar)
- {
- if (msg.Length == 0)
- return;
- bool isPrivate = e.Message.Channel.IsPrivate;
- bool hasCommandChar = msg[0] == CommandChar;
- if (hasCommandChar)
- msg = msg.Substring(1);
- if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar)
- return;
- if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar)
- return;
- }
-
- CommandPart[] args;
- if (!CommandParser.ParseArgs(msg, out args))
- return;
-
- for (int i = 0; i < _commands.Count; i++)
- {
- Command cmd = _commands[i];
-
- //Check Command Parts
- if (args.Length < cmd.Parts.Length)
- continue;
-
- bool isValid = true;
- for (int j = 0; j < cmd.Parts.Length; j++)
- {
- if (!string.Equals(args[j].Value, cmd.Parts[j], StringComparison.OrdinalIgnoreCase))
- {
- isValid = false;
- break;
- }
- }
- if (!isValid)
- continue;
-
- //Check Arg Count
- int argCount = args.Length - cmd.Parts.Length;
- if (argCount < cmd.MinArgs || argCount > cmd.MaxArgs)
- continue;
-
- //Clean Args
- string[] newArgs = new string[argCount];
- for (int j = 0; j < newArgs.Length; j++)
- newArgs[j] = args[j + cmd.Parts.Length].Value;
-
- //Get ArgText
- string argText;
- if (argCount == 0)
- argText = "";
- else
- argText = msg.Substring(args[cmd.Parts.Length].Index);
-
- //Check Permissions
- int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0;
- var eventArgs = new CommandEventArgs(e.Message, cmd, msg, argText, permissions, newArgs);
- if (permissions < cmd.MinPerms)
- {
- RaiseCommandError(eventArgs, new PermissionException());
- return;
- }
-
- //Run Command
- RaiseRanCommand(eventArgs);
- try
- {
- var task = cmd.Handler(eventArgs);
- if (task != null)
- await task.ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- RaiseCommandError(eventArgs, ex);
- }
- break;
- }
- };
- }
-
- public void CreateCommandGroup(string cmd, Action config = null)
- => config(new CommandGroupBuilder(this, cmd, 0));
- public CommandBuilder CreateCommand(string cmd)
- {
- var command = new Command(cmd);
- _commands.Add(command);
- return new CommandBuilder(command);
- }
-
- internal void AddCommand(Command command)
- {
- _commands.Add(command);
- }
- }
-}
diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj
index 0b3717e57..9447f3985 100644
--- a/src/Discord.Net.Net45/Discord.Net.csproj
+++ b/src/Discord.Net.Net45/Discord.Net.csproj
@@ -88,8 +88,8 @@
API\Enums\PermissionTarget.cs
-
- API\Enums\Regions.cs
+
+ API\Enums\Region.cs
API\Enums\StringEnum.cs
@@ -232,6 +232,9 @@
HttpException.cs
+
+ IService.cs
+
Models\Channel.cs
diff --git a/src/Discord.Net/API/Enums/ChannelType.cs b/src/Discord.Net/API/Enums/ChannelType.cs
index 066d12565..7d787ae67 100644
--- a/src/Discord.Net/API/Enums/ChannelType.cs
+++ b/src/Discord.Net/API/Enums/ChannelType.cs
@@ -24,5 +24,11 @@
return new ChannelType(value);
}
}
+
+ public static implicit operator ChannelType(string value) => FromString(value);
+ public static bool operator ==(ChannelType a, ChannelType b) => a?._value == b?._value;
+ public static bool operator !=(ChannelType a, ChannelType b) => a?._value != b?._value;
+ public override bool Equals(object obj) => (obj as ChannelType)?._value == _value;
+ public override int GetHashCode() => _value.GetHashCode();
}
}
diff --git a/src/Discord.Net/API/Enums/PermissionTarget.cs b/src/Discord.Net/API/Enums/PermissionTarget.cs
index 28427f247..d501dc72b 100644
--- a/src/Discord.Net/API/Enums/PermissionTarget.cs
+++ b/src/Discord.Net/API/Enums/PermissionTarget.cs
@@ -24,5 +24,11 @@
return new PermissionTarget(value);
}
}
+
+ public static implicit operator PermissionTarget(string value) => FromString(value);
+ public static bool operator ==(PermissionTarget a, PermissionTarget b) => a?._value == b?._value;
+ public static bool operator !=(PermissionTarget a, PermissionTarget b) => a?._value != b?._value;
+ public override bool Equals(object obj) => (obj as PermissionTarget)?._value == _value;
+ public override int GetHashCode() => _value.GetHashCode();
}
}
diff --git a/src/Discord.Net/API/Enums/Regions.cs b/src/Discord.Net/API/Enums/Region.cs
similarity index 71%
rename from src/Discord.Net/API/Enums/Regions.cs
rename to src/Discord.Net/API/Enums/Region.cs
index 505778ad5..090f7cbda 100644
--- a/src/Discord.Net/API/Enums/Regions.cs
+++ b/src/Discord.Net/API/Enums/Region.cs
@@ -34,5 +34,11 @@
return new Region(value);
}
}
+
+ public static implicit operator Region(string value) => FromString(value);
+ public static bool operator ==(Region a, Region b) => a?._value == b?._value;
+ public static bool operator !=(Region a, Region b) => a?._value != b?._value;
+ public override bool Equals(object obj) => (obj as Region)?._value == _value;
+ public override int GetHashCode() => _value.GetHashCode();
}
}
diff --git a/src/Discord.Net/API/Enums/StringEnum.cs b/src/Discord.Net/API/Enums/StringEnum.cs
index 88a527dd2..98cf70e0e 100644
--- a/src/Discord.Net/API/Enums/StringEnum.cs
+++ b/src/Discord.Net/API/Enums/StringEnum.cs
@@ -2,7 +2,7 @@
{
public abstract class StringEnum
{
- private string _value;
+ protected string _value;
protected StringEnum(string value)
{
_value = value;
@@ -10,43 +10,5 @@
public string Value => _value;
public override string ToString() => _value;
-
- public override bool Equals(object obj)
- {
- var enum2 = obj as StringEnum;
- if (enum2 == (StringEnum)null)
- return false;
- else
- return _value == enum2._value;
- }
- public override int GetHashCode()
- {
- return _value.GetHashCode();
- }
-
- public static bool operator ==(StringEnum a, StringEnum b)
- {
- return a?._value == b?._value;
- }
- public static bool operator !=(StringEnum a, StringEnum b)
- {
- return a?._value != b?._value;
- }
- public static bool operator ==(StringEnum a, string b)
- {
- return a?._value == b;
- }
- public static bool operator !=(StringEnum a, string b)
- {
- return a?._value != b;
- }
- public static bool operator ==(string a, StringEnum b)
- {
- return a == b?._value;
- }
- public static bool operator !=(string a, StringEnum b)
- {
- return a != b?._value;
- }
}
}
diff --git a/src/Discord.Net/API/Enums/UserStatus.cs b/src/Discord.Net/API/Enums/UserStatus.cs
index 493c9ec50..367a796a0 100644
--- a/src/Discord.Net/API/Enums/UserStatus.cs
+++ b/src/Discord.Net/API/Enums/UserStatus.cs
@@ -28,5 +28,11 @@
return new UserStatus(value);
}
}
+
+ public static implicit operator UserStatus(string value) => FromString(value);
+ public static bool operator ==(UserStatus a, UserStatus b) => a?._value == b?._value;
+ public static bool operator !=(UserStatus a, UserStatus b) => a?._value != b?._value;
+ public override bool Equals(object obj) => (obj as UserStatus)?._value == _value;
+ public override int GetHashCode() => _value.GetHashCode();
}
}
diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs
index 52ce846ec..beef45a35 100644
--- a/src/Discord.Net/DiscordClient.Channels.cs
+++ b/src/Discord.Net/DiscordClient.Channels.cs
@@ -84,26 +84,30 @@ namespace Discord
public IEnumerable FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false)
{
if (server == null) throw new ArgumentNullException(nameof(server));
+ if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();
+
+ var query = server.Channels.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
- IEnumerable result;
- if (!exactMatch && name.StartsWith("#"))
- {
- string name2 = name.Substring(1);
- result = _channels.Where(x => x.Server == server &&
- string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
- }
- else
+ if (!exactMatch && name.Length >= 2)
{
- result = _channels.Where(x => x.Server == server &&
- string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
+ if (name[0] == '<' && name[1] == '#' && name[name.Length - 1] == '>') //Parse mention
+ {
+ string id = name.Substring(2, name.Length - 3);
+ var channel = _channels[id];
+ if (channel != null)
+ query = query.Concat(new Channel[] { channel });
+ }
+ else if (name[0] == '#' && (type == null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention
+ {
+ string name2 = name.Substring(1);
+ query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
+ }
}
- if (type != (string)null)
- result = result.Where(x => x.Type == type);
-
- return result;
+ if (type != null)
+ query = query.Where(x => x.Type == type);
+ return query;
}
/// Creates a new channel with the provided name and type.
@@ -111,7 +115,7 @@ namespace Discord
{
if (server == null) throw new ArgumentNullException(nameof(server));
if (name == null) throw new ArgumentNullException(nameof(name));
- if (type == (string)null) throw new ArgumentNullException(nameof(type));
+ if (type == null) throw new ArgumentNullException(nameof(type));
CheckReady();
var response = await _api.CreateChannel(server.Id, name, type.Value).ConfigureAwait(false);
diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs
index 28b3c068c..aaf930941 100644
--- a/src/Discord.Net/DiscordClient.Messages.cs
+++ b/src/Discord.Net/DiscordClient.Messages.cs
@@ -93,7 +93,6 @@ namespace Discord
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
- if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();
return SendMessage(channel, text, false);
@@ -103,7 +102,6 @@ namespace Discord
{
if (channel == null) throw new ArgumentNullException(nameof(channel));
if (text == null) throw new ArgumentNullException(nameof(text));
- if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();
return SendMessage(channel, text, false);
@@ -113,7 +111,6 @@ namespace Discord
{
if (user == null) throw new ArgumentNullException(nameof(user));
if (text == null) throw new ArgumentNullException(nameof(text));
- if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();
var channel = await CreatePMChannel(user).ConfigureAwait(false);
@@ -122,7 +119,8 @@ namespace Discord
private async Task SendMessage(Channel channel, string text, bool isTextToSpeech)
{
Message msg;
- var userIds = !channel.IsPrivate ? Mention.GetUserIds(text).Distinct() : new string[0];
+ var server = channel.Server;
+
if (Config.UseMessageQueue)
{
var nonce = GenerateNonce();
@@ -136,14 +134,24 @@ namespace Discord
ChannelId = channel.Id,
IsTextToSpeech = isTextToSpeech
});
- msg.Mentions = userIds.Select(x => _users[x, channel.Server.Id]).Where(x => x != null).ToArray();
- msg.IsQueued = true;
msg.Nonce = nonce;
+ msg.IsQueued = true;
+
+ if (text.Length > MaxMessageSize)
+ throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
+
_pendingMessages.Enqueue(msg);
}
else
{
- var model = await _api.SendMessage(channel.Id, text, userIds, null, isTextToSpeech).ConfigureAwait(false);
+ var mentionedUsers = new List();
+ if (!channel.IsPrivate)
+ text = Mention.CleanUserMentions(this, server, text, mentionedUsers);
+
+ if (text.Length > MaxMessageSize)
+ throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
+
+ var model = await _api.SendMessage(channel.Id, text, mentionedUsers.Select(x => x.Id), null, isTextToSpeech).ConfigureAwait(false);
msg = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id);
msg.Update(model);
RaiseMessageSent(msg);
@@ -168,13 +176,17 @@ namespace Discord
{
if (message == null) throw new ArgumentNullException(nameof(message));
if (text == null) throw new ArgumentNullException(nameof(text));
- if (text.Length > MaxMessageSize) throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
CheckReady();
- if (text != null && text.Length > MaxMessageSize)
- text = text.Substring(0, MaxMessageSize);
+ var channel = message.Channel;
+ var mentionedUsers = new List();
+ if (!channel.IsPrivate)
+ text = Mention.CleanUserMentions(this, channel.Server, text, mentionedUsers);
+
+ if (text.Length > MaxMessageSize)
+ throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {MaxMessageSize} characters or less.");
- return _api.EditMessage(message.Id, message.Channel.Id, text, Mention.GetUserIds(text));
+ return _api.EditMessage(message.Id, message.Channel.Id, text, mentionedUsers.Select(x => x.Id));
}
/// Deletes the provided message.
@@ -258,7 +270,7 @@ namespace Discord
SendMessageResponse response = null;
try
{
- response = await _api.SendMessage(msg.Channel.Id, msg.RawText, msg.Mentions.Select(x => x.Id), msg.Nonce, msg.IsTTS).ConfigureAwait(false);
+ response = await _api.SendMessage(msg.Channel.Id, msg.RawText, msg.MentionedUsers.Select(x => x.Id), msg.Nonce, msg.IsTTS).ConfigureAwait(false);
}
catch (WebException) { break; }
catch (HttpException) { hasFailed = true; }
diff --git a/src/Discord.Net/DiscordClient.Servers.cs b/src/Discord.Net/DiscordClient.Servers.cs
index ff66948f4..2066be5b7 100644
--- a/src/Discord.Net/DiscordClient.Servers.cs
+++ b/src/Discord.Net/DiscordClient.Servers.cs
@@ -84,7 +84,7 @@ namespace Discord
public async Task CreateServer(string name, Region region)
{
if (name == null) throw new ArgumentNullException(nameof(name));
- if (region == (string)null) throw new ArgumentNullException(nameof(region));
+ if (region == null) throw new ArgumentNullException(nameof(region));
CheckReady();
var response = await _api.CreateServer(name, region.Value).ConfigureAwait(false);
diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs
index e63fd04c1..aa7e931b1 100644
--- a/src/Discord.Net/DiscordClient.Users.cs
+++ b/src/Discord.Net/DiscordClient.Users.cs
@@ -141,7 +141,7 @@ namespace Discord
return _users[user?.Id, server.Id];
}
- /// Returns all users in with the specified server and name, along with their server-specific data.
+ /// Returns all users with the specified server and name, along with their server-specific data.
/// Name formats supported: Name and @Name. Search is case-insensitive.
public IEnumerable FindUsers(Server server, string name, string discriminator = null, bool exactMatch = false)
{
@@ -149,16 +149,39 @@ namespace Discord
if (name == null) throw new ArgumentNullException(nameof(name));
CheckReady();
- IEnumerable query;
- if (!exactMatch && name.StartsWith("@"))
+ return FindUsers(server.Members, server.Id, name, discriminator, exactMatch);
+ }
+ /// Returns all users with the specified channel and name, along with their server-specific data.
+ /// Name formats supported: Name and @Name. Search is case-insensitive.
+ public IEnumerable FindUsers(Channel channel, string name, string discriminator = null, bool exactMatch = false)
+ {
+ if (channel == null) throw new ArgumentNullException(nameof(channel));
+ if (name == null) throw new ArgumentNullException(nameof(name));
+ CheckReady();
+
+ return FindUsers(channel.Members, channel.IsPrivate ? null : channel.Server.Id, name, discriminator, exactMatch);
+ }
+
+ private IEnumerable FindUsers(IEnumerable users, string serverId, string name, string discriminator = null, bool exactMatch = false)
+ {
+ var query = users.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
+
+ if (!exactMatch && name.Length >= 2)
{
- string name2 = name.Substring(1);
- query = server.Members.Where(x =>
- string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase));
+ if (name[0] == '<' && name[1] == '@' && name[name.Length - 1] == '>') //Parse mention
+ {
+ string id = name.Substring(2, name.Length - 3);
+ var channel = _users[id, serverId];
+ if (channel != null)
+ query = query.Concat(new User[] { channel });
+ }
+ else if (name[0] == '@') //If we somehow get text starting with @ but isn't a mention
+ {
+ string name2 = name.Substring(1);
+ query = query.Concat(users.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase)));
+ }
}
- else
- query = server.Members.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
+
if (discriminator != null)
query = query.Where(x => x.Discriminator == discriminator);
return query;
@@ -227,7 +250,7 @@ namespace Discord
public Task SetStatus(UserStatus status)
{
- if (status == (string)null) throw new ArgumentNullException(nameof(status));
+ if (status == null) throw new ArgumentNullException(nameof(status));
if (status != UserStatus.Online && status != UserStatus.Idle)
throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status));
CheckReady();
diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs
index 48c03e372..8dbb33b96 100644
--- a/src/Discord.Net/DiscordClient.cs
+++ b/src/Discord.Net/DiscordClient.cs
@@ -17,6 +17,7 @@ namespace Discord
private readonly JsonSerializer _serializer;
private readonly ConcurrentQueue _pendingMessages;
private readonly ConcurrentDictionary _voiceClients;
+ private readonly Dictionary _services;
private bool _sentInitialLog;
private uint _nextVoiceClientId;
private UserStatus _status;
@@ -46,6 +47,7 @@ namespace Discord
_roles = new Roles(this, cacheLock);
_servers = new Servers(this, cacheLock);
_globalUsers = new GlobalUsers(this, cacheLock);
+ _services = new Dictionary();
_status = UserStatus.Online;
@@ -168,7 +170,6 @@ namespace Discord
_serializer.MissingMemberHandling = MissingMemberHandling.Error;
#endif
}
-
internal override VoiceWebSocket CreateVoiceSocket()
{
var socket = base.CreateVoiceSocket();
@@ -216,7 +217,6 @@ namespace Discord
await Connect(token).ConfigureAwait(false);
return token;
}
-
/// Connects to the Discord server with the provided token.
public async Task Connect(string token)
{
@@ -273,6 +273,22 @@ namespace Discord
_currentUser = null;
}
+ public void AddService(T obj)
+ where T : class, IService
+ {
+ _services.Add(typeof(T), obj);
+ obj.Install(this);
+ }
+ public T GetService()
+ where T : class, IService
+ {
+ IService service;
+ if (_services.TryGetValue(typeof(T), out service))
+ return service as T;
+ else
+ return null;
+ }
+
protected override IEnumerable GetTasks()
{
if (Config.UseMessageQueue)
@@ -294,7 +310,8 @@ namespace Discord
var data = e.Payload.ToObject(_serializer);
_currentUser = _users.GetOrAdd(data.User.Id, null);
_currentUser.Update(data.User);
- foreach (var model in data.Guilds)
+ _currentUser.GlobalUser.Update(data.User);
+ foreach (var model in data.Guilds)
{
if (!model.Unavailable)
{
diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs
index d718df155..b3c6ac649 100644
--- a/src/Discord.Net/Helpers/Mention.cs
+++ b/src/Discord.Net/Helpers/Mention.cs
@@ -8,6 +8,7 @@ namespace Discord
{
private static readonly Regex _userRegex = new Regex(@"<@([0-9]+?)>", RegexOptions.Compiled);
private static readonly Regex _channelRegex = new Regex(@"<#([0-9]+?)>", RegexOptions.Compiled);
+ private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled);
/// Returns the string used to create a user mention.
public static string User(User user)
@@ -15,42 +16,50 @@ namespace Discord
/// Returns the string used to create a channel mention.
public static string Channel(Channel channel)
=> $"<#{channel.Id}>";
- /// Returns the string used to create a channel mention.
+ /// Returns the string used to create a mention to everyone in a channel.
public static string Everyone()
=> $"@everyone";
- internal static string ConvertToNames(DiscordClient client, Server server, string text)
+ internal static string CleanUserMentions(DiscordClient client, Server server, string text, List users = null)
{
- text = _userRegex.Replace(text, new MatchEvaluator(e =>
+ return _userRegex.Replace(text, new MatchEvaluator(e =>
{
string id = e.Value.Substring(2, e.Value.Length - 3);
var user = client.Users[id, server?.Id];
if (user != null)
+ {
+ if (users != null)
+ users.Add(user);
return '@' + user.Name;
+ }
else //User not found
return '@' + e.Value;
}));
- if (server != null)
+ }
+ internal static string CleanChannelMentions(DiscordClient client, Server server, string text, List channels = null)
+ {
+ return _channelRegex.Replace(text, new MatchEvaluator(e =>
{
- text = _channelRegex.Replace(text, new MatchEvaluator(e =>
+ string id = e.Value.Substring(2, e.Value.Length - 3);
+ var channel = client.Channels[id];
+ if (channel != null && channel.Server.Id == server.Id)
{
- string id = e.Value.Substring(2, e.Value.Length - 3);
- var channel = client.Channels[id];
- if (channel != null && channel.Server.Id == server.Id)
- return '#' + channel.Name;
- else //Channel not found
+ if (channels != null)
+ channels.Add(channel);
+ return '#' + channel.Name;
+ }
+ else //Channel not found
return '#' + e.Value;
- }));
- }
- return text;
+ }));
}
-
- internal static IEnumerable GetUserIds(string text)
+ internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List roles = null)
{
- return _userRegex.Matches(text)
- .OfType()
- .Select(x => x.Groups[1].Value)
- .Where(x => x != null);
+ return _roleRegex.Replace(text, new MatchEvaluator(e =>
+ {
+ if (roles != null && user.GetPermissions(channel).MentionEveryone)
+ roles.Add(channel.Server.EveryoneRole);
+ return e.Value;
+ }));
}
}
}
diff --git a/src/Discord.Net/IService.cs b/src/Discord.Net/IService.cs
new file mode 100644
index 000000000..cf60f70d1
--- /dev/null
+++ b/src/Discord.Net/IService.cs
@@ -0,0 +1,7 @@
+namespace Discord
+{
+ public interface IService
+ {
+ void Install(DiscordClient client);
+ }
+}
diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs
index 4ce0b1b51..98f2f1c9e 100644
--- a/src/Discord.Net/Models/Channel.cs
+++ b/src/Discord.Net/Models/Channel.cs
@@ -146,14 +146,12 @@ namespace Discord
var cacheLength = _client.Config.MessageCacheLength;
if (cacheLength > 0)
{
- while (_messages.Count > cacheLength - 1)
+ var oldestIds = _messages.Select(x => x.Value.Id).OrderBy(x => x).Take(_messages.Count - cacheLength);
+ foreach (var id in oldestIds)
{
- var oldest = _messages.Select(x => x.Value.Id).OrderBy(x => x).FirstOrDefault();
- if (oldest != null)
- {
- if (_messages.TryRemove(oldest, out message))
- _client.Messages.TryRemove(oldest);
- }
+ Message removed;
+ if (_messages.TryRemove(id, out removed))
+ _client.Messages.TryRemove(id);
}
_messages.TryAdd(message.Id, message);
}
@@ -207,6 +205,8 @@ namespace Discord
user.UpdateChannelPermissions(this);
}
+ public override bool Equals(object obj) => obj is Channel && (obj as Channel).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}
diff --git a/src/Discord.Net/Models/Color.cs b/src/Discord.Net/Models/Color.cs
index 5b5972d48..33887deeb 100644
--- a/src/Discord.Net/Models/Color.cs
+++ b/src/Discord.Net/Models/Color.cs
@@ -77,5 +77,9 @@ namespace Discord
uint mask = (uint)~(0xFF << bit);
_rawValue = (_rawValue & mask) | ((uint)value << bit);
}
+
+ public override bool Equals(object obj) => obj is Color && (obj as Color)._rawValue == _rawValue;
+ public override int GetHashCode() => _rawValue.GetHashCode();
+ public override string ToString() => '#' + _rawValue.ToString("X");
}
}
diff --git a/src/Discord.Net/Models/GlobalUser.cs b/src/Discord.Net/Models/GlobalUser.cs
index 34848c641..6eb3a24ae 100644
--- a/src/Discord.Net/Models/GlobalUser.cs
+++ b/src/Discord.Net/Models/GlobalUser.cs
@@ -69,6 +69,8 @@ namespace Discord
_client.GlobalUsers.TryRemove(Id);
}
+ public override bool Equals(object obj) => obj is GlobalUser && (obj as GlobalUser).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Id;
}
}
diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs
index e42c5352d..841e6353c 100644
--- a/src/Discord.Net/Models/Invite.cs
+++ b/src/Discord.Net/Models/Invite.cs
@@ -5,7 +5,62 @@ using Newtonsoft.Json;
namespace Discord
{
public sealed class Invite : CachedObject
- {
+ {
+ public sealed class ServerInfo
+ {
+ /// Returns the unique identifier of this server.
+ public string Id { get; }
+ /// Returns the name of this server.
+ public string Name { get; }
+
+ internal ServerInfo(string id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+ }
+ public sealed class ChannelInfo
+ {
+ /// Returns the unique identifier of this channel.
+ public string Id { get; }
+ /// Returns the name of this channel.
+ public string Name { get; }
+
+ internal ChannelInfo(string id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+ }
+ public sealed class InviterInfo
+ {
+ /// Returns the unique identifier for this user.
+ public string Id { get; }
+ /// Returns the name of this user.
+ public string Name { get; }
+ /// Returns the by-name unique identifier for this user.
+ public string Discriminator { get; }
+ /// Returns the unique identifier for this user's avatar.
+ public string AvatarId { get; }
+ /// Returns the full path to this user's avatar.
+ public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId);
+
+ internal InviterInfo(string id, string name, string discriminator, string avatarId)
+ {
+ Id = id;
+ Name = name;
+ Discriminator = discriminator;
+ AvatarId = avatarId;
+ }
+ }
+
+ /// Returns information about the server this invite is attached to.
+ public ServerInfo Server { get; private set; }
+ /// Returns information about the channel this invite is attached to.
+ public ChannelInfo Channel { get; private set; }
+ /// Returns information about the user that created this invite.
+ public InviterInfo Inviter { get; private set; }
+
/// Returns, if enabled, an alternative human-readable code for URLs.
public string XkcdCode { get; }
/// Time (in seconds) until the invite expires. Set to 0 to never expire.
@@ -23,77 +78,23 @@ namespace Discord
/// Returns a URL for this invite using XkcdCode if available or Id if not.
public string Url => API.Endpoints.InviteUrl(XkcdCode ?? Id);
- /// Returns the user that created this invite.
- [JsonIgnore]
- public User Inviter => _inviter.Value;
- private readonly Reference _inviter;
- private User _generatedInviter;
-
- /// Returns the server this invite is to.
- [JsonIgnore]
- public Server Server => _server.Value;
- private readonly Reference _server;
- private Server _generatedServer;
-
- /// Returns the channel this invite is to.
- [JsonIgnore]
- public Channel Channel => _channel.Value;
- private readonly Reference _channel;
- private Channel _generatedChannel;
-
internal Invite(DiscordClient client, string code, string xkcdPass, string serverId, string inviterId, string channelId)
: base(client, code)
{
XkcdCode = xkcdPass;
- _server = new Reference(serverId, x =>
- {
- var server = _client.Servers[x];
- if (server == null)
- {
- server = _generatedServer = new Server(client, x);
- server.Cache();
- }
- return server;
- });
- _inviter = new Reference(serverId, x =>
- {
- var inviter = _client.Users[x, _server.Id];
- if (inviter == null)
- {
- inviter = _generatedInviter = new User(client, x, _server.Id);
- inviter.Cache();
- }
- return inviter;
- });
- _channel = new Reference(serverId, x =>
- {
- var channel = _client.Channels[x];
- if (channel == null)
- {
- channel = _generatedChannel = new Channel(client, x, _server.Id, null);
- channel.Cache();
- }
- return channel;
- });
- }
- internal override void LoadReferences()
- {
- _server.Load();
- _inviter.Load();
- _channel.Load();
}
+ internal override void LoadReferences() { }
internal override void UnloadReferences() { }
-
internal void Update(InviteReference model)
{
- if (model.Guild != null && _generatedServer != null)
- _generatedServer.Update(model.Guild);
- if (model.Inviter != null && _generatedInviter != null)
- _generatedInviter.Update(model.Inviter);
- if (model.Channel != null && _generatedChannel != null)
- _generatedChannel.Update(model.Channel);
- }
+ if (model.Guild != null)
+ Server = new ServerInfo(model.Guild.Id, model.Guild.Name);
+ if (model.Channel != null)
+ Channel = new ChannelInfo(model.Channel.Id, model.Channel.Name);
+ if (model.Inviter != null)
+ Inviter = new InviterInfo(model.Inviter.Id, model.Inviter.Username, model.Inviter.Discriminator, model.Inviter.Avatar);
+ }
internal void Update(InviteInfo model)
{
Update(model as InviteReference);
@@ -112,6 +113,8 @@ namespace Discord
CreatedAt = model.CreatedAt.Value;
}
+ public override bool Equals(object obj) => obj is Invite && (obj as Invite).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => XkcdCode ?? Id;
}
}
diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs
index e1a676880..a9d045414 100644
--- a/src/Discord.Net/Models/Message.cs
+++ b/src/Discord.Net/Models/Message.cs
@@ -90,17 +90,13 @@ namespace Discord
Height = height;
}
}
-
- private string _cleanText;
-
+
/// Returns the local unique identifier for this message.
public string Nonce { get; internal set; }
/// Returns true if the logged-in user was mentioned.
/// This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone).
public bool IsMentioningMe { get; private set; }
- /// Returns true if @everyone was mentioned by someone with permissions to do so.
- public bool IsMentioningEveryone { get; private set; }
/// Returns true if the message was sent as text-to-speech by someone with permissions to do so.
public bool IsTTS { get; private set; }
/// Returns true if the message is still in the outgoing message queue.
@@ -110,8 +106,7 @@ namespace Discord
/// Returns the raw content of this message as it was received from the server..
public string RawText { get; private set; }
/// Returns the content of this message with any special references such as mentions converted.
- /// This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache.
- public string Text => _cleanText != null ? _cleanText : (_cleanText = Mention.ConvertToNames(_client, Server, RawText));
+ public string Text { get; private set; }
/// Returns the timestamp for when this message was sent.
public DateTime Timestamp { get; private set; }
/// Returns the timestamp for when this message was last edited.
@@ -125,8 +120,16 @@ namespace Discord
/// Returns a collection of all users mentioned in this message.
[JsonIgnore]
- public IEnumerable Mentions { get; internal set; }
-
+ public IEnumerable MentionedUsers { get; internal set; }
+
+ /// Returns a collection of all channels mentioned in this message.
+ [JsonIgnore]
+ public IEnumerable MentionedChannels { get; internal set; }
+
+ /// Returns a collection of all roles mentioned in this message.
+ [JsonIgnore]
+ public IEnumerable MentionedRoles { get; internal set; }
+
/// Returns the server containing the channel this message was sent to.
[JsonIgnore]
public Server Server => _channel.Value.Server;
@@ -174,6 +177,8 @@ namespace Discord
internal void Update(MessageInfo model)
{
+ var channel = Channel;
+ var server = channel.Server;
if (model.Attachments != null)
{
Attachments = model.Attachments
@@ -197,9 +202,7 @@ namespace Discord
return new Embed(x.Url, x.Type, x.Title, x.Description, author, provider, thumbnail);
}).ToArray();
}
-
- if (model.IsMentioningEveryone != null)
- IsMentioningEveryone = model.IsMentioningEveryone.Value;
+
if (model.IsTextToSpeech != null)
IsTTS = model.IsTextToSpeech.Value;
if (model.Timestamp != null)
@@ -208,16 +211,46 @@ namespace Discord
EditedTimestamp = model.EditedTimestamp;
if (model.Mentions != null)
{
- Mentions = model.Mentions.Select(x => _client.Users[x.Id, Channel.Server?.Id]).ToArray();
- IsMentioningMe = model.Mentions.Any(x => x.Id == _client.CurrentUserId);
+ MentionedUsers = model.Mentions
+ .Select(x => _client.Users[x.Id, Channel.Server?.Id])
+ .ToArray();
}
if (model.Content != null)
{
- RawText = model.Content;
- _cleanText = null;
+ string text = model.Content;
+ RawText = text;
+
+ //var mentionedUsers = new List();
+ var mentionedChannels = new List();
+ var mentionedRoles = new List();
+ text = Mention.CleanUserMentions(_client, server, text/*, mentionedUsers*/);
+ if (server != null)
+ {
+ text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels);
+ text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles);
+ }
+ Text = text;
+
+ //MentionedUsers = mentionedUsers;
+ MentionedChannels = mentionedChannels;
+ MentionedRoles = mentionedRoles;
}
- }
+ if (server != null)
+ {
+ var me = server.CurrentUser;
+ IsMentioningMe = (MentionedUsers?.Contains(me) ?? false) ||
+ (MentionedRoles?.Any(x => me.HasRole(x)) ?? false);
+ }
+ else
+ {
+ var me = _client.CurrentUser;
+ IsMentioningMe = MentionedUsers?.Contains(me) ?? false;
+ }
+ }
+
+ public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => $"{User}: {RawText}";
}
}
diff --git a/src/Discord.Net/Models/Permissions.cs b/src/Discord.Net/Models/Permissions.cs
index bd65436e1..492699b17 100644
--- a/src/Discord.Net/Models/Permissions.cs
+++ b/src/Discord.Net/Models/Permissions.cs
@@ -137,6 +137,9 @@ namespace Discord
if (_isLocked)
throw new InvalidOperationException("Unable to edit cached permissions directly, use Copy() to make an editable copy.");
}
+
+ public override bool Equals(object obj) => obj is Permissions && (obj as Permissions)._rawValue == _rawValue;
+ public override int GetHashCode() => _rawValue.GetHashCode();
}
public sealed class DualChannelPermissions
@@ -214,5 +217,10 @@ namespace Discord
Deny.SetBit(pos, false);
}
}
+
+ public override bool Equals(object obj) => obj is DualChannelPermissions &&
+ (obj as DualChannelPermissions).Allow.Equals(Allow) &&
+ (obj as DualChannelPermissions).Deny.Equals(Deny);
+ public override int GetHashCode() => unchecked(Allow.GetHashCode() + Deny.GetHashCode());
}
}
diff --git a/src/Discord.Net/Models/Role.cs b/src/Discord.Net/Models/Role.cs
index bbf896404..b625e738d 100644
--- a/src/Discord.Net/Models/Role.cs
+++ b/src/Discord.Net/Models/Role.cs
@@ -70,6 +70,8 @@ namespace Discord
member.UpdateServerPermissions();
}
+ public override bool Equals(object obj) => obj is Role && (obj as Role).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}
diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs
index ce4961d8f..18ea3e9b7 100644
--- a/src/Discord.Net/Models/Server.cs
+++ b/src/Discord.Net/Models/Server.cs
@@ -252,6 +252,8 @@ namespace Discord
}
}
+ public override bool Equals(object obj) => obj is Server && (obj as Server).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}
diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs
index 3abd56c74..1b75db58c 100644
--- a/src/Discord.Net/Models/User.cs
+++ b/src/Discord.Net/Models/User.cs
@@ -10,6 +10,7 @@ namespace Discord
public class User : CachedObject
{
internal static string GetId(string userId, string serverId) => (serverId ?? "Private") + '_' + userId;
+ internal static string GetAvatarUrl(string userId, string avatarId) => avatarId != null ? Endpoints.UserAvatar(userId, avatarId) : null;
private ConcurrentDictionary _channels;
private ConcurrentDictionary _permissions;
@@ -24,7 +25,7 @@ namespace Discord
/// Returns the unique identifier for this user's current avatar.
public string AvatarId { get; private set; }
/// Returns the URL to this user's current avatar.
- public string AvatarUrl => AvatarId != null ? API.Endpoints.UserAvatar(Id, AvatarId) : null;
+ public string AvatarUrl => GetAvatarUrl(Id, AvatarId);
/// Returns the datetime that this user joined this server.
public DateTime JoinedAt { get; private set; }
@@ -370,6 +371,8 @@ namespace Discord
return _roles.ContainsKey(role.Id);
}
+ public override bool Equals(object obj) => obj is User && (obj as User).Id == Id;
+ public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => Name ?? Id;
}
}
\ No newline at end of file
diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs
index ff15fbff5..c8bb858bd 100644
--- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs
+++ b/src/Discord.Net/Net/Rest/SharpRestEngine.cs
@@ -52,19 +52,15 @@ namespace Discord.Net.Rest
}
private async Task Send(RestRequest request, CancellationToken cancelToken)
{
- bool hasRetried = false;
+ int retryCount = 0;
while (true)
{
var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false);
int statusCode = (int)response.StatusCode;
if (statusCode == 0) //Internal Error
{
- if (!hasRetried)
- {
- //SSL/TTS Error seems to work if we immediately retry
- hasRetried = true;
- continue;
- }
+ if (response.ErrorException.HResult == -2146233079 && retryCount++ < 5) //The request was aborted: Could not create SSL/TLS secure channel.
+ continue; //Seems to work if we immediately retry
throw response.ErrorException;
}
if (statusCode < 200 || statusCode >= 300) //2xx = Success