| @@ -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). | 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). | Check out the [documentation](https://discordnet.readthedocs.org/en/latest/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). | ||||
| @@ -7,7 +7,7 @@ | |||||
| <ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid> | <ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid> | ||||
| <OutputType>Library</OutputType> | <OutputType>Library</OutputType> | ||||
| <AppDesignerFolder>Properties</AppDesignerFolder> | <AppDesignerFolder>Properties</AppDesignerFolder> | ||||
| <RootNamespace>Discord</RootNamespace> | |||||
| <RootNamespace>Discord.Commands</RootNamespace> | |||||
| <AssemblyName>Discord.Net.Commands</AssemblyName> | <AssemblyName>Discord.Net.Commands</AssemblyName> | ||||
| <FileAlignment>512</FileAlignment> | <FileAlignment>512</FileAlignment> | ||||
| <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | ||||
| @@ -43,14 +43,23 @@ | |||||
| <Compile Include="..\Discord.Net.Commands\CommandBuilder.cs"> | <Compile Include="..\Discord.Net.Commands\CommandBuilder.cs"> | ||||
| <Link>CommandBuilder.cs</Link> | <Link>CommandBuilder.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net.Commands\CommandExtensions.cs"> | |||||
| <Link>CommandExtensions.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net.Commands\CommandMap.cs"> | |||||
| <Link>CommandMap.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net.Commands\CommandParser.cs"> | <Compile Include="..\Discord.Net.Commands\CommandParser.cs"> | ||||
| <Link>CommandParser.cs</Link> | <Link>CommandParser.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net.Commands\CommandsPlugin.cs"> | |||||
| <Link>CommandsPlugin.cs</Link> | |||||
| <Compile Include="..\Discord.Net.Commands\CommandService.cs"> | |||||
| <Link>CommandService.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net.Commands\CommandService.Events.cs"> | |||||
| <Link>CommandService.Events.cs</Link> | |||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net.Commands\CommandsPlugin.Events.cs"> | |||||
| <Link>CommandsPlugin.Events.cs</Link> | |||||
| <Compile Include="..\Discord.Net.Commands\CommandServiceConfig.cs"> | |||||
| <Link>CommandServiceConfig.cs</Link> | |||||
| </Compile> | </Compile> | ||||
| <Compile Include="Properties\AssemblyInfo.cs" /> | <Compile Include="Properties\AssemblyInfo.cs" /> | ||||
| </ItemGroup> | </ItemGroup> | ||||
| @@ -1,21 +1,111 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Commands | 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 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 sealed class Command | ||||
| { | { | ||||
| public string Text { get; } | 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<CommandEventArgs, Task> 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<string> Aliases => _aliases; | |||||
| private string[] _aliases; | |||||
| public IEnumerable<CommandParameter> Parameters => _parameters; | |||||
| internal CommandParameter[] _parameters; | |||||
| private Func<CommandEventArgs, Task> _handler; | |||||
| internal Command(string text) | internal Command(string text) | ||||
| { | { | ||||
| Text = 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<CommandEventArgs, Task> func) | |||||
| { | |||||
| _handler = func; | |||||
| } | |||||
| internal void SetHandler(Action<CommandEventArgs> 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; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,79 +1,111 @@ | |||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| namespace Discord.Commands | namespace Discord.Commands | ||||
| { | { | ||||
| public sealed class CommandBuilder | public sealed class CommandBuilder | ||||
| { | { | ||||
| private readonly CommandService _service; | |||||
| private readonly Command _command; | private readonly Command _command; | ||||
| public CommandBuilder(Command command) | |||||
| private List<CommandParameter> _params; | |||||
| private bool _allowRequired, _isClosed; | |||||
| private string _prefix; | |||||
| public CommandBuilder(CommandService service, Command command, string prefix) | |||||
| { | { | ||||
| _service = service; | |||||
| _command = command; | _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<CommandParameter>(); | |||||
| _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; | 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; | 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; | return this; | ||||
| } | } | ||||
| public CommandBuilder AnyArgs() | |||||
| public CommandBuilder Hide() | |||||
| { | { | ||||
| _command.MinArgs = null; | |||||
| _command.MaxArgs = null; | |||||
| _command.IsHidden = true; | |||||
| return this; | return this; | ||||
| } | } | ||||
| public CommandBuilder MinPermissions(int level) | public CommandBuilder MinPermissions(int level) | ||||
| { | { | ||||
| _command.MinPerms = level; | |||||
| _command.MinPermissions = level; | |||||
| return this; | return this; | ||||
| } | } | ||||
| public CommandBuilder Do(Func<CommandEventArgs, Task> func) | |||||
| public void Do(Func<CommandEventArgs, Task> func) | |||||
| { | { | ||||
| _command.Handler = func; | |||||
| return this; | |||||
| _command.SetHandler(func); | |||||
| Build(); | |||||
| } | } | ||||
| public CommandBuilder Do(Action<CommandEventArgs> func) | |||||
| public void Do(Action<CommandEventArgs> 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 | public sealed class CommandGroupBuilder | ||||
| { | { | ||||
| private readonly CommandsPlugin _plugin; | |||||
| internal readonly CommandService _service; | |||||
| private readonly string _prefix; | private readonly string _prefix; | ||||
| private int _defaultMinPermissions; | 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; | _prefix = prefix; | ||||
| _defaultMinPermissions = defaultMinPermissions; | _defaultMinPermissions = defaultMinPermissions; | ||||
| } | } | ||||
| @@ -85,32 +117,16 @@ namespace Discord.Commands | |||||
| public CommandGroupBuilder CreateCommandGroup(string cmd, Action<CommandGroupBuilder> config = null) | public CommandGroupBuilder CreateCommandGroup(string cmd, Action<CommandGroupBuilder> config = null) | ||||
| { | { | ||||
| config(new CommandGroupBuilder(_plugin, _prefix + ' ' + cmd, _defaultMinPermissions)); | |||||
| config(new CommandGroupBuilder(_service, _prefix + ' ' + cmd, _defaultMinPermissions)); | |||||
| return this; | return this; | ||||
| } | } | ||||
| public CommandBuilder CreateCommand() | public CommandBuilder CreateCommand() | ||||
| => CreateCommand(""); | => CreateCommand(""); | ||||
| public CommandBuilder CreateCommand(string cmd) | 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); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,8 @@ | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public static class CommandExtensions | |||||
| { | |||||
| public static CommandService Commands(this DiscordClient client) | |||||
| => client.GetService<CommandService>(); | |||||
| } | |||||
| } | |||||
| @@ -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<string, CommandMap> _subCommands; | |||||
| public Command Command => _command; | |||||
| public IEnumerable<Command> SubCommands => _subCommands.Select(x => x.Value.Command).Where(x => x != null); | |||||
| public CommandMap(CommandMap parent) | |||||
| { | |||||
| _parent = parent; | |||||
| _subCommands = new Dictionary<string, CommandMap>(); | |||||
| } | |||||
| 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; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -2,51 +2,24 @@ | |||||
| namespace Discord.Commands | 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 | private enum CommandParserPart | ||||
| { | { | ||||
| None, | None, | ||||
| CommandName, | |||||
| Parameter, | Parameter, | ||||
| QuotedParameter, | QuotedParameter, | ||||
| DoubleQuotedParameter | 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 startPosition = 0; | ||||
| int endPosition = 0; | int endPosition = 0; | ||||
| int inputLength = input.Length; | |||||
| int inputLength = input.Length; | |||||
| bool isEscaped = false; | bool isEscaped = false; | ||||
| List<CommandPart> argList = new List<CommandPart>(); | |||||
| command = null; | command = null; | ||||
| args = null; | |||||
| endPos = 0; | |||||
| if (input == "") | if (input == "") | ||||
| return false; | return false; | ||||
| @@ -59,23 +32,71 @@ namespace Discord.Commands | |||||
| else if (currentChar == '\\') | else if (currentChar == '\\') | ||||
| isEscaped = true; | 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<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 == command.MaxArgs) | |||||
| return CommandErrorType.BadArgCount; | |||||
| parameter = command._parameters[argList.Count]; | |||||
| if (parameter.Type == ParameterType.Unparsed) | |||||
| { | |||||
| argList.Add(input.Substring(startPosition)); | |||||
| break; | break; | ||||
| } | |||||
| } | |||||
| char currentChar = input[endPosition++]; | |||||
| if (isEscaped) | |||||
| isEscaped = false; | |||||
| else if (currentChar == '\\') | |||||
| isEscaped = true; | |||||
| switch (currentPart) | |||||
| { | |||||
| case CommandParserPart.None: | case CommandParserPart.None: | ||||
| if ((!isEscaped && currentChar == '\"')) | if ((!isEscaped && currentChar == '\"')) | ||||
| { | { | ||||
| @@ -96,7 +117,7 @@ namespace Discord.Commands | |||||
| else | else | ||||
| { | { | ||||
| currentPart = CommandParserPart.None; | currentPart = CommandParserPart.None; | ||||
| argList.Add(new CommandPart(temp, startPosition)); | |||||
| argList.Add(temp); | |||||
| startPosition = endPosition; | startPosition = endPosition; | ||||
| } | } | ||||
| } | } | ||||
| @@ -106,31 +127,36 @@ namespace Discord.Commands | |||||
| { | { | ||||
| string temp = input.Substring(startPosition, endPosition - startPosition - 1); | string temp = input.Substring(startPosition, endPosition - startPosition - 1); | ||||
| currentPart = CommandParserPart.None; | currentPart = CommandParserPart.None; | ||||
| argList.Add(new CommandPart(temp, startPosition)); | |||||
| argList.Add(temp); | |||||
| startPosition = endPosition; | startPosition = endPosition; | ||||
| } | } | ||||
| else if (endPosition >= inputLength) | else if (endPosition >= inputLength) | ||||
| return false; | |||||
| return CommandErrorType.InvalidInput; | |||||
| break; | break; | ||||
| case CommandParserPart.DoubleQuotedParameter: | case CommandParserPart.DoubleQuotedParameter: | ||||
| if ((!isEscaped && currentChar == '\"')) | if ((!isEscaped && currentChar == '\"')) | ||||
| { | { | ||||
| string temp = input.Substring(startPosition, endPosition - startPosition - 1); | string temp = input.Substring(startPosition, endPosition - startPosition - 1); | ||||
| currentPart = CommandParserPart.None; | currentPart = CommandParserPart.None; | ||||
| argList.Add(new CommandPart(temp, startPosition)); | |||||
| argList.Add(temp); | |||||
| startPosition = endPosition; | startPosition = endPosition; | ||||
| } | } | ||||
| else if (endPosition >= inputLength) | else if (endPosition >= inputLength) | ||||
| return false; | |||||
| return CommandErrorType.InvalidInput; | |||||
| break; | 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(); | args = argList.ToArray(); | ||||
| return true; | |||||
| return null; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -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<CommandEventArgs> RanCommand; | |||||
| private void RaiseRanCommand(CommandEventArgs args) | |||||
| { | |||||
| if (RanCommand != null) | |||||
| RanCommand(this, args); | |||||
| } | |||||
| public event EventHandler<CommandErrorEventArgs> CommandError; | |||||
| private void RaiseCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) | |||||
| { | |||||
| if (CommandError != null) | |||||
| CommandError(this, new CommandErrorEventArgs(errorType, args, ex)); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,184 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| /// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary> | |||||
| public partial class CommandService : IService | |||||
| { | |||||
| private DiscordClient _client; | |||||
| CommandServiceConfig Config { get; } | |||||
| public IEnumerable<Command> Commands => _commands; | |||||
| private readonly List<Command> _commands; | |||||
| internal CommandMap Map => _map; | |||||
| private readonly CommandMap _map; | |||||
| public CommandService(CommandServiceConfig config) | |||||
| { | |||||
| Config = config; | |||||
| _commands = new List<Command>(); | |||||
| _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 <command>` 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<CommandGroupBuilder> 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,49 @@ | |||||
| using System; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| public enum HelpMode | |||||
| { | |||||
| /// <summary> Disable the automatic help command. </summary> | |||||
| Disable, | |||||
| /// <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 | |||||
| } | |||||
| public class CommandServiceConfig | |||||
| { | |||||
| public Func<User, int> PermissionResolver { get { return _permissionsResolver; } set { SetValue(ref _permissionsResolver, value); } } | |||||
| private Func<User, int> _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<T>(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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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<CommandEventArgs> RanCommand; | |||||
| private void RaiseRanCommand(CommandEventArgs args) | |||||
| { | |||||
| if (RanCommand != null) | |||||
| RanCommand(this, args); | |||||
| } | |||||
| public event EventHandler<CommandEventArgs> UnknownCommand; | |||||
| private void RaiseUnknownCommand(CommandEventArgs args) | |||||
| { | |||||
| if (UnknownCommand != null) | |||||
| UnknownCommand(this, args); | |||||
| } | |||||
| public event EventHandler<CommandErrorEventArgs> CommandError; | |||||
| private void RaiseCommandError(CommandEventArgs args, Exception ex) | |||||
| { | |||||
| if (CommandError != null) | |||||
| CommandError(this, new CommandErrorEventArgs(args, ex)); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,138 +0,0 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| namespace Discord.Commands | |||||
| { | |||||
| /// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary> | |||||
| public partial class CommandsPlugin | |||||
| { | |||||
| private readonly DiscordClient _client; | |||||
| private List<Command> _commands; | |||||
| private Func<User, int> _getPermissions; | |||||
| public IEnumerable<Command> 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<User, int> getPermissions = null) | |||||
| { | |||||
| _client = client; | |||||
| _getPermissions = getPermissions; | |||||
| _commands = new List<Command>(); | |||||
| 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<CommandGroupBuilder> 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); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -88,8 +88,8 @@ | |||||
| <Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs"> | <Compile Include="..\Discord.Net\API\Enums\PermissionTarget.cs"> | ||||
| <Link>API\Enums\PermissionTarget.cs</Link> | <Link>API\Enums\PermissionTarget.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\API\Enums\Regions.cs"> | |||||
| <Link>API\Enums\Regions.cs</Link> | |||||
| <Compile Include="..\Discord.Net\API\Enums\Region.cs"> | |||||
| <Link>API\Enums\Region.cs</Link> | |||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\API\Enums\StringEnum.cs"> | <Compile Include="..\Discord.Net\API\Enums\StringEnum.cs"> | ||||
| <Link>API\Enums\StringEnum.cs</Link> | <Link>API\Enums\StringEnum.cs</Link> | ||||
| @@ -232,6 +232,9 @@ | |||||
| <Compile Include="..\Discord.Net\HttpException.cs"> | <Compile Include="..\Discord.Net\HttpException.cs"> | ||||
| <Link>HttpException.cs</Link> | <Link>HttpException.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| <Compile Include="..\Discord.Net\IService.cs"> | |||||
| <Link>IService.cs</Link> | |||||
| </Compile> | |||||
| <Compile Include="..\Discord.Net\Models\Channel.cs"> | <Compile Include="..\Discord.Net\Models\Channel.cs"> | ||||
| <Link>Models\Channel.cs</Link> | <Link>Models\Channel.cs</Link> | ||||
| </Compile> | </Compile> | ||||
| @@ -24,5 +24,11 @@ | |||||
| return new ChannelType(value); | 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(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -24,5 +24,11 @@ | |||||
| return new PermissionTarget(value); | 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(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -34,5 +34,11 @@ | |||||
| return new Region(value); | 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(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,7 +2,7 @@ | |||||
| { | { | ||||
| public abstract class StringEnum | public abstract class StringEnum | ||||
| { | { | ||||
| private string _value; | |||||
| protected string _value; | |||||
| protected StringEnum(string value) | protected StringEnum(string value) | ||||
| { | { | ||||
| _value = value; | _value = value; | ||||
| @@ -10,43 +10,5 @@ | |||||
| public string Value => _value; | public string Value => _value; | ||||
| public override string ToString() => _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; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -28,5 +28,11 @@ | |||||
| return new UserStatus(value); | 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(); | |||||
| } | } | ||||
| } | } | ||||
| @@ -84,26 +84,30 @@ namespace Discord | |||||
| public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) | public IEnumerable<Channel> FindChannels(Server server, string name, ChannelType type = null, bool exactMatch = false) | ||||
| { | { | ||||
| if (server == null) throw new ArgumentNullException(nameof(server)); | if (server == null) throw new ArgumentNullException(nameof(server)); | ||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | |||||
| CheckReady(); | CheckReady(); | ||||
| var query = server.Channels.Where(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); | |||||
| IEnumerable<Channel> 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; | |||||
| } | } | ||||
| /// <summary> Creates a new channel with the provided name and type. </summary> | /// <summary> Creates a new channel with the provided name and type. </summary> | ||||
| @@ -111,7 +115,7 @@ namespace Discord | |||||
| { | { | ||||
| if (server == null) throw new ArgumentNullException(nameof(server)); | if (server == null) throw new ArgumentNullException(nameof(server)); | ||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | 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(); | CheckReady(); | ||||
| var response = await _api.CreateChannel(server.Id, name, type.Value).ConfigureAwait(false); | var response = await _api.CreateChannel(server.Id, name, type.Value).ConfigureAwait(false); | ||||
| @@ -93,7 +93,6 @@ namespace Discord | |||||
| { | { | ||||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | if (channel == null) throw new ArgumentNullException(nameof(channel)); | ||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | 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(); | CheckReady(); | ||||
| return SendMessage(channel, text, false); | return SendMessage(channel, text, false); | ||||
| @@ -103,7 +102,6 @@ namespace Discord | |||||
| { | { | ||||
| if (channel == null) throw new ArgumentNullException(nameof(channel)); | if (channel == null) throw new ArgumentNullException(nameof(channel)); | ||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | 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(); | CheckReady(); | ||||
| return SendMessage(channel, text, false); | return SendMessage(channel, text, false); | ||||
| @@ -113,7 +111,6 @@ namespace Discord | |||||
| { | { | ||||
| if (user == null) throw new ArgumentNullException(nameof(user)); | if (user == null) throw new ArgumentNullException(nameof(user)); | ||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | 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(); | CheckReady(); | ||||
| var channel = await CreatePMChannel(user).ConfigureAwait(false); | var channel = await CreatePMChannel(user).ConfigureAwait(false); | ||||
| @@ -122,7 +119,8 @@ namespace Discord | |||||
| private async Task<Message> SendMessage(Channel channel, string text, bool isTextToSpeech) | private async Task<Message> SendMessage(Channel channel, string text, bool isTextToSpeech) | ||||
| { | { | ||||
| Message msg; | Message msg; | ||||
| var userIds = !channel.IsPrivate ? Mention.GetUserIds(text).Distinct() : new string[0]; | |||||
| var server = channel.Server; | |||||
| if (Config.UseMessageQueue) | if (Config.UseMessageQueue) | ||||
| { | { | ||||
| var nonce = GenerateNonce(); | var nonce = GenerateNonce(); | ||||
| @@ -136,14 +134,24 @@ namespace Discord | |||||
| ChannelId = channel.Id, | ChannelId = channel.Id, | ||||
| IsTextToSpeech = isTextToSpeech | IsTextToSpeech = isTextToSpeech | ||||
| }); | }); | ||||
| msg.Mentions = userIds.Select(x => _users[x, channel.Server.Id]).Where(x => x != null).ToArray(); | |||||
| msg.IsQueued = true; | |||||
| msg.Nonce = nonce; | 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); | _pendingMessages.Enqueue(msg); | ||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| var model = await _api.SendMessage(channel.Id, text, userIds, null, isTextToSpeech).ConfigureAwait(false); | |||||
| var mentionedUsers = new List<User>(); | |||||
| 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 = _messages.GetOrAdd(model.Id, channel.Id, model.Author.Id); | ||||
| msg.Update(model); | msg.Update(model); | ||||
| RaiseMessageSent(msg); | RaiseMessageSent(msg); | ||||
| @@ -168,13 +176,17 @@ namespace Discord | |||||
| { | { | ||||
| if (message == null) throw new ArgumentNullException(nameof(message)); | if (message == null) throw new ArgumentNullException(nameof(message)); | ||||
| if (text == null) throw new ArgumentNullException(nameof(text)); | 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(); | CheckReady(); | ||||
| if (text != null && text.Length > MaxMessageSize) | |||||
| text = text.Substring(0, MaxMessageSize); | |||||
| var channel = message.Channel; | |||||
| var mentionedUsers = new List<User>(); | |||||
| 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)); | |||||
| } | } | ||||
| /// <summary> Deletes the provided message. </summary> | /// <summary> Deletes the provided message. </summary> | ||||
| @@ -258,7 +270,7 @@ namespace Discord | |||||
| SendMessageResponse response = null; | SendMessageResponse response = null; | ||||
| try | 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 (WebException) { break; } | ||||
| catch (HttpException) { hasFailed = true; } | catch (HttpException) { hasFailed = true; } | ||||
| @@ -84,7 +84,7 @@ namespace Discord | |||||
| public async Task<Server> CreateServer(string name, Region region) | public async Task<Server> CreateServer(string name, Region region) | ||||
| { | { | ||||
| if (name == null) throw new ArgumentNullException(nameof(name)); | 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(); | CheckReady(); | ||||
| var response = await _api.CreateServer(name, region.Value).ConfigureAwait(false); | var response = await _api.CreateServer(name, region.Value).ConfigureAwait(false); | ||||
| @@ -141,7 +141,7 @@ namespace Discord | |||||
| return _users[user?.Id, server.Id]; | return _users[user?.Id, server.Id]; | ||||
| } | } | ||||
| /// <summary> Returns all users in with the specified server and name, along with their server-specific data. </summary> | |||||
| /// <summary> Returns all users with the specified server and name, along with their server-specific data. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks> | /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks> | ||||
| public IEnumerable<User> FindUsers(Server server, string name, string discriminator = null, bool exactMatch = false) | public IEnumerable<User> 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)); | if (name == null) throw new ArgumentNullException(nameof(name)); | ||||
| CheckReady(); | CheckReady(); | ||||
| IEnumerable<User> query; | |||||
| if (!exactMatch && name.StartsWith("@")) | |||||
| return FindUsers(server.Members, server.Id, name, discriminator, exactMatch); | |||||
| } | |||||
| /// <summary> Returns all users with the specified channel and name, along with their server-specific data. </summary> | |||||
| /// <remarks> Name formats supported: Name and @Name. Search is case-insensitive.</remarks> | |||||
| public IEnumerable<User> 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<User> FindUsers(IEnumerable<User> 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) | if (discriminator != null) | ||||
| query = query.Where(x => x.Discriminator == discriminator); | query = query.Where(x => x.Discriminator == discriminator); | ||||
| return query; | return query; | ||||
| @@ -227,7 +250,7 @@ namespace Discord | |||||
| public Task SetStatus(UserStatus status) | 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) | if (status != UserStatus.Online && status != UserStatus.Idle) | ||||
| throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); | throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); | ||||
| CheckReady(); | CheckReady(); | ||||
| @@ -17,6 +17,7 @@ namespace Discord | |||||
| private readonly JsonSerializer _serializer; | private readonly JsonSerializer _serializer; | ||||
| private readonly ConcurrentQueue<Message> _pendingMessages; | private readonly ConcurrentQueue<Message> _pendingMessages; | ||||
| private readonly ConcurrentDictionary<string, DiscordWSClient> _voiceClients; | private readonly ConcurrentDictionary<string, DiscordWSClient> _voiceClients; | ||||
| private readonly Dictionary<Type, IService> _services; | |||||
| private bool _sentInitialLog; | private bool _sentInitialLog; | ||||
| private uint _nextVoiceClientId; | private uint _nextVoiceClientId; | ||||
| private UserStatus _status; | private UserStatus _status; | ||||
| @@ -46,6 +47,7 @@ namespace Discord | |||||
| _roles = new Roles(this, cacheLock); | _roles = new Roles(this, cacheLock); | ||||
| _servers = new Servers(this, cacheLock); | _servers = new Servers(this, cacheLock); | ||||
| _globalUsers = new GlobalUsers(this, cacheLock); | _globalUsers = new GlobalUsers(this, cacheLock); | ||||
| _services = new Dictionary<Type, IService>(); | |||||
| _status = UserStatus.Online; | _status = UserStatus.Online; | ||||
| @@ -168,7 +170,6 @@ namespace Discord | |||||
| _serializer.MissingMemberHandling = MissingMemberHandling.Error; | _serializer.MissingMemberHandling = MissingMemberHandling.Error; | ||||
| #endif | #endif | ||||
| } | } | ||||
| internal override VoiceWebSocket CreateVoiceSocket() | internal override VoiceWebSocket CreateVoiceSocket() | ||||
| { | { | ||||
| var socket = base.CreateVoiceSocket(); | var socket = base.CreateVoiceSocket(); | ||||
| @@ -216,7 +217,6 @@ namespace Discord | |||||
| await Connect(token).ConfigureAwait(false); | await Connect(token).ConfigureAwait(false); | ||||
| return token; | return token; | ||||
| } | } | ||||
| /// <summary> Connects to the Discord server with the provided token. </summary> | /// <summary> Connects to the Discord server with the provided token. </summary> | ||||
| public async Task Connect(string token) | public async Task Connect(string token) | ||||
| { | { | ||||
| @@ -273,6 +273,22 @@ namespace Discord | |||||
| _currentUser = null; | _currentUser = null; | ||||
| } | } | ||||
| public void AddService<T>(T obj) | |||||
| where T : class, IService | |||||
| { | |||||
| _services.Add(typeof(T), obj); | |||||
| obj.Install(this); | |||||
| } | |||||
| public T GetService<T>() | |||||
| where T : class, IService | |||||
| { | |||||
| IService service; | |||||
| if (_services.TryGetValue(typeof(T), out service)) | |||||
| return service as T; | |||||
| else | |||||
| return null; | |||||
| } | |||||
| protected override IEnumerable<Task> GetTasks() | protected override IEnumerable<Task> GetTasks() | ||||
| { | { | ||||
| if (Config.UseMessageQueue) | if (Config.UseMessageQueue) | ||||
| @@ -294,7 +310,8 @@ namespace Discord | |||||
| var data = e.Payload.ToObject<ReadyEvent>(_serializer); | var data = e.Payload.ToObject<ReadyEvent>(_serializer); | ||||
| _currentUser = _users.GetOrAdd(data.User.Id, null); | _currentUser = _users.GetOrAdd(data.User.Id, null); | ||||
| _currentUser.Update(data.User); | _currentUser.Update(data.User); | ||||
| foreach (var model in data.Guilds) | |||||
| _currentUser.GlobalUser.Update(data.User); | |||||
| foreach (var model in data.Guilds) | |||||
| { | { | ||||
| if (!model.Unavailable) | if (!model.Unavailable) | ||||
| { | { | ||||
| @@ -8,6 +8,7 @@ namespace Discord | |||||
| { | { | ||||
| private static readonly Regex _userRegex = new Regex(@"<@([0-9]+?)>", RegexOptions.Compiled); | 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 _channelRegex = new Regex(@"<#([0-9]+?)>", RegexOptions.Compiled); | ||||
| private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); | |||||
| /// <summary> Returns the string used to create a user mention. </summary> | /// <summary> Returns the string used to create a user mention. </summary> | ||||
| public static string User(User user) | public static string User(User user) | ||||
| @@ -15,42 +16,50 @@ namespace Discord | |||||
| /// <summary> Returns the string used to create a channel mention. </summary> | /// <summary> Returns the string used to create a channel mention. </summary> | ||||
| public static string Channel(Channel channel) | public static string Channel(Channel channel) | ||||
| => $"<#{channel.Id}>"; | => $"<#{channel.Id}>"; | ||||
| /// <summary> Returns the string used to create a channel mention. </summary> | |||||
| /// <summary> Returns the string used to create a mention to everyone in a channel. </summary> | |||||
| public static string Everyone() | public static string Everyone() | ||||
| => $"@everyone"; | => $"@everyone"; | ||||
| internal static string ConvertToNames(DiscordClient client, Server server, string text) | |||||
| internal static string CleanUserMentions(DiscordClient client, Server server, string text, List<User> 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); | string id = e.Value.Substring(2, e.Value.Length - 3); | ||||
| var user = client.Users[id, server?.Id]; | var user = client.Users[id, server?.Id]; | ||||
| if (user != null) | if (user != null) | ||||
| { | |||||
| if (users != null) | |||||
| users.Add(user); | |||||
| return '@' + user.Name; | return '@' + user.Name; | ||||
| } | |||||
| else //User not found | else //User not found | ||||
| return '@' + e.Value; | return '@' + e.Value; | ||||
| })); | })); | ||||
| if (server != null) | |||||
| } | |||||
| internal static string CleanChannelMentions(DiscordClient client, Server server, string text, List<Channel> 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 '#' + e.Value; | ||||
| })); | |||||
| } | |||||
| return text; | |||||
| })); | |||||
| } | } | ||||
| internal static IEnumerable<string> GetUserIds(string text) | |||||
| internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List<Role> roles = null) | |||||
| { | { | ||||
| return _userRegex.Matches(text) | |||||
| .OfType<Match>() | |||||
| .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; | |||||
| })); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,7 @@ | |||||
| namespace Discord | |||||
| { | |||||
| public interface IService | |||||
| { | |||||
| void Install(DiscordClient client); | |||||
| } | |||||
| } | |||||
| @@ -146,14 +146,12 @@ namespace Discord | |||||
| var cacheLength = _client.Config.MessageCacheLength; | var cacheLength = _client.Config.MessageCacheLength; | ||||
| if (cacheLength > 0) | 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); | _messages.TryAdd(message.Id, message); | ||||
| } | } | ||||
| @@ -207,6 +205,8 @@ namespace Discord | |||||
| user.UpdateChannelPermissions(this); | 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; | public override string ToString() => Name ?? Id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -77,5 +77,9 @@ namespace Discord | |||||
| uint mask = (uint)~(0xFF << bit); | uint mask = (uint)~(0xFF << bit); | ||||
| _rawValue = (_rawValue & mask) | ((uint)value << 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"); | |||||
| } | } | ||||
| } | } | ||||
| @@ -69,6 +69,8 @@ namespace Discord | |||||
| _client.GlobalUsers.TryRemove(Id); | _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; | public override string ToString() => Id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -5,7 +5,62 @@ using Newtonsoft.Json; | |||||
| namespace Discord | namespace Discord | ||||
| { | { | ||||
| public sealed class Invite : CachedObject | public sealed class Invite : CachedObject | ||||
| { | |||||
| { | |||||
| public sealed class ServerInfo | |||||
| { | |||||
| /// <summary> Returns the unique identifier of this server. </summary> | |||||
| public string Id { get; } | |||||
| /// <summary> Returns the name of this server. </summary> | |||||
| public string Name { get; } | |||||
| internal ServerInfo(string id, string name) | |||||
| { | |||||
| Id = id; | |||||
| Name = name; | |||||
| } | |||||
| } | |||||
| public sealed class ChannelInfo | |||||
| { | |||||
| /// <summary> Returns the unique identifier of this channel. </summary> | |||||
| public string Id { get; } | |||||
| /// <summary> Returns the name of this channel. </summary> | |||||
| public string Name { get; } | |||||
| internal ChannelInfo(string id, string name) | |||||
| { | |||||
| Id = id; | |||||
| Name = name; | |||||
| } | |||||
| } | |||||
| public sealed class InviterInfo | |||||
| { | |||||
| /// <summary> Returns the unique identifier for this user. </summary> | |||||
| public string Id { get; } | |||||
| /// <summary> Returns the name of this user. </summary> | |||||
| public string Name { get; } | |||||
| /// <summary> Returns the by-name unique identifier for this user. </summary> | |||||
| public string Discriminator { get; } | |||||
| /// <summary> Returns the unique identifier for this user's avatar. </summary> | |||||
| public string AvatarId { get; } | |||||
| /// <summary> Returns the full path to this user's avatar. </summary> | |||||
| 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; | |||||
| } | |||||
| } | |||||
| /// <summary> Returns information about the server this invite is attached to. </summary> | |||||
| public ServerInfo Server { get; private set; } | |||||
| /// <summary> Returns information about the channel this invite is attached to. </summary> | |||||
| public ChannelInfo Channel { get; private set; } | |||||
| /// <summary> Returns information about the user that created this invite. </summary> | |||||
| public InviterInfo Inviter { get; private set; } | |||||
| /// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary> | /// <summary> Returns, if enabled, an alternative human-readable code for URLs. </summary> | ||||
| public string XkcdCode { get; } | public string XkcdCode { get; } | ||||
| /// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary> | /// <summary> Time (in seconds) until the invite expires. Set to 0 to never expire. </summary> | ||||
| @@ -23,77 +78,23 @@ namespace Discord | |||||
| /// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | /// <summary> Returns a URL for this invite using XkcdCode if available or Id if not. </summary> | ||||
| public string Url => API.Endpoints.InviteUrl(XkcdCode ?? Id); | public string Url => API.Endpoints.InviteUrl(XkcdCode ?? Id); | ||||
| /// <summary> Returns the user that created this invite. </summary> | |||||
| [JsonIgnore] | |||||
| public User Inviter => _inviter.Value; | |||||
| private readonly Reference<User> _inviter; | |||||
| private User _generatedInviter; | |||||
| /// <summary> Returns the server this invite is to. </summary> | |||||
| [JsonIgnore] | |||||
| public Server Server => _server.Value; | |||||
| private readonly Reference<Server> _server; | |||||
| private Server _generatedServer; | |||||
| /// <summary> Returns the channel this invite is to. </summary> | |||||
| [JsonIgnore] | |||||
| public Channel Channel => _channel.Value; | |||||
| private readonly Reference<Channel> _channel; | |||||
| private Channel _generatedChannel; | |||||
| internal Invite(DiscordClient client, string code, string xkcdPass, string serverId, string inviterId, string channelId) | internal Invite(DiscordClient client, string code, string xkcdPass, string serverId, string inviterId, string channelId) | ||||
| : base(client, code) | : base(client, code) | ||||
| { | { | ||||
| XkcdCode = xkcdPass; | XkcdCode = xkcdPass; | ||||
| _server = new Reference<Server>(serverId, x => | |||||
| { | |||||
| var server = _client.Servers[x]; | |||||
| if (server == null) | |||||
| { | |||||
| server = _generatedServer = new Server(client, x); | |||||
| server.Cache(); | |||||
| } | |||||
| return server; | |||||
| }); | |||||
| _inviter = new Reference<User>(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<Channel>(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 override void UnloadReferences() { } | ||||
| internal void Update(InviteReference model) | 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) | internal void Update(InviteInfo model) | ||||
| { | { | ||||
| Update(model as InviteReference); | Update(model as InviteReference); | ||||
| @@ -112,6 +113,8 @@ namespace Discord | |||||
| CreatedAt = model.CreatedAt.Value; | 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; | public override string ToString() => XkcdCode ?? Id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -90,17 +90,13 @@ namespace Discord | |||||
| Height = height; | Height = height; | ||||
| } | } | ||||
| } | } | ||||
| private string _cleanText; | |||||
| /// <summary> Returns the local unique identifier for this message. </summary> | /// <summary> Returns the local unique identifier for this message. </summary> | ||||
| public string Nonce { get; internal set; } | public string Nonce { get; internal set; } | ||||
| /// <summary> Returns true if the logged-in user was mentioned. </summary> | /// <summary> Returns true if the logged-in user was mentioned. </summary> | ||||
| /// <remarks> This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone). </remarks> | /// <remarks> This is not set to true if the user was mentioned with @everyone (see IsMentioningEverone). </remarks> | ||||
| public bool IsMentioningMe { get; private set; } | public bool IsMentioningMe { get; private set; } | ||||
| /// <summary> Returns true if @everyone was mentioned by someone with permissions to do so. </summary> | |||||
| public bool IsMentioningEveryone { get; private set; } | |||||
| /// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary> | /// <summary> Returns true if the message was sent as text-to-speech by someone with permissions to do so. </summary> | ||||
| public bool IsTTS { get; private set; } | public bool IsTTS { get; private set; } | ||||
| /// <summary> Returns true if the message is still in the outgoing message queue. </summary> | /// <summary> Returns true if the message is still in the outgoing message queue. </summary> | ||||
| @@ -110,8 +106,7 @@ namespace Discord | |||||
| /// <summary> Returns the raw content of this message as it was received from the server.. </summary> | /// <summary> Returns the raw content of this message as it was received from the server.. </summary> | ||||
| public string RawText { get; private set; } | public string RawText { get; private set; } | ||||
| /// <summary> Returns the content of this message with any special references such as mentions converted. </summary> | /// <summary> Returns the content of this message with any special references such as mentions converted. </summary> | ||||
| /// <remarks> This value is lazy loaded and only processed on first request. Each subsequent request will pull from cache. </remarks> | |||||
| public string Text => _cleanText != null ? _cleanText : (_cleanText = Mention.ConvertToNames(_client, Server, RawText)); | |||||
| public string Text { get; private set; } | |||||
| /// <summary> Returns the timestamp for when this message was sent. </summary> | /// <summary> Returns the timestamp for when this message was sent. </summary> | ||||
| public DateTime Timestamp { get; private set; } | public DateTime Timestamp { get; private set; } | ||||
| /// <summary> Returns the timestamp for when this message was last edited. </summary> | /// <summary> Returns the timestamp for when this message was last edited. </summary> | ||||
| @@ -125,8 +120,16 @@ namespace Discord | |||||
| /// <summary> Returns a collection of all users mentioned in this message. </summary> | /// <summary> Returns a collection of all users mentioned in this message. </summary> | ||||
| [JsonIgnore] | [JsonIgnore] | ||||
| public IEnumerable<User> Mentions { get; internal set; } | |||||
| public IEnumerable<User> MentionedUsers { get; internal set; } | |||||
| /// <summary> Returns a collection of all channels mentioned in this message. </summary> | |||||
| [JsonIgnore] | |||||
| public IEnumerable<Channel> MentionedChannels { get; internal set; } | |||||
| /// <summary> Returns a collection of all roles mentioned in this message. </summary> | |||||
| [JsonIgnore] | |||||
| public IEnumerable<Role> MentionedRoles { get; internal set; } | |||||
| /// <summary> Returns the server containing the channel this message was sent to. </summary> | /// <summary> Returns the server containing the channel this message was sent to. </summary> | ||||
| [JsonIgnore] | [JsonIgnore] | ||||
| public Server Server => _channel.Value.Server; | public Server Server => _channel.Value.Server; | ||||
| @@ -174,6 +177,8 @@ namespace Discord | |||||
| internal void Update(MessageInfo model) | internal void Update(MessageInfo model) | ||||
| { | { | ||||
| var channel = Channel; | |||||
| var server = channel.Server; | |||||
| if (model.Attachments != null) | if (model.Attachments != null) | ||||
| { | { | ||||
| Attachments = model.Attachments | Attachments = model.Attachments | ||||
| @@ -197,9 +202,7 @@ namespace Discord | |||||
| return new Embed(x.Url, x.Type, x.Title, x.Description, author, provider, thumbnail); | return new Embed(x.Url, x.Type, x.Title, x.Description, author, provider, thumbnail); | ||||
| }).ToArray(); | }).ToArray(); | ||||
| } | } | ||||
| if (model.IsMentioningEveryone != null) | |||||
| IsMentioningEveryone = model.IsMentioningEveryone.Value; | |||||
| if (model.IsTextToSpeech != null) | if (model.IsTextToSpeech != null) | ||||
| IsTTS = model.IsTextToSpeech.Value; | IsTTS = model.IsTextToSpeech.Value; | ||||
| if (model.Timestamp != null) | if (model.Timestamp != null) | ||||
| @@ -208,16 +211,46 @@ namespace Discord | |||||
| EditedTimestamp = model.EditedTimestamp; | EditedTimestamp = model.EditedTimestamp; | ||||
| if (model.Mentions != null) | 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) | if (model.Content != null) | ||||
| { | { | ||||
| RawText = model.Content; | |||||
| _cleanText = null; | |||||
| string text = model.Content; | |||||
| RawText = text; | |||||
| //var mentionedUsers = new List<User>(); | |||||
| var mentionedChannels = new List<Channel>(); | |||||
| var mentionedRoles = new List<Role>(); | |||||
| 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}"; | public override string ToString() => $"{User}: {RawText}"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -137,6 +137,9 @@ namespace Discord | |||||
| if (_isLocked) | if (_isLocked) | ||||
| throw new InvalidOperationException("Unable to edit cached permissions directly, use Copy() to make an editable copy."); | 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 | public sealed class DualChannelPermissions | ||||
| @@ -214,5 +217,10 @@ namespace Discord | |||||
| Deny.SetBit(pos, false); | 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()); | |||||
| } | } | ||||
| } | } | ||||
| @@ -70,6 +70,8 @@ namespace Discord | |||||
| member.UpdateServerPermissions(); | 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; | public override string ToString() => Name ?? Id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -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; | public override string ToString() => Name ?? Id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -10,6 +10,7 @@ namespace Discord | |||||
| public class User : CachedObject | public class User : CachedObject | ||||
| { | { | ||||
| internal static string GetId(string userId, string serverId) => (serverId ?? "Private") + '_' + userId; | 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<string, Channel> _channels; | private ConcurrentDictionary<string, Channel> _channels; | ||||
| private ConcurrentDictionary<string, ChannelPermissions> _permissions; | private ConcurrentDictionary<string, ChannelPermissions> _permissions; | ||||
| @@ -24,7 +25,7 @@ namespace Discord | |||||
| /// <summary> Returns the unique identifier for this user's current avatar. </summary> | /// <summary> Returns the unique identifier for this user's current avatar. </summary> | ||||
| public string AvatarId { get; private set; } | public string AvatarId { get; private set; } | ||||
| /// <summary> Returns the URL to this user's current avatar. </summary> | /// <summary> Returns the URL to this user's current avatar. </summary> | ||||
| public string AvatarUrl => AvatarId != null ? API.Endpoints.UserAvatar(Id, AvatarId) : null; | |||||
| public string AvatarUrl => GetAvatarUrl(Id, AvatarId); | |||||
| /// <summary> Returns the datetime that this user joined this server. </summary> | /// <summary> Returns the datetime that this user joined this server. </summary> | ||||
| public DateTime JoinedAt { get; private set; } | public DateTime JoinedAt { get; private set; } | ||||
| @@ -370,6 +371,8 @@ namespace Discord | |||||
| return _roles.ContainsKey(role.Id); | 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; | public override string ToString() => Name ?? Id; | ||||
| } | } | ||||
| } | } | ||||
| @@ -52,19 +52,15 @@ namespace Discord.Net.Rest | |||||
| } | } | ||||
| private async Task<string> Send(RestRequest request, CancellationToken cancelToken) | private async Task<string> Send(RestRequest request, CancellationToken cancelToken) | ||||
| { | { | ||||
| bool hasRetried = false; | |||||
| int retryCount = 0; | |||||
| while (true) | while (true) | ||||
| { | { | ||||
| var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false); | var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false); | ||||
| int statusCode = (int)response.StatusCode; | int statusCode = (int)response.StatusCode; | ||||
| if (statusCode == 0) //Internal Error | 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; | throw response.ErrorException; | ||||
| } | } | ||||
| if (statusCode < 200 || statusCode >= 300) //2xx = Success | if (statusCode < 200 || statusCode >= 300) //2xx = Success | ||||