From 50a88e09cde88f21ca5cb02f8d167c08edba56a6 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Mon, 15 Feb 2021 22:45:24 +0200 Subject: [PATCH] Implemented SubCommands and SubCommandGroups properly. Details: To implement them I had to get creative. First thing i did was manually register a command that uses sub commands and sub command groups. Two things I noticed immediately: 1) I can create a subcommand on a "root" command - where no SubCommandGroup is used 2) The current implementation of the Interactions doesn't know what type of value an option is. Good thing is that there is only 1 option when querying subcommands and subcommand groups, so I can find out what the "path" of the subcommand is. TOP/root/rng TOP/root/usr/zero TOP/root/usr/johnny (i misspelled it in the source files, woops) [See SlashCommandsExample/DiscordClient.cs] Next I wanted to make command groups (I'll use this term as to mean a slash command with subcommands and regular slash command groups) to be implemented in code in a sort of hierarchical manner - so I made them classes with attributes. Unfortunately to make this work I had to make them re-inherit the same things as the base module - UGLY but I see no other option to do this other than making them inherit from another class that remembers the instance of the upper class and implements the same methods aka a whole mess that I decided I won't want to partake in. [See SlashCommandsExample/Modules/DevModule.cs] Next-up is to search for these sub-groups. I decided that the most intuitive way of implementing these was to make SlashModuleInfo have children and parent of the same type -- from which arose different problems, but we'll get to that. So I gave them some children and a parent and a reference to the CommandGroup attribute they have on themselves. The boolean isCommandGroup is unused, but could be useful in the future... maybe. Also I've added a path variable to internally store structure. I wanted (after the whole reflections business) for commands to be easly accessed and deal WITH NO REFLECTION because those are slow, so I changed the final string - SlashCommandInfo dictionary to containt paths instead of command infos, something like what I exemplefied above. In any case, I edited the service helper (the search for modules method) to ignore command groups and only store top level commands. After that I made a command to instantiate command groups, and the command creation and registration were changed as to be recursive - because recurion is the simpest way to do this and it's efficient enough for what we want - we only run this once anyway. The biggest change was with command building - commands no longer build themselves, but now we command each module to build itself. There are 3 cases: Top-Level commands Top-Level subcommands (or level 1 command group) subcommands within slash command groups The code is uncommented, untidy and I'll fix that in a future commit. One last thing to note is that SlashCommands can have 0 options! - fixed that bug. Also SlashCommandBuilder.WithName() for some reason was implemented wrongly - I pressume a copy-paste error, Also I implemented 0 types of enforcing rules - I'm going to leave this to other people to do. --- SlashCommandsExample/DiscordClient.cs | 69 ++++++++ SlashCommandsExample/Modules/DevModule.cs | 26 +++ .../SlashCommands/Attributes/CommandGroup.cs | 35 ++++ .../SlashCommands/Attributes/SlashCommand.cs | 4 +- .../Builders/SlashCommandBuilder.cs | 19 +-- .../SlashCommands/Info/SlashCommandInfo.cs | 26 ++- .../SlashCommands/Info/SlashModuleInfo.cs | 86 +++++++++- .../SlashCommands/SlashCommandService.cs | 52 +++++- .../SlashCommandServiceHelper.cs | 160 +++++++++++++----- .../API/Common/ApplicationCommandOption.cs | 4 +- 10 files changed, 413 insertions(+), 68 deletions(-) create mode 100644 src/Discord.Net.Commands/SlashCommands/Attributes/CommandGroup.cs diff --git a/SlashCommandsExample/DiscordClient.cs b/SlashCommandsExample/DiscordClient.cs index 408c3870b..309cbc1a6 100644 --- a/SlashCommandsExample/DiscordClient.cs +++ b/SlashCommandsExample/DiscordClient.cs @@ -4,6 +4,7 @@ using Discord.SlashCommands; using Discord.WebSocket; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; @@ -28,6 +29,8 @@ namespace SlashCommandsExample socketClient.Log += SocketClient_Log; _commands.Log += SocketClient_Log; socketClient.InteractionCreated += InteractionHandler; + socketClient.Ready += RegisterCommand; + // This is for dev purposes. // To avoid the situation in which you accidentally push your bot token to upstream, you can use // EnviromentVariables to store your key. @@ -45,6 +48,72 @@ namespace SlashCommandsExample // EnvironmentVariableTarget.User); } + public async Task RegisterCommand() + { + // Use this to manually register a command for testing. + return; + await socketClient.Rest.CreateGuildCommand(new SlashCommandCreationProperties() + { + Name = "root", + Description = "Root Command", + Options = new List() + { + new ApplicationCommandOptionProperties() + { + Name = "usr", + Description = "User Folder", + Type = ApplicationCommandOptionType.SubCommandGroup, + Options = new List() + { + // This doesn't work. This is good! + //new ApplicationCommandOptionProperties() + //{ + // Name = "strstr", + // Description = "Some random string I guess.", + // Type = ApplicationCommandOptionType.String, + //}, + new ApplicationCommandOptionProperties() + { + Name = "zero", + Description = "Zero's Home Folder - COMMAND", + Type = ApplicationCommandOptionType.SubCommand, + Options = new List() + { + new ApplicationCommandOptionProperties() + { + Name = "file", + Description = "the file you want accessed.", + Type = ApplicationCommandOptionType.String + } + } + }, + new ApplicationCommandOptionProperties() + { + Name = "johhny", + Description = "Johnny Test's Home Folder - COMMAND", + Type = ApplicationCommandOptionType.SubCommand, + Options = new List() + { + new ApplicationCommandOptionProperties() + { + Name = "file", + Description = "the file you want accessed.", + Type = ApplicationCommandOptionType.String + } + } + } + } + }, + new ApplicationCommandOptionProperties() + { + Name = "random", + Description = "Random things", + Type = ApplicationCommandOptionType.SubCommand + } + } + }, 386658607338618891) ; + } + public async Task RunAsync() { await socketClient.LoginAsync(TokenType.Bot, botToken); diff --git a/SlashCommandsExample/Modules/DevModule.cs b/SlashCommandsExample/Modules/DevModule.cs index 37ef108ec..b8c6cb08a 100644 --- a/SlashCommandsExample/Modules/DevModule.cs +++ b/SlashCommandsExample/Modules/DevModule.cs @@ -36,6 +36,32 @@ namespace SlashCommandsExample.Modules await Reply($"You gave me:\r\n {boolean}, {integer}, {myString}, <#{channel?.Id}>, {user?.Mention}, {role?.Mention}"); } + + [CommandGroup("root")] + public class DevModule_Root : SlashCommandModule + { + [SlashCommand("rng", "Gives you a random number from this \"machine\"")] + public async Task RNGAsync() + { + var rand = new Random(); + await Reply(rand.Next(0, 101).ToString()); + } + + [CommandGroup("usr")] + public class DevModule_Root_Usr : SlashCommandModule + { + [SlashCommand("zero", "Gives you a file from user zero from this \"machine\"")] + public async Task ZeroAsync([Description("The file you want.")] string file) + { + await Reply($"You don't have permissiont to access {file} from user \"zero\"."); + } + [SlashCommand("johnny", "Gives you a file from user Johnny Test from this \"machine\"")] + public async Task JohnnyAsync([Description("The file you want.")] string file) + { + await Reply($"You don't have permissiont to access {file} from user \"johnny\"."); + } + } + } } } /* diff --git a/src/Discord.Net.Commands/SlashCommands/Attributes/CommandGroup.cs b/src/Discord.Net.Commands/SlashCommands/Attributes/CommandGroup.cs new file mode 100644 index 000000000..8555862b5 --- /dev/null +++ b/src/Discord.Net.Commands/SlashCommands/Attributes/CommandGroup.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.SlashCommands +{ + /// + /// Defines the current as being a group of slash commands. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class CommandGroup : Attribute + { + /// + /// The name of this slash command. + /// + public string groupName; + + /// + /// The description of this slash command. + /// + public string description; + + /// + /// Tells the that this class/function is a slash command. + /// + /// The name of this slash command. + public CommandGroup(string groupName, string description = "No description.") + { + this.groupName = groupName.ToLower(); + this.description = description; + } + } +} diff --git a/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs b/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs index 3e7110aac..d8adf797f 100644 --- a/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs +++ b/src/Discord.Net.Commands/SlashCommands/Attributes/SlashCommand.cs @@ -9,7 +9,7 @@ namespace Discord.SlashCommands /// /// Defines the current class or function as a slash command. /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class SlashCommand : Attribute { /// @@ -28,7 +28,7 @@ namespace Discord.SlashCommands /// The name of this slash command. public SlashCommand(string commandName, string description = "No description.") { - this.commandName = commandName; + this.commandName = commandName.ToLower(); this.description = description; } } diff --git a/src/Discord.Net.Commands/SlashCommands/Builders/SlashCommandBuilder.cs b/src/Discord.Net.Commands/SlashCommands/Builders/SlashCommandBuilder.cs index 6087825b4..e9c0384bc 100644 --- a/src/Discord.Net.Commands/SlashCommands/Builders/SlashCommandBuilder.cs +++ b/src/Discord.Net.Commands/SlashCommands/Builders/SlashCommandBuilder.cs @@ -377,8 +377,8 @@ namespace Discord.Commands.Builders { bool isSubType = this.Type == ApplicationCommandOptionType.SubCommand || this.Type == ApplicationCommandOptionType.SubCommandGroup; - if (isSubType && (Options == null || !Options.Any())) - throw new ArgumentException(nameof(Options), "SubCommands/SubCommandGroups must have at least one option"); + if (this.Type == ApplicationCommandOptionType.SubCommandGroup && (Options == null || !Options.Any())) + throw new ArgumentException(nameof(Options), "SubCommandGroups must have at least one option"); if (!isSubType && (Options != null && Options.Any())) throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type"); @@ -448,20 +448,9 @@ namespace Discord.Commands.Builders return this; } - public SlashCommandOptionBuilder WithName(string Name, int Value) + public SlashCommandOptionBuilder WithName(string Name) { - if (Choices == null) - Choices = new List(); - - if (Choices.Count >= MaxChoiceCount) - throw new ArgumentOutOfRangeException(nameof(Choices), $"Cannot add more than {MaxChoiceCount} choices!"); - - Choices.Add(new ApplicationCommandOptionChoiceProperties() - { - Name = Name, - Value = Value - }); - + this.Name = Name; return this; } diff --git a/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs b/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs index eee54d32b..c475a9578 100644 --- a/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs +++ b/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs @@ -67,7 +67,7 @@ namespace Discord.SlashCommands /// Execute the function based on the interaction data we get. /// /// Interaction data from interaction - public async Task ExecuteAsync(SocketInteractionData data) + public async Task ExecuteAsync(IReadOnlyCollection data) { // List of arguments to be passed to the Delegate List args = new List(); @@ -101,14 +101,14 @@ namespace Discord.SlashCommands /// /// Get the interaction data from the name of the parameter we want to fill in. /// - private bool TryGetInteractionDataOption(SocketInteractionData data, string name, out SocketInteractionDataOption dataOption) + private bool TryGetInteractionDataOption(IReadOnlyCollection data, string name, out SocketInteractionDataOption dataOption) { - if (data.Options == null) + if (data == null) { dataOption = null; return false; } - foreach (var option in data.Options) + foreach (var option in data) { if (option.Name == name.ToLower()) { @@ -136,5 +136,23 @@ namespace Discord.SlashCommands return builder.Build(); } + + /// + /// Build the command AS A SUBCOMMAND and put it in a state in which we can use to define it to Discord. + /// + public SlashCommandOptionBuilder BuildSubCommand() + { + SlashCommandOptionBuilder builder = new SlashCommandOptionBuilder(); + builder.WithName(Name); + builder.WithDescription(Description); + builder.WithType(ApplicationCommandOptionType.SubCommand); + builder.Options = new List(); + foreach (var parameter in Parameters) + { + builder.AddOption(parameter); + } + + return builder; + } } } diff --git a/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs b/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs index baf4010f4..ffed450dc 100644 --- a/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs +++ b/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs @@ -1,6 +1,10 @@ +using Discord.Commands; +using Discord.Commands.Builders; +using Discord.WebSocket; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; @@ -8,11 +12,22 @@ namespace Discord.SlashCommands { public class SlashModuleInfo { + public const string PathSeperator = "//"; + public const string RootModuleName = "TOP"; + public const string RootCommandPrefix = RootModuleName + PathSeperator; + public SlashModuleInfo(SlashCommandService service) { Service = service; } + public bool isCommandGroup { get; set; } = false; + public CommandGroup commandGroupInfo { get; set; } + + public SlashModuleInfo parent { get; set; } + public List commandGroups { get; set; } + public string Path { get; set; } = RootModuleName; + /// /// Gets the command service associated with this module. /// @@ -27,7 +42,7 @@ namespace Discord.SlashCommands /// Used to set context. /// public ISlashCommandModule userCommandModule; - + public Type moduleType; public void SetCommands(List commands) { @@ -43,5 +58,74 @@ namespace Discord.SlashCommands this.userCommandModule = userCommandModule as ISlashCommandModule; } } + public void SetType(Type type) + { + moduleType = type; + } + + public void MakeCommandGroup(CommandGroup commandGroupInfo, SlashModuleInfo parent) + { + isCommandGroup = true; + this.commandGroupInfo = commandGroupInfo; + this.parent = parent; + } + public void SetSubCommandGroups(List subCommandGroups) + { + // this.commandGroups = new List(subCommandGroups); + this.commandGroups = subCommandGroups; + } + + public void MakePath() + { + Path = parent.Path + SlashModuleInfo.PathSeperator + commandGroupInfo.groupName; + } + + public List BuildCommands() + { + List builtCommands = new List(); + foreach (var command in Commands) + { + builtCommands.Add(command.BuildCommand()); + } + foreach(var commandGroup in commandGroups) + { + builtCommands.Add(commandGroup.BuildTopLevelCommandGroup()); + } + return builtCommands; + } + + public SlashCommandCreationProperties BuildTopLevelCommandGroup() + { + SlashCommandBuilder builder = new SlashCommandBuilder(); + builder.WithName(commandGroupInfo.groupName); + builder.WithDescription(commandGroupInfo.description); + foreach (var command in Commands) + { + builder.AddOption(command.BuildSubCommand()); + } + foreach (var commandGroup in commandGroups) + { + builder.AddOption(commandGroup.BuildNestedCommandGroup()); + } + return builder.Build(); + } + + private SlashCommandOptionBuilder BuildNestedCommandGroup() + { + SlashCommandOptionBuilder builder = new SlashCommandOptionBuilder(); + builder.WithName(commandGroupInfo.groupName); + builder.WithDescription(commandGroupInfo.description); + builder.WithType(ApplicationCommandOptionType.SubCommandGroup); + foreach (var command in Commands) + { + builder.AddOption(command.BuildSubCommand()); + } + foreach (var commandGroup in commandGroups) + { + builder.AddOption(commandGroup.BuildNestedCommandGroup()); + } + + return builder; + } } } diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs index cddc2350e..c14c1527c 100644 --- a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs +++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs @@ -41,20 +41,64 @@ namespace Discord.SlashCommands /// public async Task ExecuteAsync(SocketInteraction interaction) { - // First, get the info about this command, if it exists SlashCommandInfo commandInfo; - if (commandDefs.TryGetValue(interaction.Data.Name, out commandInfo)) + // Get the name of the actual command - be it a normal slash command or subcommand, and return the options we can give it. + string name = GetSearchName(interaction.Data, out var resultingOptions); + if (commandDefs.TryGetValue(name, out commandInfo)) { // Then, set the context in which the command will be executed commandInfo.Module.userCommandModule.SetContext(interaction); // Then run the command and pass the interaction data over to the CommandInfo class - return await commandInfo.ExecuteAsync(interaction.Data).ConfigureAwait(false); + return await commandInfo.ExecuteAsync(resultingOptions).ConfigureAwait(false); } else { return SearchResult.FromError(CommandError.UnknownCommand, $"There is no registered slash command with the name {interaction.Data.Name}"); } } + /// + /// Get the name of the command we want to search for - be it a normal slash command or a sub command. Returns as out the options to be given to the method. + /// /// + /// + /// + /// + private string GetSearchName(SocketInteractionData interactionData, out IReadOnlyCollection resultingOptions) + { + string nameToSearch = SlashModuleInfo.RootCommandPrefix + interactionData.Name; + var options = interactionData.Options; + while(options != null && options.Count == 1) + { + string newName = nameToSearch + SlashModuleInfo.PathSeperator + GetFirstOption(options).Name; + if (AnyKeyContains(commandDefs,newName)) + { + nameToSearch = newName; + options = GetFirstOption(options).Options; + } + else + { + break; + } + } + resultingOptions = options; + return nameToSearch; + } + + private bool AnyKeyContains(Dictionary commandDefs, string newName) + { + foreach (var pair in commandDefs) + { + if (pair.Key.Contains(newName)) + return true; + } + return false; + } + + private SocketInteractionDataOption GetFirstOption(IReadOnlyCollection options) + { + var it = options.GetEnumerator(); + it.MoveNext(); + return it.Current; + } /// /// Registers all previously scanned commands. @@ -66,7 +110,7 @@ namespace Discord.SlashCommands try { - await SlashCommandServiceHelper.RegisterCommands(socketClient, commandDefs, this,registrationOptions).ConfigureAwait(false); + await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this,registrationOptions).ConfigureAwait(false); } finally { diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs index 2d40185f3..0cef8f8a4 100644 --- a/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs +++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs @@ -40,7 +40,8 @@ namespace Discord.SlashCommands { // See if the base type (SlashCommandInfo) implements interface ISlashCommandModule return typeInfo.BaseType.GetInterfaces() - .Any(n => n == typeof(ISlashCommandModule)); + .Any(n => n == typeof(ISlashCommandModule)) && + typeInfo.GetCustomAttributes(typeof(CommandGroup)).Count() == 0; } /// @@ -49,18 +50,70 @@ namespace Discord.SlashCommands public static async Task> InstantiateModules(IReadOnlyList types, SlashCommandService slashCommandService) { var result = new Dictionary(); + // Here we get all modules thate are NOT sub command groups foreach (Type userModuleType in types) { SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService); + moduleInfo.SetType(userModuleType); // If they want a constructor with different parameters, this is the place to add them. object instance = userModuleType.GetConstructor(Type.EmptyTypes).Invoke(null); moduleInfo.SetCommandModule(instance); + // ,, + moduleInfo.SetSubCommandGroups(InstantiateSubCommands(userModuleType, moduleInfo, slashCommandService)); + result.Add(userModuleType, moduleInfo); } return result; } + public static List InstantiateSubCommands(Type rootModule,SlashModuleInfo rootModuleInfo, SlashCommandService slashCommandService) + { + List commandGroups = new List(); + foreach(Type commandGroupType in rootModule.GetNestedTypes()) + { + if(TryGetCommandGroupAttribute(commandGroupType, out CommandGroup commandGroup)) + { + SlashModuleInfo groupInfo = new SlashModuleInfo(slashCommandService); + groupInfo.SetType(commandGroupType); + + object instance = commandGroupType.GetConstructor(Type.EmptyTypes).Invoke(null); + groupInfo.SetCommandModule(instance); + + groupInfo.MakeCommandGroup(commandGroup,rootModuleInfo); + groupInfo.MakePath(); + + groupInfo.SetSubCommandGroups(InstantiateSubCommands(commandGroupType, groupInfo, slashCommandService)); + + commandGroups.Add(groupInfo); + } + } + return commandGroups; + } + public static bool TryGetCommandGroupAttribute(Type module, out CommandGroup commandGroup) + { + if(!module.IsPublic && !module.IsNestedPublic) + { + commandGroup = null; + return false; + } + + var commandGroupAttributes = module.GetCustomAttributes(typeof(CommandGroup)); + if( commandGroupAttributes.Count() == 0) + { + commandGroup = null; + return false; + } + else if(commandGroupAttributes.Count() > 1) + { + throw new Exception($"Too many CommandGroup attributes on a single class ({module.FullName}). It can only contain one!"); + } + else + { + commandGroup = commandGroupAttributes.First() as CommandGroup; + return true; + } + } /// /// Prepare all of the commands and register them internally. @@ -76,37 +129,52 @@ namespace Discord.SlashCommands SlashModuleInfo moduleInfo; if (moduleDefs.TryGetValue(userModule, out moduleInfo)) { - // 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(); - List commandInfos = new List(); - foreach (var commandMethod in commandMethods) - { - SlashCommand 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); - - SlashCommandInfo commandInfo = new SlashCommandInfo( - module: moduleInfo, - name: slashCommand.commandName, - 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 - ); - - result.Add(slashCommand.commandName, commandInfo); - commandInfos.Add(commandInfo); - } - } + var commandInfos = RegisterSameLevelCommands(result, userModule, moduleInfo); moduleInfo.SetCommands(commandInfos); + CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService); } } return result; } + public static void CreateSubCommandInfos(Dictionary result, List subCommandGroups, SlashCommandService slashCommandService) + { + foreach (var subCommandGroup in subCommandGroups) + { + var commandInfos = RegisterSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup); + subCommandGroup.SetCommands(commandInfos); + CreateSubCommandInfos(result, subCommandGroup.commandGroups, slashCommandService); + } + } + private static List RegisterSameLevelCommands(Dictionary result, TypeInfo userModule, SlashModuleInfo moduleInfo) + { + var commandMethods = userModule.GetMethods(); + List commandInfos = new List(); + foreach (var commandMethod in commandMethods) + { + SlashCommand 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); + + SlashCommandInfo commandInfo = new SlashCommandInfo( + module: moduleInfo, + name: slashCommand.commandName, + 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 + ); + + result.Add(commandInfo.Module.Path + SlashModuleInfo.PathSeperator + commandInfo.Name, commandInfo); + commandInfos.Add(commandInfo); + } + } + + return commandInfos; + } + /// /// Determines wheater a method can be clasified as a slash command /// @@ -187,7 +255,6 @@ namespace Discord.SlashCommands throw new Exception($"Got parameter type other than int, string, bool, guild, role, or user. {methodParameter.Name}"); } - /// /// Creae a delegate from methodInfo. Taken from /// https://stackoverflow.com/a/40579063/8455128 @@ -216,7 +283,7 @@ namespace Discord.SlashCommands return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); } - public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary commandDefs, SlashCommandService slashCommandService, CommandRegistrationOptions options) + public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary rootModuleInfos, Dictionary commandDefs, SlashCommandService slashCommandService, CommandRegistrationOptions options) { // Get existing commmands ulong devGuild = 386658607338618891; @@ -237,28 +304,39 @@ namespace Discord.SlashCommands // 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)) + !commandDefs.ContainsKey(SlashModuleInfo.RootCommandPrefix + existingCommand.Name)) { await existingCommand.DeleteAsync(); } } } - foreach (var entry in commandDefs) + //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(); + // + // await socketClient.Rest.CreateGuildCommand(command, devGuild).ConfigureAwait(false); + // } + //} + foreach (var pair in rootModuleInfos) { - 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 + var rootModuleInfo = pair.Value; + List builtCommands = rootModuleInfo.BuildCommands(); + foreach (var builtCommand in builtCommands) { - SlashCommandInfo slashCommandInfo = entry.Value; - SlashCommandCreationProperties command = slashCommandInfo.BuildCommand(); // TODO: Implement Global and Guild Commands. - await socketClient.Rest.CreateGuildCommand(command, devGuild).ConfigureAwait(false); + await socketClient.Rest.CreateGuildCommand(builtCommand, devGuild).ConfigureAwait(false); } } + return; } } diff --git a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs index 789c5549d..cde33f6d6 100644 --- a/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs +++ b/src/Discord.Net.Rest/API/Common/ApplicationCommandOption.cs @@ -67,7 +67,9 @@ namespace Discord.API ? option.Options.Select(x => new ApplicationCommandOption(x)).ToArray() : Optional.Unspecified; - this.Required = option.Required.Value; + this.Required = option.Required.HasValue + ? option.Required.Value + : Optional.Unspecified; this.Default = option.Default.HasValue ? option.Default.Value : Optional.Unspecified;