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.