Browse Source

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.
pull/1733/head^2^2
Cosma George 4 years ago
parent
commit
c76fdec8d6
8 changed files with 137 additions and 48 deletions
  1. +5
    -1
      SlashCommandsExample/DiscordClient.cs
  2. +4
    -0
      SlashCommandsExample/Modules/DevModule.cs
  3. +16
    -0
      src/Discord.Net.Commands/SlashCommands/Attributes/Global.cs
  4. +3
    -1
      src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs
  5. +13
    -2
      src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs
  6. +2
    -2
      src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs
  7. +90
    -42
      src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs
  8. +4
    -0
      src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs

+ 5
- 1
SlashCommandsExample/DiscordClient.cs View File

@@ -120,7 +120,11 @@ namespace SlashCommandsExample
await socketClient.StartAsync(); await socketClient.StartAsync();


await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
await _commands.RegisterCommandsAsync(socketClient, CommandRegistrationOptions.Default);
await _commands.RegisterCommandsAsync(socketClient, new List<ulong>()
{
386658607338618891
},
new CommandRegistrationOptions(OldCommandOptions.DELETE_UNUSED,ExistingCommandOptions.OVERWRITE));


await Task.Delay(-1); await Task.Delay(-1);
} }


+ 4
- 0
SlashCommandsExample/Modules/DevModule.cs View File

@@ -9,9 +9,12 @@ using System.Threading.Tasks;


