diff --git a/SlashCommandsExample/DiscordClient.cs b/SlashCommandsExample/DiscordClient.cs
index bbdc88b14..b2b891d13 100644
--- a/SlashCommandsExample/DiscordClient.cs
+++ b/SlashCommandsExample/DiscordClient.cs
@@ -126,6 +126,15 @@ namespace SlashCommandsExample
},
new CommandRegistrationOptions(OldCommandOptions.DELETE_UNUSED,ExistingCommandOptions.OVERWRITE));
+ // If you would like to register your commands manually use:
+ //-----------------------------------------//
+ //
+ // await _commands.BuildCommands();
+ //
+ //-----------------------------------------//
+ // Though I wouldn't highly recommend it unless you want to do something very specific with them
+ // such as only registering some commands on only some guilds, or editing them manually.
+
await Task.Delay(-1);
}
diff --git a/SlashCommandsExample/Modules/DevModule.cs b/SlashCommandsExample/Modules/DevModule.cs
index d2079573f..9d4949007 100644
--- a/SlashCommandsExample/Modules/DevModule.cs
+++ b/SlashCommandsExample/Modules/DevModule.cs
@@ -31,8 +31,11 @@ namespace SlashCommandsExample.Modules
[SlashCommand("overload","Just hit me with every type of data you got, man!")]
public async Task OverloadAsync(
- bool boolean,
- int integer,
+ [ParameterName("var1")]
+ bool? boolean,
+ [ParameterName("var2")]
+ int? integer,
+ [ParameterName("var3")]
string myString,
SocketGuildChannel channel,
SocketGuildUser user,
diff --git a/src/Discord.Net.Commands/SlashCommands/Attributes/ParameterName.cs b/src/Discord.Net.Commands/SlashCommands/Attributes/ParameterName.cs
new file mode 100644
index 000000000..c277012bb
--- /dev/null
+++ b/src/Discord.Net.Commands/SlashCommands/Attributes/ParameterName.cs
@@ -0,0 +1,29 @@
+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 custom name.
+ ///
+ [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
+ public class ParameterName : Attribute
+ {
+ ///
+ /// The name of this slash command parameter.
+ ///
+ public string name;
+
+ ///
+ /// Tells the that this parameter has a custom name.
+ ///
+ /// The name of this slash command.
+ public ParameterName(string name)
+ {
+ this.name = name;
+ }
+ }
+}
diff --git a/src/Discord.Net.Commands/SlashCommands/Info/SlashParameterInfo.cs b/src/Discord.Net.Commands/SlashCommands/Info/SlashParameterInfo.cs
index 0b43ac058..f2d8a5ce9 100644
--- a/src/Discord.Net.Commands/SlashCommands/Info/SlashParameterInfo.cs
+++ b/src/Discord.Net.Commands/SlashCommands/Info/SlashParameterInfo.cs
@@ -10,14 +10,22 @@ namespace Discord.SlashCommands
{
public class SlashParameterInfo : SlashCommandOptionBuilder
{
+ public bool Nullable { get; internal set; }
+
public object Parse(SocketInteractionDataOption dataOption)
{
switch (Type)
{
case ApplicationCommandOptionType.Boolean:
- return (bool)dataOption;
+ if (Nullable)
+ return (bool?)dataOption;
+ else
+ return (bool)dataOption;
case ApplicationCommandOptionType.Integer:
- return (int)dataOption;
+ if(Nullable)
+ return (int?)dataOption;
+ else
+ return (int)dataOption;
case ApplicationCommandOptionType.String:
return (string)dataOption;
case ApplicationCommandOptionType.Channel:
diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs
index ce7721769..c227758a7 100644
--- a/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs
+++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs
@@ -44,6 +44,7 @@ namespace Discord.SlashCommands
SlashCommandInfo 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);
+ // We still need to make sure it is registerd.
if (commandDefs.TryGetValue(name, out commandInfo))
{
// Then, set the context in which the command will be executed
@@ -62,8 +63,16 @@ namespace Discord.SlashCommands
///
///
///
- private string GetSearchName(SocketInteractionData interactionData, out IReadOnlyCollection resultingOptions)
+ public string GetSearchName(SocketInteractionData interactionData, out IReadOnlyCollection resultingOptions)
{
+ // The names are stored as such:
+ // TOP//top-level-command-name
+ // TOP//command-group//command-group//sub-command-name
+ // What we are looking for is to get from the interaction the specific (sub)command and what we need to pass to the method.
+ // So we start the search at TOP//{interactionData.name}
+ // because we are going to go through each sub-option it has. If it is a subcommand/ command group then it's going to be
+ // inside the dictionary as TOP//{interactionData.name}//{option.name}
+ // If the option is a parameter we then know that we've reached the end of the call chain - this should be our coomand!
string nameToSearch = SlashModuleInfo.RootCommandPrefix + interactionData.Name;
var options = interactionData.Options;
while(options != null && options.Count == 1)
@@ -82,7 +91,9 @@ namespace Discord.SlashCommands
resultingOptions = options;
return nameToSearch;
}
-
+ ///
+ /// Test to see if any string key contains another string inside it.
+ ///
private bool AnyKeyContains(Dictionary commandDefs, string newName)
{
foreach (var pair in commandDefs)
@@ -101,7 +112,7 @@ namespace Discord.SlashCommands
}
///
- /// Registers all previously scanned commands.
+ /// Registers with discord all previously scanned commands.
///
public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, List guildIDs, CommandRegistrationOptions registrationOptions)
{
@@ -110,6 +121,7 @@ namespace Discord.SlashCommands
try
{
+ // Build and register all of the commands.
await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this, guildIDs, registrationOptions).ConfigureAwait(false);
}
finally
@@ -119,6 +131,27 @@ namespace Discord.SlashCommands
await _logger.InfoAsync("All commands have been registered!").ConfigureAwait(false);
}
+ ///
+ /// Build all the commands and return them, for manual registration with Discord. This is automatically done in
+ ///
+ /// A list of all the valid commands found within this Assembly.
+ public async Task> BuildCommands()
+ {
+ // 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);
+ List result;
+ try
+ {
+ result = await SlashCommandServiceHelper.BuildCommands(moduleDefs).ConfigureAwait(false);
+ }
+ finally
+ {
+ _moduleLock.Release();
+ }
+ await _logger.InfoAsync("All commands have been built!").ConfigureAwait(false);
+ return result;
+ }
+
///
/// Scans the program for Attribute-based SlashCommandModules
///
diff --git a/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs b/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs
index e49846193..2239758dc 100644
--- a/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs
+++ b/src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs
@@ -50,7 +50,7 @@ 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
+ // Here we get all modules thate are NOT sub command groups and instantiate them.
foreach (Type userModuleType in types)
{
SlashModuleInfo moduleInfo = new SlashModuleInfo(slashCommandService);
@@ -61,13 +61,14 @@ namespace Discord.SlashCommands
moduleInfo.SetCommandModule(instance);
moduleInfo.isGlobal = IsCommandModuleGlobal(userModuleType);
- moduleInfo.SetSubCommandGroups(InstantiateSubCommands(userModuleType, moduleInfo, slashCommandService));
+ moduleInfo.SetSubCommandGroups(InstantiateSubModules(userModuleType, moduleInfo, slashCommandService));
result.Add(userModuleType, moduleInfo);
}
return result;
}
- public static List InstantiateSubCommands(Type rootModule,SlashModuleInfo rootModuleInfo, SlashCommandService slashCommandService)
+ public static List InstantiateSubModules(Type rootModule,SlashModuleInfo rootModuleInfo, SlashCommandService slashCommandService)
{
+ // Instantiate all of the nested modules.
List commandGroups = new List();
foreach(Type commandGroupType in rootModule.GetNestedTypes())
{
@@ -83,7 +84,7 @@ namespace Discord.SlashCommands
groupInfo.MakePath();
groupInfo.isGlobal = IsCommandModuleGlobal(commandGroupType);
- groupInfo.SetSubCommandGroups(InstantiateSubCommands(commandGroupType, groupInfo, slashCommandService));
+ groupInfo.SetSubCommandGroups(InstantiateSubModules(commandGroupType, groupInfo, slashCommandService));
commandGroups.Add(groupInfo);
}
}
@@ -142,8 +143,10 @@ namespace Discord.SlashCommands
SlashModuleInfo moduleInfo;
if (moduleDefs.TryGetValue(userModule, out moduleInfo))
{
+ // Create the root-level commands
var commandInfos = CreateSameLevelCommands(result, userModule, moduleInfo);
moduleInfo.SetCommands(commandInfos);
+ // Then create all of the command groups it has.
CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService);
}
}
@@ -153,8 +156,11 @@ namespace Discord.SlashCommands
{
foreach (var subCommandGroup in subCommandGroups)
{
+ // Create the commands that is on the same hierarchical level as this ...
var commandInfos = CreateSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup);
subCommandGroup.SetCommands(commandInfos);
+
+ // ... and continue with the lower sub command groups.
CreateSubCommandInfos(result, subCommandGroup.commandGroups, slashCommandService);
}
}
@@ -164,6 +170,7 @@ namespace Discord.SlashCommands
List commandInfos = new List();
foreach (var commandMethod in commandMethods)
{
+ // Get the SlashCommand attribute
SlashCommand slashCommand;
if (IsValidSlashCommand(commandMethod, out slashCommand))
{
@@ -210,6 +217,9 @@ namespace Discord.SlashCommands
slashCommand = slashCommandAttributes.First() as SlashCommand;
return true;
}
+ ///
+ /// Determins if the method has a [Global] Attribute.
+ ///
private static bool IsCommandGlobal(MethodInfo method)
{
// Verify that we only have one [Global] attribute
@@ -225,6 +235,9 @@ namespace Discord.SlashCommands
}
return true;
}
+ ///
+ /// Process the parameters of this method, including all the attributes.
+ ///
private static List ConstructCommandParameters(MethodInfo method)
{
// Prepare the final list of parameters
@@ -237,9 +250,15 @@ namespace Discord.SlashCommands
{
SlashParameterInfo newParameter = new SlashParameterInfo();
- // Set the parameter name to that of the method
- // TODO: Implement an annotation that lets the user choose a custom name
- newParameter.Name = methodParameter.Name;
+ // Test for the [ParameterName] Attribute. If we have it, then use that as the name,
+ // if not just use the parameter name as the option name.
+ var customNameAttributes = methodParameter.GetCustomAttributes(typeof(ParameterName));
+ if (customNameAttributes.Count() == 0)
+ newParameter.Name = methodParameter.Name;
+ else if (customNameAttributes.Count() > 1)
+ throw new Exception($"Too many ParameterName attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!");
+ else
+ newParameter.Name = (customNameAttributes.First() as ParameterName).name;
// Get to see if it has a Description Attribute.
// If it has
@@ -254,19 +273,26 @@ namespace Discord.SlashCommands
else
newParameter.Description = (descriptions.First() as Description).description;
- // And get the parameter type
+ // Set the Type of the parameter.
+ // In the case of int and int? it returns the same type - INTEGER.
+ // Same with bool and bool?.
newParameter.Type = TypeFromMethodParameter(methodParameter);
- // [Required] Parameter
+ // If we have a nullble type (int? or bool?) mark it as such.
+ newParameter.Nullable = GetNullableStatus(methodParameter);
+
+ // Test for the [Required] Attribute
var requiredAttributes = methodParameter.GetCustomAttributes(typeof(Required));
if (requiredAttributes.Count() == 1)
newParameter.Required = true;
else if (requiredAttributes.Count() > 1)
throw new Exception($"Too many Required attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!");
- // [Choice] Parameter
+ // Test for the [Choice] Attribute
+ // A parameter cna have multiple Choice attributes, and for each we're going to add it's key-value pair.
foreach (Choice choice in methodParameter.GetCustomAttributes(typeof(Choice)))
{
+ // If the parameter expects a string but the value of the choice is of type int, then throw an error.
if (newParameter.Type == ApplicationCommandOptionType.String)
{
if(String.IsNullOrEmpty(choice.choiceStringValue))
@@ -275,6 +301,7 @@ namespace Discord.SlashCommands
}
newParameter.AddChoice(choice.choiceName, choice.choiceStringValue);
}
+ // If the parameter expects a int but the value of the choice is of type string, then throw an error.
if (newParameter.Type == ApplicationCommandOptionType.Integer)
{
if (choice.choiceIntValue == null)
@@ -295,11 +322,13 @@ namespace Discord.SlashCommands
private static ApplicationCommandOptionType TypeFromMethodParameter(ParameterInfo methodParameter)
{
// Can't do switch -- who knows why?
- if (methodParameter.ParameterType == typeof(int))
+ if (methodParameter.ParameterType == typeof(int) ||
+ methodParameter.ParameterType == typeof(int?))
return ApplicationCommandOptionType.Integer;
if (methodParameter.ParameterType == typeof(string))
return ApplicationCommandOptionType.String;
- if (methodParameter.ParameterType == typeof(bool))
+ if (methodParameter.ParameterType == typeof(bool) ||
+ methodParameter.ParameterType == typeof(bool?))
return ApplicationCommandOptionType.Boolean;
if (methodParameter.ParameterType == typeof(SocketGuildChannel))
return ApplicationCommandOptionType.Channel;
@@ -310,11 +339,25 @@ namespace Discord.SlashCommands
throw new Exception($"Got parameter type other than int, string, bool, guild, role, or user. {methodParameter.Name}");
}
+
+ ///
+ /// Gets whater the parameter can be set as null, in the case that parameter type usually does not allow null.
+ /// More specifically tests to see if it is a type of 'int?' or 'bool?',
+ ///
+ private static bool GetNullableStatus(ParameterInfo methodParameter)
+ {
+ if(methodParameter.ParameterType == typeof(int?) ||
+ methodParameter.ParameterType == typeof(bool?))
+ {
+ return true;
+ }
+ return false;
+ }
///
/// Creae a delegate from methodInfo. Taken from
/// https://stackoverflow.com/a/40579063/8455128
///
- public static Delegate CreateDelegate(MethodInfo methodInfo, object target)
+ private static Delegate CreateDelegate(MethodInfo methodInfo, object target)
{
Func getType;
var isAction = methodInfo.ReturnType.Equals((typeof(void)));
@@ -341,13 +384,10 @@ namespace Discord.SlashCommands
public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary rootModuleInfos, Dictionary commandDefs, SlashCommandService slashCommandService, List guildIDs,CommandRegistrationOptions options)
{
// 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)
- {
- var rootModuleInfo = pair.Value;
- builtCommands.AddRange(rootModuleInfo.BuildCommands());
- }
+ // Build the commands
+ List builtCommands = await BuildCommands(rootModuleInfos).ConfigureAwait(false);
+ // Scan for each existing command on discord so we know what is already there.
List existingGuildCommands = new List();
List existingGlobalCommands = new List();
existingGlobalCommands.AddRange(await socketClient.Rest.GetGlobalApplicationCommands().ConfigureAwait(false));
@@ -355,6 +395,9 @@ namespace Discord.SlashCommands
{
existingGuildCommands.AddRange(await socketClient.Rest.GetGuildApplicationCommands(guildID).ConfigureAwait(false));
}
+
+ // If we want to keep the existing commands that are already registered
+ // remove the commands that share the same name from the builtCommands list as to not overwrite.
if (options.ExistingCommands == ExistingCommandOptions.KEEP_EXISTING)
{
foreach (var existingCommand in existingGuildCommands)
@@ -367,24 +410,26 @@ namespace Discord.SlashCommands
}
}
+ // If we want to delete commands that are not going to be re-implemented in builtCommands
+ // or if we just want a blank slate
if (options.OldCommands == OldCommandOptions.DELETE_UNUSED ||
options.OldCommands == OldCommandOptions.WIPE)
- {
+ {
foreach (var existingCommand in existingGuildCommands)
{
- // If we want to wipe all commands
+ // If we want to wipe all GUILD 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)))
+ !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
+ // If we want to wipe all GLOBAL commands
// or if the existing command isn't re-defined and re-built
// remove it from discord.
if (options.OldCommands == OldCommandOptions.WIPE ||
@@ -396,6 +441,8 @@ namespace Discord.SlashCommands
}
}
+ // And now register them. Globally if the 'Global' flag is set.
+ // If not then just register them as guild commands on all of the guilds given to us.
foreach (var builtCommand in builtCommands)
{
if (builtCommand.Global)
@@ -413,5 +460,19 @@ namespace Discord.SlashCommands
return;
}
+ ///
+ /// Build and return all of the commands this assembly contians.
+ ///
+ public static async Task> BuildCommands(Dictionary rootModuleInfos)
+ {
+ List builtCommands = new List();
+ foreach (var pair in rootModuleInfos)
+ {
+ var rootModuleInfo = pair.Value;
+ builtCommands.AddRange(rootModuleInfo.BuildCommands());
+ }
+
+ return builtCommands;
+ }
}
}
diff --git a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs
index bd8324eb7..3a612388d 100644
--- a/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs
+++ b/src/Discord.Net.WebSocket/Entities/Interaction/SocketInteractionDataOption.cs
@@ -52,6 +52,21 @@ namespace Discord.WebSocket
public static explicit operator string(SocketInteractionDataOption option)
=> option.Value.ToString();
+ public static explicit operator bool?(SocketInteractionDataOption option)
+ {
+ if (option.Value == null)
+ return null;
+ else
+ return (bool)option;
+ }
+ public static explicit operator int?(SocketInteractionDataOption option)
+ {
+ if (option.Value == null)
+ return null;
+ else
+ return (int)option;
+ }
+
public static explicit operator SocketGuildChannel(SocketInteractionDataOption option)
{
if (ulong.TryParse((string)option.Value, out ulong id))