Details: Subcommands and Subcommand groups not yet implemented, they will require for some parts of the code to be re-done. More attributes can and should be implemented, such as [Required] and [Choice(... , ...)]. Breakdown: * Rectified line endings to LF, as per the settings of the project. * Added a new command to SlashCommandService and SlashCommandServiceHelper to register the found commands to discord. * Implemented CommandRegistrationOptions that can be used to configure the behaviour on registration - what to do with old commands, and with commands that already exist with the same name. A default version exists and can be accessed with CommandRegistrationOptions.Default * Modified the sample program to reflect the changes made to the SlashCommandService and to also register a new command that tests all 6 types of CommandOptions (except subcommand and subcommand group) * At the moment all commands are registered in my test guild, because the update for global commands is not instant. See SlashCommandServiceHelper.RegisterCommands(...) or line 221. * Modified SlashCommandInfo to parse arguments given from Interaction, unde in ExecuteAsync, and added method BuilDcommand that returns SlashCommandCreationProperties - which can be registered to Discord. * Renamed in the sample project PingCommand.cs to DevModule.cs * Added custom attribute Description for the command method's parameters. * Implemented SlashParameterInfo - and extension of the OptionBuilder that implements a method name Parse - takes DataOptions and gives out a cast object to be passed to the command Delegate. Planning on doing more with it. * Moved SlashCommandBuilder.cs to the same directory structure * Moved SlashCommandModule.cs and ISlashCommandModule.cs to its own folder.pull/1733/head^2^2
@@ -51,6 +51,7 @@ namespace SlashCommandsExample | |||||
await socketClient.StartAsync(); | await socketClient.StartAsync(); | ||||
await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | ||||
await _commands.RegisterCommandsAsync(socketClient, CommandRegistrationOptions.Default); | |||||
await Task.Delay(-1); | await Task.Delay(-1); | ||||
} | } | ||||
@@ -0,0 +1,53 @@ | |||||
using Discord.Commands; | |||||
using Discord.Commands.SlashCommands.Types; | |||||
using Discord.SlashCommands; | |||||
using Discord.WebSocket; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace SlashCommandsExample.Modules | |||||
{ | |||||
public class DevModule : SlashCommandModule<SocketInteraction> | |||||
{ | |||||
[SlashCommand("ping", "Ping the bot to see if it's alive!")] | |||||
public async Task PingAsync() | |||||
{ | |||||
await Reply(":white_check_mark: **Bot Online**"); | |||||
} | |||||
[SlashCommand("echo", "I'll repeate everything you said to me, word for word.")] | |||||
public async Task EchoAsync([Description("The message you want repetead")]string message) | |||||
{ | |||||
await Reply($"{Interaction.Member?.Nickname ?? Interaction.Member?.Username} told me to say this: \r\n{message}"); | |||||
} | |||||
[SlashCommand("overload","Just hit me with every type of data you got, man!")] | |||||
public async Task OverloadAsync( | |||||
bool boolean, | |||||
int integer, | |||||
string myString, | |||||
SocketGuildChannel channel, | |||||
SocketGuildUser user, | |||||
SocketRole role | |||||
) | |||||
{ | |||||
await Reply($"You gave me:\r\n {boolean}, {integer}, {myString}, <#{channel?.Id}>, {user?.Mention}, {role?.Mention}"); | |||||
} | |||||
} | |||||
} | |||||
/* | |||||
The base way of defining a command using the regular command service: | |||||
public class PingModule : ModuleBase<SocketCommandContext> | |||||
{ | |||||
[Command("ping")] | |||||
[Summary("Pong! Check if the bot is alive.")] | |||||
public async Task PingAsync() | |||||
{ | |||||
await ReplyAsync(":white_check_mark: **Bot Online**"); | |||||
} | |||||
} | |||||
*/ |
@@ -1,33 +0,0 @@ | |||||
using Discord.Commands; | |||||
using Discord.Commands.SlashCommands.Types; | |||||
using Discord.SlashCommands; | |||||
using Discord.WebSocket; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace SlashCommandsExample.Modules | |||||
{ | |||||
public class PingCommand : SlashCommandModule<SocketInteraction> | |||||
{ | |||||
[SlashCommand("johnny-test", "Ping the bot to see if it is alive!")] | |||||
public async Task PingAsync() | |||||
{ | |||||
await Interaction.FollowupAsync(":white_check_mark: **Bot Online**"); | |||||
} | |||||
} | |||||
} | |||||
/* | |||||
The base way of defining a command using the regular command service: | |||||
public class PingModule : ModuleBase<SocketCommandContext> | |||||
{ | |||||
[Command("ping")] | |||||
[Summary("Pong! Check if the bot is alive.")] | |||||
public async Task PingAsync() | |||||
{ | |||||
await ReplyAsync(":white_check_mark: **Bot Online**"); | |||||
} | |||||
} | |||||
*/ |
@@ -0,0 +1,31 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
/// <summary> | |||||
/// An Attribute that gives the command parameter a description. | |||||
/// </summary> | |||||
[AttributeUsage(AttributeTargets.Parameter , AllowMultiple = false)] | |||||
public class Description : Attribute | |||||
{ | |||||
public static string DefaultDescription = "No description."; | |||||
/// <summary> | |||||
/// The description of this slash command parameter. | |||||
/// </summary> | |||||
public string description; | |||||
/// <summary> | |||||
/// Tells the <see cref="SlashCommandService"/> that this parameter has a description. | |||||
/// </summary> | |||||
/// <param name="commandName">The name of this slash command.</param> | |||||
public Description(string description) | |||||
{ | |||||
this.description = description; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,39 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
/// <summary> | |||||
/// The options that should be kept in mind when registering the slash commands to discord. | |||||
/// </summary> | |||||
public class CommandRegistrationOptions | |||||
{ | |||||
/// <summary> | |||||
/// The options that should be kept in mind when registering the slash commands to discord. | |||||
/// </summary> | |||||
/// <param name="oldCommands">What to do with the old commands that are already registered with discord</param> | |||||
/// <param name="existingCommands"> What to do with the old commands (if they weren't wiped) that we re-define.</param> | |||||
public CommandRegistrationOptions(OldCommandOptions oldCommands, ExistingCommandOptions existingCommands) | |||||
{ | |||||
OldCommands = oldCommands; | |||||
ExistingCommands = existingCommands; | |||||
} | |||||
/// <summary> | |||||
/// What to do with the old commands that are already registered with discord | |||||
/// </summary> | |||||
public OldCommandOptions OldCommands { get; set; } | |||||
/// <summary> | |||||
/// What to do with the old commands (if they weren't wiped) that we re-define. | |||||
/// </summary> | |||||
public ExistingCommandOptions ExistingCommands { get; set; } | |||||
/// <summary> | |||||
/// The default, and reccomended options - Keep the old commands, and overwrite existing commands we re-defined. | |||||
/// </summary> | |||||
public static CommandRegistrationOptions Default => | |||||
new CommandRegistrationOptions(OldCommandOptions.KEEP, ExistingCommandOptions.OVERWRITE); | |||||
} | |||||
} |
@@ -0,0 +1,14 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
public enum ExistingCommandOptions | |||||
{ | |||||
OVERWRITE, | |||||
KEEP_EXISTING | |||||
} | |||||
} |
@@ -0,0 +1,24 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
public enum OldCommandOptions | |||||
{ | |||||
/// <summary> | |||||
/// Keep the old commands intact - do nothing to them. | |||||
/// </summary> | |||||
KEEP, | |||||
/// <summary> | |||||
/// Delete the old commands that we won't be re-defined this time around. | |||||
/// </summary> | |||||
DELETE_UNUSED, | |||||
/// <summary> | |||||
/// Delete everything discord has. | |||||
/// </summary> | |||||
WIPE | |||||
} | |||||
} |
@@ -1,4 +1,6 @@ | |||||
using Discord.Commands; | using Discord.Commands; | ||||
using Discord.Commands.Builders; | |||||
using Discord.WebSocket; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -21,6 +23,10 @@ namespace Discord.SlashCommands | |||||
/// Gets the name of the command. | /// Gets the name of the command. | ||||
/// </summary> | /// </summary> | ||||
public string Description { get; } | public string Description { get; } | ||||
/// <summary> | |||||
/// The parameters we are expecting - an extension of SlashCommandOptionBuilder | |||||
/// </summary> | |||||
public List<SlashParameterInfo> Parameters { get; } | |||||
/// <summary> | /// <summary> | ||||
/// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters | /// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters | ||||
@@ -31,11 +37,12 @@ namespace Discord.SlashCommands | |||||
/// </summary> | /// </summary> | ||||
public Func<object[], Task<IResult>> callback; | public Func<object[], Task<IResult>> callback; | ||||
public SlashCommandInfo(SlashModuleInfo module, string name, string description, Delegate userMethod) | |||||
public SlashCommandInfo(SlashModuleInfo module, string name, string description,List<SlashParameterInfo> parameters , Delegate userMethod) | |||||
{ | { | ||||
Module = module; | Module = module; | ||||
Name = name; | Name = name; | ||||
Description = description; | Description = description; | ||||
Parameters = parameters; | |||||
this.userMethod = userMethod; | this.userMethod = userMethod; | ||||
this.callback = new Func<object[], Task<IResult>>(async (args) => | this.callback = new Func<object[], Task<IResult>>(async (args) => | ||||
{ | { | ||||
@@ -56,9 +63,78 @@ namespace Discord.SlashCommands | |||||
}); | }); | ||||
} | } | ||||
public async Task<IResult> ExecuteAsync(object[] args) | |||||
/// <summary> | |||||
/// Execute the function based on the interaction data we get. | |||||
/// </summary> | |||||
/// <param name="data">Interaction data from interaction</param> | |||||
public async Task<IResult> ExecuteAsync(SocketInteractionData data) | |||||
{ | { | ||||
return await callback.Invoke(args).ConfigureAwait(false); | |||||
// List of arguments to be passed to the Delegate | |||||
List<object> args = new List<object>(); | |||||
try | |||||
{ | |||||
foreach (var parameter in Parameters) | |||||
{ | |||||
// For each parameter to try find its coresponding DataOption based on the name. | |||||
// !!! names from `data` will always be lowercase regardless if we defined the command with any | |||||
// number of upercase letters !!! | |||||
if (TryGetInteractionDataOption(data, parameter.Name, out SocketInteractionDataOption dataOption)) | |||||
{ | |||||
// Parse the dataOption to one corresponding argument type, and then add it to the list of arguments | |||||
args.Add(parameter.Parse(dataOption)); | |||||
} | |||||
else | |||||
{ | |||||
// There was no input from the user on this field. | |||||
args.Add(null); | |||||
} | |||||
} | |||||
} | |||||
catch(Exception e) | |||||
{ | |||||
return ExecuteResult.FromError(e); | |||||
} | |||||
return await callback.Invoke(args.ToArray()).ConfigureAwait(false); | |||||
} | |||||
/// <summary> | |||||
/// Get the interaction data from the name of the parameter we want to fill in. | |||||
/// </summary> | |||||
private bool TryGetInteractionDataOption(SocketInteractionData data, string name, out SocketInteractionDataOption dataOption) | |||||
{ | |||||
if (data.Options == null) | |||||
{ | |||||
dataOption = null; | |||||
return false; | |||||
} | |||||
foreach (var option in data.Options) | |||||
{ | |||||
if (option.Name == name.ToLower()) | |||||
{ | |||||
dataOption = option; | |||||
return true; | |||||
} | |||||
} | |||||
dataOption = null; | |||||
return false; | |||||
} | |||||
/// <summary> | |||||
/// Build the command and put it in a state in which we can use to define it to Discord. | |||||
/// </summary> | |||||
public SlashCommandCreationProperties BuildCommand() | |||||
{ | |||||
SlashCommandBuilder builder = new SlashCommandBuilder(); | |||||
builder.WithName(Name); | |||||
builder.WithDescription(Description); | |||||
builder.Options = new List<SlashCommandOptionBuilder>(); | |||||
foreach (var parameter in Parameters) | |||||
{ | |||||
builder.AddOptions(parameter); | |||||
} | |||||
return builder.Build(); | |||||
} | } | ||||
} | } | ||||
} | } |
@@ -0,0 +1,37 @@ | |||||
using Discord.Commands.Builders; | |||||
using Discord.WebSocket; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | |||||
{ | |||||
public class SlashParameterInfo : SlashCommandOptionBuilder | |||||
{ | |||||
public object Parse(SocketInteractionDataOption dataOption) | |||||
{ | |||||
switch (Type) | |||||
{ | |||||
case ApplicationCommandOptionType.Boolean: | |||||
return (bool)dataOption; | |||||
case ApplicationCommandOptionType.Integer: | |||||
return (int)dataOption; | |||||
case ApplicationCommandOptionType.String: | |||||
return (string)dataOption; | |||||
case ApplicationCommandOptionType.Channel: | |||||
return (SocketGuildChannel)dataOption; | |||||
case ApplicationCommandOptionType.Role: | |||||
return (SocketRole)dataOption; | |||||
case ApplicationCommandOptionType.User: | |||||
return (SocketGuildUser)dataOption; | |||||
case ApplicationCommandOptionType.SubCommandGroup: | |||||
throw new NotImplementedException(); | |||||
case ApplicationCommandOptionType.SubCommand: | |||||
throw new NotImplementedException(); | |||||
} | |||||
throw new NotImplementedException($"There is no such type of data... unless we missed it. Please report this error on the Discord.Net github page! Type: {Type}"); | |||||
} | |||||
} | |||||
} |
@@ -34,11 +34,6 @@ namespace Discord.SlashCommands | |||||
_logger = new Logger(_logManager, "SlshCommand"); | _logger = new Logger(_logManager, "SlshCommand"); | ||||
} | } | ||||
public void AddAssembly() | |||||
{ | |||||
} | |||||
/// <summary> | /// <summary> | ||||
/// Execute a slash command. | /// Execute a slash command. | ||||
/// </summary> | /// </summary> | ||||
@@ -50,19 +45,39 @@ namespace Discord.SlashCommands | |||||
SlashCommandInfo commandInfo; | SlashCommandInfo commandInfo; | ||||
if (commandDefs.TryGetValue(interaction.Data.Name, out commandInfo)) | if (commandDefs.TryGetValue(interaction.Data.Name, out commandInfo)) | ||||
{ | { | ||||
// TODO: implement everything that has to do with parameters :) | |||||
// Then, set the context in which the command will be executed | // Then, set the context in which the command will be executed | ||||
commandInfo.Module.userCommandModule.SetContext(interaction); | commandInfo.Module.userCommandModule.SetContext(interaction); | ||||
// Then run the command (with no parameters) | |||||
return await commandInfo.ExecuteAsync(new object[] { }).ConfigureAwait(false); | |||||
// Then run the command and pass the interaction data over to the CommandInfo class | |||||
return await commandInfo.ExecuteAsync(interaction.Data).ConfigureAwait(false); | |||||
} | } | ||||
else | else | ||||
{ | { | ||||
return SearchResult.FromError(CommandError.UnknownCommand, $"There is no registered slash command with the name {interaction.Data.Name}"); | return SearchResult.FromError(CommandError.UnknownCommand, $"There is no registered slash command with the name {interaction.Data.Name}"); | ||||
} | } | ||||
} | } | ||||
/// <summary> | |||||
/// Registers all previously scanned commands. | |||||
/// </summary> | |||||
public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, CommandRegistrationOptions registrationOptions) | |||||
{ | |||||
// First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | |||||
await _moduleLock.WaitAsync().ConfigureAwait(false); | |||||
try | |||||
{ | |||||
await SlashCommandServiceHelper.RegisterCommands(socketClient, commandDefs, this,registrationOptions).ConfigureAwait(false); | |||||
} | |||||
finally | |||||
{ | |||||
_moduleLock.Release(); | |||||
} | |||||
await _logger.InfoAsync("All commands have been registered!").ConfigureAwait(false); | |||||
} | |||||
/// <summary> | |||||
/// Scans the program for Attribute-based SlashCommandModules | |||||
/// </summary> | |||||
public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | ||||
{ | { | ||||
// First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | // First take a hold of the module lock, as to make sure we aren't editing stuff while we do our business | ||||
@@ -75,9 +90,7 @@ namespace Discord.SlashCommands | |||||
// Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | // Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | ||||
moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | ||||
// After that, internally register all of the commands into SlashCommandInfo | // After that, internally register all of the commands into SlashCommandInfo | ||||
commandDefs = await SlashCommandServiceHelper.PrepareAsync(types,moduleDefs,this).ConfigureAwait(false); | |||||
// TODO: And finally, register the commands with discord. | |||||
await SlashCommandServiceHelper.RegisterCommands(commandDefs, this, services).ConfigureAwait(false); | |||||
commandDefs = await SlashCommandServiceHelper.CreateCommandInfos(types,moduleDefs,this).ConfigureAwait(false); | |||||
} | } | ||||
finally | finally | ||||
{ | { | ||||
@@ -1,3 +1,5 @@ | |||||
using Discord.Commands.Builders; | |||||
using Discord.WebSocket; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -63,17 +65,19 @@ namespace Discord.SlashCommands | |||||
/// <summary> | /// <summary> | ||||
/// Prepare all of the commands and register them internally. | /// Prepare all of the commands and register them internally. | ||||
/// </summary> | /// </summary> | ||||
public static async Task<Dictionary<string, SlashCommandInfo>> PrepareAsync(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||||
public static async Task<Dictionary<string, SlashCommandInfo>> CreateCommandInfos(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||||
{ | { | ||||
// Create the resulting dictionary ahead of time | |||||
var result = new Dictionary<string, SlashCommandInfo>(); | var result = new Dictionary<string, SlashCommandInfo>(); | ||||
// fore each user-defined module | |||||
// For each user-defined module ... | |||||
foreach (var userModule in types) | foreach (var userModule in types) | ||||
{ | { | ||||
// Get its associated information | |||||
// Get its associated information. If there isn't any it means something went wrong, but it's not a critical error. | |||||
SlashModuleInfo moduleInfo; | SlashModuleInfo moduleInfo; | ||||
if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | ||||
{ | { | ||||
// and get all of its method, and check if they are valid, and if so create a new SlashCommandInfo for them. | |||||
// TODO: handle sub-command groups | |||||
// And get all of its method, and check if they are valid, and if so create a new SlashCommandInfo for them. | |||||
var commandMethods = userModule.GetMethods(); | var commandMethods = userModule.GetMethods(); | ||||
List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | ||||
foreach (var commandMethod in commandMethods) | foreach (var commandMethod in commandMethods) | ||||
@@ -81,13 +85,19 @@ namespace Discord.SlashCommands | |||||
SlashCommand slashCommand; | SlashCommand slashCommand; | ||||
if (IsValidSlashCommand(commandMethod, out slashCommand)) | if (IsValidSlashCommand(commandMethod, out slashCommand)) | ||||
{ | { | ||||
// Create the delegate for the method we want to call once the user interacts with the bot. | |||||
// We use a delegate because of the unknown number and type of parameters we will have. | |||||
Delegate delegateMethod = CreateDelegate(commandMethod, moduleInfo.userCommandModule); | Delegate delegateMethod = CreateDelegate(commandMethod, moduleInfo.userCommandModule); | ||||
SlashCommandInfo commandInfo = new SlashCommandInfo( | SlashCommandInfo commandInfo = new SlashCommandInfo( | ||||
module: moduleInfo, | module: moduleInfo, | ||||
name: slashCommand.commandName, | name: slashCommand.commandName, | ||||
description: slashCommand.description, | description: slashCommand.description, | ||||
// Generate the parameters. Due to it's complicated way the algorithm has been moved to its own function. | |||||
parameters: ConstructCommandParameters(commandMethod), | |||||
userMethod: delegateMethod | userMethod: delegateMethod | ||||
); | ); | ||||
result.Add(slashCommand.commandName, commandInfo); | result.Add(slashCommand.commandName, commandInfo); | ||||
commandInfos.Add(commandInfo); | commandInfos.Add(commandInfo); | ||||
} | } | ||||
@@ -97,6 +107,9 @@ namespace Discord.SlashCommands | |||||
} | } | ||||
return result; | return result; | ||||
} | } | ||||
/// <summary> | |||||
/// Determines wheater a method can be clasified as a slash command | |||||
/// </summary> | |||||
private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | ||||
{ | { | ||||
// Verify that we only have one [SlashCommand(...)] attribute | // Verify that we only have one [SlashCommand(...)] attribute | ||||
@@ -115,6 +128,66 @@ namespace Discord.SlashCommands | |||||
slashCommand = slashCommandAttributes.First() as SlashCommand; | slashCommand = slashCommandAttributes.First() as SlashCommand; | ||||
return true; | return true; | ||||
} | } | ||||
private static List<SlashParameterInfo> ConstructCommandParameters(MethodInfo method) | |||||
{ | |||||
// Prepare the final list of parameters | |||||
List<SlashParameterInfo> finalParameters = new List<SlashParameterInfo>(); | |||||
// For each mehod parameter ... | |||||
// ex: ... MyCommand(string abc, int myInt) | |||||
// `abc` and `myInt` are parameters | |||||
foreach (var methodParameter in method.GetParameters()) | |||||
{ | |||||
SlashParameterInfo newParameter = new SlashParameterInfo(); | |||||
// Set the parameter name to that of the method | |||||
// TODO: Implement an annotation that lets the user choose a custom name | |||||
newParameter.Name = methodParameter.Name; | |||||
// Get to see if it has a Description Attribute. | |||||
// If it has | |||||
// 0 -> then use the default description | |||||
// 1 -> Use the value from that attribute | |||||
// 2+ -> Throw an error. This shouldn't normaly happen, but we check for sake of sanity | |||||
var descriptions = methodParameter.GetCustomAttributes(typeof(Description)); | |||||
if (descriptions.Count() == 0) | |||||
newParameter.Description = Description.DefaultDescription; | |||||
else if (descriptions.Count() > 1) | |||||
throw new Exception($"Too many Description attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!"); | |||||
else | |||||
newParameter.Description = (descriptions.First() as Description).description; | |||||
// And get the parameter type | |||||
newParameter.Type = TypeFromMethodParameter(methodParameter); | |||||
// TODO: implement more attributes, such as [Required] | |||||
finalParameters.Add(newParameter); | |||||
} | |||||
return finalParameters; | |||||
} | |||||
/// <summary> | |||||
/// Get the type of command option from a method parameter info. | |||||
/// </summary> | |||||
private static ApplicationCommandOptionType TypeFromMethodParameter(ParameterInfo methodParameter) | |||||
{ | |||||
// Can't do switch -- who knows why? | |||||
if (methodParameter.ParameterType == typeof(int)) | |||||
return ApplicationCommandOptionType.Integer; | |||||
if (methodParameter.ParameterType == typeof(string)) | |||||
return ApplicationCommandOptionType.String; | |||||
if (methodParameter.ParameterType == typeof(bool)) | |||||
return ApplicationCommandOptionType.Boolean; | |||||
if (methodParameter.ParameterType == typeof(SocketGuildChannel)) | |||||
return ApplicationCommandOptionType.Channel; | |||||
if (methodParameter.ParameterType == typeof(SocketRole)) | |||||
return ApplicationCommandOptionType.Role; | |||||
if (methodParameter.ParameterType == typeof(SocketGuildUser)) | |||||
return ApplicationCommandOptionType.User; | |||||
throw new Exception($"Got parameter type other than int, string, bool, guild, role, or user. {methodParameter.Name}"); | |||||
} | |||||
/// <summary> | /// <summary> | ||||
/// Creae a delegate from methodInfo. Taken from | /// Creae a delegate from methodInfo. Taken from | ||||
/// https://stackoverflow.com/a/40579063/8455128 | /// https://stackoverflow.com/a/40579063/8455128 | ||||
@@ -143,8 +216,49 @@ namespace Discord.SlashCommands | |||||
return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); | return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); | ||||
} | } | ||||
public static async Task RegisterCommands(Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, IServiceProvider services) | |||||
public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, CommandRegistrationOptions options) | |||||
{ | { | ||||
// Get existing commmands | |||||
ulong devGuild = 386658607338618891; | |||||
var existingCommands = await socketClient.Rest.GetGuildApplicationCommands(devGuild).ConfigureAwait(false); | |||||
List<string> existingCommandNames = new List<string>(); | |||||
foreach (var existingCommand in existingCommands) | |||||
{ | |||||
existingCommandNames.Add(existingCommand.Name); | |||||
} | |||||
// Delete old ones that we want to re-implement | |||||
if (options.OldCommands == OldCommandOptions.DELETE_UNUSED || | |||||
options.OldCommands == OldCommandOptions.WIPE) | |||||
{ | |||||
foreach (var existingCommand in existingCommands) | |||||
{ | |||||
// If we want to wipe all commands | |||||
// or if the existing command isn't re-defined (probably code deleted by user) | |||||
// remove it from discord. | |||||
if(options.OldCommands == OldCommandOptions.WIPE || | |||||
!commandDefs.ContainsKey(existingCommand.Name)) | |||||
{ | |||||
await existingCommand.DeleteAsync(); | |||||
} | |||||
} | |||||
} | |||||
foreach (var entry in commandDefs) | |||||
{ | |||||
if (existingCommandNames.Contains(entry.Value.Name) && | |||||
options.ExistingCommands == ExistingCommandOptions.KEEP_EXISTING) | |||||
{ | |||||
continue; | |||||
} | |||||
// If it's a new command or we want to overwrite an old one... | |||||
else | |||||
{ | |||||
SlashCommandInfo slashCommandInfo = entry.Value; | |||||
SlashCommandCreationProperties command = slashCommandInfo.BuildCommand(); | |||||
// TODO: Implement Global and Guild Commands. | |||||
await socketClient.Rest.CreateGuildCommand(command, devGuild).ConfigureAwait(false); | |||||
} | |||||
} | |||||
return; | return; | ||||
} | } | ||||
} | } | ||||
@@ -1,5 +1,7 @@ | |||||
using Discord.Commands.SlashCommands.Types; | using Discord.Commands.SlashCommands.Types; | ||||
using Discord.WebSocket; | |||||
using System; | using System; | ||||
using System.Threading.Tasks; | |||||
namespace Discord.SlashCommands | namespace Discord.SlashCommands | ||||
{ | { | ||||
@@ -17,5 +19,16 @@ namespace Discord.SlashCommands | |||||
var newValue = interaction as T; | var newValue = interaction as T; | ||||
Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | ||||
} | } | ||||
public async Task<IMessage> Reply(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, | |||||
AllowedMentions allowedMentions = null, RequestOptions options = null) | |||||
{ | |||||
if(Interaction is SocketInteraction) | |||||
{ | |||||
return await (Interaction as SocketInteraction).FollowupAsync(text, isTTS, embed, Type, allowedMentions, options); | |||||
} | |||||
return null; | |||||
} | |||||
} | } | ||||
} | } |
@@ -30,7 +30,7 @@ namespace Discord.WebSocket | |||||
internal SocketInteractionDataOption() { } | internal SocketInteractionDataOption() { } | ||||
internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild) | internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild) | ||||
{ | { | ||||
this.Name = Name; | |||||
this.Name = model.Name; | |||||
this.Value = model.Value.IsSpecified ? model.Value.Value : null; | this.Value = model.Value.IsSpecified ? model.Value.Value : null; | ||||
this.discord = discord; | this.discord = discord; | ||||
this.guild = guild; | this.guild = guild; | ||||
@@ -44,14 +44,17 @@ namespace Discord.WebSocket | |||||
// Converters | // Converters | ||||
public static explicit operator bool(SocketInteractionDataOption option) | public static explicit operator bool(SocketInteractionDataOption option) | ||||
=> (bool)option.Value; | => (bool)option.Value; | ||||
// The default value is of type long, so an implementaiton of of the long option is trivial | |||||
public static explicit operator int(SocketInteractionDataOption option) | public static explicit operator int(SocketInteractionDataOption option) | ||||
=> (int)option.Value; | |||||
=> unchecked( | |||||
(int)( (long)option.Value ) | |||||
); | |||||
public static explicit operator string(SocketInteractionDataOption option) | public static explicit operator string(SocketInteractionDataOption option) | ||||
=> option.Value.ToString(); | => option.Value.ToString(); | ||||
public static explicit operator SocketGuildChannel(SocketInteractionDataOption option) | public static explicit operator SocketGuildChannel(SocketInteractionDataOption option) | ||||
{ | { | ||||
if (option.Value is ulong id) | |||||
if (ulong.TryParse((string)option.Value, out ulong id)) | |||||
{ | { | ||||
var guild = option.discord.GetGuild(option.guild); | var guild = option.discord.GetGuild(option.guild); | ||||
@@ -66,7 +69,7 @@ namespace Discord.WebSocket | |||||
public static explicit operator SocketRole(SocketInteractionDataOption option) | public static explicit operator SocketRole(SocketInteractionDataOption option) | ||||
{ | { | ||||
if (option.Value is ulong id) | |||||
if (ulong.TryParse((string)option.Value, out ulong id)) | |||||
{ | { | ||||
var guild = option.discord.GetGuild(option.guild); | var guild = option.discord.GetGuild(option.guild); | ||||
@@ -81,7 +84,7 @@ namespace Discord.WebSocket | |||||
public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | ||||
{ | { | ||||
if(option.Value is ulong id) | |||||
if (ulong.TryParse((string)option.Value, out ulong id)) | |||||
{ | { | ||||
var guild = option.discord.GetGuild(option.guild); | var guild = option.discord.GetGuild(option.guild); | ||||