namespace SlashCommandsExample.Modules namespace SlashCommandsExample.Modules
{ {
// You can make the whole module Global
//[Global]
public class DevModule : SlashCommandModule<SocketInteraction> public class DevModule : SlashCommandModule<SocketInteraction>
{ {
[SlashCommand("ping", "Ping the bot to see if it's alive!")] [SlashCommand("ping", "Ping the bot to see if it's alive!")]
[Global]
public async Task PingAsync() public async Task PingAsync()
{ {
await Reply(":white_check_mark: **Bot Online**"); await Reply(":white_check_mark: **Bot Online**");
@@ -38,6 +41,7 @@ namespace SlashCommandsExample.Modules
} }


[CommandGroup("root")] [CommandGroup("root")]
//[Global]
public class DevModule_Root : SlashCommandModule<SocketInteraction> public class DevModule_Root : SlashCommandModule<SocketInteraction>
{ {
[SlashCommand("rng", "Gives you a random number from this \"machine\"")] [SlashCommand("rng", "Gives you a random number from this \"machine\"")]


+ 16
- 0
src/Discord.Net.Commands/SlashCommands/Attributes/Global.cs View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Discord.SlashCommands
{
/// <summary>
/// An Attribute that gives the command parameter a description.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
public class Global : Attribute
{
}
}

+ 3
- 1
src/Discord.Net.Commands/SlashCommands/Info/SlashCommandInfo.cs View File

@@ -28,6 +28,7 @@ namespace Discord.SlashCommands
/// </summary> /// </summary>
public List<SlashParameterInfo> Parameters { get; } public List<SlashParameterInfo> Parameters { get; }


public bool isGlobal { get; }
/// <summary> /// <summary>
/// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters /// The user method as a delegate. We need to use Delegate because there is an unknown number of parameters
/// </summary> /// </summary>
@@ -37,13 +38,14 @@ namespace Discord.SlashCommands
/// </summary> /// </summary>
public Func<object[], Task<IResult>> callback; public Func<object[], Task<IResult>> callback;


public SlashCommandInfo(SlashModuleInfo module, string name, string description,List<SlashParameterInfo> parameters , Delegate userMethod)
public SlashCommandInfo(SlashModuleInfo module, string name, string description,List<SlashParameterInfo> parameters , Delegate userMethod , bool isGlobal = false)
{ {
Module = module; Module = module;
Name = name; Name = name;
Description = description; Description = description;
Parameters = parameters; Parameters = parameters;
this.userMethod = userMethod; this.userMethod = userMethod;
this.isGlobal = isGlobal;
this.callback = new Func<object[], Task<IResult>>(async (args) => this.callback = new Func<object[], Task<IResult>>(async (args) =>
{ {
// Try-catch it and see what we get - error or success // Try-catch it and see what we get - error or success


+ 13
- 2
src/Discord.Net.Commands/SlashCommands/Info/SlashModuleInfo.cs View File

@@ -28,6 +28,7 @@ namespace Discord.SlashCommands
public List<SlashModuleInfo> commandGroups { get; set; } public List<SlashModuleInfo> commandGroups { get; set; }
public string Path { get; set; } = RootModuleName; public string Path { get; set; } = RootModuleName;


public bool isGlobal { get; set; } = false;
/// <summary> /// <summary>
/// Gets the command service associated with this module. /// Gets the command service associated with this module.
/// </summary> /// </summary>
@@ -85,11 +86,21 @@ namespace Discord.SlashCommands
List<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>(); List<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>();
foreach (var command in Commands) 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) 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; return builtCommands;
} }


+ 2
- 2
src/Discord.Net.Commands/SlashCommands/SlashCommandService.cs View File

@@ -103,14 +103,14 @@ namespace Discord.SlashCommands
/// <summary> /// <summary>
/// Registers all previously scanned commands. /// Registers all previously scanned commands.
/// </summary> /// </summary>
public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, CommandRegistrationOptions registrationOptions)
public async Task RegisterCommandsAsync(DiscordSocketClient socketClient, List<ulong> 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 // 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); await _moduleLock.WaitAsync().ConfigureAwait(false);


try try
{ {
await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this,registrationOptions).ConfigureAwait(false);
await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this, guildIDs, registrationOptions).ConfigureAwait(false);
} }
finally finally
{ {


+ 90
- 42
src/Discord.Net.Commands/SlashCommands/SlashCommandServiceHelper.cs View File

@@ -59,10 +59,9 @@ namespace Discord.SlashCommands
// 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.isGlobal = IsCommandModuleGlobal(userModuleType);


// ,,
moduleInfo.SetSubCommandGroups(InstantiateSubCommands(userModuleType, moduleInfo, slashCommandService)); moduleInfo.SetSubCommandGroups(InstantiateSubCommands(userModuleType, moduleInfo, slashCommandService));

result.Add(userModuleType, moduleInfo); result.Add(userModuleType, moduleInfo);
} }
return result; return result;
@@ -82,9 +81,9 @@ namespace Discord.SlashCommands


groupInfo.MakeCommandGroup(commandGroup,rootModuleInfo); groupInfo.MakeCommandGroup(commandGroup,rootModuleInfo);
groupInfo.MakePath(); groupInfo.MakePath();
groupInfo.isGlobal = IsCommandModuleGlobal(commandGroupType);


groupInfo.SetSubCommandGroups(InstantiateSubCommands(commandGroupType, groupInfo, slashCommandService)); groupInfo.SetSubCommandGroups(InstantiateSubCommands(commandGroupType, groupInfo, slashCommandService));

commandGroups.Add(groupInfo); commandGroups.Add(groupInfo);
} }
} }
@@ -114,7 +113,21 @@ namespace Discord.SlashCommands
return true; return true;
} }
} }

