@@ -7,7 +7,7 @@ | |||
<ProjectGuid>{1B5603B4-6F8F-4289-B945-7BAAE523D740}</ProjectGuid> | |||
<OutputType>Library</OutputType> | |||
<AppDesignerFolder>Properties</AppDesignerFolder> | |||
<RootNamespace>Discord</RootNamespace> | |||
<RootNamespace>Discord.Commands</RootNamespace> | |||
<AssemblyName>Discord.Net.Commands</AssemblyName> | |||
<FileAlignment>512</FileAlignment> | |||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion> | |||
@@ -43,17 +43,23 @@ | |||
<Compile Include="..\Discord.Net.Commands\CommandBuilder.cs"> | |||
<Link>CommandBuilder.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Commands\CommandExtensions.cs"> | |||
<Link>CommandExtensions.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Commands\CommandMap.cs"> | |||
<Link>CommandMap.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Commands\CommandParser.cs"> | |||
<Link>CommandParser.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Commands\CommandsPlugin.cs"> | |||
<Link>CommandsPlugin.cs</Link> | |||
<Compile Include="..\Discord.Net.Commands\CommandService.cs"> | |||
<Link>CommandService.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Commands\CommandService.Events.cs"> | |||
<Link>CommandService.Events.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net.Commands\CommandsPlugin.Events.cs"> | |||
<Link>CommandsPlugin.Events.cs</Link> | |||
<Compile Include="..\Discord.Net.Commands\CommandServiceConfig.cs"> | |||
<Link>CommandServiceConfig.cs</Link> | |||
</Compile> | |||
<Compile Include="Properties\AssemblyInfo.cs" /> | |||
</ItemGroup> | |||
@@ -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<CommandParameter> _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<CommandParameter>(); | |||
_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<CommandGroupBuilder> 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); | |||
} | |||
} | |||
} |
@@ -0,0 +1,8 @@ | |||
namespace Discord.Commands | |||
{ | |||
public static class CommandExtensions | |||
{ | |||
public static CommandService Commands(this DiscordClient client) | |||
=> client.GetService<CommandService>(); | |||
} | |||
} |
@@ -36,7 +36,7 @@ namespace Discord.Commands | |||
} | |||
} | |||
public partial class CommandsPlugin | |||
public partial class CommandService | |||
{ | |||
public event EventHandler<CommandEventArgs> RanCommand; | |||
private void RaiseRanCommand(CommandEventArgs args) |
@@ -0,0 +1,184 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
/// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary> | |||
public partial class CommandService : IService | |||
{ | |||
private DiscordClient _client; | |||
CommandServiceConfig Config { get; } | |||
public IEnumerable<Command> Commands => _commands; | |||
private readonly List<Command> _commands; | |||
internal CommandMap Map => _map; | |||
private readonly CommandMap _map; | |||
public CommandService(CommandServiceConfig config) | |||
{ | |||
Config = config; | |||
_commands = new List<Command>(); | |||
_map = new CommandMap(null); | |||
} | |||
void IService.Install(DiscordClient client) | |||
{ | |||
_client = client; | |||
Config.Lock(); | |||
if (Config.HelpMode != HelpMode.Disable) | |||
{ | |||
CreateCommand("help") | |||
.Parameter("command", ParameterType.Multiple) | |||
.Hide() | |||
.Info("Returns information about commands.") | |||
.Do(async e => | |||
{ | |||
Channel channel = Config.HelpMode == HelpMode.Public ? e.Channel : await client.CreatePMChannel(e.User); | |||
if (e.Args.Length > 0) //Show command help | |||
{ | |||
var cmd = _map.GetCommand(string.Join(" ", e.Args)); | |||
if (cmd != null) | |||
await ShowHelp(cmd, e.User, channel); | |||
else | |||
await client.SendMessage(channel, "Unable to display help: unknown command."); | |||
} | |||
else //Show general help | |||
await ShowHelp(e.User, channel); | |||
}); | |||
} | |||
client.MessageReceived += async (s, e) => | |||
{ | |||
if (_commands.Count == 0) return; | |||
if (e.Message.IsAuthor) return; | |||
string msg = e.Message.Text; | |||
if (msg.Length == 0) return; | |||
//Check for command char if one is provided | |||
var chars = Config.CommandChars; | |||
if (chars.Length > 0) | |||
{ | |||
if (!chars.Contains(msg[0])) | |||
return; | |||
msg = msg.Substring(1); | |||
} | |||
//Parse command | |||
Command command; | |||
int argPos; | |||
CommandParser.ParseCommand(msg, _map, out command, out argPos); | |||
if (command == null) | |||
{ | |||
CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null, null); | |||
RaiseCommandError(CommandErrorType.UnknownCommand, errorArgs); | |||
return; | |||
} | |||
else | |||
{ | |||
int userPermissions = Config.PermissionResolver?.Invoke(e.Message.User) ?? 0; | |||
//Parse arguments | |||
string[] args; | |||
var error = CommandParser.ParseArgs(msg, argPos, command, out args); | |||
if (error != null) | |||
{ | |||
var errorArgs = new CommandEventArgs(e.Message, command, userPermissions, null); | |||
RaiseCommandError(error.Value, errorArgs); | |||
return; | |||
} | |||
var eventArgs = new CommandEventArgs(e.Message, command, userPermissions, args); | |||
// Check permissions | |||
if (userPermissions < command.MinPermissions) | |||
{ | |||
RaiseCommandError(CommandErrorType.BadPermissions, eventArgs); | |||
return; | |||
} | |||
// Run the command | |||
try | |||
{ | |||
RaiseRanCommand(eventArgs); | |||
await command.Run(eventArgs).ConfigureAwait(false); | |||
} | |||
catch (Exception ex) | |||
{ | |||
RaiseCommandError(CommandErrorType.Exception, eventArgs, ex); | |||
} | |||
} | |||
}; | |||
} | |||
public Task ShowHelp(User user, Channel channel) | |||
{ | |||
int permissions = Config.PermissionResolver(user); | |||
StringBuilder output = new StringBuilder(); | |||
output.AppendLine("These are the commands you can use:"); | |||
output.Append(string.Join(", ", _commands | |||
.Where(x => permissions >= x.MinPermissions && !x.IsHidden) | |||
.Select(x => '`' + x.Text + '`'))); | |||
var chars = Config.CommandChars; | |||
if (chars.Length > 0) | |||
{ | |||
if (chars.Length == 1) | |||
output.AppendLine($"\nYou can use `{chars[0]}` to call a command."); | |||
else | |||
output.AppendLine($"\nYou can use `{string.Join(" ", chars.Take(chars.Length - 1))}` or `{chars.Last()}` to call a command."); | |||
} | |||
output.AppendLine("`help <command>` can tell you more about how to use a command."); | |||
return _client.SendMessage(channel, output.ToString()); | |||
} | |||
public Task ShowHelp(Command command, User user, Channel channel) | |||
{ | |||
StringBuilder output = new StringBuilder(); | |||
output.Append($"`{command.Text}`"); | |||
if (command.MinArgs != null && command.MaxArgs != null) | |||
{ | |||
if (command.MinArgs == command.MaxArgs) | |||
{ | |||
if (command.MaxArgs != 0) | |||
output.Append($" {command.MinArgs.ToString()} Args"); | |||
} | |||
else | |||
output.Append($" {command.MinArgs.ToString()} - {command.MaxArgs.ToString()} Args"); | |||
} | |||
else if (command.MinArgs != null && command.MaxArgs == null) | |||
output.Append($" ≥{command.MinArgs.ToString()} Args"); | |||
else if (command.MinArgs == null && command.MaxArgs != null) | |||
output.Append($" ≤{command.MaxArgs.ToString()} Args"); | |||
output.Append($": {command.Description ?? "No description set for this command."}"); | |||
return _client.SendMessage(channel, output.ToString()); | |||
} | |||
public void CreateCommandGroup(string cmd, Action<CommandGroupBuilder> config = null) | |||
=> config(new CommandGroupBuilder(this, cmd, 0)); | |||
public CommandBuilder CreateCommand(string cmd) | |||
{ | |||
var command = new Command(cmd); | |||
_commands.Add(command); | |||
return new CommandBuilder(this, command, ""); | |||
} | |||
internal void AddCommand(Command command) | |||
{ | |||
_commands.Add(command); | |||
_map.AddCommand(command.Text, command); | |||
} | |||
} | |||
} |
@@ -0,0 +1,49 @@ | |||
using System; | |||
namespace Discord.Commands | |||
{ | |||
public enum HelpMode | |||
{ | |||
/// <summary> Disable the automatic help command. </summary> | |||
Disable, | |||
/// <summary> Use the automatic help command and respond in the channel the command is used. </summary> | |||
Public, | |||
/// <summary> Use the automatic help command and respond in a private message. </summary> | |||
Private | |||
} | |||
public class CommandServiceConfig | |||
{ | |||
public Func<User, int> PermissionResolver { get { return _permissionsResolver; } set { SetValue(ref _permissionsResolver, value); } } | |||
private Func<User, int> _permissionsResolver; | |||
public char? CommandChar | |||
{ | |||
get | |||
{ | |||
return _commandChars.Length > 0 ? _commandChars[0] : (char?)null; | |||
} | |||
set | |||
{ | |||
if (value != null) | |||
CommandChars = new char[] { value.Value }; | |||
else | |||
CommandChars = new char[0]; | |||
} | |||
} | |||
public char[] CommandChars { get { return _commandChars; } set { SetValue(ref _commandChars, value); } } | |||
private char[] _commandChars = new char[] { '!' }; | |||
public HelpMode HelpMode { get { return _helpMode; } set { SetValue(ref _helpMode, value); } } | |||
private HelpMode _helpMode = HelpMode.Disable; | |||
//Lock | |||
protected bool _isLocked; | |||
internal void Lock() { _isLocked = true; } | |||
protected void SetValue<T>(ref T storage, T value) | |||
{ | |||
if (_isLocked) | |||
throw new InvalidOperationException("Unable to modify a discord client's configuration after it has been created."); | |||
storage = value; | |||
} | |||
} | |||
} |
@@ -1,211 +0,0 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Discord.Commands | |||
{ | |||
/// <summary> A Discord.Net client with extensions for handling common bot operations like text commands. </summary> | |||
public partial class CommandsPlugin | |||
{ | |||
private readonly DiscordClient _client; | |||
private readonly Func<User, int> _getPermissions; | |||
public IEnumerable<Command> Commands => _commands; | |||
private readonly List<Command> _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<char> 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<User, int> getPermissions = null, bool builtInHelp = false) | |||
{ | |||
_client = client; | |||
_getPermissions = getPermissions; | |||
_commands = new List<Command>(); | |||
_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 <command>` 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<CommandGroupBuilder> config = null) | |||
=> config(new CommandGroupBuilder(this, cmd, 0)); | |||
public CommandBuilder CreateCommand(string cmd) | |||
{ | |||
var command = new Command(cmd); | |||
_commands.Add(command); | |||
return new CommandBuilder(null, command, ""); | |||
} | |||
internal void AddCommand(Command command) | |||
{ | |||
_commands.Add(command); | |||
_map.AddCommand(command.Text, command); | |||
} | |||
} | |||
} |
@@ -232,6 +232,9 @@ | |||
<Compile Include="..\Discord.Net\HttpException.cs"> | |||
<Link>HttpException.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\IService.cs"> | |||
<Link>IService.cs</Link> | |||
</Compile> | |||
<Compile Include="..\Discord.Net\Models\Channel.cs"> | |||
<Link>Models\Channel.cs</Link> | |||
</Compile> | |||
@@ -17,6 +17,7 @@ namespace Discord | |||
private readonly JsonSerializer _serializer; | |||
private readonly ConcurrentQueue<Message> _pendingMessages; | |||
private readonly ConcurrentDictionary<string, DiscordWSClient> _voiceClients; | |||
private readonly Dictionary<Type, IService> _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<Type, IService>(); | |||
_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; | |||
} | |||
/// <summary> Connects to the Discord server with the provided token. </summary> | |||
public async Task Connect(string token) | |||
{ | |||
@@ -273,6 +273,22 @@ namespace Discord | |||
_currentUser = null; | |||
} | |||
public void AddService<T>(T obj) | |||
where T : class, IService | |||
{ | |||
_services.Add(typeof(T), obj); | |||
obj.Install(this); | |||
} | |||
public T GetService<T>() | |||
where T : class, IService | |||
{ | |||
IService service; | |||
if (_services.TryGetValue(typeof(T), out service)) | |||
return service as T; | |||
else | |||
return null; | |||
} | |||
protected override IEnumerable<Task> GetTasks() | |||
{ | |||
if (Config.UseMessageQueue) | |||
@@ -0,0 +1,7 @@ | |||
namespace Discord | |||
{ | |||
public interface IService | |||
{ | |||
void Install(DiscordClient client); | |||
} | |||
} |