Slash command service v2pull/1733/head^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,12 +48,92 @@ 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); | ||||
| await socketClient.StartAsync(); | await socketClient.StartAsync(); | ||||
| await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); | ||||
| await _commands.RegisterCommandsAsync(socketClient, new List<ulong>() | |||||
| { | |||||
| 386658607338618891 | |||||
| }, | |||||
| 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); | await Task.Delay(-1); | ||||
| } | } | ||||
| @@ -0,0 +1,104 @@ | |||||
| using Discord.Commands; | |||||
| using Discord.Commands.SlashCommands.Types; | |||||
| using Discord.SlashCommands; | |||||
| using Discord.WebSocket; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace SlashCommandsExample.Modules | |||||
| { | |||||
| // You can make the whole module Global | |||||
| //[Global] | |||||
| public class DevModule : SlashCommandModule<SocketInteraction> | |||||
| { | |||||
| [SlashCommand("ping", "Ping the bot to see if it's alive!")] | |||||
| [Global] | |||||
| public async Task PingAsync() | |||||
| { | |||||
| await Reply(":white_check_mark: **Bot Online**"); | |||||
| } | |||||
| [SlashCommand("echo", "I'll repeate everything you said to me, word for word.")] | |||||
| public async Task EchoAsync( | |||||
| [Description("The message you want repetead")] | |||||
| [Required] | |||||
| string message) | |||||
| { | |||||
| await Reply($"{Interaction.Member?.Nickname ?? Interaction.Member?.Username} told me to say this: \r\n{message}"); | |||||
| } | |||||
| [SlashCommand("overload","Just hit me with every type of data you got, man!")] | |||||
| public async Task OverloadAsync( | |||||
| [ParameterName("var1")] | |||||
| bool? boolean, | |||||
| [ParameterName("var2")] | |||||
| int? integer, | |||||
| [ParameterName("var3")] | |||||
| string myString, | |||||
| SocketGuildChannel channel, | |||||
| SocketGuildUser user, | |||||
| SocketRole role | |||||
| ) | |||||
| { | |||||
| await Reply($"You gave me:\r\n {boolean}, {integer}, {myString}, <#{channel?.Id}>, {user?.Mention}, {role?.Mention}"); | |||||
| } | |||||
| [SlashCommand("stats","Get the stats from Game(tm) for players or teams.")] | |||||
| public async Task GetStatsAsync( | |||||
| [Required] | |||||
| [Choice("XBOX","xbox")] | |||||
| [Choice("PlayStation","ps")] | |||||
| [Choice("PC","pc")] | |||||
| string platform, | |||||
| [Choice("Player",1)] | |||||
| [Choice("Team",2)] | |||||
| int searchType | |||||
| ) | |||||
| { | |||||
| await Reply($"Well I got this: {platform}, {searchType}"); | |||||
| } | |||||
| [CommandGroup("root")] | |||||
| //[Global] | |||||
| 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\"."); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| /* | |||||
| The base way of defining a command using the regular command service: | |||||
| public class PingModule : ModuleBase<SocketCommandContext> | |||||
| { | |||||
| [Command("ping")] | |||||
| [Summary("Pong! Check if the bot is alive.")] | |||||
| public async Task PingAsync() | |||||
| { | |||||
| await ReplyAsync(":white_check_mark: **Bot Online**"); | |||||
| } | |||||
| } | |||||
| */ | |||||
| @@ -1,33 +0,0 @@ | |||||
| using Discord.Commands; | |||||
| using Discord.Commands.SlashCommands.Types; | |||||
| using Discord.SlashCommands; | |||||
| using Discord.WebSocket; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace SlashCommandsExample.Modules | |||||
| { | |||||
| public class PingCommand : SlashCommandModule<SocketInteraction> | |||||
| { | |||||
| [SlashCommand("johnny-test", "Ping the bot to see if it is alive!")] | |||||
| public async Task PingAsync() | |||||
| { | |||||
| await Interaction.FollowupAsync(":white_check_mark: **Bot Online**"); | |||||
| } | |||||
| } | |||||
| } | |||||
| /* | |||||
| The base way of defining a command using the regular command service: | |||||
| public class PingModule : ModuleBase<SocketCommandContext> | |||||
| { | |||||
| [Command("ping")] | |||||
| [Summary("Pong! Check if the bot is alive.")] | |||||
| public async Task PingAsync() | |||||
| { | |||||
| await ReplyAsync(":white_check_mark: **Bot Online**"); | |||||
| } | |||||
| } | |||||
| */ | |||||
| @@ -0,0 +1,41 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | |||||
| { | |||||
| /// <summary> | |||||
| /// Defines the parameter as a choice. | |||||
| /// </summary> | |||||
| [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] | |||||
| public class Choice : Attribute | |||||
| { | |||||
| /// <summary> | |||||
| /// The internal value of this choice. | |||||
| /// </summary> | |||||
| public string choiceStringValue; | |||||
| /// <summary> | |||||
| /// The internal value of this choice. | |||||
| /// </summary> | |||||
| public int? choiceIntValue = null; | |||||
| /// <summary> | |||||
| /// The display value of this choice. | |||||
| /// </summary> | |||||
| public string choiceName; | |||||
| public Choice(string choiceName, string choiceValue) | |||||
| { | |||||
| this.choiceName = choiceName; | |||||
| this.choiceStringValue = choiceValue; | |||||
| } | |||||
| public Choice(string choiceName, int choiceValue) | |||||
| { | |||||
| this.choiceName = choiceName; | |||||
| this.choiceIntValue = choiceValue; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,31 @@ | |||||
| 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.Parameter , AllowMultiple = false)] | |||||
| public class Description : Attribute | |||||
| { | |||||
| public static string DefaultDescription = "No description."; | |||||
| /// <summary> | |||||
| /// The description of this slash command parameter. | |||||
| /// </summary> | |||||
| public string description; | |||||
| /// <summary> | |||||
| /// Tells the <see cref="SlashCommandService"/> that this parameter has a description. | |||||
| /// </summary> | |||||
| /// <param name="commandName">The name of this slash command.</param> | |||||
| public Description(string description) | |||||
| { | |||||
| this.description = description; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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 | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| 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 custom name. | |||||
| /// </summary> | |||||
| [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] | |||||
| public class ParameterName : Attribute | |||||
| { | |||||
| /// <summary> | |||||
| /// The name of this slash command parameter. | |||||
| /// </summary> | |||||
| public string name; | |||||
| /// <summary> | |||||
| /// Tells the <see cref="SlashCommandService"/> that this parameter has a custom name. | |||||
| /// </summary> | |||||
| /// <param name="name">The name of this slash command.</param> | |||||
| public ParameterName(string name) | |||||
| { | |||||
| this.name = name; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -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.Parameter, AllowMultiple = false)] | |||||
| public class Required : Attribute | |||||
| { | |||||
| } | |||||
| } | |||||
| @@ -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; | ||||
| } | } | ||||
| @@ -0,0 +1,39 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | |||||
| { | |||||
| /// <summary> | |||||
| /// The options that should be kept in mind when registering the slash commands to discord. | |||||
| /// </summary> | |||||
| public class CommandRegistrationOptions | |||||
| { | |||||
| /// <summary> | |||||
| /// The options that should be kept in mind when registering the slash commands to discord. | |||||
| /// </summary> | |||||
| /// <param name="oldCommands">What to do with the old commands that are already registered with discord</param> | |||||
| /// <param name="existingCommands"> What to do with the old commands (if they weren't wiped) that we re-define.</param> | |||||
| public CommandRegistrationOptions(OldCommandOptions oldCommands, ExistingCommandOptions existingCommands) | |||||
| { | |||||
| OldCommands = oldCommands; | |||||
| ExistingCommands = existingCommands; | |||||
| } | |||||
| /// <summary> | |||||
| /// What to do with the old commands that are already registered with discord | |||||
| /// </summary> | |||||
| public OldCommandOptions OldCommands { get; set; } | |||||
| /// <summary> | |||||
| /// What to do with the old commands (if they weren't wiped) that we re-define. | |||||
| /// </summary> | |||||
| public ExistingCommandOptions ExistingCommands { get; set; } | |||||
| /// <summary> | |||||
| /// The default, and reccomended options - Keep the old commands, and overwrite existing commands we re-defined. | |||||
| /// </summary> | |||||
| public static CommandRegistrationOptions Default => | |||||
| new CommandRegistrationOptions(OldCommandOptions.KEEP, ExistingCommandOptions.OVERWRITE); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | |||||
| { | |||||
| public enum ExistingCommandOptions | |||||
| { | |||||
| OVERWRITE, | |||||
| KEEP_EXISTING | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,24 @@ | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | |||||
| { | |||||
| public enum OldCommandOptions | |||||
| { | |||||
| /// <summary> | |||||
| /// Keep the old commands intact - do nothing to them. | |||||
| /// </summary> | |||||
| KEEP, | |||||
| /// <summary> | |||||
| /// Delete the old commands that we won't be re-defined this time around. | |||||
| /// </summary> | |||||
| DELETE_UNUSED, | |||||
| /// <summary> | |||||
| /// Delete everything discord has. | |||||
| /// </summary> | |||||
| WIPE | |||||
| } | |||||
| } | |||||
| @@ -1,4 +1,6 @@ | |||||
| using Discord.Commands; | 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; | ||||
| @@ -21,7 +23,12 @@ namespace Discord.SlashCommands | |||||
| /// Gets the name of the command. | /// Gets the name of the command. | ||||
| /// </summary> | /// </summary> | ||||
| public string Description { get; } | public string Description { get; } | ||||
| /// <summary> | |||||
| /// The parameters we are expecting - an extension of SlashCommandOptionBuilder | |||||
| /// </summary> | |||||
| 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> | ||||
| @@ -31,12 +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, 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; | |||||
| 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 | ||||
| @@ -56,9 +65,96 @@ namespace Discord.SlashCommands | |||||
| }); | }); | ||||
| } | } | ||||
| public async Task<IResult> ExecuteAsync(object[] args) | |||||
| /// <summary> | |||||
| /// Execute the function based on the interaction data we get. | |||||
| /// </summary> | |||||
| /// <param name="data">Interaction data from interaction</param> | |||||
| public async Task<IResult> ExecuteAsync(IReadOnlyCollection<SocketInteractionDataOption> data) | |||||
| { | |||||
| // List of arguments to be passed to the Delegate | |||||
| List<object> args = new List<object>(); | |||||
| try | |||||
| { | |||||
| foreach (var parameter in Parameters) | |||||
| { | |||||
| // For each parameter to try find its coresponding DataOption based on the name. | |||||
| // !!! names from `data` will always be lowercase regardless if we defined the command with any | |||||
| // number of upercase letters !!! | |||||
| if (TryGetInteractionDataOption(data, parameter.Name, out SocketInteractionDataOption dataOption)) | |||||
| { | |||||
| // Parse the dataOption to one corresponding argument type, and then add it to the list of arguments | |||||
| args.Add(parameter.Parse(dataOption)); | |||||
| } | |||||
| else | |||||
| { | |||||
| // There was no input from the user on this field. | |||||
| args.Add(null); | |||||
| } | |||||
| } | |||||
| } | |||||
| catch(Exception e) | |||||
| { | |||||
| return ExecuteResult.FromError(e); | |||||
| } | |||||
| return await callback.Invoke(args.ToArray()).ConfigureAwait(false); | |||||
| } | |||||
| /// <summary> | |||||
| /// Get the interaction data from the name of the parameter we want to fill in. | |||||
| /// </summary> | |||||
| private bool TryGetInteractionDataOption(IReadOnlyCollection<SocketInteractionDataOption> data, string name, out SocketInteractionDataOption dataOption) | |||||
| { | |||||
| if (data == null) | |||||
| { | |||||
| dataOption = null; | |||||
| return false; | |||||
| } | |||||
| foreach (var option in data) | |||||
| { | |||||
| if (option.Name == name.ToLower()) | |||||
| { | |||||
| dataOption = option; | |||||
| return true; | |||||
| } | |||||
| } | |||||
| dataOption = null; | |||||
| return false; | |||||
| } | |||||
| /// <summary> | |||||
| /// Build the command and put it in a state in which we can use to define it to Discord. | |||||
| /// </summary> | |||||
| public SlashCommandCreationProperties BuildCommand() | |||||
| { | { | ||||
| return await callback.Invoke(args).ConfigureAwait(false); | |||||
| SlashCommandBuilder builder = new SlashCommandBuilder(); | |||||
| builder.WithName(Name); | |||||
| builder.WithDescription(Description); | |||||
| builder.Options = new List<SlashCommandOptionBuilder>(); | |||||
| foreach (var parameter in Parameters) | |||||
| { | |||||
| builder.AddOptions(parameter); | |||||
| } | |||||
| 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,23 @@ 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; | |||||
| 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> | ||||
| @@ -27,7 +43,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 +59,84 @@ 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) | |||||
| { | |||||
| var builtCommand = command.BuildCommand(); | |||||
| if (isGlobal || command.isGlobal) | |||||
| { | |||||
| builtCommand.Global = true; | |||||
| } | |||||
| builtCommands.Add(builtCommand); | |||||
| } | |||||
| foreach(var commandGroup in commandGroups) | |||||
| { | |||||
| var builtCommand = commandGroup.BuildTopLevelCommandGroup(); | |||||
| if (isGlobal || commandGroup.isGlobal) | |||||
| { | |||||
| builtCommand.Global = true; | |||||
| } | |||||
| builtCommands.Add(builtCommand); | |||||
| } | |||||
| 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; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -0,0 +1,45 @@ | |||||
| using Discord.Commands.Builders; | |||||
| using Discord.WebSocket; | |||||
| using System; | |||||
| using System.Collections.Generic; | |||||
| using System.Linq; | |||||
| using System.Text; | |||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | |||||
| { | |||||
| public class SlashParameterInfo : SlashCommandOptionBuilder | |||||
| { | |||||
| public bool Nullable { get; internal set; } | |||||
| public object Parse(SocketInteractionDataOption dataOption) | |||||
| { | |||||
| switch (Type) | |||||
| { | |||||
| case ApplicationCommandOptionType.Boolean: | |||||
| if (Nullable) | |||||
| return (bool?)dataOption; | |||||
| else | |||||
| return (bool)dataOption; | |||||
| case ApplicationCommandOptionType.Integer: | |||||
| if(Nullable) | |||||
| return (int?)dataOption; | |||||
| else | |||||
| return (int)dataOption; | |||||
| case ApplicationCommandOptionType.String: | |||||
| return (string)dataOption; | |||||
| case ApplicationCommandOptionType.Channel: | |||||
| return (SocketGuildChannel)dataOption; | |||||
| case ApplicationCommandOptionType.Role: | |||||
| return (SocketRole)dataOption; | |||||
| case ApplicationCommandOptionType.User: | |||||
| return (SocketGuildUser)dataOption; | |||||
| case ApplicationCommandOptionType.SubCommandGroup: | |||||
| throw new NotImplementedException(); | |||||
| case ApplicationCommandOptionType.SubCommand: | |||||
| throw new NotImplementedException(); | |||||
| } | |||||
| throw new NotImplementedException($"There is no such type of data... unless we missed it. Please report this error on the Discord.Net github page! Type: {Type}"); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -34,11 +34,6 @@ namespace Discord.SlashCommands | |||||
| _logger = new Logger(_logManager, "SlshCommand"); | _logger = new Logger(_logManager, "SlshCommand"); | ||||
| } | } | ||||
| public void AddAssembly() | |||||
| { | |||||
| } | |||||
| /// <summary> | /// <summary> | ||||
| /// Execute a slash command. | /// Execute a slash command. | ||||
| /// </summary> | /// </summary> | ||||
| @@ -46,23 +41,120 @@ 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); | |||||
| // We still need to make sure it is registerd. | |||||
| if (commandDefs.TryGetValue(name, out commandInfo)) | |||||
| { | { | ||||
| // TODO: implement everything that has to do with parameters :) | |||||
| // 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 (with no parameters) | |||||
| return await commandInfo.ExecuteAsync(new object[] { }).ConfigureAwait(false); | |||||
| // Then run the command and pass the interaction data over to the CommandInfo class | |||||
| 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> | |||||
| public string GetSearchName(SocketInteractionData interactionData, out IReadOnlyCollection<SocketInteractionDataOption> 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) | |||||
| { | |||||
| string newName = nameToSearch + SlashModuleInfo.PathSeperator + GetFirstOption(options).Name; | |||||
| if (AnyKeyContains(commandDefs,newName)) | |||||
| { | |||||
| nameToSearch = newName; | |||||
| options = GetFirstOption(options).Options; | |||||
| } | |||||
| else | |||||
| { | |||||
| break; | |||||
| } | |||||
| } | |||||
| resultingOptions = options; | |||||
| return nameToSearch; | |||||
| } | |||||
| /// <summary> | |||||
| /// Test to see if any <b>string</b> key contains another string inside it. | |||||
| /// </summary> | |||||
| 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> | |||||
| /// Registers with discord all previously scanned commands. | |||||
| /// </summary> | |||||
| 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 | |||||
| await _moduleLock.WaitAsync().ConfigureAwait(false); | |||||
| try | |||||
| { | |||||
| // Build and register all of the commands. | |||||
| await SlashCommandServiceHelper.RegisterCommands(socketClient, moduleDefs, commandDefs, this, guildIDs, registrationOptions).ConfigureAwait(false); | |||||
| } | |||||
| finally | |||||
| { | |||||
| _moduleLock.Release(); | |||||
| } | |||||
| await _logger.InfoAsync("All commands have been registered!").ConfigureAwait(false); | |||||
| } | |||||
| /// <summary> | |||||
| /// Build all the commands and return them, for manual registration with Discord. This is automatically done in <see cref="RegisterCommandsAsync(DiscordSocketClient, List{ulong}, CommandRegistrationOptions)"/> | |||||
| /// </summary> | |||||
| /// <returns>A list of all the valid commands found within this Assembly.</returns> | |||||
| public async Task<List<SlashCommandCreationProperties>> 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<SlashCommandCreationProperties> result; | |||||
| try | |||||
| { | |||||
| result = await SlashCommandServiceHelper.BuildCommands(moduleDefs).ConfigureAwait(false); | |||||
| } | |||||
| finally | |||||
| { | |||||
| _moduleLock.Release(); | |||||
| } | |||||
| await _logger.InfoAsync("All commands have been built!").ConfigureAwait(false); | |||||
| return result; | |||||
| } | |||||
| /// <summary> | |||||
| /// Scans the program for Attribute-based SlashCommandModules | |||||
| /// </summary> | |||||
| public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | public async Task AddModulesAsync(Assembly assembly, IServiceProvider services) | ||||
| { | { | ||||
| // 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 | ||||
| @@ -75,9 +167,7 @@ namespace Discord.SlashCommands | |||||
| // Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | // Then, based on that, make an instance out of each of them, and get the resulting SlashModuleInfo s | ||||
| moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | moduleDefs = await SlashCommandServiceHelper.InstantiateModules(types, this).ConfigureAwait(false); | ||||
| // After that, internally register all of the commands into SlashCommandInfo | // After that, internally register all of the commands into SlashCommandInfo | ||||
| commandDefs = await SlashCommandServiceHelper.PrepareAsync(types,moduleDefs,this).ConfigureAwait(false); | |||||
| // TODO: And finally, register the commands with discord. | |||||
| await SlashCommandServiceHelper.RegisterCommands(commandDefs, this, services).ConfigureAwait(false); | |||||
| commandDefs = await SlashCommandServiceHelper.CreateCommandInfos(types,moduleDefs,this).ConfigureAwait(false); | |||||
| } | } | ||||
| finally | finally | ||||
| { | { | ||||
| @@ -1,3 +1,5 @@ | |||||
| 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; | ||||
| @@ -38,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> | ||||
| @@ -47,56 +50,155 @@ 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 and instantiate them. | |||||
| 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.isGlobal = IsCommandModuleGlobal(userModuleType); | |||||
| moduleInfo.SetSubCommandGroups(InstantiateSubModules(userModuleType, moduleInfo, slashCommandService)); | |||||
| result.Add(userModuleType, moduleInfo); | result.Add(userModuleType, moduleInfo); | ||||
| } | } | ||||
| return result; | return result; | ||||
| } | } | ||||
| public static List<SlashModuleInfo> InstantiateSubModules(Type rootModule,SlashModuleInfo rootModuleInfo, SlashCommandService slashCommandService) | |||||
| { | |||||
| // Instantiate all of the nested modules. | |||||
| 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.isGlobal = IsCommandModuleGlobal(commandGroupType); | |||||
| groupInfo.SetSubCommandGroups(InstantiateSubModules(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; | |||||
| } | |||||
| } | |||||
| 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> | ||||
| public static async Task<Dictionary<string, SlashCommandInfo>> PrepareAsync(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||||
| public static async Task<Dictionary<string, SlashCommandInfo>> CreateCommandInfos(IReadOnlyList<TypeInfo> types, Dictionary<Type, SlashModuleInfo> moduleDefs, SlashCommandService slashCommandService) | |||||
| { | { | ||||
| // Create the resulting dictionary ahead of time | |||||
| var result = new Dictionary<string, SlashCommandInfo>(); | var result = new Dictionary<string, SlashCommandInfo>(); | ||||
| // fore each user-defined module | |||||
| // For each user-defined module ... | |||||
| foreach (var userModule in types) | foreach (var userModule in types) | ||||
| { | { | ||||
| // Get its associated information | |||||
| // Get its associated information. If there isn't any it means something went wrong, but it's not a critical error. | |||||
| SlashModuleInfo moduleInfo; | SlashModuleInfo moduleInfo; | ||||
| if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | if (moduleDefs.TryGetValue(userModule, out moduleInfo)) | ||||
| { | { | ||||
| // 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)) | |||||
| { | |||||
| Delegate delegateMethod = CreateDelegate(commandMethod, moduleInfo.userCommandModule); | |||||
| SlashCommandInfo commandInfo = new SlashCommandInfo( | |||||
| module: moduleInfo, | |||||
| name: slashCommand.commandName, | |||||
| description: slashCommand.description, | |||||
| userMethod: delegateMethod | |||||
| ); | |||||
| result.Add(slashCommand.commandName, commandInfo); | |||||
| commandInfos.Add(commandInfo); | |||||
| } | |||||
| } | |||||
| // Create the root-level commands | |||||
| var commandInfos = CreateSameLevelCommands(result, userModule, moduleInfo); | |||||
| moduleInfo.SetCommands(commandInfos); | moduleInfo.SetCommands(commandInfos); | ||||
| // Then create all of the command groups it has. | |||||
| 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) | |||||
| { | |||||
| // 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); | |||||
| } | |||||
| } | |||||
| private static List<SlashCommandInfo> CreateSameLevelCommands(Dictionary<string, SlashCommandInfo> result, TypeInfo userModule, SlashModuleInfo moduleInfo) | |||||
| { | |||||
| var commandMethods = userModule.GetMethods(); | |||||
| List<SlashCommandInfo> commandInfos = new List<SlashCommandInfo>(); | |||||
| foreach (var commandMethod in commandMethods) | |||||
| { | |||||
| // Get the SlashCommand attribute | |||||
| 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, | |||||
| isGlobal: IsCommandGlobal(commandMethod) | |||||
| ); | |||||
| result.Add(commandInfo.Module.Path + SlashModuleInfo.PathSeperator + commandInfo.Name, commandInfo); | |||||
| commandInfos.Add(commandInfo); | |||||
| } | |||||
| } | |||||
| return commandInfos; | |||||
| } | |||||
| /// <summary> | |||||
| /// Determines wheater a method can be clasified as a slash command | |||||
| /// </summary> | |||||
| private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | private static bool IsValidSlashCommand(MethodInfo method, out SlashCommand slashCommand) | ||||
| { | { | ||||
| // Verify that we only have one [SlashCommand(...)] attribute | // Verify that we only have one [SlashCommand(...)] attribute | ||||
| @@ -116,10 +218,146 @@ namespace Discord.SlashCommands | |||||
| return true; | return true; | ||||
| } | } | ||||
| /// <summary> | /// <summary> | ||||
| /// Determins if the method has a [Global] Attribute. | |||||
| /// </summary> | |||||
| 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; | |||||
| } | |||||
| /// <summary> | |||||
| /// Process the parameters of this method, including all the attributes. | |||||
| /// </summary> | |||||
| private static List<SlashParameterInfo> ConstructCommandParameters(MethodInfo method) | |||||
| { | |||||
| // Prepare the final list of parameters | |||||
| List<SlashParameterInfo> finalParameters = new List<SlashParameterInfo>(); | |||||
| // For each mehod parameter ... | |||||
| // ex: ... MyCommand(string abc, int myInt) | |||||
| // `abc` and `myInt` are parameters | |||||
| foreach (var methodParameter in method.GetParameters()) | |||||
| { | |||||
| SlashParameterInfo newParameter = new SlashParameterInfo(); | |||||
| // 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 | |||||
| // 0 -> then use the default description | |||||
| // 1 -> Use the value from that attribute | |||||
| // 2+ -> Throw an error. This shouldn't normaly happen, but we check for sake of sanity | |||||
| var descriptions = methodParameter.GetCustomAttributes(typeof(Description)); | |||||
| if (descriptions.Count() == 0) | |||||
| newParameter.Description = Description.DefaultDescription; | |||||
| else if (descriptions.Count() > 1) | |||||
| throw new Exception($"Too many Description attributes on a single parameter ({method.Name} -> {methodParameter.Name}). It can only contain one!"); | |||||
| else | |||||
| newParameter.Description = (descriptions.First() as Description).description; | |||||
| // 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); | |||||
| // 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!"); | |||||
| // 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)) | |||||
| { | |||||
| throw new Exception($"Parameter ({method.Name} -> {methodParameter.Name}) is of type string, but choice is of type int!"); | |||||
| } | |||||
| 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) | |||||
| { | |||||
| throw new Exception($"Parameter ({method.Name} -> {methodParameter.Name}) is of type int, but choice is of type string!"); | |||||
| } | |||||
| newParameter.AddChoice(choice.choiceName, (int)choice.choiceIntValue); | |||||
| } | |||||
| } | |||||
| finalParameters.Add(newParameter); | |||||
| } | |||||
| return finalParameters; | |||||
| } | |||||
| /// <summary> | |||||
| /// Get the type of command option from a method parameter info. | |||||
| /// </summary> | |||||
| private static ApplicationCommandOptionType TypeFromMethodParameter(ParameterInfo methodParameter) | |||||
| { | |||||
| // Can't do switch -- who knows why? | |||||
| 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) || | |||||
| methodParameter.ParameterType == typeof(bool?)) | |||||
| return ApplicationCommandOptionType.Boolean; | |||||
| if (methodParameter.ParameterType == typeof(SocketGuildChannel)) | |||||
| return ApplicationCommandOptionType.Channel; | |||||
| if (methodParameter.ParameterType == typeof(SocketRole)) | |||||
| return ApplicationCommandOptionType.Role; | |||||
| if (methodParameter.ParameterType == typeof(SocketGuildUser)) | |||||
| return ApplicationCommandOptionType.User; | |||||
| throw new Exception($"Got parameter type other than int, string, bool, guild, role, or user. {methodParameter.Name}"); | |||||
| } | |||||
| /// <summary> | |||||
| /// 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?', | |||||
| /// </summary> | |||||
| private static bool GetNullableStatus(ParameterInfo methodParameter) | |||||
| { | |||||
| if(methodParameter.ParameterType == typeof(int?) || | |||||
| methodParameter.ParameterType == typeof(bool?)) | |||||
| { | |||||
| return true; | |||||
| } | |||||
| return false; | |||||
| } | |||||
| /// <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 | ||||
| /// </summary> | /// </summary> | ||||
| public static Delegate CreateDelegate(MethodInfo methodInfo, object target) | |||||
| private static Delegate CreateDelegate(MethodInfo methodInfo, object target) | |||||
| { | { | ||||
| Func<Type[], Type> getType; | Func<Type[], Type> getType; | ||||
| var isAction = methodInfo.ReturnType.Equals((typeof(void))); | var isAction = methodInfo.ReturnType.Equals((typeof(void))); | ||||
| @@ -143,9 +381,98 @@ 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(Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, IServiceProvider services) | |||||
| public static async Task RegisterCommands(DiscordSocketClient socketClient, Dictionary<Type, SlashModuleInfo> rootModuleInfos, Dictionary<string, SlashCommandInfo> commandDefs, SlashCommandService slashCommandService, List<ulong> 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. | |||||
| // Build the commands | |||||
| List<SlashCommandCreationProperties> builtCommands = await BuildCommands(rootModuleInfos).ConfigureAwait(false); | |||||
| // Scan for each existing command on discord so we know what is already there. | |||||
| 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 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) | |||||
| { | |||||
| builtCommands.RemoveAll(x => (!x.Global && x.Name == existingCommand.Name)); | |||||
| } | |||||
| foreach (var existingCommand in existingGlobalCommands) | |||||
| { | |||||
| builtCommands.RemoveAll(x => (x.Global && x.Name == existingCommand.Name)); | |||||
| } | |||||
| } | |||||
| // 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 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))) | |||||
| { | |||||
| await existingCommand.DeleteAsync(); | |||||
| } | |||||
| } | |||||
| foreach (var existingCommand in existingGlobalCommands) | |||||
| { | |||||
| // 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 || | |||||
| // There are no commands which contain this existing command. | |||||
| !builtCommands.Any(x => x.Global && x.Name.Contains(SlashModuleInfo.PathSeperator + existingCommand.Name))) | |||||
| { | |||||
| await existingCommand.DeleteAsync(); | |||||
| } | |||||
| } | |||||
| } | |||||
| // 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) | |||||
| { | |||||
| await socketClient.Rest.CreateGlobalCommand(builtCommand).ConfigureAwait(false); | |||||
| } | |||||
| else | |||||
| { | |||||
| foreach (ulong guildID in guildIDs) | |||||
| { | |||||
| await socketClient.Rest.CreateGuildCommand(builtCommand, guildID).ConfigureAwait(false); | |||||
| } | |||||
| } | |||||
| } | |||||
| return; | return; | ||||
| } | } | ||||
| /// <summary> | |||||
| /// Build and return all of the commands this assembly contians. | |||||
| /// </summary> | |||||
| public static async Task<List<SlashCommandCreationProperties>> BuildCommands(Dictionary<Type, SlashModuleInfo> rootModuleInfos) | |||||
| { | |||||
| List<SlashCommandCreationProperties> builtCommands = new List<SlashCommandCreationProperties>(); | |||||
| foreach (var pair in rootModuleInfos) | |||||
| { | |||||
| var rootModuleInfo = pair.Value; | |||||
| builtCommands.AddRange(rootModuleInfo.BuildCommands()); | |||||
| } | |||||
| return builtCommands; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,5 +1,7 @@ | |||||
| using Discord.Commands.SlashCommands.Types; | using Discord.Commands.SlashCommands.Types; | ||||
| using Discord.WebSocket; | |||||
| using System; | using System; | ||||
| using System.Threading.Tasks; | |||||
| namespace Discord.SlashCommands | namespace Discord.SlashCommands | ||||
| { | { | ||||
| @@ -17,5 +19,16 @@ namespace Discord.SlashCommands | |||||
| var newValue = interaction as T; | var newValue = interaction as T; | ||||
| Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | Interaction = newValue ?? throw new InvalidOperationException($"Invalid interaction type. Expected {typeof(T).Name}, got {interaction.GetType().Name}."); | ||||
| } | } | ||||
| public async Task<IMessage> Reply(string text = null, bool isTTS = false, Embed embed = null, InteractionResponseType Type = InteractionResponseType.ChannelMessageWithSource, | |||||
| AllowedMentions allowedMentions = null, RequestOptions options = null) | |||||
| { | |||||
| if(Interaction is SocketInteraction) | |||||
| { | |||||
| return await (Interaction as SocketInteraction).FollowupAsync(text, isTTS, embed, Type, allowedMentions, options); | |||||
| } | |||||
| return null; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -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. | ||||
| @@ -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; | ||||
| @@ -30,7 +30,7 @@ namespace Discord.WebSocket | |||||
| internal SocketInteractionDataOption() { } | internal SocketInteractionDataOption() { } | ||||
| internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild) | internal SocketInteractionDataOption(Model model, DiscordSocketClient discord, ulong guild) | ||||
| { | { | ||||
| this.Name = Name; | |||||
| this.Name = model.Name; | |||||
| this.Value = model.Value.IsSpecified ? model.Value.Value : null; | this.Value = model.Value.IsSpecified ? model.Value.Value : null; | ||||
| this.discord = discord; | this.discord = discord; | ||||
| this.guild = guild; | this.guild = guild; | ||||
| @@ -44,14 +44,32 @@ namespace Discord.WebSocket | |||||
| // Converters | // Converters | ||||
| public static explicit operator bool(SocketInteractionDataOption option) | public static explicit operator bool(SocketInteractionDataOption option) | ||||
| => (bool)option.Value; | => (bool)option.Value; | ||||
| // The default value is of type long, so an implementaiton of of the long option is trivial | |||||
| public static explicit operator int(SocketInteractionDataOption option) | public static explicit operator int(SocketInteractionDataOption option) | ||||
| => (int)option.Value; | |||||
| => unchecked( | |||||
| (int)( (long)option.Value ) | |||||
| ); | |||||
| public static explicit operator string(SocketInteractionDataOption option) | public static explicit operator string(SocketInteractionDataOption option) | ||||
| => option.Value.ToString(); | => 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) | public static explicit operator SocketGuildChannel(SocketInteractionDataOption option) | ||||
| { | { | ||||
| if (option.Value is ulong id) | |||||
| if (ulong.TryParse((string)option.Value, out ulong id)) | |||||
| { | { | ||||
| var guild = option.discord.GetGuild(option.guild); | var guild = option.discord.GetGuild(option.guild); | ||||
| @@ -66,7 +84,7 @@ namespace Discord.WebSocket | |||||
| public static explicit operator SocketRole(SocketInteractionDataOption option) | public static explicit operator SocketRole(SocketInteractionDataOption option) | ||||
| { | { | ||||
| if (option.Value is ulong id) | |||||
| if (ulong.TryParse((string)option.Value, out ulong id)) | |||||
| { | { | ||||
| var guild = option.discord.GetGuild(option.guild); | var guild = option.discord.GetGuild(option.guild); | ||||
| @@ -81,7 +99,7 @@ namespace Discord.WebSocket | |||||
| public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | public static explicit operator SocketGuildUser(SocketInteractionDataOption option) | ||||
| { | { | ||||
| if(option.Value is ulong id) | |||||
| if (ulong.TryParse((string)option.Value, out ulong id)) | |||||
| { | { | ||||
| var guild = option.discord.GetGuild(option.guild); | var guild = option.discord.GetGuild(option.guild); | ||||