public static bool IsCommandModuleGlobal(Type userModuleType)
{
// Verify that we only have one [Global] attribute
IEnumerable<Attribute> 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;
}
/// <summary> /// <summary>
/// Prepare all of the commands and register them internally. /// Prepare all of the commands and register them internally.
/// </summary> /// </summary>
@@ -129,7 +142,7 @@ namespace Discord.SlashCommands
SlashModuleInfo moduleInfo; SlashModuleInfo moduleInfo;
if (moduleDefs.TryGetValue(userModule, out moduleInfo)) if (moduleDefs.TryGetValue(userModule, out moduleInfo))
{ {
var commandInfos = RegisterSameLevelCommands(result, userModule, moduleInfo);
var commandInfos = CreateSameLevelCommands(result, userModule, moduleInfo);
moduleInfo.SetCommands(commandInfos); moduleInfo.SetCommands(commandInfos);
CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService); CreateSubCommandInfos(result, moduleInfo.commandGroups, slashCommandService);
} }
@@ -140,12 +153,12 @@ namespace Discord.SlashCommands
{ {
foreach (var subCommandGroup in subCommandGroups) foreach (var subCommandGroup in subCommandGroups)
{ {
var commandInfos = RegisterSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup);
var commandInfos = CreateSameLevelCommands(result, subCommandGroup.moduleType.GetTypeInfo(), subCommandGroup);
subCommandGroup.SetCommands(commandInfos); subCommandGroup.SetCommands(commandInfos);
CreateSubCommandInfos(result, subCommandGroup.commandGroups, slashCommandService); CreateSubCommandInfos(result, subCommandGroup.commandGroups, slashCommandService);
} }
} }
private static List<SlashCommandInfo> RegisterSameLevelCommands(Dictionary<string, SlashCommandInfo> result, TypeInfo userModule, SlashModuleInfo moduleInfo)
private static List<SlashCommandInfo> CreateSameLevelCommands(Dictionary<string, SlashCommandInfo> result, TypeInfo userModule, SlashModuleInfo moduleInfo)
{ {
var commandMethods = userModule.GetMethods(); var commandMethods = userModule.GetMethods();
List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>();
@@ -164,7 +177,8 @@ namespace Discord.SlashCommands
description: slashCommand.description, description: slashCommand.description,
// Generate the parameters. Due to it's complicated way the algorithm has been moved to its own function. // Generate the parameters. Due to it's complicated way the algorithm has been moved to its own function.
parameters: ConstructCommandParameters(commandMethod), parameters: ConstructCommandParameters(commandMethod),
userMethod: delegateMethod
userMethod: delegateMethod,
isGlobal: IsCommandGlobal(commandMethod)
); );


result.Add(commandInfo.Module.Path + SlashModuleInfo.PathSeperator + commandInfo.Name, commandInfo); result.Add(commandInfo.Module.Path + SlashModuleInfo.PathSeperator + commandInfo.Name, commandInfo);
@@ -196,6 +210,21 @@ namespace Discord.SlashCommands
slashCommand = slashCommandAttributes.First() as SlashCommand; slashCommand = slashCommandAttributes.First() as SlashCommand;
return true; return true;
} }
private static bool IsCommandGlobal(MethodInfo method)
{
// Verify that we only have one [Global] attribute
IEnumerable<Attribute> 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<SlashParameterInfo> ConstructCommandParameters(MethodInfo method) private static List<SlashParameterInfo> ConstructCommandParameters(MethodInfo method)
{ {
// Prepare the final list of parameters // Prepare the final list of parameters
@@ -283,57 +312,76 @@ 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<Type, SlashModuleInfo> rootModuleInfos, 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, List<ulong> guildIDs,CommandRegistrationOptions options)
{ {
// Get existing commmands
ulong devGuild = 386658607338618891;
var existingCommands = await socketClient.Rest.GetGuildApplicationCommands(devGuild).ConfigureAwait(false);
List<string> existingCommandNames = new List<string>();
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<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>();
foreach (var pair in rootModuleInfos)
{ {
existingCommandNames.Add(existingCommand.Name);
var rootModuleInfo = pair.Value;
builtCommands.AddRange(rootModuleInfo.BuildCommands());
}

List<Rest.RestGuildCommand> existingGuildCommands = new List<Rest.RestGuildCommand>();
List<Rest.RestGlobalCommand> existingGlobalCommands = new List<Rest.RestGlobalCommand>();
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 || if (options.OldCommands == OldCommandOptions.DELETE_UNUSED ||
options.OldCommands == OldCommandOptions.WIPE) options.OldCommands == OldCommandOptions.WIPE)
{ {
foreach (var existingCommand in existingCommands)
foreach (var existingCommand in existingGuildCommands)
{ {
// If we want to wipe all commands // 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. // 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(); 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<SlashCommandCreationProperties> 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);
}
} }
} }




+ 4
- 0
src/Discord.Net.Core/Entities/Interactions/SlashCommandCreationProperties.cs View File

@@ -21,6 +21,10 @@ namespace Discord
/// </summary> /// </summary>
public string Description { get; set; } public string Description { get; set; }


/// <summary>
/// If the command should be defined as a global command.
/// </summary>
public bool Global { get; set; } = false;


/// <summary> /// <summary>
/// Gets or sets the options for this command. /// Gets or sets the options for this command.


Loading…
Cancel
Save