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