diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs deleted file mode 100644 index a44a30210..000000000 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ /dev/null @@ -1,1346 +0,0 @@ -// This file is part of the DisCatSharp project. -// -// Copyright (c) 2021 AITSYS -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -using System; -using System.Reflection; -using System.Threading.Tasks; -using System.Collections.Generic; -using DisCatSharp.Entities; -using System.Linq; -using DisCatSharp.EventArgs; -using Microsoft.Extensions.Logging; -using DisCatSharp.Common.Utilities; -using Microsoft.Extensions.DependencyInjection; -using DisCatSharp.ApplicationCommands.EventArgs; -using DisCatSharp.Exceptions; -using DisCatSharp.Enums; -using DisCatSharp.ApplicationCommands.Attributes; -using System.Text.RegularExpressions; -using DisCatSharp.Common; - -namespace DisCatSharp.ApplicationCommands -{ - /// - /// A class that handles slash commands for a client. - /// - public sealed class ApplicationCommandsExtension : BaseExtension - { - /// - /// A list of methods for top level commands. - /// - private static List _commandMethods { get; set; } = new List(); - - /// - /// List of groups. - /// - private static List _groupCommands { get; set; } = new List(); - - /// - /// List of groups with subgroups. - /// - private static List _subGroupCommands { get; set; } = new List(); - - /// - /// List of context menus. - /// - private static List _contextMenuCommands { get; set; } = new List(); - - /// - /// Singleton modules. - /// - private static List _singletonModules { get; set; } = new List(); - - /// - /// List of modules to register. - /// - private List> _updateList { get; set; } = new List>(); - - /// - /// Configuration for Discord. - /// - private readonly ApplicationCommandsConfiguration _configuration; - - /// - /// Set to true if anything fails when registering. - /// - private static bool _errored { get; set; } = false; - - /// - /// Gets a list of registered commands. The key is the guild id (null if global). - /// - public IReadOnlyList>> RegisteredCommands - => _registeredCommands; - private static List>> _registeredCommands = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The configuration. - internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration) - { - this._configuration = configuration; - } - - /// - /// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. - /// - /// The client to setup on. - protected internal override void Setup(DiscordClient client) - { - if (this.Client != null) - throw new InvalidOperationException("What did I tell you?"); - - this.Client = client; - - this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null); - this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null); - this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null); - this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null); - - this.Client.Ready += this.Update; - this.Client.InteractionCreated += this.InteractionHandler; - this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; - } - - /// - /// Registers a command class. - /// - /// The command class to register. - /// The guild id to register it on. If you want global commands, leave it null. - public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule - { - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T)))); - } - - /// - /// Registers a command class. - /// - /// The of the command class to register. - /// The guild id to register it on. If you want global commands, leave it null. - public void RegisterCommands(Type type, ulong? guildId = null) - { - if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) - throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); - //If sharding, only register for shard 0 - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type))); - } - - /// - /// Registers a command class with permission setup. - /// - /// The command class to register. - /// The guild id to register it on. - /// A callback to setup permissions with. - public void RegisterCommands(ulong guildId, Action permissionSetup = null) where T : ApplicationCommandsModule - { - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); - } - - /// - /// Registers a command class with permission setup. - /// - /// The of the command class to register. - /// The guild id to register it on. - /// A callback to setup permissions with. - public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = null) - { - if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) - throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); - //If sharding, only register for shard 0 - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); - } - /* - /// - /// Registers a command class with permission setup but without a guild id. - /// - /// The command class to register. - /// A callback to setup permissions with. - public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule - { - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); - } - - /// - /// Registers a command class with permission setup but without a guild id. - /// - /// The of the command class to register. - /// A callback to setup permissions with. - public void RegisterCommands(Type type, Action permissionSetup = null) - { - if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) - throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); - //If sharding, only register for shard 0 - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); - } - */ - /// - /// To be run on ready. - /// - /// The client. - /// The ready event args. - internal Task Update(DiscordClient client, ReadyEventArgs e) - => this.Update(); - - /// - /// Actual method for registering, used for RegisterCommands and on Ready. - /// - internal Task Update() - { - //Only update for shard 0 - if (this.Client.ShardId == 0) - { - //Groups commands by guild id or global - foreach (var key in this._updateList.Select(x => x.Key).Distinct()) - { - this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); - } - } - return Task.CompletedTask; - } - - /// - /// Method for registering commands for a target from modules. - /// - /// The types. - /// The optional guild id. - private void RegisterCommands(IEnumerable types, ulong? guildid) - { - //Initialize empty lists to be added to the global ones at the end - var commandMethods = new List(); - var groupCommands = new List(); - var subGroupCommands = new List(); - var contextMenuCommands = new List(); - var updateList = new List(); - - var commandTypeSources = new List>(); - - _ = Task.Run(async () => - { - //Iterates over all the modules - foreach (var config in types) - { - var type = config.Type; - try - { - var module = type.GetTypeInfo(); - var classes = new List(); - - //Add module to classes list if it's a group - if (module.GetCustomAttribute() != null) - { - classes.Add(module); - } - else - { - //Otherwise add the nested groups - classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); - } - - //Handles groups - foreach (var subclassinfo in classes) - { - //Gets the attribute and methods in the group - var groupAttribute = subclassinfo.GetCustomAttribute(); - var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); - var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null); - if (subclasses.Any() && submethods.Any()) - { - throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!"); - } - - //Initializes the command - var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, default_permission: groupAttribute.DefaultPermission); - commandTypeSources.Add(new KeyValuePair(type, type)); - - var commandmethods = new List>(); - //Handles commands in the group - foreach (var submethod in submethods) - { - var commandAttribute = submethod.GetCustomAttribute(); - - //Gets the paramaters and accounts for InteractionContext - var parameters = submethod.GetParameters(); - if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) - throw new ArgumentException($"The first argument must be an InteractionContext!"); - parameters = parameters.Skip(1).ToArray(); - - var options = await this.ParseParameters(parameters, guildid); - - //Creates the subcommand and adds it to the main command - var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, options); - payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission); - commandTypeSources.Add(new KeyValuePair(subclassinfo, type)); - - //Adds it to the method lists - commandmethods.Add(new KeyValuePair(commandAttribute.Name, submethod)); - groupCommands.Add(new GroupCommand { Name = groupAttribute.Name, Methods = commandmethods }); - } - - var command = new SubGroupCommand { Name = groupAttribute.Name }; - //Handles subgroups - foreach (var subclass in subclasses) - { - var subGroupAttribute = subclass.GetCustomAttribute(); - //I couldn't think of more creative naming - var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); - - var options = new List(); - - var currentMethods = new List>(); - - //Similar to the one for regular groups - foreach (var subsubmethod in subsubmethods) - { - var suboptions = new List(); - var commatt = subsubmethod.GetCustomAttribute(); - var parameters = subsubmethod.GetParameters(); - if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) - throw new ArgumentException($"The first argument must be an InteractionContext!"); - parameters = parameters.Skip(1).ToArray(); - suboptions = suboptions.Concat(await this.ParseParameters(parameters, guildid)).ToList(); - - var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, suboptions); - options.Add(subsubpayload); - commandmethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); - currentMethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); - } - - //Adds the group to the command and method lists - var subpayload = new DiscordApplicationCommandOption(subGroupAttribute.Name, subGroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, null, null, options); - command.SubCommands.Add(new GroupCommand { Name = subGroupAttribute.Name, Methods = currentMethods }); - payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission); - commandTypeSources.Add(new KeyValuePair(subclass, type)); - - //Accounts for lifespans for the sub group - if (subclass.GetCustomAttribute() != null) - { - if (subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) - { - _singletonModules.Add(this.CreateInstance(subclass, this._configuration?.Services)); - } - } - } - if (command.SubCommands.Any()) subGroupCommands.Add(command); - updateList.Add(payload); - - //Accounts for lifespans - if (subclassinfo.GetCustomAttribute() != null) - { - if (subclassinfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) - { - _singletonModules.Add(this.CreateInstance(subclassinfo, this._configuration?.Services)); - } - } - } - - //Handles methods and context menus, only if the module isn't a group itself - if (module.GetCustomAttribute() == null) - { - //Slash commands (again, similar to the one for groups) - var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); - foreach (var method in methods) - { - var commandattribute = method.GetCustomAttribute(); - - var parameters = method.GetParameters(); - if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext))) - throw new ArgumentException($"The first argument must be an InteractionContext!"); - parameters = parameters.Skip(1).ToArray(); - var options = await this.ParseParameters(parameters, guildid); - - commandMethods.Add(new CommandMethod { Method = method, Name = commandattribute.Name }); - - var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, options, commandattribute.DefaultPermission); - updateList.Add(payload); - commandTypeSources.Add(new KeyValuePair(type, type)); - } - - //Context Menus - var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); - foreach (var contextMethod in contextMethods) - { - var contextAttribute = contextMethod.GetCustomAttribute(); - var command = new DiscordApplicationCommand(contextAttribute.Name, null, type: contextAttribute.Type, default_permission: contextAttribute.DefaultPermission); - - var parameters = contextMethod.GetParameters(); - if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext))) - throw new ArgumentException($"The first argument must be a ContextMenuContext!"); - if (parameters.Length > 1) - throw new ArgumentException($"A context menu cannot have parameters!"); - - contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name }); - - updateList.Add(command); - commandTypeSources.Add(new KeyValuePair(type, type)); - } - - //Accounts for lifespans - if (module.GetCustomAttribute() != null) - { - if (module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) - { - _singletonModules.Add(this.CreateInstance(module, this._configuration?.Services)); - } - } - } - } - catch (Exception ex) - { - //This isn't really much more descriptive but I added a separate case for it anyway - if (ex is BadRequestException brex) - this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); - else - this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); - _errored = true; - } - } - if (!_errored) - { - try - { - async Task UpdateCommandPermission(ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType) - { - if (guildid == null) - { - //throw new NotImplementedException("You can't set global permissions till yet. See https://discord.com/developers/docs/interactions/application-commands#permissions"); - } - else - { - var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName); - var conf = types.First(t => t.Type == commandRootType); - conf.Setup?.Invoke(ctx); - - if (ctx.Permissions.Count == 0) - return; - - await this.Client.OverwriteGuildApplicationCommandPermissionsAsync(guildid.Value, commandId, ctx.Permissions); - } - } - - async Task UpdateCommandPermissionGroup(GroupCommand groupCommand) - { - foreach (var com in groupCommand.Methods) - { - var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Value.DeclaringType); - - await UpdateCommandPermission(groupCommand.CommandId, com.Key, source.Key, source.Value); - } - } - - var commands = guildid == null - ? await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(updateList) - : (IEnumerable)await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, updateList); - - //Creates a guild command if a guild id is specified, otherwise global - //Checks against the ids and adds them to the command method lists - foreach (var command in commands) - { - if (commandMethods.Any(x => x.Name == command.Name)) - { - var com = commandMethods.First(x => x.Name == command.Name); - com.CommandId = command.Id; - - var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType); - await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key); - } - - else if (groupCommands.Any(x => x.Name == command.Name)) - { - var com = groupCommands.First(x => x.Name == command.Name); - com.CommandId = command.Id; - - await UpdateCommandPermissionGroup(com); - } - - else if (subGroupCommands.Any(x => x.Name == command.Name)) - { - var com = subGroupCommands.First(x => x.Name == command.Name); - com.CommandId = command.Id; - - foreach (var groupComs in com.SubCommands) - await UpdateCommandPermissionGroup(groupComs); - } - - else if (contextMenuCommands.Any(x => x.Name == command.Name)) - { - var com = contextMenuCommands.First(x => x.Name == command.Name); - com.CommandId = command.Id; - - var source = commandTypeSources.First(f => f.Key == com.Method.DeclaringType); - await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key); - } - } - - //Adds to the global lists finally - _commandMethods.AddRange(commandMethods); - _groupCommands.AddRange(groupCommands); - _subGroupCommands.AddRange(subGroupCommands); - _contextMenuCommands.AddRange(contextMenuCommands); - - _registeredCommands.Add(new KeyValuePair>(guildid, commands.ToList())); - - foreach (var command in commandMethods) - { - var app = types.First(t => t.Type == command.Method.DeclaringType); - } - } - catch (Exception ex) - { - if (ex is BadRequestException brex) - this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); - else - this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); - _errored = true; - } - } - }); - } - - /// - /// Interaction handler. - /// - /// The client. - /// The event args. - private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) - { - _ = Task.Run(async () => - { - if (e.Interaction.Type == InteractionType.ApplicationCommand) - { - //Creates the context - var context = new InteractionContext - { - Interaction = e.Interaction, - Channel = e.Interaction.Channel, - Guild = e.Interaction.Guild, - User = e.Interaction.User, - Client = client, - ApplicationCommandsExtension = this, - CommandName = e.Interaction.Data.Name, - InteractionId = e.Interaction.Id, - Token = e.Interaction.Token, - Services = this._configuration?.Services, - ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), - ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), - ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), - Type = ApplicationCommandType.ChatInput - }; - - try - { - if (_errored) - throw new InvalidOperationException("Slash commands failed to register properly on startup."); - - var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); - var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); - var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); - if (!methods.Any() && !groups.Any() && !subgroups.Any()) - throw new InvalidOperationException("A slash command was executed, but no command was registered for it."); - - if (methods.Any()) - { - var method = methods.First().Method; - - var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); - - await this.RunCommandAsync(context, method, args); - } - else if (groups.Any()) - { - var command = e.Interaction.Data.Options.First(); - var method = groups.First().Methods.First(x => x.Key == command.Name).Value; - - var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options); - - await this.RunCommandAsync(context, method, args); - } - else if (subgroups.Any()) - { - var command = e.Interaction.Data.Options.First(); - var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); - - var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; - - var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options); - - await this.RunCommandAsync(context, method, args); - } - - await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs { Context = context }); - } - catch (Exception ex) - { - await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs { Context = context, Exception = ex }); - } - } - else if (e.Interaction.Type == InteractionType.AutoComplete) - { - if (_errored) - throw new InvalidOperationException("Slash commands failed to register properly on startup."); - - var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); - var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); - var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); - if (!methods.Any() && !groups.Any() && !subgroups.Any()) - throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it."); - - try - { - if (methods.Any()) - { - var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); - var method = methods.First().Method; - - var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); - var provider = option.GetCustomAttribute().ProviderType; - var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); - var providerInstance = Activator.CreateInstance(provider); - - var context = new AutocompleteContext - { - Interaction = e.Interaction, - Options = e.Interaction.Data.Options.ToList(), - FocusedOption = focusedOption - }; - - var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); - await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); - } - else if (groups.Any()) - { - var command = e.Interaction.Data.Options.First(); - var group = groups.First().Methods.First(x => x.Key == command.Name).Value; - - var focusedOption = command.Options.First(o => o.Focused); - var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); - var provider = option.GetCustomAttribute().ProviderType; - var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); - var providerInstance = Activator.CreateInstance(provider); - - var context = new AutocompleteContext - { - Interaction = e.Interaction, - Options = command.Options.ToList(), - FocusedOption = focusedOption - }; - - var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); - await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); - } - /*else if (subgroups.Any()) - { - var command = e.Interaction.Data.Options.First(); - var method = methods.First().Method; - var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); - - var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused); - this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue); - - var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value; - var provider = option.GetCustomAttribute().ProviderType; - var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); - var providerInstance = Activator.CreateInstance(provider); - - var context = new AutocompleteContext - { - Interaction = e.Interaction, - Options = command.Options.First(x => x.Name == group.Name).Options.ToList(), - FocusedOption = focusedOption - }; - - var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); - await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); - }*/ - } - catch (Exception ex) - { - this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); - } - } - }); - return Task.CompletedTask; - } - - /// - /// Context menu handler. - /// - /// The client. - /// The event args. - private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) - { - _ = Task.Run(async () => - { - //Creates the context - var context = new ContextMenuContext - { - Interaction = e.Interaction, - Channel = e.Interaction.Channel, - Client = client, - Services = this._configuration?.Services, - CommandName = e.Interaction.Data.Name, - ApplicationCommandsExtension = this, - Guild = e.Interaction.Guild, - InteractionId = e.Interaction.Id, - User = e.Interaction.User, - Token = e.Interaction.Token, - TargetUser = e.TargetUser, - TargetMessage = e.TargetMessage, - Type = e.Type - }; - - try - { - if (_errored) - throw new InvalidOperationException("Context menus failed to register properly on startup."); - - //Gets the method for the command - var method = _contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); - - if (method == null) - throw new InvalidOperationException("A context menu was executed, but no command was registered for it."); - - await this.RunCommandAsync(context, method.Method, new[] { context }); - - await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs { Context = context }); - } - catch (Exception ex) - { - await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs { Context = context, Exception = ex }); - } - }); - - return Task.CompletedTask; - } - - /// - /// Runs a command. - /// - /// The base context. - /// The method info. - /// The arguments. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] - internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) - { - object classInstance; - - //Accounts for lifespans - var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; - switch (moduleLifespan) - { - case ApplicationCommandModuleLifespan.Scoped: - //Accounts for static methods and adds DI - classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.Services.CreateScope().ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.Services.CreateScope().ServiceProvider); - break; - - case ApplicationCommandModuleLifespan.Transient: - //Accounts for static methods and adds DI - classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.Services, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.Services); - break; - - //If singleton, gets it from the singleton list - case ApplicationCommandModuleLifespan.Singleton: - classInstance = _singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); - break; - - default: - throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); - } - - ApplicationCommandsModule module = null; - if (classInstance is ApplicationCommandsModule mod) - module = mod; - - // Slash commands - if (context is InteractionContext slashContext) - { - await this.RunPreexecutionChecksAsync(method, slashContext); - - var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); - - if (shouldExecute) - { - await (Task)method.Invoke(classInstance, args.ToArray()); - - await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); - } - } - // Context menus - if (context is ContextMenuContext contextMenuContext) - { - await this.RunPreexecutionChecksAsync(method, contextMenuContext); - - var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); - - if (shouldExecute) - { - await (Task)method.Invoke(classInstance, args.ToArray()); - - await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); - } - } - } - - /// - /// Property injection copied over from CommandsNext - /// - /// The type. - /// The services. - internal object CreateInstance(Type t, IServiceProvider services) - { - var ti = t.GetTypeInfo(); - var constructors = ti.DeclaredConstructors - .Where(xci => xci.IsPublic) - .ToArray(); - - if (constructors.Length != 1) - throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); - - var constructor = constructors[0]; - var constructorArgs = constructor.GetParameters(); - var args = new object[constructorArgs.Length]; - - if (constructorArgs.Length != 0 && services == null) - throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); - - // inject via constructor - if (constructorArgs.Length != 0) - for (var i = 0; i < args.Length; i++) - args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); - - var moduleInstance = Activator.CreateInstance(t, args); - - // inject into properties - var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); - foreach (var prop in props) - { - if (prop.GetCustomAttribute() != null) - continue; - - var service = services.GetService(prop.PropertyType); - if (service == null) - continue; - - prop.SetValue(moduleInstance, service); - } - - // inject into fields - var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); - foreach (var field in fields) - { - if (field.GetCustomAttribute() != null) - continue; - - var service = services.GetService(field.FieldType); - if (service == null) - continue; - - field.SetValue(moduleInstance, service); - } - - return moduleInstance; - } - - /// - /// Resolves the slash command parameters. - /// - /// The event arguments. - /// The interaction context. - /// The method info. - /// The options. - private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) - { - var args = new List { context }; - var parameters = method.GetParameters().Skip(1); - - for (var i = 0; i < parameters.Count(); i++) - { - var parameter = parameters.ElementAt(i); - - //Accounts for optional arguments without values given - if (parameter.IsOptional && (options == null || - (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) - args.Add(parameter.DefaultValue); - else - { - var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); - - //Checks the type and casts/references resolved and adds the value to the list - //This can probably reference the slash command's type property that didn't exist when I wrote this and it could use a cleaner switch instead, but if it works it works - if (parameter.ParameterType == typeof(string)) - args.Add(option.Value.ToString()); - else if (parameter.ParameterType.IsEnum) - args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); - else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) - args.Add((long?)option.Value); - else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) - args.Add((bool?)option.Value); - else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) - args.Add((double?)option.Value); - else if (parameter.ParameterType == typeof(DiscordUser)) - { - //Checks through resolved - if (e.Interaction.Data.Resolved.Members != null && - e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) - args.Add(member); - else if (e.Interaction.Data.Resolved.Users != null && - e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) - args.Add(user); - else - args.Add(await this.Client.GetUserAsync((ulong)option.Value)); - } - else if (parameter.ParameterType == typeof(DiscordChannel)) - { - //Checks through resolved - if (e.Interaction.Data.Resolved.Channels != null && - e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) - args.Add(channel); - else - args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); - } - else if (parameter.ParameterType == typeof(DiscordRole)) - { - //Checks through resolved - if (e.Interaction.Data.Resolved.Roles != null && - e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) - args.Add(role); - else - args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); - } - else if (parameter.ParameterType == typeof(SnowflakeObject)) - { - //Checks through resolved - if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) - args.Add(role); - else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) - args.Add(member); - else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) - args.Add(user); - else - throw new ArgumentException("Error resolving mentionable option."); - } - else - throw new ArgumentException($"Error resolving interaction."); - } - } - - return args; - } - - /// - /// Runs the preexecution checks. - /// - /// The method info. - /// The basecontext. - private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) - { - if (context is InteractionContext ctx) - { - //Gets all attributes from parent classes as well and stuff - var attributes = new List(); - attributes.AddRange(method.GetCustomAttributes(true)); - attributes.AddRange(method.DeclaringType.GetCustomAttributes()); - if (method.DeclaringType.DeclaringType != null) - { - attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); - if (method.DeclaringType.DeclaringType.DeclaringType != null) - { - attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); - } - } - - var dict = new Dictionary(); - foreach (var att in attributes) - { - //Runs the check and adds the result to a list - var result = await att.ExecuteChecksAsync(ctx); - dict.Add(att, result); - } - - //Checks if any failed, and throws an exception - if (dict.Any(x => x.Value == false)) - throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; - } - if (context is ContextMenuContext CMctx) - { - var attributes = new List(); - attributes.AddRange(method.GetCustomAttributes(true)); - attributes.AddRange(method.DeclaringType.GetCustomAttributes()); - if (method.DeclaringType.DeclaringType != null) - { - attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); - if (method.DeclaringType.DeclaringType.DeclaringType != null) - { - attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); - } - } - - var dict = new Dictionary(); - foreach (var att in attributes) - { - //Runs the check and adds the result to a list - var result = await att.ExecuteChecksAsync(CMctx); - dict.Add(att, result); - } - - //Checks if any failed, and throws an exception - if (dict.Any(x => x.Value == false)) - throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; - } - } - - /// - /// Gets the choice attributes from choice provider. - /// - /// The custom attributes. - /// - private async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null) - { - var choices = new List(); - foreach (var choiceProviderAttribute in customAttributes) - { - var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); - - if (method == null) - throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); - else - { - var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); - - // Abstract class offers more properties that can be set - if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) - { - choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) - ?.SetValue(instance, guildId); - - choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) - ?.SetValue(instance, _configuration.Services); - } - - //Gets the choices from the method - var result = await (Task>)method.Invoke(instance, null); - - if (result.Any()) - { - choices.AddRange(result); - } - } - } - - return choices; - } - /// - /// Gets the choice attributes from enum parameter. - /// - /// The enum parameter. - private static List GetChoiceAttributesFromEnumParameter(Type enumParam) - { - var choices = new List(); - foreach (Enum enumValue in Enum.GetValues(enumParam)) - { - choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); - } - return choices; - } - - /// - /// Gets the parameter type. - /// - /// The type. - private ApplicationCommandOptionType GetParameterType(Type type) - { - var parametertype = type == typeof(string) - ? ApplicationCommandOptionType.String - : type == typeof(long) || type == typeof(long?) - ? ApplicationCommandOptionType.Integer - : type == typeof(bool) || type == typeof(bool?) - ? ApplicationCommandOptionType.Boolean - : type == typeof(double) || type == typeof(double?) - ? ApplicationCommandOptionType.Number - : type == typeof(DiscordChannel) - ? ApplicationCommandOptionType.Channel - : type == typeof(DiscordUser) - ? ApplicationCommandOptionType.User - : type == typeof(DiscordRole) - ? ApplicationCommandOptionType.Role - : type == typeof(SnowflakeObject) - ? ApplicationCommandOptionType.Mentionable - : type.IsEnum - ? ApplicationCommandOptionType.String - : throw new ArgumentException("Cannot convert type! Argument types must be string, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject or an Enum."); - return parametertype; - } - - /// - /// Gets the choice attributes from parameter. - /// - /// The choice attributes. - private List GetChoiceAttributesFromParameter(IEnumerable choiceattributes) - { - return !choiceattributes.Any() - ? null - : choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); - } - - /// - /// Parses the parameters. - /// - /// The parameters. - /// The guild id. - /// A Task. - private async Task> ParseParameters(ParameterInfo[] parameters, ulong? guildId) - { - var options = new List(); - foreach (var parameter in parameters) - { - //Gets the attribute - var optionattribute = parameter.GetCustomAttribute(); - if (optionattribute == null) - throw new ArgumentException("Arguments must have the Option attribute!"); - - var autocompleteAttribute = parameter.GetCustomAttribute(); - if (optionattribute.Autocomplete && autocompleteAttribute == null) - throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!"); - if (!optionattribute.Autocomplete && autocompleteAttribute != null) - throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!"); - - //Sets the type - var type = parameter.ParameterType; - var parametertype = this.GetParameterType(type); - - //Handles choices - //From attributes - var choices = this.GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); - //From enums - if (parameter.ParameterType.IsEnum) - { - choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); - } - //From choice provider - var choiceProviders = parameter.GetCustomAttributes(); - if (choiceProviders.Any()) - { - choices = await this.GetChoiceAttributesFromProvider(choiceProviders, guildId); - } - - var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; - - options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes, optionattribute.Autocomplete)); - } - - return options; - } - - /// - /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. - /// Should only be run on the slash command extension linked to shard 0 if sharding. - /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. - /// - public async Task RefreshCommandsAsync() - { - _commandMethods.Clear(); - _groupCommands.Clear(); - _subGroupCommands.Clear(); - _registeredCommands.Clear(); - _contextMenuCommands.Clear(); - - await this.Update(); - } - - /// - /// Fires when the execution of a slash command fails. - /// - public event AsyncEventHandler SlashCommandErrored - { - add { this._slashError.Register(value); } - remove { this._slashError.Unregister(value); } - } - private AsyncEvent _slashError; - - /// - /// Fires when the execution of a slash command is successful. - /// - public event AsyncEventHandler SlashCommandExecuted - { - add { this._slashExecuted.Register(value); } - remove { this._slashExecuted.Unregister(value); } - } - private AsyncEvent _slashExecuted; - - /// - /// Fires when the execution of a context menu fails. - /// - public event AsyncEventHandler ContextMenuErrored - { - add { this._contextMenuErrored.Register(value); } - remove { this._contextMenuErrored.Unregister(value); } - } - private AsyncEvent _contextMenuErrored; - - /// - /// Fire when the execution of a context menu is successful. - /// - public event AsyncEventHandler ContextMenuExecuted - { - add { this._contextMenuExecuted.Register(value); } - remove { this._contextMenuExecuted.Unregister(value); } - } - private AsyncEvent _contextMenuExecuted; - } - - /// - /// Holds configuration data for setting up an application command. - /// - internal class ApplicationCommandsModuleConfiguration - { - /// - /// The type of the command module. - /// - public Type Type { get; } - - /// - /// The permission setup. - /// - public Action Setup { get; } - - /// - /// Creates a new command configuration. - /// - /// The type of the command module. - /// The permission setup callback. - public ApplicationCommandsModuleConfiguration(Type type, Action setup = null) - { - this.Type = type; - this.Setup = setup; - } - } - - /// - /// Links a command to its original command module. - /// - internal class ApplicationCommandSourceLink - { - /// - /// The command. - /// - public DiscordApplicationCommand ApplicationCommand { get; set; } - - /// - /// The base/root module the command is contained in. - /// - public Type RootCommandContainerType { get; set; } - - /// - /// The direct group the command is contained in. - /// - public Type CommandContainerType { get; set; } - } - - /// - /// The command method. - /// - internal class CommandMethod - { - /// - /// Gets or sets the command id. - /// - public ulong CommandId { get; set; } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the method. - /// - public MethodInfo Method { get; set; } - } - - /// - /// The group command. - /// - internal class GroupCommand - { - /// - /// Gets or sets the command id. - /// - public ulong CommandId { get; set; } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the methods. - /// - public List> Methods { get; set; } = null; - } - - /// - /// The sub group command. - /// - internal class SubGroupCommand - { - /// - /// Gets or sets the command id. - /// - public ulong CommandId { get; set; } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the sub commands. - /// - public List SubCommands { get; set; } = new List(); - } - - /// - /// The context menu command. - /// - internal class ContextMenuCommand - { - /// - /// Gets or sets the command id. - /// - public ulong CommandId { get; set; } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } - - /// - /// Gets or sets the method. - /// - public MethodInfo Method { get; set; } - } -}