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.pull/1733/head^2^2
| @@ -4,6 +4,7 @@ using Discord.SlashCommands; | |||||
| using Discord.WebSocket; | using Discord.WebSocket; | ||||
| using Microsoft.Extensions.DependencyInjection; | using Microsoft.Extensions.DependencyInjection; | ||||
| using System; | using System; | ||||
| using System.Collections.Generic; | |||||
| using System.Reflection; | using System.Reflection; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -28,6 +29,8 @@ namespace SlashCommandsExample | |||||
| socketClient.Log += SocketClient_Log; | socketClient.Log += SocketClient_Log; | ||||
| _commands.Log += SocketClient_Log; | _commands.Log += SocketClient_Log; | ||||
| socketClient.InteractionCreated += InteractionHandler; | socketClient.InteractionCreated += InteractionHandler; | ||||
| socketClient.Ready += RegisterCommand; | |||||
| // This is for dev purposes. | // This is for dev purposes. | ||||
| // To avoid the situation in which you accidentally push your bot token to upstream, you can use | // To avoid the situation in which you accidentally push your bot token to upstream, you can use | ||||
| // EnviromentVariables to store your key. | // EnviromentVariables to store your key. | ||||
| @@ -45,6 +48,72 @@ namespace SlashCommandsExample | |||||
| // EnvironmentVariableTarget.User); | // 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<ApplicationCommandOptionProperties>() | |||||
| { | |||||
| new ApplicationCommandOptionProperties() | |||||
| { | |||||
| Name = "usr", | |||||
| Description = "User Folder", | |||||
| Type = ApplicationCommandOptionType.SubCommandGroup, | |||||
| Options = new List<ApplicationCommandOptionProperties>() | |||||
| { | |||||
| // 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<ApplicationCommandOptionProperties>() | |||||
| { | |||||
| 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<ApplicationCommandOptionProperties>() | |||||
| { | |||||
| 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() | public async Task RunAsync() | ||||
| { | { | ||||
| await socketClient.LoginAsync(TokenType.Bot, botToken); | await socketClient.LoginAsync(TokenType.Bot, botToken); | ||||
| @@ -36,6 +36,32 @@ namespace SlashCommandsExample.Modules | |||||
| await Reply($"You gave me:\r\n {boolean}, {integer}, {myString}, <#{channel?.Id}>, {user?.Mention}, {role?.Mention}"); | await Reply($"You gave me:\r\n {boolean}, {integer}, {myString}, <#{channel?.Id}>, {user?.Mention}, {role?.Mention}"); | ||||
| } | } | ||||
| [CommandGroup("root")] | |||||
| public class DevModule_Root : SlashCommandModule<SocketInteraction> | |||||
| { | |||||
| [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<SocketInteraction> | |||||
| { | |||||
| [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\"."); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| /* | /* | ||||
| @@ -0,0 +1,35 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | |||||
| { | |||||
| /// <summary> | |||||
| /// Defines the current as being a group of slash commands. | |||||
| /// </summary> | |||||
| [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] | |||||
| public class CommandGroup : Attribute | |||||
| { | |||||
| /// <summary> | |||||
| /// The name of this slash command. | |||||
| /// </summary> | |||||
| public string groupName; | |||||
| /// <summary> | |||||
| /// The description of this slash command. | |||||
| /// </summary> | |||||
| public string description; | |||||
| /// <summary> | |||||
| /// Tells the <see cref="SlashCommandService"/> that this class/function is a slash command. | |||||
| /// </summary> | |||||
| /// <param name="commandName">The name of this slash command.</param> | |||||
| public CommandGroup(string groupName, string description = "No description.") | |||||
| { | |||||
| this.groupName = groupName.ToLower(); | |||||
| this.description = description; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -9,7 +9,7 @@ namespace Discord.SlashCommands | |||||
| /// <summary> | /// <summary> | ||||
| /// Defines the current class or function as a slash command. | /// Defines the current class or function as a slash command. | ||||
| /// </summary> | /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] | |||||
| [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] | |||||
| public class SlashCommand : Attribute | public class SlashCommand : Attribute | ||||
| { | { | ||||
| /// <summary> | /// <summary> | ||||
| @@ -28,7 +28,7 @@ namespace Discord.SlashCommands | |||||
| /// <param name="commandName">The name of this slash command.</param> | /// <param name="commandName">The name of this slash command.</param> | ||||
| public SlashCommand(string commandName, string description = "No description.") | public SlashCommand(string commandName, string description = "No description.") | ||||
| { | { | ||||
| this.commandName = commandName; | |||||
| this.commandName = commandName.ToLower(); | |||||
| this.description = description; | this.description = description; | ||||
| } | } | ||||
| } | } | ||||
| @@ -377,8 +377,8 @@ namespace Discord.Commands.Builders | |||||
| { | { | ||||
| bool isSubType = this.Type == ApplicationCommandOptionType.SubCommand || this.Type == ApplicationCommandOptionType.SubCommandGroup; | 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())) | if (!isSubType && (Options != null && Options.Any())) | ||||
| throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type"); | throw new ArgumentException(nameof(Options), $"Cannot have options on {Type} type"); | ||||
| @@ -448,20 +448,9 @@ namespace Discord.Commands.Builders | |||||
| return this; | return this; | ||||
| } | } | ||||
| public SlashCommandOptionBuilder WithName(string Name, int Value) | |||||
| public SlashCommandOptionBuilder WithName(string Name) | |||||
| { | { | ||||
| if (Choices == null) | |||||
| Choices = new List<ApplicationCommandOptionChoiceProperties>(); | |||||
| 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; | return this; | ||||
| } | } | ||||
| @@ -67,7 +67,7 @@ namespace Discord.SlashCommands | |||||
| /// Execute the function based on the interaction data we get. | /// Execute the function based on the interaction data we get. | ||||
| /// </summary> | /// </summary> | ||||
| /// <param name="data">Interaction data from interaction</param> | /// <param name="data">Interaction data from interaction</param> | ||||
| public async Task<IResult> ExecuteAsync(SocketInteractionData data) | |||||
| public async Task<IResult> ExecuteAsync(IReadOnlyCollection<SocketInteractionDataOption> data) | |||||
| { | { | ||||
| // List of arguments to be passed to the Delegate | // List of arguments to be passed to the Delegate | ||||
| List<object> args = new List<object>(); | List<object> args = new List<object>(); | ||||
| @@ -101,14 +101,14 @@ namespace Discord.SlashCommands | |||||
| /// <summary> | /// <summary> | ||||
| /// Get the interaction data from the name of the parameter we want to fill in. | /// Get the interaction data from the name of the parameter we want to fill in. | ||||
| /// </summary> | /// </summary> | ||||
| private bool TryGetInteractionDataOption(SocketInteractionData data, string name, out SocketInteractionDataOption dataOption) | |||||
| private bool TryGetInteractionDataOption(IReadOnlyCollection<SocketInteractionDataOption> data, string name, out SocketInteractionDataOption dataOption) | |||||
| { | { | ||||
| if (data.Options == null) | |||||
| if (data == null) | |||||
| { | { | ||||
| dataOption = null; | dataOption = null; | ||||
| return false; | return false; | ||||
| } | } | ||||
| foreach (var option in data.Options) | |||||
| foreach (var option in data) | |||||
| { | { | ||||
| if (option.Name == name.ToLower()) | if (option.Name == name.ToLower()) | ||||
| { | { | ||||
| @@ -136,5 +136,23 @@ namespace Discord.SlashCommands | |||||
| return builder.Build(); | return builder.Build(); | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Build the command AS A SUBCOMMAND and put it in a state in which we can use to define it to Discord. | |||||
| /// </summary> | |||||
| public SlashCommandOptionBuilder BuildSubCommand() | |||||
| { | |||||
| SlashCommandOptionBuilder builder = new SlashCommandOptionBuilder(); | |||||
| builder.WithName(Name); | |||||
| builder.WithDescription(Description); | |||||
| builder.WithType(ApplicationCommandOptionType.SubCommand); | |||||
| builder.Options = new List<SlashCommandOptionBuilder>(); | |||||
| foreach (var parameter in Parameters) | |||||
| { | |||||
| builder.AddOption(parameter); | |||||
| } | |||||
| return builder; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,6 +1,10 @@ | |||||
| 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; | ||||
| using System.Reflection; | |||||
| using System.Text; | using System.Text; | ||||
| using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
| @@ -8,11 +12,22 @@ namespace Discord.SlashCommands | |||||
| { | { | ||||
| public class SlashModuleInfo | public class SlashModuleInfo | ||||
| { | { | ||||
| public const string PathSeperator = "//"; | |||||
| public const string RootModuleName = "TOP"; | |||||
| public const string RootCommandPrefix = RootModuleName + PathSeperator; | |||||
| public SlashModuleInfo(SlashCommandService service) | public SlashModuleInfo(SlashCommandService service) | ||||
| { | { | ||||
| Service = service; | Service = service; | ||||
| } | } | ||||
| public bool isCommandGroup { get; set; } = false; | |||||
| public CommandGroup commandGroupInfo { get; set; } | |||||
| public SlashModuleInfo parent { get; set; } | |||||
| public List<SlashModuleInfo> commandGroups { get; set; } | |||||
| public string Path { get; set; } = RootModuleName; | |||||
| /// <summary> | /// <summary> | ||||
| /// Gets the command service associated with this module. | /// Gets the command service associated with this module. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -27,7 +42,7 @@ namespace Discord.SlashCommands | |||||
| /// Used to set context. | /// Used to set context. | ||||
| /// </summary> | /// </summary> | ||||
| public ISlashCommandModule userCommandModule; | public ISlashCommandModule userCommandModule; | ||||
| public Type moduleType; | |||||
| public void SetCommands(List<SlashCommandInfo> commands) | public void SetCommands(List<SlashCommandInfo> commands) | ||||
| { | { | ||||
| @@ -43,5 +58,74 @@ namespace Discord.SlashCommands | |||||
| this.userCommandModule = userCommandModule as ISlashCommandModule; | 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<SlashModuleInfo> subCommandGroups) | |||||
| { | |||||
| // this.commandGroups = new List<SlashModuleInfo>(subCommandGroups); | |||||
| this.commandGroups = subCommandGroups; | |||||
| } | |||||
| public void MakePath() | |||||
| { | |||||
| Path = parent.Path + SlashModuleInfo.PathSeperator + commandGroupInfo.groupName; | |||||
| } | |||||
| public List<SlashCommandCreationProperties> BuildCommands() | |||||
| { | |||||
| List<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>(); | |||||
| 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; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -41,20 +41,64 @@ namespace Discord.SlashCommands | |||||
| /// <returns></returns> | /// <returns></returns> | ||||
| public async Task<IResult> ExecuteAsync(SocketInteraction interaction) | public async Task<IResult> ExecuteAsync(SocketInteraction interaction) | ||||
| { | { | ||||
| // First, get the info about this command, if it exists | |||||
| SlashCommandInfo commandInfo; | 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 | // 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 and pass the interaction data over to the CommandInfo class | // 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 | 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> | |||||
| /// 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. | |||||
| /// /// </summary> | |||||
| /// <param name="interactionData"></param> | |||||
| /// <param name="resultingOptions"></param> | |||||
| /// <returns></returns> | |||||
| private string GetSearchName(SocketInteractionData interactionData, out IReadOnlyCollection<SocketInteractionDataOption> 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<string, SlashCommandInfo> commandDefs, string newName) | |||||
| { | |||||
| foreach (var pair in commandDefs) | |||||
| { | |||||
| if (pair.Key.Contains(newName)) | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| private SocketInteractionDataOption GetFirstOption(IReadOnlyCollection<SocketInteractionDataOption> options) | |||||
| { | |||||
| var it = options.GetEnumerator(); | |||||
| it.MoveNext(); | |||||
| return it.Current; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Registers all previously scanned commands. | /// Registers all previously scanned commands. | ||||
| @@ -66,7 +110,7 @@ namespace Discord.SlashCommands | |||||
| try | try | ||||
| { | { | ||||
| await SlashCommandServiceHelper.RegisterCommands(socketClient, commandDefs, this,registrationOptions).ConfigureAwait(false); | |||||
| await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this,registrationOptions).ConfigureAwait(false); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| @@ -40,7 +40,8 @@ namespace Discord.SlashCommands | |||||
| { | { | ||||
| // See if the base type (SlashCommandInfo<T>) implements interface ISlashCommandModule | // See if the base type (SlashCommandInfo<T>) implements interface ISlashCommandModule | ||||
| return typeInfo.BaseType.GetInterfaces() | return typeInfo.BaseType.GetInterfaces() | ||||
| .Any(n => n == typeof(ISlashCommandModule)); | |||||
| .Any(n => n == typeof(ISlashCommandModule)) && | |||||
| typeInfo.GetCustomAttributes(typeof(CommandGroup)).Count() == 0; | |||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| @@ -49,18 +50,70 @@ namespace Discord.SlashCommands | |||||
| public static async Task<Dictionary<Type, SlashModuleInfo>> InstantiateModules(IReadOnlyList<TypeInfo> types, SlashCommandService slashCommandService) | public static async Task<Dictionary<Type, SlashModuleInfo>> InstantiateModules(IReadOnlyList<TypeInfo> types, SlashCommandService slashCommandService) | ||||
| { | { | ||||
| var result = new Dictionary<Type, SlashModuleInfo>(); | var result = new Dictionary<Type, SlashModuleInfo>(); | ||||
| // Here we get all modules thate are NOT sub command groups | |||||
| foreach (Type userModuleType in types) | foreach (Type userModuleType in types) | ||||
| { | { | ||||
| SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService); | SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService); | ||||
| moduleInfo.SetType(userModuleType); | |||||
| // If they want a constructor with different parameters, this is the place to add them. | // If they want a constructor with different parameters, this is the place to add them. | ||||
| object instance = userModuleType.GetConstructor(Type.EmptyTypes).Invoke(null); | object instance = userModuleType.GetConstructor(Type.EmptyTypes).Invoke(null); | ||||
| moduleInfo.SetCommandModule(instance); | moduleInfo.SetCommandModule(instance); | ||||
| // ,, | |||||
| moduleInfo.SetSubCommandGroups(InstantiateSubCommands(userModuleType, moduleInfo, slashCommandService)); | |||||
| result.Add(userModuleType, moduleInfo); | result.Add(userModuleType, moduleInfo); | ||||
| } | } | ||||
| return result; | return result; | ||||
| } | } | ||||
| public static List<SlashModuleInfo> InstantiateSubCommands(Type rootModule,SlashModuleInfo rootModuleInfo, SlashCommandService slashCommandService) | |||||
| { | |||||
| List<SlashModuleInfo> commandGroups = new List<SlashModuleInfo>(); | |||||
| 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; | |||||
| } | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Prepare all of the commands and register them internally. | /// Prepare all of the commands and register them internally. | ||||
| @@ -76,37 +129,52 @@ namespace Discord.SlashCommands | |||||
| SlashModuleInfo moduleInfo; | SlashModuleInfo moduleInfo; | ||||
| if (moduleDefs.TryGetValue(userModule, out 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<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | |||||
| 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); | moduleInfo.SetCommands(commandInfos); | ||||
| CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService); | |||||
| } | } | ||||
| } | } | ||||
| return result; | return result; | ||||
| } | } | ||||
| public static void CreateSubCommandInfos(Dictionary<string, SlashCommandInfo> result, List<SlashModuleInfo> 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<SlashCommandInfo> RegisterSameLevelCommands(Dictionary<string, SlashCommandInfo> result, TypeInfo userModule, SlashModuleInfo moduleInfo) | |||||
| { | |||||
| var commandMethods = userModule.GetMethods(); | |||||
| List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | |||||
| 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; | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Determines wheater a method can be clasified as a slash command | /// Determines wheater a method can be clasified as a slash command | ||||
| /// </summary> | /// </summary> | ||||
| @@ -187,7 +255,6 @@ namespace Discord.SlashCommands | |||||
| throw new Exception($"Got parameter type other than int, string, bool, guild, role, or user. {methodParameter.Name}"); | 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 | ||||
| @@ -216,7 +283,7 @@ 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(DiscordSocketClient socketClient, Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, CommandRegistrationOptions options) | |||||
| public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary<Type, SlashModuleInfo> rootModuleInfos, Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, CommandRegistrationOptions options) | |||||
| { | { | ||||
| // Get existing commmands | // Get existing commmands | ||||
| ulong devGuild = 386658607338618891; | ulong devGuild = 386658607338618891; | ||||
| @@ -237,28 +304,39 @@ namespace Discord.SlashCommands | |||||
| // or if the existing command isn't re-defined (probably code deleted by user) | // or if the existing command isn't re-defined (probably code deleted by user) | ||||
| // remove it from discord. | // remove it from discord. | ||||
| if(options.OldCommands == OldCommandOptions.WIPE || | if(options.OldCommands == OldCommandOptions.WIPE || | ||||
| !commandDefs.ContainsKey(existingCommand.Name)) | |||||
| !commandDefs.ContainsKey(SlashModuleInfo.RootCommandPrefix + existingCommand.Name)) | |||||
| { | { | ||||
| await existingCommand.DeleteAsync(); | 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<SlashCommandCreationProperties> builtCommands = rootModuleInfo.BuildCommands(); | |||||
| foreach (var builtCommand in builtCommands) | |||||
| { | { | ||||
| SlashCommandInfo slashCommandInfo = entry.Value; | |||||
| SlashCommandCreationProperties command = slashCommandInfo.BuildCommand(); | |||||
| // TODO: Implement Global and Guild Commands. | // TODO: Implement Global and Guild Commands. | ||||
| await socketClient.Rest.CreateGuildCommand(command, devGuild).ConfigureAwait(false); | |||||
| await socketClient.Rest.CreateGuildCommand(builtCommand, devGuild).ConfigureAwait(false); | |||||
| } | } | ||||
| } | } | ||||
| return; | return; | ||||
| } | } | ||||
| } | } | ||||
| @@ -67,7 +67,9 @@ namespace Discord.API | |||||
| ? option.Options.Select(x => new ApplicationCommandOption(x)).ToArray() | ? option.Options.Select(x => new ApplicationCommandOption(x)).ToArray() | ||||
| : Optional<ApplicationCommandOption[]>.Unspecified; | : Optional<ApplicationCommandOption[]>.Unspecified; | ||||
| this.Required = option.Required.Value; | |||||
| this.Required = option.Required.HasValue | |||||
| ? option.Required.Value | |||||
| : Optional<bool>.Unspecified; | |||||
| this.Default = option.Default.HasValue | this.Default = option.Default.HasValue | ||||
| ? option.Default.Value | ? option.Default.Value | ||||
| : Optional<bool>.Unspecified; | : Optional<bool>.Unspecified; | ||||