From db4e8a1ec6a67daa18e4ee49aabe00f96e691f91 Mon Sep 17 00:00:00 2001 From: Googie2149 Date: Wed, 28 Oct 2015 23:11:15 -0400 Subject: [PATCH 01/47] Revamped CommandsPlugin This uses a dictionary for the commands list, if a command has a max args set it'll only get that amount, will call the UnkownCommand event, and now has a built in help command that can be optionally enabled. CommandChar is now a list, but a single character can still be used. Externally, not much should have changed, but commands can be hidden from the help command and a description can be set. There's probably more that I've forgotten about. --- Discord.Net.sln | 9 + src/Discord.Net.Commands/Command.cs | 6 +- src/Discord.Net.Commands/CommandBuilder.cs | 12 + .../CommandsPlugin.Events.cs | 7 +- src/Discord.Net.Commands/CommandsPlugin.cs | 321 ++++++++++++------ 5 files changed, 243 insertions(+), 112 deletions(-) diff --git a/Discord.Net.sln b/Discord.Net.sln index 1c32308ff..3aefab91d 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -30,6 +30,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Modules", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Modules", "src\Discord.Net.Modules.Net45\Discord.Net.Modules.csproj", "{3091164F-66AE-4543-A63D-167C1116241D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingProject", "TestingProject\TestingProject.csproj", "{6CD8116D-6749-4174-81EB-C4EB4B1F185B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,12 @@ Global {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,5 +101,6 @@ Global {1B5603B4-6F8F-4289-B945-7BAAE523D740} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} {01584E8A-78DA-486F-9EF9-A894E435841B} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} {3091164F-66AE-4543-A63D-167C1116241D} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} + {6CD8116D-6749-4174-81EB-C4EB4B1F185B} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index f8728f4c6..2f4978566 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -9,13 +9,15 @@ namespace Discord.Commands public int? MinArgs { get; internal set; } public int? MaxArgs { get; internal set; } public int MinPerms { get; internal set; } - internal readonly string[] Parts; + public bool IsHidden { get; internal set; } + public string Description { get; internal set; } internal Func Handler; internal Command(string text) { Text = text; - Parts = text.ToLowerInvariant().Split(' '); + IsHidden = false; // Set false by default to avoid null error + Description = "No description set for this command."; } } } diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 0db5a388f..13e3ff89f 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -54,6 +54,18 @@ namespace Discord.Commands return this; } + public CommandBuilder Desc(string desc) + { + _command.Description = desc; + return this; + } + + public CommandBuilder IsHidden() + { + _command.IsHidden = true; + return this; + } + public CommandBuilder Do(Func func) { _command.Handler = func; diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 5901aa2a7..482855f4d 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -3,10 +3,12 @@ namespace Discord.Commands { public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } } + public class ArgumentException : Exception { public ArgumentException() : base("This command requires more arguments.") { } } public class CommandEventArgs { public Message Message { get; } public Command Command { get; } + public string MessageText { get; } public string CommandText { get; } public string ArgText { get; } public int? Permissions { get; } @@ -16,10 +18,11 @@ namespace Discord.Commands 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) + public CommandEventArgs(Message message, Command command, string messageText, string commandText, string argText, int? permissions, string[] args) { Message = message; Command = command; + MessageText = messageText; CommandText = commandText; ArgText = argText; Permissions = permissions; @@ -31,7 +34,7 @@ namespace Discord.Commands public Exception Exception { get; } public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) + : base(baseArgs.Message, baseArgs.Command, baseArgs.MessageText, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) { Exception = ex; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 44abce8f4..17199f3d5 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace Discord.Commands { @@ -7,132 +10,234 @@ namespace Discord.Commands public partial class CommandsPlugin { private readonly DiscordClient _client; - private List _commands; private Func _getPermissions; - public IEnumerable Commands => _commands; + private Dictionary _commands; + + public Dictionary Commands => _commands; - public char CommandChar { get; set; } + public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure. + public List CommandChars { 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 + public bool HelpInPublic { get; set; } + + public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) + { + _client = client; // Wait why is this even set + _getPermissions = getPermissions; + + _commands = new Dictionary(); + + CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary. + CommandChars = new List { '!', '?', '/' }; + UseCommandChar = true; + RequireCommandCharInPublic = true; + RequireCommandCharInPrivate = true; + HelpInPublic = true; + + if (builtInHelp) + { + CreateCommand("help") + .ArgsBetween(0, 1) + .IsHidden() + .Desc("Returns information about commands.") + .Do(async e => + { + if (e.Command.Text != "help") + { + await Reply(e, CommandDetails(e.Command)); + } + else + { + if (e.Args == null) + { + StringBuilder output = new StringBuilder(); + bool first = true; + output.AppendLine("These are the commands you can use:"); + output.Append("`"); + int permissions = getPermissions(e.User); + foreach (KeyValuePair k in _commands) + { + if (permissions >= k.Value.MinPerms && !k.Value.IsHidden) + if (first) + { + output.Append(k.Key); + first = false; + } + else + output.Append($", {k.Key}"); + } + output.Append("`"); + + if (CommandChars.Count == 1) + output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command."); + else + output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command."); + + output.AppendLine("`help ` can tell you more about how to use a command."); + + await Reply(e, output.ToString()); + } + else + { + if (_commands.ContainsKey(e.Args[0])) + await Reply(e, CommandDetails(_commands[e.Args[0]])); + else + await Reply(e, $"`{e.Args[0]}` is not a valid command."); + } + } + }); + + } + + client.MessageReceived += async (s, e) => + { + // This will need to be changed once a built in help command is made + if (_commands.Count == 0) + return; + + if (e.Message.IsAuthor) + return; + + string msg = e.Message.Text; + + if (msg.Length == 0) + return; + + if (UseCommandChar) + { + bool isPrivate = e.Message.Channel.IsPrivate; + bool hasCommandChar = CommandChars.Contains(msg[0]); + if (hasCommandChar) + msg = msg.Substring(1); + + if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar) + return; // If private, and command char is required, and it doesn't have it, ignore it. + if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar) + return; // Same, but public. + } + + string cmd; + CommandPart[] args; + if (!CommandParser.Parse(msg, out cmd, out args)) + return; + + if (_commands.ContainsKey(cmd)) + { + Command comm = _commands[cmd]; + + //Get ArgText + int argCount = args.Length; + string argText; + if (argCount == 0) + argText = ""; + else + argText = msg.Substring(args[0].Index); + + //Clean Args + string[] newArgs = null; + + if (comm.MaxArgs != null && argCount > 0) + { + newArgs = new string[(int)comm.MaxArgs]; + for (int j = 0; j < newArgs.Length; j++) + newArgs[j] = args[j].Value; + } + + // Check permissions here + int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); + if (permissions < comm.MinPerms) + { + RaiseCommandError(eventArgs, new PermissionException()); + return; + } + + //Check Arg Count + if (argCount < comm.MinArgs) + { + RaiseCommandError(eventArgs, new ArgumentException()); + if (builtInHelp) + await _commands["help"].Handler(eventArgs); + return; + } + + // Actually run the command here RaiseRanCommand(eventArgs); - try - { - var task = cmd.Handler(eventArgs); - if (task != null) - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - RaiseCommandError(eventArgs, ex); - } - break; - } - }; - } + try + { + var task = comm.Handler(eventArgs); + if (task != null) + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + RaiseCommandError(eventArgs, ex); + } + } + else + { + CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); + RaiseUnknownCommand(eventArgs); + if (builtInHelp) + await Reply(eventArgs, $"Command `cmd` does not exist."); + return; + } + }; + } + + internal string CommandDetails(Command comm) + { + StringBuilder output = new StringBuilder(); + + output.Append($"`{comm.Text}`"); + + if (comm.MinArgs != null && comm.MaxArgs != null) + { + if (comm.MinArgs == comm.MaxArgs) + { + if (comm.MaxArgs != 0) + output.Append($" {comm.MinArgs.ToString()} Args"); + } + else + output.Append($" {comm.MinArgs.ToString()} - {comm.MaxArgs.ToString()} Args"); + } + else if (comm.MinArgs != null && comm.MaxArgs == null) + { + output.Append($" ≥{comm.MinArgs.ToString()} Args"); + } + else if (comm.MinArgs == null && comm.MaxArgs != null) + { + output.Append($" ≤{comm.MaxArgs.ToString()} Args"); + } + + output.Append($": {comm.Description}"); + + return output.ToString(); + } + + internal async Task Reply(CommandEventArgs e, string message) + { + if (HelpInPublic) + await _client.SendMessage(e.Channel, message); + else + await _client.SendPrivateMessage(e.User, message); + } 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); + _commands.Add(cmd, command); return new CommandBuilder(command); } internal void AddCommand(Command command) { - _commands.Add(command); + _commands.Add(command.Text, command); } } } From d8d0df290bf441231d8ec761868e82f641ef5a04 Mon Sep 17 00:00:00 2001 From: Googie2149 Date: Thu, 29 Oct 2015 01:26:12 -0400 Subject: [PATCH 02/47] Fixed commands with .AnyArgs() set --- src/Discord.Net.Commands/CommandsPlugin.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 17199f3d5..4b78b4c34 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -145,9 +145,15 @@ namespace Discord.Commands for (int j = 0; j < newArgs.Length; j++) newArgs[j] = args[j].Value; } + else if (comm.MaxArgs == null && comm.MinArgs == null) + { + newArgs = new string[argCount]; + for (int j = 0; j < newArgs.Length; j++) + newArgs[j] = args[j].Value; + } - // Check permissions here - int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + // Check permissions here + int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); if (permissions < comm.MinPerms) { @@ -182,7 +188,7 @@ namespace Discord.Commands CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); RaiseUnknownCommand(eventArgs); if (builtInHelp) - await Reply(eventArgs, $"Command `cmd` does not exist."); + await Reply(eventArgs, $"The command `{cmd}` does not exist."); return; } }; From 694a5840f2bd92cef1360d31f0c362ca8a7ae06a Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 20:29:51 -0300 Subject: [PATCH 03/47] Cleaned up a few parts, combined all errors into one event. --- .../CommandsPlugin.Events.cs | 35 +++--- src/Discord.Net.Commands/CommandsPlugin.cs | 105 +++++++----------- 2 files changed, 55 insertions(+), 85 deletions(-) diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 482855f4d..0b3f4ee14 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -2,43 +2,40 @@ namespace Discord.Commands { - public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } } - public class ArgumentException : Exception { public ArgumentException() : base("This command requires more arguments.") { } } public class CommandEventArgs { public Message Message { get; } public Command Command { get; } - public string MessageText { get; } - public string CommandText { get; } - public string ArgText { get; } - public int? Permissions { 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, string messageText, string commandText, string argText, int? permissions, string[] args) + public CommandEventArgs(Message message, Command command, int? userPermissions, string[] args) { Message = message; Command = command; - MessageText = messageText; - CommandText = commandText; - ArgText = argText; - Permissions = permissions; + UserPermissions = userPermissions; Args = args; } } + + public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount } public class CommandErrorEventArgs : CommandEventArgs { + public CommandErrorType ErrorType { get; } public Exception Exception { get; } - public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.MessageText, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) + public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex) + : base(baseArgs.Message, baseArgs.Command, baseArgs.UserPermissions, baseArgs.Args) { Exception = ex; - } + ErrorType = errorType; + } } + public partial class CommandsPlugin { public event EventHandler RanCommand; @@ -47,17 +44,11 @@ namespace Discord.Commands 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) + private void RaiseCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) { if (CommandError != null) - CommandError(this, new CommandErrorEventArgs(args, ex)); + CommandError(this, new CommandErrorEventArgs(errorType, args, ex)); } } } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 4b78b4c34..7f7149b95 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -46,9 +46,7 @@ namespace Discord.Commands .Do(async e => { if (e.Command.Text != "help") - { await Reply(e, CommandDetails(e.Command)); - } else { if (e.Args == null) @@ -95,16 +93,11 @@ namespace Discord.Commands client.MessageReceived += async (s, e) => { // This will need to be changed once a built in help command is made - if (_commands.Count == 0) - return; - - if (e.Message.IsAuthor) - return; + if (_commands.Count == 0) return; + if (e.Message.IsAuthor) return; string msg = e.Message.Text; - - if (msg.Length == 0) - return; + if (msg.Length == 0) return; if (UseCommandChar) { @@ -127,17 +120,10 @@ namespace Discord.Commands if (_commands.ContainsKey(cmd)) { Command comm = _commands[cmd]; - - //Get ArgText - int argCount = args.Length; - string argText; - if (argCount == 0) - argText = ""; - else - argText = msg.Substring(args[0].Index); - - //Clean Args - string[] newArgs = null; + + //Clean args + int argCount = args.Length; + string[] newArgs = null; if (comm.MaxArgs != null && argCount > 0) { @@ -152,74 +138,67 @@ namespace Discord.Commands newArgs[j] = args[j].Value; } - // Check permissions here - int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); - if (permissions < comm.MinPerms) - { - RaiseCommandError(eventArgs, new PermissionException()); - return; - } - - //Check Arg Count - if (argCount < comm.MinArgs) - { - RaiseCommandError(eventArgs, new ArgumentException()); - if (builtInHelp) - await _commands["help"].Handler(eventArgs); - return; - } + int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + var eventArgs = new CommandEventArgs(e.Message, comm, userPermissions, newArgs); - // Actually run the command here - RaiseRanCommand(eventArgs); - try - { + // Check permissions + if (userPermissions < comm.MinPerms) + { + RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); + return; + } + + //Check arg count + if (argCount < comm.MinArgs) + { + RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); + return; + } + + // Run the command + try + { + RaiseRanCommand(eventArgs); var task = comm.Handler(eventArgs); if (task != null) await task.ConfigureAwait(false); } catch (Exception ex) { - RaiseCommandError(eventArgs, ex); + RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); } } else { - CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); - RaiseUnknownCommand(eventArgs); - if (builtInHelp) - await Reply(eventArgs, $"The command `{cmd}` does not exist."); + CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, null, null); + RaiseCommandError(CommandErrorType.UnknownCommand, eventArgs); return; } }; } - internal string CommandDetails(Command comm) + private string CommandDetails(Command command) { StringBuilder output = new StringBuilder(); - output.Append($"`{comm.Text}`"); + output.Append($"`{command.Text}`"); - if (comm.MinArgs != null && comm.MaxArgs != null) + if (command.MinArgs != null && command.MaxArgs != null) { - if (comm.MinArgs == comm.MaxArgs) + if (command.MinArgs == command.MaxArgs) { - if (comm.MaxArgs != 0) - output.Append($" {comm.MinArgs.ToString()} Args"); + if (command.MaxArgs != 0) + output.Append($" {command.MinArgs.ToString()} Args"); } else - output.Append($" {comm.MinArgs.ToString()} - {comm.MaxArgs.ToString()} Args"); - } - else if (comm.MinArgs != null && comm.MaxArgs == null) - { - output.Append($" ≥{comm.MinArgs.ToString()} Args"); - } - else if (comm.MinArgs == null && comm.MaxArgs != null) - { - output.Append($" ≤{comm.MaxArgs.ToString()} Args"); + 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($": {comm.Description}"); + output.Append($": {command.Description}"); return output.ToString(); } From 44fb33b511069dca485b81ed4e2fe9e40495d4c8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 23:33:05 -0300 Subject: [PATCH 04/47] Cleaned up CommandsPlugin, added CommandMap, new parameter declaration and aliases. --- .../Discord.Net.Commands.csproj | 3 + src/Discord.Net.Commands/Command.cs | 89 ++++++++++- src/Discord.Net.Commands/CommandBuilder.cs | 130 ++++++++-------- src/Discord.Net.Commands/CommandMap.cs | 84 ++++++++++ src/Discord.Net.Commands/CommandParser.cs | 80 ++++++---- .../CommandsPlugin.Events.cs | 2 +- src/Discord.Net.Commands/CommandsPlugin.cs | 145 ++++++++---------- 7 files changed, 348 insertions(+), 185 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandMap.cs diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj index 69f042ac3..1de1757f1 100644 --- a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj @@ -43,6 +43,9 @@ CommandBuilder.cs + + CommandMap.cs + CommandParser.cs diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 2f4978566..50f958ba2 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -1,23 +1,102 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord.Commands { + public sealed class CommandParameter + { + public string Name { get; } + public bool IsOptional { get; } + public bool IsCatchAll { get; } + + public CommandParameter(string name, bool isOptional, bool isCatchAll) + { + Name = name; + IsOptional = isOptional; + IsCatchAll = isCatchAll; + } + } + public sealed class Command { public string Text { get; } - public int? MinArgs { get; internal set; } - public int? MaxArgs { get; internal set; } + public int? MinArgs { get; private set; } + public int? MaxArgs { get; private set; } public int MinPerms { get; internal set; } public bool IsHidden { get; internal set; } public string Description { get; internal set; } - internal Func Handler; + + public IEnumerable Aliases => _aliases; + private string[] _aliases; + + public IEnumerable Parameters => _parameters; + private CommandParameter[] _parameters; + + private Func _handler; internal Command(string text) { Text = text; - IsHidden = false; // Set false by default to avoid null error - Description = "No description set for this command."; + 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].IsCatchAll) + MaxArgs = null; + else + MaxArgs = parameters.Length; + + int? optionalStart = null; + for (int i = parameters.Length - 1; i >= 0; i--) + { + if (parameters[i].IsOptional) + 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 13e3ff89f..981bc23df 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -1,50 +1,55 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Discord.Commands { public sealed class CommandBuilder { + private readonly CommandsPlugin _plugin; private readonly Command _command; - public CommandBuilder(Command command) + private List _params; + private bool _hasOptional, _hasCatchAll; + private string _prefix; + + public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) { + _plugin = plugin; _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; + } + + 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, bool isOptional = false, bool isCatchAll = false) { - _command.MinArgs = 0; - _command.MaxArgs = 0; + if (_hasCatchAll) + throw new Exception("No parameters may be added after the catch-all"); + if (_hasOptional && isOptional) + throw new Exception("Non-optional parameters may not be added after an optional one"); + + _params.Add(new CommandParameter(name, isOptional, isCatchAll)); + + if (isOptional) + _hasOptional = true; + if (isCatchAll) + _hasCatchAll = true; return this; } - public CommandBuilder AnyArgs() + public CommandBuilder IsHidden() { - _command.MinArgs = null; - _command.MaxArgs = null; + _command.IsHidden = true; return this; } @@ -54,32 +59,45 @@ namespace Discord.Commands return this; } - public CommandBuilder Desc(string desc) - { - _command.Description = desc; - return this; - } - - public CommandBuilder IsHidden() - { - _command.IsHidden = true; - 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) + _plugin.Map.AddCommand(alias, _command); + _plugin.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 CommandsPlugin _plugin; private readonly string _prefix; private int _defaultMinPermissions; @@ -104,25 +122,9 @@ namespace Discord.Commands => 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); + var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); command.MinPerms = _defaultMinPermissions; - _plugin.AddCommand(command); - return new CommandBuilder(command); + return new CommandBuilder(_plugin, command, _prefix); } } } diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs new file mode 100644 index 000000000..504479400 --- /dev/null +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Commands +{ + internal class CommandMap + { + private CommandMap _parent; + private Command _command; + private readonly Dictionary _subCommands; + + public CommandMap(CommandMap parent) + { + _parent = parent; + _subCommands = new Dictionary(); + } + + public CommandMap GetMap(string text) + { + CommandMap map; + if (_subCommands.TryGetValue(text, out map)) + return map; + else + return null; + } + + 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..08241eb1b 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -15,37 +15,69 @@ namespace Discord.Commands } //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) + public static bool ParseCommand(string input, CommandMap map, out Command command, out int endPos) { - 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); + int startPosition = 0; + int endPosition = 0; + int inputLength = input.Length; + bool isEscaped = false; + command = null; + endPos = 0; + + if (input == "") + return false; + + while (endPosition < inputLength) + { + char currentChar = input[endPosition++]; + if (isEscaped) + isEscaped = false; + else if (currentChar == '\\') + isEscaped = true; + + 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) + { + 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; } - private static bool Parse(string input, out string command, out CommandPart[] args, bool parseCommand) + public static bool ParseArgs(string input, int startPos, Command command, out CommandPart[] args) { - CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None; - int startPosition = 0; - int endPosition = 0; + CommandParserPart currentPart = CommandParserPart.None; + int startPosition = startPos; + int endPosition = startPos; int inputLength = input.Length; bool isEscaped = false; List argList = new List(); - - command = null; + args = null; if (input == "") @@ -61,21 +93,6 @@ namespace Discord.Commands switch (currentPart) { - 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 - { - currentPart = CommandParserPart.None; - command = temp; - startPosition = endPosition; - } - } - break; case CommandParserPart.None: if ((!isEscaped && currentChar == '\"')) { @@ -126,9 +143,6 @@ namespace Discord.Commands } } - if (parseCommand && (command == null || command == "")) - return false; - args = argList.ToArray(); return true; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 0b3f4ee14..da31fa6f7 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -22,7 +22,7 @@ namespace Discord.Commands } } - public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount } + public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput } public class CommandErrorEventArgs : CommandEventArgs { public CommandErrorType ErrorType { get; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 7f7149b95..f15664348 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -10,29 +10,30 @@ namespace Discord.Commands public partial class CommandsPlugin { private readonly DiscordClient _client; - private Func _getPermissions; - - private Dictionary _commands; + private readonly Func _getPermissions; - public Dictionary Commands => _commands; + public IEnumerable Commands => _commands; + private readonly List _commands; + + internal CommandMap Map => _map; + private readonly CommandMap _map; - public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure. - public List CommandChars { get; set; } - public bool UseCommandChar { get; set; } + public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } + private char[] _commandChars; + public bool RequireCommandCharInPublic { get; set; } public bool RequireCommandCharInPrivate { get; set; } public bool HelpInPublic { get; set; } public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) { - _client = client; // Wait why is this even set + _client = client; _getPermissions = getPermissions; - _commands = new Dictionary(); + _commands = new List(); + _map = new CommandMap(null); - CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary. - CommandChars = new List { '!', '?', '/' }; - UseCommandChar = true; + _commandChars = new char[] { '!' }; RequireCommandCharInPublic = true; RequireCommandCharInPrivate = true; HelpInPublic = true; @@ -40,9 +41,9 @@ namespace Discord.Commands if (builtInHelp) { CreateCommand("help") - .ArgsBetween(0, 1) + .Parameter("command", isOptional: true) .IsHidden() - .Desc("Returns information about commands.") + .Info("Returns information about commands.") .Do(async e => { if (e.Command.Text != "help") @@ -50,29 +51,18 @@ namespace Discord.Commands else { if (e.Args == null) - { - StringBuilder output = new StringBuilder(); - bool first = true; + { + int permissions = getPermissions(e.User); + StringBuilder output = new StringBuilder(); output.AppendLine("These are the commands you can use:"); output.Append("`"); - int permissions = getPermissions(e.User); - foreach (KeyValuePair k in _commands) - { - if (permissions >= k.Value.MinPerms && !k.Value.IsHidden) - if (first) - { - output.Append(k.Key); - first = false; - } - else - output.Append($", {k.Key}"); - } + output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPerms && !x.IsHidden))); output.Append("`"); - if (CommandChars.Count == 1) - output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command."); + if (_commandChars.Length == 1) + output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); else - output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command."); + output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); output.AppendLine("`help ` can tell you more about how to use a command."); @@ -80,8 +70,9 @@ namespace Discord.Commands } else { - if (_commands.ContainsKey(e.Args[0])) - await Reply(e, CommandDetails(_commands[e.Args[0]])); + var cmd = _map.GetCommand(e.Args[0]); + if (cmd != null) + await Reply(e, CommandDetails(cmd)); else await Reply(e, $"`{e.Args[0]}` is not a valid command."); } @@ -99,7 +90,7 @@ namespace Discord.Commands string msg = e.Message.Text; if (msg.Length == 0) return; - if (UseCommandChar) + if (_commandChars.Length > 0) { bool isPrivate = e.Message.Channel.IsPrivate; bool hasCommandChar = CommandChars.Contains(msg[0]); @@ -112,44 +103,41 @@ namespace Discord.Commands return; // Same, but public. } - string cmd; - CommandPart[] args; - if (!CommandParser.Parse(msg, out cmd, out args)) - return; - - if (_commands.ContainsKey(cmd)) - { - Command comm = _commands[cmd]; - - //Clean args + //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 + { + //Parse arguments + CommandPart[] args; + if (!CommandParser.ParseArgs(msg, argPos, command, out args)) + { + CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); + RaiseCommandError(CommandErrorType.InvalidInput, errorArgs); + return; + } int argCount = args.Length; - string[] newArgs = null; - - if (comm.MaxArgs != null && argCount > 0) - { - newArgs = new string[(int)comm.MaxArgs]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j].Value; - } - else if (comm.MaxArgs == null && comm.MinArgs == null) - { - newArgs = new string[argCount]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j].Value; - } + //Get information for the rest of the steps int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, comm, userPermissions, newArgs); + var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args.Select(x => x.Value).ToArray()); // Check permissions - if (userPermissions < comm.MinPerms) + if (userPermissions < command.MinPerms) { RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); return; } - + //Check arg count - if (argCount < comm.MinArgs) + if (argCount < command.MinArgs) { RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); return; @@ -159,21 +147,13 @@ namespace Discord.Commands try { RaiseRanCommand(eventArgs); - var task = comm.Handler(eventArgs); - if (task != null) - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); - } - } - else - { - CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, null, null); - RaiseCommandError(CommandErrorType.UnknownCommand, eventArgs); - return; - } + await command.Run(eventArgs).ConfigureAwait(false); + } + catch (Exception ex) + { + RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); + } + } }; } @@ -198,7 +178,7 @@ namespace Discord.Commands else if (command.MinArgs == null && command.MaxArgs != null) output.Append($" ≤{command.MaxArgs.ToString()} Args"); - output.Append($": {command.Description}"); + output.Append($": {command.Description ?? "No description set for this command."}"); return output.ToString(); } @@ -216,13 +196,14 @@ namespace Discord.Commands public CommandBuilder CreateCommand(string cmd) { var command = new Command(cmd); - _commands.Add(cmd, command); - return new CommandBuilder(command); + _commands.Add(command); + return new CommandBuilder(null, command, ""); } internal void AddCommand(Command command) { - _commands.Add(command.Text, command); + _commands.Add(command); + _map.AddCommand(command.Text, command); } } } From 8991bad44783570566ac0ec850cf45a8037b5867 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 23:35:16 -0300 Subject: [PATCH 05/47] Fixed crash for having multiple optional parameters --- src/Discord.Net.Commands/CommandBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 981bc23df..a97ff8d73 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -36,7 +36,7 @@ namespace Discord.Commands { if (_hasCatchAll) throw new Exception("No parameters may be added after the catch-all"); - if (_hasOptional && isOptional) + if (_hasOptional && !isOptional) throw new Exception("Non-optional parameters may not be added after an optional one"); _params.Add(new CommandParameter(name, isOptional, isCatchAll)); From 6c6d181b4b05bd1d53dda8733257236a4f8ba85b Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 09:45:42 -0300 Subject: [PATCH 06/47] FindUsers and FindChannels support mentions, FindUsers has a channel overload, and less bugs. --- src/Discord.Net/DiscordClient.Channels.cs | 32 ++++++++++-------- src/Discord.Net/DiscordClient.Users.cs | 41 ++++++++++++++++++----- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs index 52ce846ec..0c89a7a13 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] == '#') //If we somehow get text starting with # but isn't a mention + { + string name2 = name.Substring(1); + query = query.Concat(server.Channels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + } } if (type != (string)null) - result = result.Where(x => x.Type == type); - - return result; + query = query.Where(x => x.Type == type); + return query; } /// Creates a new channel with the provided name and type. diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs index e63fd04c1..ed4e048e7 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; From 063d56687c506024081cd997cfc97b86559787a9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 09:55:08 -0300 Subject: [PATCH 07/47] Added ParameterType --- src/Discord.Net.Commands/Command.cs | 23 ++++++++++++++------- src/Discord.Net.Commands/CommandBuilder.cs | 24 ++++++++++++---------- src/Discord.Net.Commands/CommandsPlugin.cs | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 50f958ba2..bc1013245 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -4,17 +4,26 @@ 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 bool IsOptional { get; } - public bool IsCatchAll { get; } + public ParameterType Type { get; } - public CommandParameter(string name, bool isOptional, bool isCatchAll) + public CommandParameter(string name, ParameterType type) { Name = name; - IsOptional = isOptional; - IsCatchAll = isCatchAll; + Type = type; } } @@ -60,7 +69,7 @@ namespace Discord.Commands } else { - if (parameters[parameters.Length - 1].IsCatchAll) + if (parameters[parameters.Length - 1].Type == ParameterType.Multiple) MaxArgs = null; else MaxArgs = parameters.Length; @@ -68,7 +77,7 @@ namespace Discord.Commands int? optionalStart = null; for (int i = parameters.Length - 1; i >= 0; i--) { - if (parameters[i].IsOptional) + if (parameters[i].Type == ParameterType.Optional) optionalStart = i; else break; diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index a97ff8d73..e0b4a5d54 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -10,7 +10,7 @@ namespace Discord.Commands private readonly CommandsPlugin _plugin; private readonly Command _command; private List _params; - private bool _hasOptional, _hasCatchAll; + private bool _allowRequired, _isClosed; private string _prefix; public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) @@ -19,6 +19,8 @@ namespace Discord.Commands _command = command; _params = new List(); _prefix = prefix; + _allowRequired = true; + _isClosed = false; } public CommandBuilder Alias(params string[] aliases) @@ -32,19 +34,19 @@ namespace Discord.Commands _command.Description = description; return this; } - public CommandBuilder Parameter(string name, bool isOptional = false, bool isCatchAll = false) + public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required) { - if (_hasCatchAll) - throw new Exception("No parameters may be added after the catch-all"); - if (_hasOptional && !isOptional) - throw new Exception("Non-optional parameters may not be added after an optional one"); + 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, isOptional, isCatchAll)); + _params.Add(new CommandParameter(name, type)); - if (isOptional) - _hasOptional = true; - if (isCatchAll) - _hasCatchAll = true; + if (type == ParameterType.Optional) + _allowRequired = false; + if (type == ParameterType.Multiple || type == ParameterType.Unparsed) + _isClosed = true; return this; } public CommandBuilder IsHidden() diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index f15664348..f34bbbfe1 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -41,7 +41,7 @@ namespace Discord.Commands if (builtInHelp) { CreateCommand("help") - .Parameter("command", isOptional: true) + .Parameter("command", ParameterType.Optional) .IsHidden() .Info("Returns information about commands.") .Do(async e => From b25b0e92b2268cf8bfbd9d3a40c1ad420bf4139d Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:01:05 -0300 Subject: [PATCH 08/47] Renamed IsHidden -> Hide --- src/Discord.Net.Commands/CommandBuilder.cs | 2 +- src/Discord.Net.Commands/CommandsPlugin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index e0b4a5d54..34a77f1c8 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -49,7 +49,7 @@ namespace Discord.Commands _isClosed = true; return this; } - public CommandBuilder IsHidden() + public CommandBuilder Hide() { _command.IsHidden = true; return this; diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index f34bbbfe1..3afee42fa 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -42,7 +42,7 @@ namespace Discord.Commands { CreateCommand("help") .Parameter("command", ParameterType.Optional) - .IsHidden() + .Hide() .Info("Returns information about commands.") .Do(async e => { From ace8ad676e0513eef6afbba6c08c2919b2f66629 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:04:20 -0300 Subject: [PATCH 09/47] Brought back CommandPlugin.CommandChar --- src/Discord.Net.Commands/CommandsPlugin.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 3afee42fa..1f5e5d450 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -18,7 +18,8 @@ namespace Discord.Commands internal CommandMap Map => _map; private readonly CommandMap _map; - public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } + public char ComamndChar { get { return _commandChars[0]; } set { _commandChars = new char[] { value }; } } + public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } private char[] _commandChars; public bool RequireCommandCharInPublic { get; set; } From 0fe2785b1567755a72b01d9df27ce23624eb0dc2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:35:53 -0300 Subject: [PATCH 10/47] Added support for unparsed parameters, more cleanup --- src/Discord.Net.Commands/Command.cs | 4 +- src/Discord.Net.Commands/CommandBuilder.cs | 6 +-- src/Discord.Net.Commands/CommandParser.cs | 53 ++++++++++++---------- src/Discord.Net.Commands/CommandsPlugin.cs | 51 +++++++++++---------- 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index bc1013245..15ad98b95 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -32,7 +32,7 @@ namespace Discord.Commands public string Text { get; } public int? MinArgs { get; private set; } public int? MaxArgs { get; private set; } - public int MinPerms { get; internal set; } + public int MinPermissions { get; internal set; } public bool IsHidden { get; internal set; } public string Description { get; internal set; } @@ -40,7 +40,7 @@ namespace Discord.Commands private string[] _aliases; public IEnumerable Parameters => _parameters; - private CommandParameter[] _parameters; + internal CommandParameter[] _parameters; private Func _handler; diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 34a77f1c8..dd8035dc7 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -38,7 +38,7 @@ namespace Discord.Commands { 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) + 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)); @@ -57,7 +57,7 @@ namespace Discord.Commands public CommandBuilder MinPermissions(int level) { - _command.MinPerms = level; + _command.MinPermissions = level; return this; } @@ -125,7 +125,7 @@ namespace Discord.Commands public CommandBuilder CreateCommand(string cmd) { var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); - command.MinPerms = _defaultMinPermissions; + command.MinPermissions = _defaultMinPermissions; return new CommandBuilder(_plugin, command, _prefix); } } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 08241eb1b..14af9f550 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -2,19 +2,6 @@ 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 internal static class CommandParser { private enum CommandParserPart @@ -69,23 +56,40 @@ namespace Discord.Commands return command != null; } - public static bool ParseArgs(string input, int startPos, Command command, out CommandPart[] args) + //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; - List argList = new List(); + + var expectedArgs = command._parameters; + List argList = new List(); + CommandParameter parameter = null; args = null; if (input == "") - return false; + return CommandErrorType.InvalidInput; while (endPosition < inputLength) { - char currentChar = input[endPosition++]; + 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 == '\\') @@ -113,7 +117,7 @@ namespace Discord.Commands else { currentPart = CommandParserPart.None; - argList.Add(new CommandPart(temp, startPosition)); + argList.Add(temp); startPosition = endPosition; } } @@ -123,28 +127,31 @@ 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 (argList.Count < command.MinArgs) + return CommandErrorType.BadArgCount; + args = argList.ToArray(); - return true; + return null; } } } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 1f5e5d450..e77210e5f 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -18,7 +18,11 @@ namespace Discord.Commands internal CommandMap Map => _map; private readonly CommandMap _map; - public char ComamndChar { get { return _commandChars[0]; } set { _commandChars = new char[] { value }; } } + public char? ComamndChar + { + get { return _commandChars.Length > 0 ? _commandChars[0] : (char?)null; } + set { _commandChars = value != null ? new char[] { value.Value } : new char[0]; } + } public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } private char[] _commandChars; @@ -54,16 +58,20 @@ namespace Discord.Commands if (e.Args == null) { int permissions = getPermissions(e.User); + StringBuilder output = new StringBuilder(); output.AppendLine("These are the commands you can use:"); output.Append("`"); - output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPerms && !x.IsHidden))); + output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPermissions && !x.IsHidden))); output.Append("`"); - if (_commandChars.Length == 1) - output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); - else - output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); + if (_commandChars.Length > 0) + { + if (_commandChars.Length == 1) + output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); + else + output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); + } output.AppendLine("`help ` can tell you more about how to use a command."); @@ -84,17 +92,17 @@ namespace Discord.Commands client.MessageReceived += async (s, e) => { - // This will need to be changed once a built in help command is made 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 if (_commandChars.Length > 0) { bool isPrivate = e.Message.Channel.IsPrivate; - bool hasCommandChar = CommandChars.Contains(msg[0]); + bool hasCommandChar = _commandChars.Contains(msg[0]); if (hasCommandChar) msg = msg.Substring(1); @@ -116,34 +124,27 @@ namespace Discord.Commands } else { + int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + //Parse arguments - CommandPart[] args; - if (!CommandParser.ParseArgs(msg, argPos, command, out args)) + string[] args; + var error = CommandParser.ParseArgs(msg, argPos, command, out args); + if (error != null) { - CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); - RaiseCommandError(CommandErrorType.InvalidInput, errorArgs); + var errorArgs = new CommandEventArgs(e.Message, command, userPermissions, null); + RaiseCommandError(error.Value, errorArgs); return; } - int argCount = args.Length; - - //Get information for the rest of the steps - int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args.Select(x => x.Value).ToArray()); + + var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args); // Check permissions - if (userPermissions < command.MinPerms) + if (userPermissions < command.MinPermissions) { RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); return; } - //Check arg count - if (argCount < command.MinArgs) - { - RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); - return; - } - // Run the command try { From 44ebf50d81d75774673ea3a15022914976905c58 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:54:33 -0300 Subject: [PATCH 11/47] Added Message.MentionedChannels --- src/Discord.Net/DiscordClient.Messages.cs | 17 ++++++++++++--- src/Discord.Net/Helpers/Mention.cs | 7 +++++++ src/Discord.Net/Models/Message.cs | 25 +++++++++++++++++++---- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs index 28b3c068c..5928f4132 100644 --- a/src/Discord.Net/DiscordClient.Messages.cs +++ b/src/Discord.Net/DiscordClient.Messages.cs @@ -125,6 +125,7 @@ namespace Discord var userIds = !channel.IsPrivate ? Mention.GetUserIds(text).Distinct() : new string[0]; if (Config.UseMessageQueue) { + var channelIds = !channel.IsPrivate ? Mention.GetChannelIds(text).Distinct() : new string[0]; var nonce = GenerateNonce(); msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, _userId); var currentUser = msg.User; @@ -136,9 +137,19 @@ 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; + + //IsPrivate check is already done earlier + msg.MentionedUsers = userIds + .Select(x => _users[x, channel.Server.Id]) + .Where(x => x != null) + .ToArray(); + msg.MentionedChannels = channelIds + .Select(x => _channels[x]) + .Where(x => x != null && x.Server == channel.Server) + .ToArray(); + _pendingMessages.Enqueue(msg); } else @@ -258,7 +269,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/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index 3c91a917c..02cdb13f7 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -49,5 +49,12 @@ namespace Discord .Select(x => x.Groups[1].Value) .Where(x => x != null); } + internal static IEnumerable GetChannelIds(string text) + { + return _channelRegex.Matches(text) + .OfType() + .Select(x => x.Groups[1].Value) + .Where(x => x != null); + } } } diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index e1a676880..5787b749b 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -125,8 +125,12 @@ 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 the server containing the channel this message was sent to. [JsonIgnore] public Server Server => _channel.Value.Server; @@ -208,13 +212,26 @@ 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(); + IsMentioningMe = model.Mentions + .Any(x => x.Id == _client.CurrentUserId); } if (model.Content != null) { RawText = model.Content; _cleanText = null; + + if (!Channel.IsPrivate) + { + MentionedChannels = Mention.GetChannelIds(model.Content) + .Select(x => _client.Channels[x]) + .Where(x => x.Server == Channel.Server) + .ToArray(); + } + else + MentionedChannels = new Channel[0]; } } From e6ee813b1fbc3cf1ce6c3987c0c418ff9b573897 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 12:55:36 -0300 Subject: [PATCH 12/47] Added basic service model, even more commands cleanup! --- .../Discord.Net.Commands.csproj | 16 +- src/Discord.Net.Commands/CommandBuilder.cs | 20 +- src/Discord.Net.Commands/CommandExtensions.cs | 8 + ...gin.Events.cs => CommandService.Events.cs} | 2 +- src/Discord.Net.Commands/CommandService.cs | 184 +++++++++++++++ .../CommandServiceConfig.cs | 49 ++++ src/Discord.Net.Commands/CommandsPlugin.cs | 211 ------------------ src/Discord.Net.Net45/Discord.Net.csproj | 3 + src/Discord.Net/DiscordClient.cs | 20 +- src/Discord.Net/IService.cs | 7 + 10 files changed, 291 insertions(+), 229 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandExtensions.cs rename src/Discord.Net.Commands/{CommandsPlugin.Events.cs => CommandService.Events.cs} (97%) create mode 100644 src/Discord.Net.Commands/CommandService.cs create mode 100644 src/Discord.Net.Commands/CommandServiceConfig.cs delete mode 100644 src/Discord.Net.Commands/CommandsPlugin.cs create mode 100644 src/Discord.Net/IService.cs diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj index 1de1757f1..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,17 +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/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index dd8035dc7..99a8ae97c 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -7,15 +7,15 @@ namespace Discord.Commands { public sealed class CommandBuilder { - private readonly CommandsPlugin _plugin; + private readonly CommandService _service; private readonly Command _command; private List _params; private bool _allowRequired, _isClosed; private string _prefix; - public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) + public CommandBuilder(CommandService service, Command command, string prefix) { - _plugin = plugin; + _service = service; _command = command; _params = new List(); _prefix = prefix; @@ -75,8 +75,8 @@ namespace Discord.Commands { _command.SetParameters(_params.ToArray()); foreach (var alias in _command.Aliases) - _plugin.Map.AddCommand(alias, _command); - _plugin.AddCommand(_command); + _service.Map.AddCommand(alias, _command); + _service.AddCommand(_command); } internal static string AppendPrefix(string prefix, string cmd) @@ -99,13 +99,13 @@ namespace Discord.Commands } public sealed class CommandGroupBuilder { - internal 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; } @@ -117,7 +117,7 @@ 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() @@ -126,7 +126,7 @@ namespace Discord.Commands { var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); command.MinPermissions = _defaultMinPermissions; - return new CommandBuilder(_plugin, command, _prefix); + 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/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandService.Events.cs similarity index 97% rename from src/Discord.Net.Commands/CommandsPlugin.Events.cs rename to src/Discord.Net.Commands/CommandService.Events.cs index da31fa6f7..92a3d429c 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandService.Events.cs @@ -36,7 +36,7 @@ namespace Discord.Commands } } - public partial class CommandsPlugin + public partial class CommandService { public event EventHandler RanCommand; private void RaiseRanCommand(CommandEventArgs args) 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.cs b/src/Discord.Net.Commands/CommandsPlugin.cs deleted file mode 100644 index e77210e5f..000000000 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ /dev/null @@ -1,211 +0,0 @@ -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 CommandsPlugin - { - private readonly DiscordClient _client; - private readonly Func _getPermissions; - - public IEnumerable Commands => _commands; - private readonly List _commands; - - internal CommandMap Map => _map; - private readonly CommandMap _map; - - public char? ComamndChar - { - get { return _commandChars.Length > 0 ? _commandChars[0] : (char?)null; } - set { _commandChars = value != null ? new char[] { value.Value } : new char[0]; } - } - public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } - private char[] _commandChars; - - public bool RequireCommandCharInPublic { get; set; } - public bool RequireCommandCharInPrivate { get; set; } - public bool HelpInPublic { get; set; } - - public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) - { - _client = client; - _getPermissions = getPermissions; - - _commands = new List(); - _map = new CommandMap(null); - - _commandChars = new char[] { '!' }; - RequireCommandCharInPublic = true; - RequireCommandCharInPrivate = true; - HelpInPublic = true; - - if (builtInHelp) - { - CreateCommand("help") - .Parameter("command", ParameterType.Optional) - .Hide() - .Info("Returns information about commands.") - .Do(async e => - { - if (e.Command.Text != "help") - await Reply(e, CommandDetails(e.Command)); - else - { - if (e.Args == null) - { - int permissions = getPermissions(e.User); - - StringBuilder output = new StringBuilder(); - output.AppendLine("These are the commands you can use:"); - output.Append("`"); - output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPermissions && !x.IsHidden))); - output.Append("`"); - - if (_commandChars.Length > 0) - { - if (_commandChars.Length == 1) - output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); - else - output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); - } - - output.AppendLine("`help ` can tell you more about how to use a command."); - - await Reply(e, output.ToString()); - } - else - { - var cmd = _map.GetCommand(e.Args[0]); - if (cmd != null) - await Reply(e, CommandDetails(cmd)); - else - await Reply(e, $"`{e.Args[0]}` is not a valid command."); - } - } - }); - - } - - 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 - if (_commandChars.Length > 0) - { - bool isPrivate = e.Message.Channel.IsPrivate; - bool hasCommandChar = _commandChars.Contains(msg[0]); - if (hasCommandChar) - msg = msg.Substring(1); - - if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar) - return; // If private, and command char is required, and it doesn't have it, ignore it. - if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar) - return; // Same, but public. - } - - //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 = _getPermissions != null ? _getPermissions(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); - } - } - }; - } - - private string CommandDetails(Command command) - { - 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 output.ToString(); - } - - internal async Task Reply(CommandEventArgs e, string message) - { - if (HelpInPublic) - await _client.SendMessage(e.Channel, message); - else - await _client.SendPrivateMessage(e.User, message); - } - - 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(null, command, ""); - } - - internal void AddCommand(Command command) - { - _commands.Add(command); - _map.AddCommand(command.Text, command); - } - } -} diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 0b3717e57..ae77302da 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -232,6 +232,9 @@ HttpException.cs + + IService.cs + Models\Channel.cs diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 48c03e372..f00048c20 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) 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); + } +} From 48000ec66e8f26e293c4e97aa35fe8df86c986d8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 12:56:03 -0300 Subject: [PATCH 13/47] Dont run .Cache on fake cache objects. --- src/Discord.Net/Models/Invite.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index e42c5352d..2c06e8673 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -51,7 +51,7 @@ namespace Discord if (server == null) { server = _generatedServer = new Server(client, x); - server.Cache(); + //server.Cache(); } return server; }); @@ -61,7 +61,7 @@ namespace Discord if (inviter == null) { inviter = _generatedInviter = new User(client, x, _server.Id); - inviter.Cache(); + //inviter.Cache(); } return inviter; }); @@ -71,7 +71,7 @@ namespace Discord if (channel == null) { channel = _generatedChannel = new Channel(client, x, _server.Id, null); - channel.Cache(); + //channel.Cache(); } return channel; }); From 1f07a73ec0c2d683d688edd52e9da6ee180da608 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 13:10:48 -0300 Subject: [PATCH 14/47] Improved equality checks for cache objects --- src/Discord.Net/Models/Channel.cs | 2 ++ src/Discord.Net/Models/Color.cs | 4 ++++ src/Discord.Net/Models/GlobalUser.cs | 2 ++ src/Discord.Net/Models/Invite.cs | 2 ++ src/Discord.Net/Models/Message.cs | 2 ++ src/Discord.Net/Models/Permissions.cs | 8 ++++++++ src/Discord.Net/Models/Role.cs | 2 ++ src/Discord.Net/Models/Server.cs | 2 ++ src/Discord.Net/Models/User.cs | 2 ++ 9 files changed, 26 insertions(+) diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 4ce0b1b51..2aa913988 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -207,6 +207,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 2c06e8673..a9f50af93 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -112,6 +112,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 5787b749b..5e301758b 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -235,6 +235,8 @@ namespace Discord } } + 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..9757864d3 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -370,6 +370,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 From de139337af2ba14a8c251c8db32fbe99ec19e30c Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 13:24:49 -0300 Subject: [PATCH 15/47] Removed references from Invite. --- src/Discord.Net/Models/Invite.cs | 127 ++++++++++++++++--------------- src/Discord.Net/Models/User.cs | 3 +- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index a9f50af93..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); diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 9757864d3..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; } From 503a2ee37d4ddd565049aadb221db9f7e1455e0e Mon Sep 17 00:00:00 2001 From: Brandon Smith Date: Sat, 31 Oct 2015 00:31:14 -0300 Subject: [PATCH 16/47] Improve message cache management --- src/Discord.Net/Models/Channel.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 2aa913988..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); } From 8d953a69e998a5ebf89c21a5a4f7d9bb553f09ca Mon Sep 17 00:00:00 2001 From: RogueException Date: Sat, 31 Oct 2015 00:36:24 -0300 Subject: [PATCH 17/47] Dont crash if a mention is sent in PM --- src/Discord.Net/Helpers/Mention.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index 02cdb13f7..a48aecc07 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -24,7 +24,7 @@ namespace Discord text = _userRegex.Replace(text, new MatchEvaluator(e => { 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) return '@' + user.Name; else //User not found @@ -34,7 +34,7 @@ namespace Discord { string id = e.Value.Substring(2, e.Value.Length - 3); var channel = client.Channels[id]; - if (channel != null && channel.Server.Id == server.Id) + if (channel != null && channel.Server?.Id == server?.Id) return '#' + channel.Name; else //Channel not found return '#' + e.Value; From c729eaae8b51dfdaaf140c959b05eb66a0e170be Mon Sep 17 00:00:00 2001 From: Googie2149 Date: Wed, 28 Oct 2015 23:11:15 -0400 Subject: [PATCH 18/47] Revamped CommandsPlugin This uses a dictionary for the commands list, if a command has a max args set it'll only get that amount, will call the UnkownCommand event, and now has a built in help command that can be optionally enabled. CommandChar is now a list, but a single character can still be used. Externally, not much should have changed, but commands can be hidden from the help command and a description can be set. There's probably more that I've forgotten about. --- Discord.Net.sln | 9 + src/Discord.Net.Commands/Command.cs | 6 +- src/Discord.Net.Commands/CommandBuilder.cs | 12 + .../CommandsPlugin.Events.cs | 7 +- src/Discord.Net.Commands/CommandsPlugin.cs | 321 ++++++++++++------ 5 files changed, 243 insertions(+), 112 deletions(-) diff --git a/Discord.Net.sln b/Discord.Net.sln index 1c32308ff..3aefab91d 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -30,6 +30,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Modules", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Modules", "src\Discord.Net.Modules.Net45\Discord.Net.Modules.csproj", "{3091164F-66AE-4543-A63D-167C1116241D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingProject", "TestingProject\TestingProject.csproj", "{6CD8116D-6749-4174-81EB-C4EB4B1F185B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -79,6 +81,12 @@ Global {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,5 +101,6 @@ Global {1B5603B4-6F8F-4289-B945-7BAAE523D740} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} {01584E8A-78DA-486F-9EF9-A894E435841B} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} {3091164F-66AE-4543-A63D-167C1116241D} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} + {6CD8116D-6749-4174-81EB-C4EB4B1F185B} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9} EndGlobalSection EndGlobal diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index f8728f4c6..2f4978566 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -9,13 +9,15 @@ namespace Discord.Commands public int? MinArgs { get; internal set; } public int? MaxArgs { get; internal set; } public int MinPerms { get; internal set; } - internal readonly string[] Parts; + public bool IsHidden { get; internal set; } + public string Description { get; internal set; } internal Func Handler; internal Command(string text) { Text = text; - Parts = text.ToLowerInvariant().Split(' '); + IsHidden = false; // Set false by default to avoid null error + Description = "No description set for this command."; } } } diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 0db5a388f..13e3ff89f 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -54,6 +54,18 @@ namespace Discord.Commands return this; } + public CommandBuilder Desc(string desc) + { + _command.Description = desc; + return this; + } + + public CommandBuilder IsHidden() + { + _command.IsHidden = true; + return this; + } + public CommandBuilder Do(Func func) { _command.Handler = func; diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 5901aa2a7..482855f4d 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -3,10 +3,12 @@ namespace Discord.Commands { public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } } + public class ArgumentException : Exception { public ArgumentException() : base("This command requires more arguments.") { } } public class CommandEventArgs { public Message Message { get; } public Command Command { get; } + public string MessageText { get; } public string CommandText { get; } public string ArgText { get; } public int? Permissions { get; } @@ -16,10 +18,11 @@ namespace Discord.Commands 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) + public CommandEventArgs(Message message, Command command, string messageText, string commandText, string argText, int? permissions, string[] args) { Message = message; Command = command; + MessageText = messageText; CommandText = commandText; ArgText = argText; Permissions = permissions; @@ -31,7 +34,7 @@ namespace Discord.Commands public Exception Exception { get; } public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) + : base(baseArgs.Message, baseArgs.Command, baseArgs.MessageText, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) { Exception = ex; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 44abce8f4..17199f3d5 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; namespace Discord.Commands { @@ -7,132 +10,234 @@ namespace Discord.Commands public partial class CommandsPlugin { private readonly DiscordClient _client; - private List _commands; private Func _getPermissions; - public IEnumerable Commands => _commands; + private Dictionary _commands; + + public Dictionary Commands => _commands; - public char CommandChar { get; set; } + public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure. + public List CommandChars { 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 + public bool HelpInPublic { get; set; } + + public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) + { + _client = client; // Wait why is this even set + _getPermissions = getPermissions; + + _commands = new Dictionary(); + + CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary. + CommandChars = new List { '!', '?', '/' }; + UseCommandChar = true; + RequireCommandCharInPublic = true; + RequireCommandCharInPrivate = true; + HelpInPublic = true; + + if (builtInHelp) + { + CreateCommand("help") + .ArgsBetween(0, 1) + .IsHidden() + .Desc("Returns information about commands.") + .Do(async e => + { + if (e.Command.Text != "help") + { + await Reply(e, CommandDetails(e.Command)); + } + else + { + if (e.Args == null) + { + StringBuilder output = new StringBuilder(); + bool first = true; + output.AppendLine("These are the commands you can use:"); + output.Append("`"); + int permissions = getPermissions(e.User); + foreach (KeyValuePair k in _commands) + { + if (permissions >= k.Value.MinPerms && !k.Value.IsHidden) + if (first) + { + output.Append(k.Key); + first = false; + } + else + output.Append($", {k.Key}"); + } + output.Append("`"); + + if (CommandChars.Count == 1) + output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command."); + else + output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command."); + + output.AppendLine("`help ` can tell you more about how to use a command."); + + await Reply(e, output.ToString()); + } + else + { + if (_commands.ContainsKey(e.Args[0])) + await Reply(e, CommandDetails(_commands[e.Args[0]])); + else + await Reply(e, $"`{e.Args[0]}` is not a valid command."); + } + } + }); + + } + + client.MessageReceived += async (s, e) => + { + // This will need to be changed once a built in help command is made + if (_commands.Count == 0) + return; + + if (e.Message.IsAuthor) + return; + + string msg = e.Message.Text; + + if (msg.Length == 0) + return; + + if (UseCommandChar) + { + bool isPrivate = e.Message.Channel.IsPrivate; + bool hasCommandChar = CommandChars.Contains(msg[0]); + if (hasCommandChar) + msg = msg.Substring(1); + + if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar) + return; // If private, and command char is required, and it doesn't have it, ignore it. + if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar) + return; // Same, but public. + } + + string cmd; + CommandPart[] args; + if (!CommandParser.Parse(msg, out cmd, out args)) + return; + + if (_commands.ContainsKey(cmd)) + { + Command comm = _commands[cmd]; + + //Get ArgText + int argCount = args.Length; + string argText; + if (argCount == 0) + argText = ""; + else + argText = msg.Substring(args[0].Index); + + //Clean Args + string[] newArgs = null; + + if (comm.MaxArgs != null && argCount > 0) + { + newArgs = new string[(int)comm.MaxArgs]; + for (int j = 0; j < newArgs.Length; j++) + newArgs[j] = args[j].Value; + } + + // Check permissions here + int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); + if (permissions < comm.MinPerms) + { + RaiseCommandError(eventArgs, new PermissionException()); + return; + } + + //Check Arg Count + if (argCount < comm.MinArgs) + { + RaiseCommandError(eventArgs, new ArgumentException()); + if (builtInHelp) + await _commands["help"].Handler(eventArgs); + return; + } + + // Actually run the command here RaiseRanCommand(eventArgs); - try - { - var task = cmd.Handler(eventArgs); - if (task != null) - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - RaiseCommandError(eventArgs, ex); - } - break; - } - }; - } + try + { + var task = comm.Handler(eventArgs); + if (task != null) + await task.ConfigureAwait(false); + } + catch (Exception ex) + { + RaiseCommandError(eventArgs, ex); + } + } + else + { + CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); + RaiseUnknownCommand(eventArgs); + if (builtInHelp) + await Reply(eventArgs, $"Command `cmd` does not exist."); + return; + } + }; + } + + internal string CommandDetails(Command comm) + { + StringBuilder output = new StringBuilder(); + + output.Append($"`{comm.Text}`"); + + if (comm.MinArgs != null && comm.MaxArgs != null) + { + if (comm.MinArgs == comm.MaxArgs) + { + if (comm.MaxArgs != 0) + output.Append($" {comm.MinArgs.ToString()} Args"); + } + else + output.Append($" {comm.MinArgs.ToString()} - {comm.MaxArgs.ToString()} Args"); + } + else if (comm.MinArgs != null && comm.MaxArgs == null) + { + output.Append($" ≥{comm.MinArgs.ToString()} Args"); + } + else if (comm.MinArgs == null && comm.MaxArgs != null) + { + output.Append($" ≤{comm.MaxArgs.ToString()} Args"); + } + + output.Append($": {comm.Description}"); + + return output.ToString(); + } + + internal async Task Reply(CommandEventArgs e, string message) + { + if (HelpInPublic) + await _client.SendMessage(e.Channel, message); + else + await _client.SendPrivateMessage(e.User, message); + } 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); + _commands.Add(cmd, command); return new CommandBuilder(command); } internal void AddCommand(Command command) { - _commands.Add(command); + _commands.Add(command.Text, command); } } } From 89eb8f168b48eb1c14f47404728c0bea37f56fa8 Mon Sep 17 00:00:00 2001 From: Googie2149 Date: Thu, 29 Oct 2015 01:26:12 -0400 Subject: [PATCH 19/47] Fixed commands with .AnyArgs() set --- src/Discord.Net.Commands/CommandsPlugin.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 17199f3d5..4b78b4c34 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -145,9 +145,15 @@ namespace Discord.Commands for (int j = 0; j < newArgs.Length; j++) newArgs[j] = args[j].Value; } + else if (comm.MaxArgs == null && comm.MinArgs == null) + { + newArgs = new string[argCount]; + for (int j = 0; j < newArgs.Length; j++) + newArgs[j] = args[j].Value; + } - // Check permissions here - int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + // Check permissions here + int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); if (permissions < comm.MinPerms) { @@ -182,7 +188,7 @@ namespace Discord.Commands CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); RaiseUnknownCommand(eventArgs); if (builtInHelp) - await Reply(eventArgs, $"Command `cmd` does not exist."); + await Reply(eventArgs, $"The command `{cmd}` does not exist."); return; } }; From ecc6ee648eb17a11ee2318b646833e4d201e1d0c Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 20:29:51 -0300 Subject: [PATCH 20/47] Cleaned up a few parts, combined all errors into one event. --- .../CommandsPlugin.Events.cs | 35 +++--- src/Discord.Net.Commands/CommandsPlugin.cs | 105 +++++++----------- 2 files changed, 55 insertions(+), 85 deletions(-) diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 482855f4d..0b3f4ee14 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -2,43 +2,40 @@ namespace Discord.Commands { - public class PermissionException : Exception { public PermissionException() : base("User does not have permission to run this command.") { } } - public class ArgumentException : Exception { public ArgumentException() : base("This command requires more arguments.") { } } public class CommandEventArgs { public Message Message { get; } public Command Command { get; } - public string MessageText { get; } - public string CommandText { get; } - public string ArgText { get; } - public int? Permissions { 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, string messageText, string commandText, string argText, int? permissions, string[] args) + public CommandEventArgs(Message message, Command command, int? userPermissions, string[] args) { Message = message; Command = command; - MessageText = messageText; - CommandText = commandText; - ArgText = argText; - Permissions = permissions; + UserPermissions = userPermissions; Args = args; } } + + public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount } public class CommandErrorEventArgs : CommandEventArgs { + public CommandErrorType ErrorType { get; } public Exception Exception { get; } - public CommandErrorEventArgs(CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.MessageText, baseArgs.CommandText, baseArgs.ArgText, baseArgs.Permissions, baseArgs.Args) + public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex) + : base(baseArgs.Message, baseArgs.Command, baseArgs.UserPermissions, baseArgs.Args) { Exception = ex; - } + ErrorType = errorType; + } } + public partial class CommandsPlugin { public event EventHandler RanCommand; @@ -47,17 +44,11 @@ namespace Discord.Commands 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) + private void RaiseCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) { if (CommandError != null) - CommandError(this, new CommandErrorEventArgs(args, ex)); + CommandError(this, new CommandErrorEventArgs(errorType, args, ex)); } } } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 4b78b4c34..7f7149b95 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -46,9 +46,7 @@ namespace Discord.Commands .Do(async e => { if (e.Command.Text != "help") - { await Reply(e, CommandDetails(e.Command)); - } else { if (e.Args == null) @@ -95,16 +93,11 @@ namespace Discord.Commands client.MessageReceived += async (s, e) => { // This will need to be changed once a built in help command is made - if (_commands.Count == 0) - return; - - if (e.Message.IsAuthor) - return; + if (_commands.Count == 0) return; + if (e.Message.IsAuthor) return; string msg = e.Message.Text; - - if (msg.Length == 0) - return; + if (msg.Length == 0) return; if (UseCommandChar) { @@ -127,17 +120,10 @@ namespace Discord.Commands if (_commands.ContainsKey(cmd)) { Command comm = _commands[cmd]; - - //Get ArgText - int argCount = args.Length; - string argText; - if (argCount == 0) - argText = ""; - else - argText = msg.Substring(args[0].Index); - - //Clean Args - string[] newArgs = null; + + //Clean args + int argCount = args.Length; + string[] newArgs = null; if (comm.MaxArgs != null && argCount > 0) { @@ -152,74 +138,67 @@ namespace Discord.Commands newArgs[j] = args[j].Value; } - // Check permissions here - int permissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, comm, msg, cmd, argText, permissions, newArgs); - if (permissions < comm.MinPerms) - { - RaiseCommandError(eventArgs, new PermissionException()); - return; - } - - //Check Arg Count - if (argCount < comm.MinArgs) - { - RaiseCommandError(eventArgs, new ArgumentException()); - if (builtInHelp) - await _commands["help"].Handler(eventArgs); - return; - } + int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + var eventArgs = new CommandEventArgs(e.Message, comm, userPermissions, newArgs); - // Actually run the command here - RaiseRanCommand(eventArgs); - try - { + // Check permissions + if (userPermissions < comm.MinPerms) + { + RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); + return; + } + + //Check arg count + if (argCount < comm.MinArgs) + { + RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); + return; + } + + // Run the command + try + { + RaiseRanCommand(eventArgs); var task = comm.Handler(eventArgs); if (task != null) await task.ConfigureAwait(false); } catch (Exception ex) { - RaiseCommandError(eventArgs, ex); + RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); } } else { - CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, msg, cmd, null, null, null); - RaiseUnknownCommand(eventArgs); - if (builtInHelp) - await Reply(eventArgs, $"The command `{cmd}` does not exist."); + CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, null, null); + RaiseCommandError(CommandErrorType.UnknownCommand, eventArgs); return; } }; } - internal string CommandDetails(Command comm) + private string CommandDetails(Command command) { StringBuilder output = new StringBuilder(); - output.Append($"`{comm.Text}`"); + output.Append($"`{command.Text}`"); - if (comm.MinArgs != null && comm.MaxArgs != null) + if (command.MinArgs != null && command.MaxArgs != null) { - if (comm.MinArgs == comm.MaxArgs) + if (command.MinArgs == command.MaxArgs) { - if (comm.MaxArgs != 0) - output.Append($" {comm.MinArgs.ToString()} Args"); + if (command.MaxArgs != 0) + output.Append($" {command.MinArgs.ToString()} Args"); } else - output.Append($" {comm.MinArgs.ToString()} - {comm.MaxArgs.ToString()} Args"); - } - else if (comm.MinArgs != null && comm.MaxArgs == null) - { - output.Append($" ≥{comm.MinArgs.ToString()} Args"); - } - else if (comm.MinArgs == null && comm.MaxArgs != null) - { - output.Append($" ≤{comm.MaxArgs.ToString()} Args"); + 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($": {comm.Description}"); + output.Append($": {command.Description}"); return output.ToString(); } From c5a89655f77abafa7e8a786065511d3deaa8b7d2 Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 23:33:05 -0300 Subject: [PATCH 21/47] Cleaned up CommandsPlugin, added CommandMap, new parameter declaration and aliases. --- .../Discord.Net.Commands.csproj | 3 + src/Discord.Net.Commands/Command.cs | 89 ++++++++++- src/Discord.Net.Commands/CommandBuilder.cs | 130 ++++++++-------- src/Discord.Net.Commands/CommandMap.cs | 84 ++++++++++ src/Discord.Net.Commands/CommandParser.cs | 80 ++++++---- .../CommandsPlugin.Events.cs | 2 +- src/Discord.Net.Commands/CommandsPlugin.cs | 145 ++++++++---------- 7 files changed, 348 insertions(+), 185 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandMap.cs diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj index 69f042ac3..1de1757f1 100644 --- a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj +++ b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj @@ -43,6 +43,9 @@ CommandBuilder.cs + + CommandMap.cs + CommandParser.cs diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 2f4978566..50f958ba2 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -1,23 +1,102 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; namespace Discord.Commands { + public sealed class CommandParameter + { + public string Name { get; } + public bool IsOptional { get; } + public bool IsCatchAll { get; } + + public CommandParameter(string name, bool isOptional, bool isCatchAll) + { + Name = name; + IsOptional = isOptional; + IsCatchAll = isCatchAll; + } + } + public sealed class Command { public string Text { get; } - public int? MinArgs { get; internal set; } - public int? MaxArgs { get; internal set; } + public int? MinArgs { get; private set; } + public int? MaxArgs { get; private set; } public int MinPerms { get; internal set; } public bool IsHidden { get; internal set; } public string Description { get; internal set; } - internal Func Handler; + + public IEnumerable Aliases => _aliases; + private string[] _aliases; + + public IEnumerable Parameters => _parameters; + private CommandParameter[] _parameters; + + private Func _handler; internal Command(string text) { Text = text; - IsHidden = false; // Set false by default to avoid null error - Description = "No description set for this command."; + 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].IsCatchAll) + MaxArgs = null; + else + MaxArgs = parameters.Length; + + int? optionalStart = null; + for (int i = parameters.Length - 1; i >= 0; i--) + { + if (parameters[i].IsOptional) + 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 13e3ff89f..981bc23df 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -1,50 +1,55 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Discord.Commands { public sealed class CommandBuilder { + private readonly CommandsPlugin _plugin; private readonly Command _command; - public CommandBuilder(Command command) + private List _params; + private bool _hasOptional, _hasCatchAll; + private string _prefix; + + public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) { + _plugin = plugin; _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; + } + + 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, bool isOptional = false, bool isCatchAll = false) { - _command.MinArgs = 0; - _command.MaxArgs = 0; + if (_hasCatchAll) + throw new Exception("No parameters may be added after the catch-all"); + if (_hasOptional && isOptional) + throw new Exception("Non-optional parameters may not be added after an optional one"); + + _params.Add(new CommandParameter(name, isOptional, isCatchAll)); + + if (isOptional) + _hasOptional = true; + if (isCatchAll) + _hasCatchAll = true; return this; } - public CommandBuilder AnyArgs() + public CommandBuilder IsHidden() { - _command.MinArgs = null; - _command.MaxArgs = null; + _command.IsHidden = true; return this; } @@ -54,32 +59,45 @@ namespace Discord.Commands return this; } - public CommandBuilder Desc(string desc) - { - _command.Description = desc; - return this; - } - - public CommandBuilder IsHidden() - { - _command.IsHidden = true; - 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) + _plugin.Map.AddCommand(alias, _command); + _plugin.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 CommandsPlugin _plugin; private readonly string _prefix; private int _defaultMinPermissions; @@ -104,25 +122,9 @@ namespace Discord.Commands => 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); + var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); command.MinPerms = _defaultMinPermissions; - _plugin.AddCommand(command); - return new CommandBuilder(command); + return new CommandBuilder(_plugin, command, _prefix); } } } diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs new file mode 100644 index 000000000..504479400 --- /dev/null +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Commands +{ + internal class CommandMap + { + private CommandMap _parent; + private Command _command; + private readonly Dictionary _subCommands; + + public CommandMap(CommandMap parent) + { + _parent = parent; + _subCommands = new Dictionary(); + } + + public CommandMap GetMap(string text) + { + CommandMap map; + if (_subCommands.TryGetValue(text, out map)) + return map; + else + return null; + } + + 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..08241eb1b 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -15,37 +15,69 @@ namespace Discord.Commands } //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) + public static bool ParseCommand(string input, CommandMap map, out Command command, out int endPos) { - 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); + int startPosition = 0; + int endPosition = 0; + int inputLength = input.Length; + bool isEscaped = false; + command = null; + endPos = 0; + + if (input == "") + return false; + + while (endPosition < inputLength) + { + char currentChar = input[endPosition++]; + if (isEscaped) + isEscaped = false; + else if (currentChar == '\\') + isEscaped = true; + + 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) + { + 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; } - private static bool Parse(string input, out string command, out CommandPart[] args, bool parseCommand) + public static bool ParseArgs(string input, int startPos, Command command, out CommandPart[] args) { - CommandParserPart currentPart = parseCommand ? CommandParserPart.CommandName : CommandParserPart.None; - int startPosition = 0; - int endPosition = 0; + CommandParserPart currentPart = CommandParserPart.None; + int startPosition = startPos; + int endPosition = startPos; int inputLength = input.Length; bool isEscaped = false; List argList = new List(); - - command = null; + args = null; if (input == "") @@ -61,21 +93,6 @@ namespace Discord.Commands switch (currentPart) { - 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 - { - currentPart = CommandParserPart.None; - command = temp; - startPosition = endPosition; - } - } - break; case CommandParserPart.None: if ((!isEscaped && currentChar == '\"')) { @@ -126,9 +143,6 @@ namespace Discord.Commands } } - if (parseCommand && (command == null || command == "")) - return false; - args = argList.ToArray(); return true; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandsPlugin.Events.cs index 0b3f4ee14..da31fa6f7 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.Events.cs @@ -22,7 +22,7 @@ namespace Discord.Commands } } - public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount } + public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput } public class CommandErrorEventArgs : CommandEventArgs { public CommandErrorType ErrorType { get; } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 7f7149b95..f15664348 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -10,29 +10,30 @@ namespace Discord.Commands public partial class CommandsPlugin { private readonly DiscordClient _client; - private Func _getPermissions; - - private Dictionary _commands; + private readonly Func _getPermissions; - public Dictionary Commands => _commands; + public IEnumerable Commands => _commands; + private readonly List _commands; + + internal CommandMap Map => _map; + private readonly CommandMap _map; - public char CommandChar { get { return CommandChars[0]; } set { CommandChars = new List { value }; } } // This could possibly be removed entirely. Not sure. - public List CommandChars { get; set; } - public bool UseCommandChar { get; set; } + public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } + private char[] _commandChars; + public bool RequireCommandCharInPublic { get; set; } public bool RequireCommandCharInPrivate { get; set; } public bool HelpInPublic { get; set; } public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) { - _client = client; // Wait why is this even set + _client = client; _getPermissions = getPermissions; - _commands = new Dictionary(); + _commands = new List(); + _map = new CommandMap(null); - CommandChar = '!'; // Kept around to keep from possibly throwing an error. Might not be necessary. - CommandChars = new List { '!', '?', '/' }; - UseCommandChar = true; + _commandChars = new char[] { '!' }; RequireCommandCharInPublic = true; RequireCommandCharInPrivate = true; HelpInPublic = true; @@ -40,9 +41,9 @@ namespace Discord.Commands if (builtInHelp) { CreateCommand("help") - .ArgsBetween(0, 1) + .Parameter("command", isOptional: true) .IsHidden() - .Desc("Returns information about commands.") + .Info("Returns information about commands.") .Do(async e => { if (e.Command.Text != "help") @@ -50,29 +51,18 @@ namespace Discord.Commands else { if (e.Args == null) - { - StringBuilder output = new StringBuilder(); - bool first = true; + { + int permissions = getPermissions(e.User); + StringBuilder output = new StringBuilder(); output.AppendLine("These are the commands you can use:"); output.Append("`"); - int permissions = getPermissions(e.User); - foreach (KeyValuePair k in _commands) - { - if (permissions >= k.Value.MinPerms && !k.Value.IsHidden) - if (first) - { - output.Append(k.Key); - first = false; - } - else - output.Append($", {k.Key}"); - } + output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPerms && !x.IsHidden))); output.Append("`"); - if (CommandChars.Count == 1) - output.AppendLine($"{Environment.NewLine}You can use `{CommandChars[0]}` to call a command."); + if (_commandChars.Length == 1) + output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); else - output.AppendLine($"{Environment.NewLine}You can use `{String.Join(" ", CommandChars.Take(CommandChars.Count - 1))}` and `{CommandChars.Last()}` to call a command."); + output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); output.AppendLine("`help ` can tell you more about how to use a command."); @@ -80,8 +70,9 @@ namespace Discord.Commands } else { - if (_commands.ContainsKey(e.Args[0])) - await Reply(e, CommandDetails(_commands[e.Args[0]])); + var cmd = _map.GetCommand(e.Args[0]); + if (cmd != null) + await Reply(e, CommandDetails(cmd)); else await Reply(e, $"`{e.Args[0]}` is not a valid command."); } @@ -99,7 +90,7 @@ namespace Discord.Commands string msg = e.Message.Text; if (msg.Length == 0) return; - if (UseCommandChar) + if (_commandChars.Length > 0) { bool isPrivate = e.Message.Channel.IsPrivate; bool hasCommandChar = CommandChars.Contains(msg[0]); @@ -112,44 +103,41 @@ namespace Discord.Commands return; // Same, but public. } - string cmd; - CommandPart[] args; - if (!CommandParser.Parse(msg, out cmd, out args)) - return; - - if (_commands.ContainsKey(cmd)) - { - Command comm = _commands[cmd]; - - //Clean args + //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 + { + //Parse arguments + CommandPart[] args; + if (!CommandParser.ParseArgs(msg, argPos, command, out args)) + { + CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); + RaiseCommandError(CommandErrorType.InvalidInput, errorArgs); + return; + } int argCount = args.Length; - string[] newArgs = null; - - if (comm.MaxArgs != null && argCount > 0) - { - newArgs = new string[(int)comm.MaxArgs]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j].Value; - } - else if (comm.MaxArgs == null && comm.MinArgs == null) - { - newArgs = new string[argCount]; - for (int j = 0; j < newArgs.Length; j++) - newArgs[j] = args[j].Value; - } + //Get information for the rest of the steps int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, comm, userPermissions, newArgs); + var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args.Select(x => x.Value).ToArray()); // Check permissions - if (userPermissions < comm.MinPerms) + if (userPermissions < command.MinPerms) { RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); return; } - + //Check arg count - if (argCount < comm.MinArgs) + if (argCount < command.MinArgs) { RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); return; @@ -159,21 +147,13 @@ namespace Discord.Commands try { RaiseRanCommand(eventArgs); - var task = comm.Handler(eventArgs); - if (task != null) - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); - } - } - else - { - CommandEventArgs eventArgs = new CommandEventArgs(e.Message, null, null, null); - RaiseCommandError(CommandErrorType.UnknownCommand, eventArgs); - return; - } + await command.Run(eventArgs).ConfigureAwait(false); + } + catch (Exception ex) + { + RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); + } + } }; } @@ -198,7 +178,7 @@ namespace Discord.Commands else if (command.MinArgs == null && command.MaxArgs != null) output.Append($" ≤{command.MaxArgs.ToString()} Args"); - output.Append($": {command.Description}"); + output.Append($": {command.Description ?? "No description set for this command."}"); return output.ToString(); } @@ -216,13 +196,14 @@ namespace Discord.Commands public CommandBuilder CreateCommand(string cmd) { var command = new Command(cmd); - _commands.Add(cmd, command); - return new CommandBuilder(command); + _commands.Add(command); + return new CommandBuilder(null, command, ""); } internal void AddCommand(Command command) { - _commands.Add(command.Text, command); + _commands.Add(command); + _map.AddCommand(command.Text, command); } } } From 5c2c36722865579559fd10ad9ad695879bd850cf Mon Sep 17 00:00:00 2001 From: RogueException Date: Thu, 29 Oct 2015 23:35:16 -0300 Subject: [PATCH 22/47] Fixed crash for having multiple optional parameters --- src/Discord.Net.Commands/CommandBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 981bc23df..a97ff8d73 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -36,7 +36,7 @@ namespace Discord.Commands { if (_hasCatchAll) throw new Exception("No parameters may be added after the catch-all"); - if (_hasOptional && isOptional) + if (_hasOptional && !isOptional) throw new Exception("Non-optional parameters may not be added after an optional one"); _params.Add(new CommandParameter(name, isOptional, isCatchAll)); From 91cfab1b5dc83b6b79ec1d8dd5fa20b4fe557672 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 09:45:42 -0300 Subject: [PATCH 23/47] FindUsers and FindChannels support mentions, FindUsers has a channel overload, and less bugs. --- src/Discord.Net/DiscordClient.Channels.cs | 32 ++++++++++-------- src/Discord.Net/DiscordClient.Users.cs | 41 ++++++++++++++++++----- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs index 52ce846ec..0c89a7a13 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] == '#') //If we somehow get text starting with # but isn't a mention + { + string name2 = name.Substring(1); + query = query.Concat(server.Channels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + } } if (type != (string)null) - result = result.Where(x => x.Type == type); - - return result; + query = query.Where(x => x.Type == type); + return query; } /// Creates a new channel with the provided name and type. diff --git a/src/Discord.Net/DiscordClient.Users.cs b/src/Discord.Net/DiscordClient.Users.cs index e63fd04c1..ed4e048e7 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; From db556c358d66a626bc5f905f2336da2679a64055 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 09:55:08 -0300 Subject: [PATCH 24/47] Added ParameterType --- src/Discord.Net.Commands/Command.cs | 23 ++++++++++++++------- src/Discord.Net.Commands/CommandBuilder.cs | 24 ++++++++++++---------- src/Discord.Net.Commands/CommandsPlugin.cs | 2 +- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 50f958ba2..bc1013245 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -4,17 +4,26 @@ 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 bool IsOptional { get; } - public bool IsCatchAll { get; } + public ParameterType Type { get; } - public CommandParameter(string name, bool isOptional, bool isCatchAll) + public CommandParameter(string name, ParameterType type) { Name = name; - IsOptional = isOptional; - IsCatchAll = isCatchAll; + Type = type; } } @@ -60,7 +69,7 @@ namespace Discord.Commands } else { - if (parameters[parameters.Length - 1].IsCatchAll) + if (parameters[parameters.Length - 1].Type == ParameterType.Multiple) MaxArgs = null; else MaxArgs = parameters.Length; @@ -68,7 +77,7 @@ namespace Discord.Commands int? optionalStart = null; for (int i = parameters.Length - 1; i >= 0; i--) { - if (parameters[i].IsOptional) + if (parameters[i].Type == ParameterType.Optional) optionalStart = i; else break; diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index a97ff8d73..e0b4a5d54 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -10,7 +10,7 @@ namespace Discord.Commands private readonly CommandsPlugin _plugin; private readonly Command _command; private List _params; - private bool _hasOptional, _hasCatchAll; + private bool _allowRequired, _isClosed; private string _prefix; public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) @@ -19,6 +19,8 @@ namespace Discord.Commands _command = command; _params = new List(); _prefix = prefix; + _allowRequired = true; + _isClosed = false; } public CommandBuilder Alias(params string[] aliases) @@ -32,19 +34,19 @@ namespace Discord.Commands _command.Description = description; return this; } - public CommandBuilder Parameter(string name, bool isOptional = false, bool isCatchAll = false) + public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required) { - if (_hasCatchAll) - throw new Exception("No parameters may be added after the catch-all"); - if (_hasOptional && !isOptional) - throw new Exception("Non-optional parameters may not be added after an optional one"); + 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, isOptional, isCatchAll)); + _params.Add(new CommandParameter(name, type)); - if (isOptional) - _hasOptional = true; - if (isCatchAll) - _hasCatchAll = true; + if (type == ParameterType.Optional) + _allowRequired = false; + if (type == ParameterType.Multiple || type == ParameterType.Unparsed) + _isClosed = true; return this; } public CommandBuilder IsHidden() diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index f15664348..f34bbbfe1 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -41,7 +41,7 @@ namespace Discord.Commands if (builtInHelp) { CreateCommand("help") - .Parameter("command", isOptional: true) + .Parameter("command", ParameterType.Optional) .IsHidden() .Info("Returns information about commands.") .Do(async e => From ee227652157734ed2f8cf25f20e50bc95d47be62 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:01:05 -0300 Subject: [PATCH 25/47] Renamed IsHidden -> Hide --- src/Discord.Net.Commands/CommandBuilder.cs | 2 +- src/Discord.Net.Commands/CommandsPlugin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index e0b4a5d54..34a77f1c8 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -49,7 +49,7 @@ namespace Discord.Commands _isClosed = true; return this; } - public CommandBuilder IsHidden() + public CommandBuilder Hide() { _command.IsHidden = true; return this; diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index f34bbbfe1..3afee42fa 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -42,7 +42,7 @@ namespace Discord.Commands { CreateCommand("help") .Parameter("command", ParameterType.Optional) - .IsHidden() + .Hide() .Info("Returns information about commands.") .Do(async e => { From faae530ae138f703d3e5cb0de9dd3a8930278fad Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:04:20 -0300 Subject: [PATCH 26/47] Brought back CommandPlugin.CommandChar --- src/Discord.Net.Commands/CommandsPlugin.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 3afee42fa..1f5e5d450 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -18,7 +18,8 @@ namespace Discord.Commands internal CommandMap Map => _map; private readonly CommandMap _map; - public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } + public char ComamndChar { get { return _commandChars[0]; } set { _commandChars = new char[] { value }; } } + public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } private char[] _commandChars; public bool RequireCommandCharInPublic { get; set; } From c6a81a14b625c1207cc5da74c4a04bfab639b22c Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:35:53 -0300 Subject: [PATCH 27/47] Added support for unparsed parameters, more cleanup --- src/Discord.Net.Commands/Command.cs | 4 +- src/Discord.Net.Commands/CommandBuilder.cs | 6 +-- src/Discord.Net.Commands/CommandParser.cs | 53 ++++++++++++---------- src/Discord.Net.Commands/CommandsPlugin.cs | 51 +++++++++++---------- 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index bc1013245..15ad98b95 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -32,7 +32,7 @@ namespace Discord.Commands public string Text { get; } public int? MinArgs { get; private set; } public int? MaxArgs { get; private set; } - public int MinPerms { get; internal set; } + public int MinPermissions { get; internal set; } public bool IsHidden { get; internal set; } public string Description { get; internal set; } @@ -40,7 +40,7 @@ namespace Discord.Commands private string[] _aliases; public IEnumerable Parameters => _parameters; - private CommandParameter[] _parameters; + internal CommandParameter[] _parameters; private Func _handler; diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 34a77f1c8..dd8035dc7 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -38,7 +38,7 @@ namespace Discord.Commands { 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) + 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)); @@ -57,7 +57,7 @@ namespace Discord.Commands public CommandBuilder MinPermissions(int level) { - _command.MinPerms = level; + _command.MinPermissions = level; return this; } @@ -125,7 +125,7 @@ namespace Discord.Commands public CommandBuilder CreateCommand(string cmd) { var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); - command.MinPerms = _defaultMinPermissions; + command.MinPermissions = _defaultMinPermissions; return new CommandBuilder(_plugin, command, _prefix); } } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 08241eb1b..14af9f550 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -2,19 +2,6 @@ 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 internal static class CommandParser { private enum CommandParserPart @@ -69,23 +56,40 @@ namespace Discord.Commands return command != null; } - public static bool ParseArgs(string input, int startPos, Command command, out CommandPart[] args) + //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; - List argList = new List(); + + var expectedArgs = command._parameters; + List argList = new List(); + CommandParameter parameter = null; args = null; if (input == "") - return false; + return CommandErrorType.InvalidInput; while (endPosition < inputLength) { - char currentChar = input[endPosition++]; + 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 == '\\') @@ -113,7 +117,7 @@ namespace Discord.Commands else { currentPart = CommandParserPart.None; - argList.Add(new CommandPart(temp, startPosition)); + argList.Add(temp); startPosition = endPosition; } } @@ -123,28 +127,31 @@ 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 (argList.Count < command.MinArgs) + return CommandErrorType.BadArgCount; + args = argList.ToArray(); - return true; + return null; } } } diff --git a/src/Discord.Net.Commands/CommandsPlugin.cs b/src/Discord.Net.Commands/CommandsPlugin.cs index 1f5e5d450..e77210e5f 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ b/src/Discord.Net.Commands/CommandsPlugin.cs @@ -18,7 +18,11 @@ namespace Discord.Commands internal CommandMap Map => _map; private readonly CommandMap _map; - public char ComamndChar { get { return _commandChars[0]; } set { _commandChars = new char[] { value }; } } + public char? ComamndChar + { + get { return _commandChars.Length > 0 ? _commandChars[0] : (char?)null; } + set { _commandChars = value != null ? new char[] { value.Value } : new char[0]; } + } public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } private char[] _commandChars; @@ -54,16 +58,20 @@ namespace Discord.Commands if (e.Args == null) { int permissions = getPermissions(e.User); + StringBuilder output = new StringBuilder(); output.AppendLine("These are the commands you can use:"); output.Append("`"); - output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPerms && !x.IsHidden))); + output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPermissions && !x.IsHidden))); output.Append("`"); - if (_commandChars.Length == 1) - output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); - else - output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); + if (_commandChars.Length > 0) + { + if (_commandChars.Length == 1) + output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); + else + output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); + } output.AppendLine("`help ` can tell you more about how to use a command."); @@ -84,17 +92,17 @@ namespace Discord.Commands client.MessageReceived += async (s, e) => { - // This will need to be changed once a built in help command is made 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 if (_commandChars.Length > 0) { bool isPrivate = e.Message.Channel.IsPrivate; - bool hasCommandChar = CommandChars.Contains(msg[0]); + bool hasCommandChar = _commandChars.Contains(msg[0]); if (hasCommandChar) msg = msg.Substring(1); @@ -116,34 +124,27 @@ namespace Discord.Commands } else { + int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; + //Parse arguments - CommandPart[] args; - if (!CommandParser.ParseArgs(msg, argPos, command, out args)) + string[] args; + var error = CommandParser.ParseArgs(msg, argPos, command, out args); + if (error != null) { - CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); - RaiseCommandError(CommandErrorType.InvalidInput, errorArgs); + var errorArgs = new CommandEventArgs(e.Message, command, userPermissions, null); + RaiseCommandError(error.Value, errorArgs); return; } - int argCount = args.Length; - - //Get information for the rest of the steps - int userPermissions = _getPermissions != null ? _getPermissions(e.Message.User) : 0; - var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args.Select(x => x.Value).ToArray()); + + var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args); // Check permissions - if (userPermissions < command.MinPerms) + if (userPermissions < command.MinPermissions) { RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); return; } - //Check arg count - if (argCount < command.MinArgs) - { - RaiseCommandError(CommandErrorType.BadArgCount, eventArgs); - return; - } - // Run the command try { From e2bf2bcc891b1f9d78af224fa0cc8c90f96c086f Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 10:54:33 -0300 Subject: [PATCH 28/47] Added Message.MentionedChannels --- src/Discord.Net/DiscordClient.Messages.cs | 17 ++++++++++++--- src/Discord.Net/Helpers/Mention.cs | 7 +++++++ src/Discord.Net/Models/Message.cs | 25 +++++++++++++++++++---- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs index 28b3c068c..5928f4132 100644 --- a/src/Discord.Net/DiscordClient.Messages.cs +++ b/src/Discord.Net/DiscordClient.Messages.cs @@ -125,6 +125,7 @@ namespace Discord var userIds = !channel.IsPrivate ? Mention.GetUserIds(text).Distinct() : new string[0]; if (Config.UseMessageQueue) { + var channelIds = !channel.IsPrivate ? Mention.GetChannelIds(text).Distinct() : new string[0]; var nonce = GenerateNonce(); msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, _userId); var currentUser = msg.User; @@ -136,9 +137,19 @@ 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; + + //IsPrivate check is already done earlier + msg.MentionedUsers = userIds + .Select(x => _users[x, channel.Server.Id]) + .Where(x => x != null) + .ToArray(); + msg.MentionedChannels = channelIds + .Select(x => _channels[x]) + .Where(x => x != null && x.Server == channel.Server) + .ToArray(); + _pendingMessages.Enqueue(msg); } else @@ -258,7 +269,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/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index d718df155..f9f44646b 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -52,5 +52,12 @@ namespace Discord .Select(x => x.Groups[1].Value) .Where(x => x != null); } + internal static IEnumerable GetChannelIds(string text) + { + return _channelRegex.Matches(text) + .OfType() + .Select(x => x.Groups[1].Value) + .Where(x => x != null); + } } } diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index e1a676880..5787b749b 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -125,8 +125,12 @@ 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 the server containing the channel this message was sent to. [JsonIgnore] public Server Server => _channel.Value.Server; @@ -208,13 +212,26 @@ 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(); + IsMentioningMe = model.Mentions + .Any(x => x.Id == _client.CurrentUserId); } if (model.Content != null) { RawText = model.Content; _cleanText = null; + + if (!Channel.IsPrivate) + { + MentionedChannels = Mention.GetChannelIds(model.Content) + .Select(x => _client.Channels[x]) + .Where(x => x.Server == Channel.Server) + .ToArray(); + } + else + MentionedChannels = new Channel[0]; } } From 92ceacd01eaf9a5870f4b9f29ea446fe1e9bcfd8 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 12:55:36 -0300 Subject: [PATCH 29/47] Added basic service model, even more commands cleanup! --- .../Discord.Net.Commands.csproj | 16 +- src/Discord.Net.Commands/CommandBuilder.cs | 20 +- src/Discord.Net.Commands/CommandExtensions.cs | 8 + ...gin.Events.cs => CommandService.Events.cs} | 2 +- src/Discord.Net.Commands/CommandService.cs | 184 +++++++++++++++ .../CommandServiceConfig.cs | 49 ++++ src/Discord.Net.Commands/CommandsPlugin.cs | 211 ------------------ src/Discord.Net.Net45/Discord.Net.csproj | 3 + src/Discord.Net/DiscordClient.cs | 20 +- src/Discord.Net/IService.cs | 7 + 10 files changed, 291 insertions(+), 229 deletions(-) create mode 100644 src/Discord.Net.Commands/CommandExtensions.cs rename src/Discord.Net.Commands/{CommandsPlugin.Events.cs => CommandService.Events.cs} (97%) create mode 100644 src/Discord.Net.Commands/CommandService.cs create mode 100644 src/Discord.Net.Commands/CommandServiceConfig.cs delete mode 100644 src/Discord.Net.Commands/CommandsPlugin.cs create mode 100644 src/Discord.Net/IService.cs diff --git a/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj b/src/Discord.Net.Commands.Net45/Discord.Net.Commands.csproj index 1de1757f1..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,17 +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/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index dd8035dc7..99a8ae97c 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -7,15 +7,15 @@ namespace Discord.Commands { public sealed class CommandBuilder { - private readonly CommandsPlugin _plugin; + private readonly CommandService _service; private readonly Command _command; private List _params; private bool _allowRequired, _isClosed; private string _prefix; - public CommandBuilder(CommandsPlugin plugin, Command command, string prefix) + public CommandBuilder(CommandService service, Command command, string prefix) { - _plugin = plugin; + _service = service; _command = command; _params = new List(); _prefix = prefix; @@ -75,8 +75,8 @@ namespace Discord.Commands { _command.SetParameters(_params.ToArray()); foreach (var alias in _command.Aliases) - _plugin.Map.AddCommand(alias, _command); - _plugin.AddCommand(_command); + _service.Map.AddCommand(alias, _command); + _service.AddCommand(_command); } internal static string AppendPrefix(string prefix, string cmd) @@ -99,13 +99,13 @@ namespace Discord.Commands } public sealed class CommandGroupBuilder { - internal 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; } @@ -117,7 +117,7 @@ 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() @@ -126,7 +126,7 @@ namespace Discord.Commands { var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); command.MinPermissions = _defaultMinPermissions; - return new CommandBuilder(_plugin, command, _prefix); + 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/CommandsPlugin.Events.cs b/src/Discord.Net.Commands/CommandService.Events.cs similarity index 97% rename from src/Discord.Net.Commands/CommandsPlugin.Events.cs rename to src/Discord.Net.Commands/CommandService.Events.cs index da31fa6f7..92a3d429c 100644 --- a/src/Discord.Net.Commands/CommandsPlugin.Events.cs +++ b/src/Discord.Net.Commands/CommandService.Events.cs @@ -36,7 +36,7 @@ namespace Discord.Commands } } - public partial class CommandsPlugin + public partial class CommandService { public event EventHandler RanCommand; private void RaiseRanCommand(CommandEventArgs args) 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.cs b/src/Discord.Net.Commands/CommandsPlugin.cs deleted file mode 100644 index e77210e5f..000000000 --- a/src/Discord.Net.Commands/CommandsPlugin.cs +++ /dev/null @@ -1,211 +0,0 @@ -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 CommandsPlugin - { - private readonly DiscordClient _client; - private readonly Func _getPermissions; - - public IEnumerable Commands => _commands; - private readonly List _commands; - - internal CommandMap Map => _map; - private readonly CommandMap _map; - - public char? ComamndChar - { - get { return _commandChars.Length > 0 ? _commandChars[0] : (char?)null; } - set { _commandChars = value != null ? new char[] { value.Value } : new char[0]; } - } - public IEnumerable CommandChars { get { return _commandChars; } set { _commandChars = value.ToArray(); } } - private char[] _commandChars; - - public bool RequireCommandCharInPublic { get; set; } - public bool RequireCommandCharInPrivate { get; set; } - public bool HelpInPublic { get; set; } - - public CommandsPlugin(DiscordClient client, Func getPermissions = null, bool builtInHelp = false) - { - _client = client; - _getPermissions = getPermissions; - - _commands = new List(); - _map = new CommandMap(null); - - _commandChars = new char[] { '!' }; - RequireCommandCharInPublic = true; - RequireCommandCharInPrivate = true; - HelpInPublic = true; - - if (builtInHelp) - { - CreateCommand("help") - .Parameter("command", ParameterType.Optional) - .Hide() - .Info("Returns information about commands.") - .Do(async e => - { - if (e.Command.Text != "help") - await Reply(e, CommandDetails(e.Command)); - else - { - if (e.Args == null) - { - int permissions = getPermissions(e.User); - - StringBuilder output = new StringBuilder(); - output.AppendLine("These are the commands you can use:"); - output.Append("`"); - output.Append(string.Join(", ", _commands.Select(x => permissions >= x.MinPermissions && !x.IsHidden))); - output.Append("`"); - - if (_commandChars.Length > 0) - { - if (_commandChars.Length == 1) - output.AppendLine($"\nYou can use `{_commandChars[0]}` to call a command."); - else - output.AppendLine($"\nYou can use `{string.Join(" ", CommandChars.Take(_commandChars.Length - 1))}` and `{_commandChars.Last()}` to call a command."); - } - - output.AppendLine("`help ` can tell you more about how to use a command."); - - await Reply(e, output.ToString()); - } - else - { - var cmd = _map.GetCommand(e.Args[0]); - if (cmd != null) - await Reply(e, CommandDetails(cmd)); - else - await Reply(e, $"`{e.Args[0]}` is not a valid command."); - } - } - }); - - } - - 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 - if (_commandChars.Length > 0) - { - bool isPrivate = e.Message.Channel.IsPrivate; - bool hasCommandChar = _commandChars.Contains(msg[0]); - if (hasCommandChar) - msg = msg.Substring(1); - - if (isPrivate && RequireCommandCharInPrivate && !hasCommandChar) - return; // If private, and command char is required, and it doesn't have it, ignore it. - if (!isPrivate && RequireCommandCharInPublic && !hasCommandChar) - return; // Same, but public. - } - - //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 = _getPermissions != null ? _getPermissions(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); - } - } - }; - } - - private string CommandDetails(Command command) - { - 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 output.ToString(); - } - - internal async Task Reply(CommandEventArgs e, string message) - { - if (HelpInPublic) - await _client.SendMessage(e.Channel, message); - else - await _client.SendPrivateMessage(e.User, message); - } - - 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(null, command, ""); - } - - internal void AddCommand(Command command) - { - _commands.Add(command); - _map.AddCommand(command.Text, command); - } - } -} diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 0b3717e57..ae77302da 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -232,6 +232,9 @@ HttpException.cs + + IService.cs + Models\Channel.cs diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 48c03e372..f00048c20 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) 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); + } +} From eb0c14e2873651874d96f728eb521a37b415e3de Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 12:56:03 -0300 Subject: [PATCH 30/47] Dont run .Cache on fake cache objects. --- src/Discord.Net/Models/Invite.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index e42c5352d..2c06e8673 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -51,7 +51,7 @@ namespace Discord if (server == null) { server = _generatedServer = new Server(client, x); - server.Cache(); + //server.Cache(); } return server; }); @@ -61,7 +61,7 @@ namespace Discord if (inviter == null) { inviter = _generatedInviter = new User(client, x, _server.Id); - inviter.Cache(); + //inviter.Cache(); } return inviter; }); @@ -71,7 +71,7 @@ namespace Discord if (channel == null) { channel = _generatedChannel = new Channel(client, x, _server.Id, null); - channel.Cache(); + //channel.Cache(); } return channel; }); From 38dd1efa0f715175ba0b4bd551cef2b127d0cd03 Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 13:10:48 -0300 Subject: [PATCH 31/47] Improved equality checks for cache objects --- src/Discord.Net/Models/Channel.cs | 2 ++ src/Discord.Net/Models/Color.cs | 4 ++++ src/Discord.Net/Models/GlobalUser.cs | 2 ++ src/Discord.Net/Models/Invite.cs | 2 ++ src/Discord.Net/Models/Message.cs | 2 ++ src/Discord.Net/Models/Permissions.cs | 8 ++++++++ src/Discord.Net/Models/Role.cs | 2 ++ src/Discord.Net/Models/Server.cs | 2 ++ src/Discord.Net/Models/User.cs | 2 ++ 9 files changed, 26 insertions(+) diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 4ce0b1b51..2aa913988 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -207,6 +207,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 2c06e8673..a9f50af93 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -112,6 +112,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 5787b749b..5e301758b 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -235,6 +235,8 @@ namespace Discord } } + 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..9757864d3 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -370,6 +370,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 From 370a3e9b0882f85c33d237d1bfb2b2812ca44aad Mon Sep 17 00:00:00 2001 From: RogueException Date: Fri, 30 Oct 2015 13:24:49 -0300 Subject: [PATCH 32/47] Removed references from Invite. --- src/Discord.Net/Models/Invite.cs | 127 ++++++++++++++++--------------- src/Discord.Net/Models/User.cs | 3 +- 2 files changed, 66 insertions(+), 64 deletions(-) diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index a9f50af93..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); diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 9757864d3..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; } From d418c9548111ac13e6766975c4e46c437b352905 Mon Sep 17 00:00:00 2001 From: Brandon Smith Date: Sat, 31 Oct 2015 00:31:14 -0300 Subject: [PATCH 33/47] Improve message cache management --- src/Discord.Net/Models/Channel.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index 2aa913988..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); } From 4f4075aae2109537753dcdbd000ac9a87b92d631 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 2 Nov 2015 09:03:56 -0400 Subject: [PATCH 34/47] Minor doc edit --- src/Discord.Net/Helpers/Mention.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index f9f44646b..02d96bbf8 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -15,7 +15,7 @@ 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"; From ce63d8c2a78dd326baff2af9452ad001aa6ac3ff Mon Sep 17 00:00:00 2001 From: WSIContractor Date: Mon, 2 Nov 2015 09:49:57 -0800 Subject: [PATCH 35/47] Changed the dev branch version number Changed the documentation to reflect the right version number and to prevent confusion. Changed from 0.8.0-beta1 to 0.8.1-beta. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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). From ab9368f5650c0ee04a545f782db1a4937009a90e Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 2 Nov 2015 14:06:50 -0400 Subject: [PATCH 36/47] Removed TestingProject --- Discord.Net.sln | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Discord.Net.sln b/Discord.Net.sln index 3aefab91d..1c32308ff 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -30,8 +30,6 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Modules", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Modules", "src\Discord.Net.Modules.Net45\Discord.Net.Modules.csproj", "{3091164F-66AE-4543-A63D-167C1116241D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestingProject", "TestingProject\TestingProject.csproj", "{6CD8116D-6749-4174-81EB-C4EB4B1F185B}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -81,12 +79,6 @@ Global {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU - {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6CD8116D-6749-4174-81EB-C4EB4B1F185B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -101,6 +93,5 @@ Global {1B5603B4-6F8F-4289-B945-7BAAE523D740} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} {01584E8A-78DA-486F-9EF9-A894E435841B} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} {3091164F-66AE-4543-A63D-167C1116241D} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {6CD8116D-6749-4174-81EB-C4EB4B1F185B} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9} EndGlobalSection EndGlobal From dcdea2b380cc56303c37632eb89080b548e6bee4 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 2 Nov 2015 14:12:51 -0400 Subject: [PATCH 37/47] FindChannels limits #channel format to text channels only. --- src/Discord.Net/DiscordClient.Channels.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Channels.cs b/src/Discord.Net/DiscordClient.Channels.cs index 0c89a7a13..2f3462d22 100644 --- a/src/Discord.Net/DiscordClient.Channels.cs +++ b/src/Discord.Net/DiscordClient.Channels.cs @@ -98,10 +98,10 @@ namespace Discord if (channel != null) query = query.Concat(new Channel[] { channel }); } - else if (name[0] == '#') //If we somehow get text starting with # but isn't a mention + else if (name[0] == '#' && (type == (ChannelType)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.Channels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); + query = query.Concat(server.TextChannels.Where(x => string.Equals(x.Name, name2, StringComparison.OrdinalIgnoreCase))); } } From c0e6c2eb249f8d08b13152958e24b4e857d4b979 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 2 Nov 2015 14:26:06 -0400 Subject: [PATCH 38/47] Cleaned up string enums because implicit operators are picky. --- src/Discord.Net.Net45/Discord.Net.csproj | 4 +- src/Discord.Net/API/Enums/ChannelType.cs | 6 +++ src/Discord.Net/API/Enums/PermissionTarget.cs | 6 +++ .../API/Enums/{Regions.cs => Region.cs} | 6 +++ src/Discord.Net/API/Enums/StringEnum.cs | 40 +------------------ src/Discord.Net/API/Enums/UserStatus.cs | 6 +++ src/Discord.Net/DiscordClient.Channels.cs | 6 +-- src/Discord.Net/DiscordClient.Servers.cs | 2 +- src/Discord.Net/DiscordClient.Users.cs | 2 +- 9 files changed, 32 insertions(+), 46 deletions(-) rename src/Discord.Net/API/Enums/{Regions.cs => Region.cs} (71%) diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index ae77302da..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 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 2f3462d22..beef45a35 100644 --- a/src/Discord.Net/DiscordClient.Channels.cs +++ b/src/Discord.Net/DiscordClient.Channels.cs @@ -98,14 +98,14 @@ namespace Discord if (channel != null) query = query.Concat(new Channel[] { channel }); } - else if (name[0] == '#' && (type == (ChannelType)null || type == ChannelType.Text)) //If we somehow get text starting with # but isn't a mention + 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) + if (type != null) query = query.Where(x => x.Type == type); return query; } @@ -115,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.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 ed4e048e7..aa7e931b1 100644 --- a/src/Discord.Net/DiscordClient.Users.cs +++ b/src/Discord.Net/DiscordClient.Users.cs @@ -250,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(); From a3c7b08d948ee729e9b423bd0913148ce6e9f661 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 12:39:50 -0400 Subject: [PATCH 39/47] Reworked the mention system --- src/Discord.Net/DiscordClient.Messages.cs | 39 ++++++++--------- src/Discord.Net/Helpers/Mention.cs | 51 ++++++++++++----------- src/Discord.Net/Models/Message.cs | 36 +++++++++------- 3 files changed, 67 insertions(+), 59 deletions(-) diff --git a/src/Discord.Net/DiscordClient.Messages.cs b/src/Discord.Net/DiscordClient.Messages.cs index 5928f4132..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,10 +119,10 @@ 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 channelIds = !channel.IsPrivate ? Mention.GetChannelIds(text).Distinct() : new string[0]; var nonce = GenerateNonce(); msg = _messages.GetOrAdd("nonce_" + nonce, channel.Id, _userId); var currentUser = msg.User; @@ -140,21 +137,21 @@ namespace Discord msg.Nonce = nonce; msg.IsQueued = true; - //IsPrivate check is already done earlier - msg.MentionedUsers = userIds - .Select(x => _users[x, channel.Server.Id]) - .Where(x => x != null) - .ToArray(); - msg.MentionedChannels = channelIds - .Select(x => _channels[x]) - .Where(x => x != null && x.Server == channel.Server) - .ToArray(); + 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); @@ -179,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. diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index f9f44646b..d128e30bd 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) @@ -19,45 +20,45 @@ namespace Discord 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) - { - return _userRegex.Matches(text) - .OfType() - .Select(x => x.Groups[1].Value) - .Where(x => x != null); + })); } - internal static IEnumerable GetChannelIds(string text) + internal static string CleanRoleMentions(DiscordClient client, Server server, string text, List roles = null) { - return _channelRegex.Matches(text) - .OfType() - .Select(x => x.Groups[1].Value) - .Where(x => x != null); + return _roleRegex.Replace(text, new MatchEvaluator(e => + { + roles.Add(server.EveryoneRole); + return e.Value; + })); } } } diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 5e301758b..30595c683 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -90,9 +90,7 @@ namespace Discord Height = height; } } - - private string _cleanText; - + /// Returns the local unique identifier for this message. public string Nonce { get; internal set; } @@ -110,8 +108,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. @@ -131,6 +128,10 @@ namespace Discord [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; @@ -220,18 +221,23 @@ namespace Discord } if (model.Content != null) { - RawText = model.Content; - _cleanText = null; - - if (!Channel.IsPrivate) + var server = Server; + 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) { - MentionedChannels = Mention.GetChannelIds(model.Content) - .Select(x => _client.Channels[x]) - .Where(x => x.Server == Channel.Server) - .ToArray(); + text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels); + text = Mention.CleanRoleMentions(_client, server, text, mentionedRoles); } - else - MentionedChannels = new Channel[0]; + + //MentionedUsers = mentionedUsers; + MentionedChannels = mentionedChannels; + MentionedRoles = mentionedRoles; } } From 2977a9367dd3860c3c2d6a7f52be2df8996fe6f1 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 12:41:23 -0400 Subject: [PATCH 40/47] Removed IsMentioningEveryone --- src/Discord.Net/Models/Message.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 30595c683..0787ae579 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -97,8 +97,6 @@ namespace Discord /// 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. @@ -202,9 +200,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) From 9ae1761d34fa1a5e0c5d279f223019af95595546 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 14:59:13 -0400 Subject: [PATCH 41/47] Set IsMentioningMe to true if your role is mentioned --- src/Discord.Net/Models/Message.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 0787ae579..c39ad08d1 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -177,6 +177,7 @@ namespace Discord internal void Update(MessageInfo model) { + var server = Server; if (model.Attachments != null) { Attachments = model.Attachments @@ -212,12 +213,9 @@ namespace Discord MentionedUsers = model.Mentions .Select(x => _client.Users[x.Id, Channel.Server?.Id]) .ToArray(); - IsMentioningMe = model.Mentions - .Any(x => x.Id == _client.CurrentUserId); } if (model.Content != null) { - var server = Server; string text = model.Content; RawText = text; @@ -235,7 +233,11 @@ namespace Discord MentionedChannels = mentionedChannels; MentionedRoles = mentionedRoles; } - } + + IsMentioningMe = model.Mentions + .Any(x => x.Id == _client.CurrentUserId) || + (server != null && MentionedRoles.Any(x => server.CurrentUser.HasRole(x))); + } public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id; public override int GetHashCode() => Id.GetHashCode(); From c280ed0bda9dd15389b58e8a8a07298db98ddbb6 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 17:13:01 -0400 Subject: [PATCH 42/47] Fixed nullref in Message.Update --- src/Discord.Net/Models/Message.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index c39ad08d1..57efc1e64 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -228,15 +228,24 @@ namespace Discord text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels); text = Mention.CleanRoleMentions(_client, server, text, mentionedRoles); } + Text = text; //MentionedUsers = mentionedUsers; MentionedChannels = mentionedChannels; MentionedRoles = mentionedRoles; } - - IsMentioningMe = model.Mentions - .Any(x => x.Id == _client.CurrentUserId) || - (server != null && MentionedRoles.Any(x => server.CurrentUser.HasRole(x))); + + 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; From b3b30256fdb7df6801a6a4d9e81a5cd6986303e5 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 17:22:25 -0400 Subject: [PATCH 43/47] Update email on READY --- src/Discord.Net/DiscordClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index f00048c20..8dbb33b96 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -310,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) { From 0eb0e14bbaf38fee4d131a792d14f8fa1b1e37a0 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 17:29:59 -0400 Subject: [PATCH 44/47] Only add everyone to mentionedroles if the user has permissions --- src/Discord.Net/Helpers/Mention.cs | 5 +++-- src/Discord.Net/Models/Message.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net/Helpers/Mention.cs b/src/Discord.Net/Helpers/Mention.cs index 69553fbd7..b3c6ac649 100644 --- a/src/Discord.Net/Helpers/Mention.cs +++ b/src/Discord.Net/Helpers/Mention.cs @@ -52,11 +52,12 @@ namespace Discord return '#' + e.Value; })); } - internal static string CleanRoleMentions(DiscordClient client, Server server, string text, List roles = null) + internal static string CleanRoleMentions(DiscordClient client, User user, Channel channel, string text, List roles = null) { return _roleRegex.Replace(text, new MatchEvaluator(e => { - roles.Add(server.EveryoneRole); + if (roles != null && user.GetPermissions(channel).MentionEveryone) + roles.Add(channel.Server.EveryoneRole); return e.Value; })); } diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 57efc1e64..a9d045414 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -177,7 +177,8 @@ namespace Discord internal void Update(MessageInfo model) { - var server = Server; + var channel = Channel; + var server = channel.Server; if (model.Attachments != null) { Attachments = model.Attachments @@ -226,7 +227,7 @@ namespace Discord if (server != null) { text = Mention.CleanChannelMentions(_client, server, text, mentionedChannels); - text = Mention.CleanRoleMentions(_client, server, text, mentionedRoles); + text = Mention.CleanRoleMentions(_client, User, channel, text, mentionedRoles); } Text = text; From 60610abe5d101f1f3acd452e51edd82033661a71 Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 19:08:17 -0400 Subject: [PATCH 45/47] Improved handling of SSL/TLS error --- src/Discord.Net/Net/Rest/SharpRestEngine.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) 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 From 83696c359bfcc64eb0d7b0372e99a83b6edf727b Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 19:24:07 -0400 Subject: [PATCH 46/47] Added CommandMap.GetMap overloads --- src/Discord.Net.Commands/CommandMap.cs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs index 504479400..c147d47ff 100644 --- a/src/Discord.Net.Commands/CommandMap.cs +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; namespace Discord.Commands { @@ -9,19 +10,29 @@ namespace Discord.Commands 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) { - CommandMap map; - if (_subCommands.TryGetValue(text, out map)) - return map; - else - return null; + 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); + } + return this; } public Command GetCommand() From 9a783df90ad4375b349be3c6132ac43b09eaf7ef Mon Sep 17 00:00:00 2001 From: RogueException Date: Tue, 3 Nov 2015 21:01:42 -0400 Subject: [PATCH 47/47] Fixed a couple arg parsing errors --- src/Discord.Net.Commands/CommandMap.cs | 2 ++ src/Discord.Net.Commands/CommandParser.cs | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs index c147d47ff..d066a8910 100644 --- a/src/Discord.Net.Commands/CommandMap.cs +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -31,6 +31,8 @@ namespace Discord.Commands CommandMap nextGroup; if (_subCommands.TryGetValue(nextPart, out nextGroup)) return nextGroup.GetMap(index + 1, parts); + else + return null; } return this; } diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 14af9f550..90b00e977 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -148,7 +148,12 @@ namespace Discord.Commands } if (argList.Count < command.MinArgs) - return CommandErrorType.BadArgCount; + { + /*if (command._parameters[command._parameters.Length - 1].Type == ParameterType.Unparsed) + argList.Add(""); + else*/ + return CommandErrorType.BadArgCount; + } args = argList.ToArray(); return null;