From c76fdec8d6e8da992577386ffa5736d38dd96092 Mon Sep 17 00:00:00 2001 From: Cosma George Date: Tue, 16 Feb 2021 15:24:41 +0200 Subject: [PATCH] Implemented Global Attribute and added a way to register all commands in particualr guilds. Details: There's nothing much to say, just added a variable to track with modules/commands/command groups have the global attribute and depending on that I register them globally or locally. There is currently no way to implement two commands with the same name, but one on the guild level and one on the global level. There is also currently no implemented way to register only some commands to only some guilds - this could be done through attributes, but another solution for users who want to do complex stuff would be to just give them the built commands and let them maually register them as they see fit. --- SlashCommandsExample/DiscordClient.cs | 6 +- SlashCommandsExample/Modules/DevModule.cs | 4 + .../SlashCommands/Attributes/Global.cs | 16 +++ .../SlashCommands/Info/SlashCommandInfo.cs | 4 +- .../SlashCommands/Info/SlashModuleInfo.cs | 15 +- .../SlashCommands/SlashCommandService.cs | 4 +- .../SlashCommandServiceHelper.cs | 132 ++++++++++++------ .../SlashCommandCreationProperties.cs | 4 + 8 files changed, 137 insertions(+), 48 deletions(-) create mode 100644 src/Discord.Net.Commands/SlashCommands/Attributes/Global.cs diff --git a/SlashCommandsExample/DiscordClient.cs b/SlashCommandsExample/DiscordClient.cs index 309cbc1a6..bbdc88b14 100644 --- a/SlashCommandsExample/DiscordClient.cs +++ b/SlashCommandsExample/DiscordClient.cs @@ -120,7 +120,11 @@ namespace SlashCommandsExample await socketClient.StartAsync(); await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); - await _commands.RegisterCommandsAsync(socketClient, CommandRegistrationOptions.Default); + await _commands.RegisterCommandsAsync(socketClient, new List() + { + 386658607338618891 + }, + new CommandRegistrationOptions(OldCommandOptions.DELETE_UNUSED,ExistingCommandOptions.OVERWRITE)); await Task.Delay(-1); } diff --git a/SlashCommandsExample/Modules/DevModule.cs b/SlashCommandsExample/Modules/DevModule.cs index b8c6cb08a..4ff82e0de 100644 --- a/SlashCommandsExample/Modules/DevModule.cs +++ b/SlashCommandsExample/Modules/DevModule.cs @@ -9,9 +9,12 @@ using System.Threading.Tasks; namespace SlashCommandsExample.Modules { + // You can make the whole module Global + //[Global] public class DevModule : SlashCommandModule { [SlashCommand("ping", "Ping the bot to see if it's alive!")] + [Global] public async Task PingAsync() { await Reply(":white_check_mark: **Bot Online**"); @@ -38,6 +41,7 @@ namespace SlashCommandsExample.Modules } [CommandGroup("root")] + //[Global] public class DevModule_Root : SlashCommandModule { [SlashCommand("rng", "Gives you a random number from this \"machine\"")] diff --git a/src/Discord.Net.Commands/SlashCommands/Attributes/Global.cs b/src/Discord.Net.Commands/SlashCommands/Attributes/Global.cs new file mode 100644 index 000000000..7fd35e898 --- /dev/null +++ b/src/Discord.Net.Commands/SlashCommands/Attributes/Global.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.SlashCommands +{ + /// + /// An Attribute that gives the command parameter a description. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] + public class Global : Attribute + { + } +} diff --git a/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs b/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs index c475a9578..f5542df52 100644 --- a/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs +++ b/src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs @@ -28,6 +28,7 @@ namespace Discord.SlashCommands /// public List Parameters { get; } + public bool isGlobal { get; } /// /// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters /// @@ -37,13 +38,14 @@ namespace Discord.SlashCommands /// public Func> callback; - public SlashCommandInfo(SlashModuleInfo module, string name, string description,List parameters , Delegate userMethod) + public SlashCommandInfo(SlashModuleInfo module, string name, string description,List parameters , Delegate userMethod , bool isGlobal = false) { Module = module; Name = name; Description = description; Parameters = parameters; this.userMethod = userMethod; + this.isGlobal = isGlobal; this.callback = new Func>(async (args) => { // Try-catch it and see what we get - error or success diff --git a/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs b/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs index ffed450dc..b62e11122 100644 --- a/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs +++ b/src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs @@ -28,6 +28,7 @@ namespace Discord.SlashCommands public List commandGroups { get; set; } public string Path { get; set; } = RootModuleName; + public bool isGlobal { get; set; } = false; /// /// Gets the command service associated with this module. /// @@ -85,11 +86,21 @@ namespace Discord.SlashCommands List builtCommands = new List(); foreach (var command in Commands) { - builtCommands.Add(command.BuildCommand()); + var builtCommand = command.BuildCommand(); + if (isGlobal || command.isGlobal) + { + builtCommand.Global = true; + } + builtCommands.Add(builtCommand); } foreach(var commandGroup in commandGroups) { - builtCommands.Add(commandGroup.BuildTopLevelCommandGroup()); + var builtCommand = commandGroup.BuildTopLevelCommandGroup(); + if (isGlobal || commandGroup.isGlobal) + { + builtCommand.Global = true; + } + builtCommands.Add(builtCommand); } return builtCommands; } diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs index c14c1527c..ce7721769 100644 --- a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs +++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs @@ -103,14 +103,14 @@ namespace Discord.SlashCommands /// /// Registers all previously scanned commands. /// - public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, CommandRegistrationOptions registrationOptions) + public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, List guildIDs, 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, moduleDefs, commandDefs, this,registrationOptions).ConfigureAwait(false); + await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this, guildIDs, registrationOptions).ConfigureAwait(false); } finally { diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs index 0cef8f8a4..a63455ff3 100644 --- a/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs +++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs @@ -59,10 +59,9 @@ namespace Discord.SlashCommands // 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.isGlobal = IsCommandModuleGlobal(userModuleType); - // ,, moduleInfo.SetSubCommandGroups(InstantiateSubCommands(userModuleType, moduleInfo, slashCommandService)); - result.Add(userModuleType, moduleInfo); } return result; @@ -82,9 +81,9 @@ namespace Discord.SlashCommands groupInfo.MakeCommandGroup(commandGroup,rootModuleInfo); groupInfo.MakePath(); + groupInfo.isGlobal = IsCommandModuleGlobal(commandGroupType); groupInfo.SetSubCommandGroups(InstantiateSubCommands(commandGroupType, groupInfo, slashCommandService)); - commandGroups.Add(groupInfo); } } @@ -114,7 +113,21 @@ namespace Discord.SlashCommands return true; } } - + public static bool IsCommandModuleGlobal(Type userModuleType) + { + // Verify that we only have one [Global] attribute + IEnumerable slashCommandAttributes = userModuleType.GetCustomAttributes(typeof(Global)); + if (slashCommandAttributes.Count() > 1) + { + throw new Exception("Too many Global attributes on a single method. It can only contain one!"); + } + // And at least one + if (slashCommandAttributes.Count() == 0) + { + return false; + } + return true; + } /// /// Prepare all of the commands and register them internally. /// @@ -129,7 +142,7 @@ namespace Discord.SlashCommands SlashModuleInfo moduleInfo; if (moduleDefs.TryGetValue(userModule, out moduleInfo)) { - var commandInfos = RegisterSameLevelCommands(result, userModule, moduleInfo); + var commandInfos = CreateSameLevelCommands(result, userModule, moduleInfo); moduleInfo.SetCommands(commandInfos); CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService); } @@ -140,12 +153,12 @@ namespace Discord.SlashCommands { foreach (var subCommandGroup in subCommandGroups) { - var commandInfos = RegisterSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup); + var commandInfos = CreateSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup); subCommandGroup.SetCommands(commandInfos); CreateSubCommandInfos(result, subCommandGroup.commandGroups, slashCommandService); } } - private static List RegisterSameLevelCommands(Dictionary result, TypeInfo userModule, SlashModuleInfo moduleInfo) + private static List CreateSameLevelCommands(Dictionary result, TypeInfo userModule, SlashModuleInfo moduleInfo) { var commandMethods = userModule.GetMethods(); List commandInfos = new List(); @@ -164,7 +177,8 @@ namespace Discord.SlashCommands 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, + isGlobal: IsCommandGlobal(commandMethod) ); result.Add(commandInfo.Module.Path + SlashModuleInfo.PathSeperator + commandInfo.Name, commandInfo); @@ -196,6 +210,21 @@ namespace Discord.SlashCommands slashCommand = slashCommandAttributes.First() as SlashCommand; return true; } + private static bool IsCommandGlobal(MethodInfo method) + { + // Verify that we only have one [Global] attribute + IEnumerable slashCommandAttributes = method.GetCustomAttributes(typeof(Global)); + if (slashCommandAttributes.Count() > 1) + { + throw new Exception("Too many Global attributes on a single method. It can only contain one!"); + } + // And at least one + if (slashCommandAttributes.Count() == 0) + { + return false; + } + return true; + } private static List ConstructCommandParameters(MethodInfo method) { // Prepare the final list of parameters @@ -283,57 +312,76 @@ namespace Discord.SlashCommands return Delegate.CreateDelegate(getType(types.ToArray()), target, methodInfo.Name); } - public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary rootModuleInfos, Dictionary commandDefs, SlashCommandService slashCommandService, CommandRegistrationOptions options) + public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary rootModuleInfos, Dictionary commandDefs, SlashCommandService slashCommandService, List guildIDs,CommandRegistrationOptions options) { - // Get existing commmands - ulong devGuild = 386658607338618891; - var existingCommands = await socketClient.Rest.GetGuildApplicationCommands(devGuild).ConfigureAwait(false); - List existingCommandNames = new List(); - foreach (var existingCommand in existingCommands) + // TODO: see how we should handle if user wants to register two commands with the same name, one global and one not. + List builtCommands = new List(); + foreach (var pair in rootModuleInfos) { - existingCommandNames.Add(existingCommand.Name); + var rootModuleInfo = pair.Value; + builtCommands.AddRange(rootModuleInfo.BuildCommands()); + } + + List existingGuildCommands = new List(); + List existingGlobalCommands = new List(); + existingGlobalCommands.AddRange(await socketClient.Rest.GetGlobalApplicationCommands().ConfigureAwait(false)); + foreach (ulong guildID in guildIDs) + { + existingGuildCommands.AddRange(await socketClient.Rest.GetGuildApplicationCommands(guildID).ConfigureAwait(false)); + } + if (options.ExistingCommands == ExistingCommandOptions.KEEP_EXISTING) + { + foreach (var existingCommand in existingGuildCommands) + { + builtCommands.RemoveAll(x => (!x.Global && x.Name == existingCommand.Name)); + } + foreach (var existingCommand in existingGlobalCommands) + { + builtCommands.RemoveAll(x => (x.Global && x.Name == 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) + foreach (var existingCommand in existingGuildCommands) { // If we want to wipe all commands - // or if the existing command isn't re-defined (probably code deleted by user) + // or if the existing command isn't re-defined and re-built // remove it from discord. - if(options.OldCommands == OldCommandOptions.WIPE || - !commandDefs.ContainsKey(SlashModuleInfo.RootCommandPrefix + existingCommand.Name)) + if (options.OldCommands == OldCommandOptions.WIPE || + // There are no commands which contain this existing command. + !builtCommands.Any( x => !x.Global && x.Name.Contains(SlashModuleInfo.PathSeperator + existingCommand.Name))) + { + await existingCommand.DeleteAsync(); + } + } + foreach (var existingCommand in existingGlobalCommands) + { + // If we want to wipe all commands + // or if the existing command isn't re-defined and re-built + // remove it from discord. + if (options.OldCommands == OldCommandOptions.WIPE || + // There are no commands which contain this existing command. + !builtCommands.Any(x => x.Global && x.Name.Contains(SlashModuleInfo.PathSeperator + 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(); - // - // await socketClient.Rest.CreateGuildCommand(command, devGuild).ConfigureAwait(false); - // } - //} - foreach (var pair in rootModuleInfos) + + foreach (var builtCommand in builtCommands) { - var rootModuleInfo = pair.Value; - List builtCommands = rootModuleInfo.BuildCommands(); - foreach (var builtCommand in builtCommands) + if (builtCommand.Global) + { + await socketClient.Rest.CreateGlobalCommand(builtCommand).ConfigureAwait(false); + } + else { - // TODO: Implement Global and Guild Commands. - await socketClient.Rest.CreateGuildCommand(builtCommand, devGuild).ConfigureAwait(false); + foreach (ulong guildID in guildIDs) + { + await socketClient.Rest.CreateGuildCommand(builtCommand, guildID).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs b/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs index df9f39809..5894ab655 100644 --- a/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs +++ b/src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs @@ -21,6 +21,10 @@ namespace Discord /// public string Description { get; set; } + /// + /// If the command should be defined as a global command. + /// + public bool Global { get; set; } = false; /// /// Gets or sets the options for this command.