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); + } +}