diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 3851d2d69..16f4f1714 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1838 +1,1852 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; 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 s_commandMethods { get; set; } = new(); /// /// List of groups. /// private static List s_groupCommands { get; set; } = new(); /// /// List of groups with subgroups. /// private static List s_subGroupCommands { get; set; } = new(); /// /// List of context menus. /// private static List s_contextMenuCommands { get; set; } = new(); /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = new(); /// /// List of modules to register. /// private readonly List> _updateList = new(); /// /// Configuration for Discord. /// internal static ApplicationCommandsConfiguration Configuration; /// /// Discord client. /// internal static DiscordClient ClientInternal; /// /// Set to true if anything fails when registering. /// private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => s_registeredCommands; private static readonly List>> s_registeredCommands = new(); /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => GlobalCommandsInternal; internal static readonly List GlobalCommandsInternal = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; internal static readonly Dictionary> GuildCommandsInternal = new(); /// /// Gets the registration count. /// private static int s_registrationCount { get; set; } /// /// Gets the expected count. /// private static int s_expectedCount { get; set; } /// /// Gets the guild ids where the applications.commands scope is missing. /// private IReadOnlyList _missingScopeGuildIds; /// /// Gets whether debug is enabled. /// internal static bool DebugEnabled { get; set; } internal static LogLevel ApplicationCommandsLogLevel => DebugEnabled ? LogLevel.Debug : LogLevel.Trace; /// /// Gets whether check through all guilds is enabled. /// internal static bool CheckAllGuilds { get; set; } /// /// Gets whether the registration check should be manually overridden. /// internal static bool ManOr { get; set; } /// /// Gets whether interactions should be automatically deffered. /// internal static bool AutoDeferEnabled { get; set; } /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration = null) { Configuration = configuration; DebugEnabled = configuration?.DebugStartup ?? false; CheckAllGuilds = configuration?.CheckAllGuilds ?? false; ManOr = configuration?.ManualOverride ?? false; AutoDeferEnabled = configuration?.AutoDefer ?? false; } /// /// 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; ClientInternal = 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._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null); this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null); this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null); this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null); this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync(); this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")); private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")); private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Cleans the module for a new start of the bot. /// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES. /// public void CleanModule() { this._updateList.Clear(); s_singletonModules.Clear(); s_errored = false; s_expectedCount = 0; s_registrationCount = 0; s_commandMethods.Clear(); s_groupCommands.Clear(); s_contextMenuCommands.Clear(); s_subGroupCommands.Clear(); s_singletonModules.Clear(); s_registeredCommands.Clear(); GlobalCommandsInternal.Clear(); GuildCommandsInternal.Clear(); } /// /// Registers a command class. /// /// The command class to register. public void RegisterGlobalCommands() where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T)))); } /// /// Registers a command class. /// /// The of the command class to register. public void RegisterGlobalCommands(Type type) { 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))); } /// /// Cleans all guild application commands. /// You normally don't need to execute it. /// public async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) { await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } } /// /// Cleans the global application commands. /// You normally don't need to execute it. /// public async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with permission and translation setup. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); } /// /// Registers a command class with permission and translation setup. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = 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, translationSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// /// The command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// /// The of the command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Type type, Action translationSetup = 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, translationSetup))); } /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add => this._applicationCommandsModuleReady.Register(value); remove => this._applicationCommandsModuleReady.Unregister(value); } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add => this._applicationCommandsModuleStartupFinished.Register(value); remove => this._applicationCommandsModuleStartupFinished.Unregister(value); } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add => this._guildApplicationCommandsRegistered.Register(value); remove => this._guildApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add => this._globalApplicationCommandsRegistered.Register(value); remove => this._globalApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { //Only update for shard 0 if (this.Client.ShardId == 0) { GlobalDiscordCommands = new(); GuildDiscordCommands = new(); var commandsPending = this._updateList.Select(x => x.Key).Distinct(); s_expectedCount = commandsPending.Count(); this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}"); List failedGuilds = new(); IEnumerable globalCommands = null; globalCommands = await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false) ?? null; var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys : this._updateList.Select(x => x.Key)?.Distinct().Where(x => x != null)?.Select(x => x.Value); foreach (var guild in guilds) { IEnumerable commands = null; var unauthorized = false; try { commands = await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false) ?? null; } catch (UnauthorizedException) { unauthorized = true; } finally { if (!unauthorized && commands != null && commands.Any()) GuildDiscordCommands.Add(guild, commands.ToList()); else if (!unauthorized) GuildDiscordCommands.Add(guild, null); else failedGuilds.Add(guild); } } //Default should be to add the help and slash commands can be added without setting any configuration //so this should still add the default help if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp)) { this._updateList.Add(new KeyValuePair (null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule)))); commandsPending = this._updateList.Select(x => x.Key).Distinct(); } if (globalCommands != null && globalCommands.Any()) GlobalDiscordCommands.AddRange(globalCommands); foreach (var key in commandsPending.ToList()) { this.Client.Logger.LogInformation(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); await this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); } this._missingScopeGuildIds = failedGuilds; await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildsWithoutScope = failedGuilds }); } } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private async Task 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>(); //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); //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(); } List groupTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } var slashGroupsTuple = NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations).Result; if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any()) updateList.AddRange(slashGroupsTuple.applicationCommands); if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any()) commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources); if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any()) s_singletonModules.AddRange(slashGroupsTuple.singletonModules); if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any()) groupCommands.AddRange(slashGroupsTuple.groupCommands); if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any()) subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands); //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { List commandTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } //Slash commands var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var slashCommands = CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations).Result; if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any()) updateList.AddRange(slashCommands.applicationCommands); if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any()) commandTypeSources.AddRange(slashCommands.commandTypeSources); if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any()) commandMethods.AddRange(slashCommands.commandMethods); //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var contextCommands = CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations).Result; if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any()) updateList.AddRange(contextCommands.applicationCommands); if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any()) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any()) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.WebResponse.Response}"); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, $"There was an error registering application commands: {brex1.WebResponse.Response}"); else this.Client.Logger.LogCritical(ex, $"There was an error parsing the application commands"); } s_errored = true; } } if (!s_errored) { try { List commands = new(); try { if (guildId == null) { if (updateList != null && updateList.Any()) { var regCommands = RegistrationWorker.RegisterGlobalCommandsAsync(updateList).Result; var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } else { foreach (var cmd in GlobalDiscordCommands) { try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete global command {cmd.Id}. Please clean up manually"); } } } } else { if (updateList != null && updateList.Any()) { var regCommands = RegistrationWorker.RegisterGuildCommandsAsync(guildId.Value, updateList).Result; var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildId.Value, actualCommands); if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild)) { guild.InternalRegisteredApplicationCommands = new(); guild.InternalRegisteredApplicationCommands.AddRange(actualCommands); } } else { foreach (var cmd in GuildDiscordCommands[guildId.Value]) { try { await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete guild command {cmd.Id} in guild {guildId.Value}. Please clean up manually"); } } } } } catch (UnauthorizedException ex) { this.Client.Logger.LogError($"Could not register application commands for guild {guildId}.\nError: {ex.JsonMessage}"); return; } //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.GetFirstValueWhere(x => x.Name == command.Name, out var com)) com.CommandId = command.Id; else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) groupCom.CommandId = command.Id; else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom)) subCom.CommandId = command.Id; else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) cmCom.CommandId = command.Id; } //Adds to the global lists finally s_commandMethods.AddRange(commandMethods); s_groupCommands.AddRange(groupCommands); s_subGroupCommands.AddRange(subGroupCommands); s_contextMenuCommands.AddRange(contextMenuCommands); s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}"); if (guildId.HasValue) { await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null }); } else { await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }); } s_registrationCount++; this.CheckRegistrationStartup(ManOr); } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.WebResponse.Response}"); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, $"There was an error registering application commands: {brex1.WebResponse.Response}"); else this.Client.Logger.LogCritical(ex, $"There was an general error registering application commands"); } s_errored = true; } } } private async void CheckRegistrationStartup(bool man = false) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Checking counts...\n\nExpected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}"); if ((s_registrationCount == s_expectedCount) || man) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredGlobalCommands = GlobalCommandsInternal, RegisteredGuildCommands = GuildCommandsInternal, GuildsWithoutScope = this._missingScopeGuildIds }); this.FinishedRegistration(); } } /// /// 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 = Configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(), Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_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(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (s_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_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, Client = this.Client, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; 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, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; 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 group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value; var focusedOption = command.Options.First().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, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.First().Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; 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 = Configuration?.ServiceProvider, 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, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) throw new InvalidOperationException("Context menus failed to register properly on startup."); //Gets the method for the command var method = s_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(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { 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(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = s_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) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); 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) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection /// /// The type. /// The services. internal static 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); foreach (var parameter in parameters) { //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()); 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(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } 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 pre-execution checks. /// /// The method info. /// The base context. 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. /// The optional guild id private static 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.ServiceProvider); } //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 static ApplicationCommandOptionType GetParameterType(Type type) { var parameterType = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : 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, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parameterType; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) => !choiceAttributes.Any() ? null : choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); /// /// Parses the parameters. /// /// The parameters. /// The optional guild id. internal static async Task> ParseParametersAsync(IEnumerable 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 minimumValue = parameter.GetCustomAttribute()?.Value ?? null; - var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; - + var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; + var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; + var minimumLength = parameter.GetCustomAttribute()?.Value ?? null; + var maximumLength = parameter.GetCustomAttribute()?.Value ?? null; + var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; 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 = GetParameterType(type); + if (parameterType == ApplicationCommandOptionType.String) + { + minimumValue = null; + maximumValue = null; + } + else if (parameterType == ApplicationCommandOptionType.Integer || parameterType == ApplicationCommandOptionType.Number) + { + minimumLength = null; + maximumLength = null; + } + + if (parameterType != ApplicationCommandOptionType.Channel) + channelTypes = null; + //Handles choices //From attributes var choices = 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 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, minimumValue, maximumValue)); + options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength)); } 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() { s_commandMethods.Clear(); s_groupCommands.Clear(); s_subGroupCommands.Clear(); s_registeredCommands.Clear(); s_contextMenuCommands.Clear(); GlobalDiscordCommands.Clear(); GuildDiscordCommands.Clear(); GuildCommandsInternal.Clear(); GlobalCommandsInternal.Clear(); GlobalDiscordCommands = null; GuildDiscordCommands = null; s_errored = false; if (Configuration != null && Configuration.EnableDefaultHelp) { this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule)); } await this.UpdateAsync(); } /// /// 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 translation setup. /// public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action translations = null) { this.Type = type; this.Translations = translations; } } /// /// 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(); } /// /// 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; } } #region Default Help /// /// Represents the default help module. /// internal class DefaultHelpModule : ApplicationCommandsModule { public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } foreach (var sc in slashCommands.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())); } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase)); if (command is null || command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand && c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); return options.AsEnumerable(); } var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (foundCommand is null || foundCommand.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand && x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } [SlashCommand("help", "Displays command help")] internal async Task DefaultHelpAsync(InteractionContext ctx, [Autocomplete(typeof(DefaultHelpAutoCompleteProvider))] [Option("option_one", "top level command to provide help for", true)] string commandName, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))] [Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))] [Option("option_three", "command to provide help for", true)] string commandTwoName = null) { var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); var applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); if (applicationCommands.Count < 1) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands for guild {ctx.Guild.Name}").AsEphemeral(true)); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options .FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{Formatter.InlineCode(cmd.Name)}: {cmd.Description ?? "No description provided."}" }; if (cmd.Options is not null) { var commandOptions = cmd.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{Formatter.InlineCode(subCommand.Name)}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true)); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{Formatter.InlineCode(command.Name)}: {command.Description ?? "No description provided."}" }.AddField(new DiscordEmbedField("Command is NSFW", command.IsNsfw.ToString())); if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } } } #endregion diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/MinimumMaximumAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/MinimumMaximumAttribute.cs index 0b93261fd..f0b96bddd 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/MinimumMaximumAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/MinimumMaximumAttribute.cs @@ -1,97 +1,140 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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; namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Sets a minimum value for this slash command option. Only valid for , or parameters. /// [AttributeUsage(AttributeTargets.Parameter)] -public class MinimumAttribute : Attribute +public class MinimumValueAttribute : Attribute { /// /// The value. /// public object Value { get; internal set; } /// /// Sets a minimum value for this slash command option. Only valid for , or parameters. /// - public MinimumAttribute(int value) + public MinimumValueAttribute(int value) { this.Value = value; } /// /// Sets a minimum value for this slash command option. Only valid for , or parameters. /// - public MinimumAttribute(long value) + public MinimumValueAttribute(long value) { this.Value = value; } /// /// Sets a minimum value for this slash command option. Only valid for , or parameters. /// - public MinimumAttribute(double value) + public MinimumValueAttribute(double value) { this.Value = value; } } /// /// Sets a maximum value for this slash command option. Only valid for , or parameters. /// [AttributeUsage(AttributeTargets.Parameter)] -public class MaximumAttribute : Attribute +public class MaximumValueAttribute : Attribute { /// /// The value. /// public object Value { get; internal set; } /// /// Sets a maximum value for this slash command option. Only valid for , or parameters. /// - public MaximumAttribute(int value) + public MaximumValueAttribute(int value) { this.Value = value; } /// /// Sets a maximum value for this slash command option. Only valid for , or parameters. /// - public MaximumAttribute(long value) + public MaximumValueAttribute(long value) { this.Value = value; } /// /// Sets a maximum value for this slash command option. Only valid for , or parameters. /// - public MaximumAttribute(double value) + public MaximumValueAttribute(double value) { this.Value = value; } } + + +/// +/// Sets a minimum value for this slash command option. Only valid for parameters. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class MinimumLengthAttribute : Attribute +{ + /// + /// The value. + /// + public int? Value { get; internal set; } + + /// + /// Sets a minimum value for this slash command option. Only valid for parameters. + /// + public MinimumLengthAttribute(int value) + { + this.Value = value; + } +} + +/// +/// Sets a maximum value for this slash command option. Only valid for parameters. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public class MaximumLengthAttribute : Attribute +{ + /// + /// The value. + /// + public int? Value { get; internal set; } + + /// + /// Sets a maximum value for this slash command option. Only valid for parameters. + /// + public MaximumLengthAttribute(int value) + { + if (value == 0) + throw new ArgumentException("Maximum length cannot be 0."); + this.Value = value; + } +} diff --git a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs index 0f30565f5..775442df2 100644 --- a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs +++ b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs @@ -1,280 +1,286 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.Generic; using System.Linq; using DisCatSharp.Entities; using DisCatSharp.Enums; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands; internal static class ApplicationCommandEqualityChecks { /// /// Whether two application commands are equal. /// /// Source command. /// Command to check against. internal static bool IsEqualTo(this DiscordApplicationCommand ac1, DiscordApplicationCommand targetApplicationCommand) { if (targetApplicationCommand is null || ac1 is null) return false; DiscordApplicationCommand sourceApplicationCommand = new( ac1.Name, ac1.Description, ac1.Options, ac1.Type, ac1.NameLocalizations, ac1.DescriptionLocalizations ); ApplicationCommandsExtension.ClientInternal.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, $"[AC Change Check] Command {ac1.Name}\n\n[{JsonConvert.SerializeObject(sourceApplicationCommand)},{JsonConvert.SerializeObject(targetApplicationCommand)}]\n\n"); return ac1.Type == targetApplicationCommand.Type && sourceApplicationCommand.SoftEqual(targetApplicationCommand, ac1.Type, ApplicationCommandsExtension.Configuration?.EnableLocalization ?? false); } /// /// Checks softly whether two s are the same. /// Excluding id, application id and version here. /// /// Source application command. /// Application command to check against. /// The application command type. /// Whether localization is enabled. internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApplicationCommand target, ApplicationCommandType type, bool localizationEnabled = false) { return localizationEnabled ? type switch { ApplicationCommandType.ChatInput => DeepEqual(source, target, localizationEnabled), _ => (source.Name == target.Name) && (source.Type == target.Type) && (source.NameLocalizations == target.NameLocalizations) && (source.DefaultMemberPermissions == target.DefaultMemberPermissions) //&& (source.IsNsfw == target.IsNsfw) } : type switch { ApplicationCommandType.ChatInput => DeepEqual(source, target), _ => (source.Name == target.Name) && (source.Type == target.Type) && (source.DefaultMemberPermissions == target.DefaultMemberPermissions) //&& (source.IsNsfw == target.IsNsfw) }; } /// /// Checks deeply whether two s are the same. /// Excluding id, application id and version here. /// /// Source application command. /// Application command to check against. /// Whether localization is enabled. internal static bool DeepEqual(DiscordApplicationCommand source, DiscordApplicationCommand target, bool localizationEnabled = false) { var rootCheck = (source.Name == target.Name) && (source.Description == target.Description) && (source.Type == target.Type) && (source.DefaultMemberPermissions == target.DefaultMemberPermissions) && (source.DmPermission == target.DmPermission)/* && (source.IsNsfw == target.IsNsfw)*/; if (localizationEnabled) rootCheck = rootCheck && (source.NameLocalizations == target.NameLocalizations) && (source.DescriptionLocalizations == target.DescriptionLocalizations); if (source.Options == null && target.Options == null) return rootCheck; else if ((source.Options != null && target.Options == null) || (source.Options == null && target.Options != null)) return false; else if (source.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommandGroup) && target.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommandGroup)) { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) { List minimalSubSourceOptions = new(); foreach (var subOption in option.Options) { List minimalSubSubSourceOptions = null; if (subOption.Options != null) { minimalSubSubSourceOptions = new(); foreach (var subSubOption in subOption.Options) { minimalSubSubSourceOptions.Add(new DiscordApplicationCommandOption( subSubOption.Name, subSubOption.Description, subSubOption.Type, subSubOption.Required ?? false, subSubOption.Choices, null, subSubOption.ChannelTypes, subSubOption.AutoComplete ?? false, subSubOption.MinimumValue, subSubOption.MaximumValue, localizationEnabled ? subSubOption.NameLocalizations : null, - localizationEnabled ? subSubOption.DescriptionLocalizations : null + localizationEnabled ? subSubOption.DescriptionLocalizations : null, + subSubOption.MinimumLength, subSubOption.MaximumLength )); } minimalSubSourceOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, options: minimalSubSubSourceOptions, nameLocalizations: localizationEnabled ? subOption.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubSourceOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } foreach (var option in target.Options) { List minimalSubTargetOptions = new(); foreach (var subOption in option.Options) { List minimalSubSubTargetOptions = null; if (subOption.Options != null && subOption.Options.Any()) { minimalSubSubTargetOptions = new(); foreach (var subSubOption in subOption.Options) { minimalSubSubTargetOptions.Add(new DiscordApplicationCommandOption( subSubOption.Name, subSubOption.Description, subSubOption.Type, subSubOption.Required ?? false, subSubOption.Choices, null, subSubOption.ChannelTypes, subSubOption.AutoComplete ?? false, subSubOption.MinimumValue, subSubOption.MaximumValue, localizationEnabled ? subSubOption.NameLocalizations : null, - localizationEnabled ? subSubOption.DescriptionLocalizations : null + localizationEnabled ? subSubOption.DescriptionLocalizations : null, + subSubOption.MinimumLength, subSubOption.MaximumLength )); } minimalSubTargetOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, options: minimalSubSubTargetOptions, nameLocalizations: localizationEnabled ? subOption.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubTargetOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } return rootCheck && JsonConvert.SerializeObject(minimalSourceOptions) == JsonConvert.SerializeObject(minimalTargetOptions); } else if (source.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommand) && target.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommand)) { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) { List minimalSubSourceOptions =null; if (option.Options != null) { minimalSubSourceOptions = new(); foreach (var subOption in option.Options) { minimalSubSourceOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, subOption.Required ?? false, subOption.Choices, null, subOption.ChannelTypes, subOption.AutoComplete ?? false, subOption.MinimumValue, subOption.MaximumValue, localizationEnabled ? subOption.NameLocalizations : null, - localizationEnabled ? subOption.DescriptionLocalizations : null + localizationEnabled ? subOption.DescriptionLocalizations : null, + subOption.MinimumLength, subOption.MaximumLength )); } } minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubSourceOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } foreach (var option in target.Options) { List minimalSubTargetOptions = null; if (option.Options != null && option.Options.Any()) { minimalSubTargetOptions = new(); foreach (var subOption in option.Options) { minimalSubTargetOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, subOption.Required ?? false, subOption.Choices, null, subOption.ChannelTypes, subOption.AutoComplete ?? false, subOption.MinimumValue, subOption.MaximumValue, localizationEnabled ? subOption.NameLocalizations : null, - localizationEnabled ? subOption.DescriptionLocalizations : null + localizationEnabled ? subOption.DescriptionLocalizations : null, + subOption.MinimumLength, subOption.MaximumLength )); } } minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubTargetOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } return rootCheck && JsonConvert.SerializeObject(minimalSourceOptions) == JsonConvert.SerializeObject(minimalTargetOptions); } else { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, option.Required ?? false, option.Choices, null, option.ChannelTypes, option.AutoComplete ?? false, option.MinimumValue, option.MaximumValue, localizationEnabled ? option.NameLocalizations : null, - localizationEnabled ? option.DescriptionLocalizations : null + localizationEnabled ? option.DescriptionLocalizations : null, + option.MinimumLength, option.MaximumLength )); foreach (var option in target.Options) minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, option.Required ?? false, option.Choices, null, option.ChannelTypes, option.AutoComplete ?? false, option.MinimumValue, option.MaximumValue, localizationEnabled ? option.NameLocalizations : null, - localizationEnabled ? option.DescriptionLocalizations : null + localizationEnabled ? option.DescriptionLocalizations : null, + option.MinimumLength, option.MaximumLength )); return rootCheck && JsonConvert.SerializeObject(minimalSourceOptions) == JsonConvert.SerializeObject(minimalTargetOptions); } } } diff --git a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs index 00c8004e0..6012bd50f 100644 --- a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs +++ b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs @@ -1,382 +1,385 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Enums; namespace DisCatSharp.ApplicationCommands; /// /// Represents a . /// internal class CommandWorker { /// /// Parses context menu application commands. /// /// The type. /// List of method infos. /// The optional command translations. /// Too much. internal static Task< ( List applicationCommands, List> commandTypeSources, List contextMenuCommands, bool withLocalization ) > ParseContextMenuCommands(Type type, IEnumerable methods, List translator = null) { List commands = new(); List> commandTypeSources = new(); List contextMenuCommands = new(); foreach (var contextMethod in methods) { var contextAttribute = contextMethod.GetCustomAttribute(); DiscordApplicationCommandLocalization nameLocalizations = null; var commandTranslation = translator?.Single(c => c.Name == contextAttribute.Name && c.Type == contextAttribute.Type); if (commandTranslation != null) nameLocalizations = commandTranslation.NameTranslations; var command = new DiscordApplicationCommand(contextAttribute.Name, null, null, contextAttribute.Type, nameLocalizations, null, contextAttribute.DefaultMemberPermissions, contextAttribute.DmPermission); 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 }); commands.Add(command); commandTypeSources.Add(new KeyValuePair(type, type)); } return Task.FromResult((commands, commandTypeSources, contextMenuCommands, translator != null)); } /// /// Parses single application commands. /// /// The type. /// List of method infos. /// The optional guild id. /// The optional command translations. /// Too much. internal static async Task< ( List applicationCommands, List> commandTypeSources, List commandMethods, bool withLocalization ) > ParseBasicSlashCommandsAsync(Type type, IEnumerable methods, ulong? guildId = null, List translator = null) { List commands = new(); List> commandTypeSources = new(); List commandMethods = new(); 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!"); var options = await ApplicationCommandsExtension.ParseParametersAsync(parameters.Skip(1), guildId); commandMethods.Add(new CommandMethod { Method = method, Name = commandAttribute.Name }); DiscordApplicationCommandLocalization nameLocalizations = null; DiscordApplicationCommandLocalization descriptionLocalizations = null; List localizedOptions = null; var commandTranslation = translator?.Single(c => c.Name == commandAttribute.Name && c.Type == ApplicationCommandType.ChatInput); if (commandTranslation != null && commandTranslation.Options != null) { localizedOptions = new List(options.Count); foreach (var option in options) { var choices = option.Choices != null ? new List(option.Choices.Count) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, commandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } localizedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, - commandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, commandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations + commandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, commandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations, + option.MinimumLength, option.MaximumLength )); } nameLocalizations = commandTranslation.NameTranslations; descriptionLocalizations = commandTranslation.DescriptionTranslations; } var payload = new DiscordApplicationCommand(commandAttribute.Name, commandAttribute.Description, (localizedOptions != null && localizedOptions.Any() ? localizedOptions : null) ?? (options != null && options.Any() ? options : null), ApplicationCommandType.ChatInput, nameLocalizations, descriptionLocalizations, commandAttribute.DefaultMemberPermissions, commandAttribute.DmPermission); commands.Add(payload); commandTypeSources.Add(new KeyValuePair(type, type)); } return (commands, commandTypeSources, commandMethods, translator != null); } } /// /// Represents a . /// internal class NestedCommandWorker { /// /// Parses application command groups. /// /// The type. /// List of type infos. /// The optional guild id. /// The optional group translations. /// Too much. internal static async Task< ( List applicationCommands, List> commandTypeSources, List singletonModules, List groupCommands, List subGroupCommands, bool withLocalization ) > ParseSlashGroupsAsync(Type type, List types, ulong? guildId = null, List translator = null) { List commands = new(); List> commandTypeSources = new(); List groupCommands = new(); List subGroupCommands = new(); List singletonModules = new(); //Handles groups foreach (var subclassInfo in types) { //Gets the attribute and methods in the group var groupAttribute = subclassInfo.GetCustomAttribute(); var submethods = subclassInfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null).ToList(); var subclasses = subclassInfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); if (subclasses.Any() && submethods.Any()) { throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!"); } DiscordApplicationCommandLocalization nameLocalizations = null; DiscordApplicationCommandLocalization descriptionLocalizations = null; if (translator != null) { var commandTranslation = translator.Single(c => c.Name == groupAttribute.Name); if (commandTranslation != null) { nameLocalizations = commandTranslation.NameTranslations; descriptionLocalizations = commandTranslation.DescriptionTranslations; } } //Initializes the command var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, nameLocalizations: nameLocalizations, descriptionLocalizations: descriptionLocalizations, defaultMemberPermissions: groupAttribute.DefaultMemberPermissions, dmPermission: groupAttribute.DmPermission); 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 parameters 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!"); var options = await ApplicationCommandsExtension.ParseParametersAsync(parameters.Skip(1), guildId); DiscordApplicationCommandLocalization subNameLocalizations = null; DiscordApplicationCommandLocalization subDescriptionLocalizations = null; List localizedOptions = null; var commandTranslation = translator?.Single(c => c.Name == payload.Name); if (commandTranslation?.Commands != null) { var subCommandTranslation = commandTranslation.Commands.Single(sc => sc.Name == commandAttribute.Name); if (subCommandTranslation.Options != null) { localizedOptions = new List(options.Count); foreach (var option in options) { var choices = option.Choices != null ? new List(option.Choices.Count) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } localizedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, - subCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations + subCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations, + option.MinimumLength, option.MaximumLength )); } } subNameLocalizations = subCommandTranslation.NameTranslations; subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; } //Creates the subcommand and adds it to the main command var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, localizedOptions ?? options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations, defaultMemberPermissions: payload.DefaultMemberPermissions, dmPermission: payload.DmPermission); 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(); var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var options = new List(); var currentMethods = new List>(); DiscordApplicationCommandLocalization subNameLocalizations = null; DiscordApplicationCommandLocalization subDescriptionLocalizations = null; if (translator != null) { var commandTranslation = translator.Single(c => c.Name == payload.Name); if (commandTranslation != null && commandTranslation.SubGroups != null) { var subCommandTranslation = commandTranslation.SubGroups.Single(sc => sc.Name == subgroupAttribute.Name); if (subCommandTranslation != null) { subNameLocalizations = subCommandTranslation.NameTranslations; subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; } } } //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!"); suboptions = suboptions.Concat(await ApplicationCommandsExtension.ParseParametersAsync(parameters.Skip(1), guildId)).ToList(); DiscordApplicationCommandLocalization subSubNameLocalizations = null; DiscordApplicationCommandLocalization subSubDescriptionLocalizations = null; List localizedOptions = null; var commandTranslation = translator?.Single(c => c.Name == payload.Name); var subCommandTranslation = commandTranslation?.SubGroups?.Single(sc => sc.Name == commatt.Name); var subSubCommandTranslation = subCommandTranslation?.Commands.Single(sc => sc.Name == commatt.Name); if (subSubCommandTranslation != null && subSubCommandTranslation.Options != null) { localizedOptions = new List(suboptions.Count); foreach (var option in suboptions) { var choices = option.Choices != null ? new List(option.Choices.Count) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } localizedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, - subSubCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations + subSubCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations, + option.MinimumLength, option.MaximumLength )); } subSubNameLocalizations = subSubCommandTranslation.NameTranslations; subSubDescriptionLocalizations = subSubCommandTranslation.DescriptionTranslations; } var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, (localizedOptions != null && localizedOptions.Any() ? localizedOptions : null) ?? (suboptions != null && suboptions.Any() ? suboptions : null), nameLocalizations: subSubNameLocalizations, descriptionLocalizations: subSubDescriptionLocalizations); 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, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations); command.SubCommands.Add(new GroupCommand { Name = subgroupAttribute.Name, Methods = currentMethods }); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations, defaultMemberPermissions: payload.DefaultMemberPermissions, dmPermission: payload.DmPermission); commandTypeSources.Add(new KeyValuePair(subclass, type)); //Accounts for lifespans for the sub group if (subclass.GetCustomAttribute() != null && subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { singletonModules.Add(ApplicationCommandsExtension.CreateInstance(subclass, ApplicationCommandsExtension.Configuration?.ServiceProvider)); } } if (command.SubCommands.Any()) subGroupCommands.Add(command); commands.Add(payload); //Accounts for lifespans if (subclassInfo.GetCustomAttribute() != null && subclassInfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { singletonModules.Add(ApplicationCommandsExtension.CreateInstance(subclassInfo, ApplicationCommandsExtension.Configuration?.ServiceProvider)); } } return (commands, commandTypeSources, singletonModules, groupCommands, subGroupCommands, translator != null); } } diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs index 0891efb24..0fe683425 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommandOption.cs @@ -1,165 +1,181 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 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.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a parameter for a . /// public sealed class DiscordApplicationCommandOption { /// /// Gets the type of this command parameter. /// [JsonProperty("type")] public ApplicationCommandOptionType Type { get; internal set; } /// /// Gets the name of this command parameter. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Sets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary RawNameLocalizations { get; set; } /// /// Gets the name localizations. /// [JsonIgnore] public DiscordApplicationCommandLocalization NameLocalizations => new(this.RawNameLocalizations); /// /// Gets the description of this command parameter. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Sets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary RawDescriptionLocalizations { get; set; } /// /// Gets the description localizations. /// [JsonIgnore] public DiscordApplicationCommandLocalization DescriptionLocalizations => new(this.RawDescriptionLocalizations); /// /// Gets whether this command parameter is required. /// [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] public bool? Required { get; internal set; } /// /// Gets the optional choices for this command parameter. /// Not applicable for auto-complete options. /// [JsonProperty("choices", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Choices { get; internal set; } /// /// Gets the optional subcommand parameters for this parameter. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Options { get; internal set; } /// /// Gets the optional allowed channel types. /// [JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection ChannelTypes { get; internal set; } /// /// Gets whether this option provides autocompletion. /// [JsonProperty("autocomplete", NullValueHandling = NullValueHandling.Ignore)] public bool? AutoComplete { get; internal set; } /// /// Gets the minimum value for this slash command parameter. /// [JsonProperty("min_value", NullValueHandling = NullValueHandling.Ignore)] public object MinimumValue { get; internal set; } + /// + /// Gets the maximum value for this slash command parameter. + /// + [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MaximumLength { get; internal set; } + + /// + /// Gets the minimum value for this slash command parameter. + /// + [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MinimumLength { get; internal set; } + /// /// Gets the maximum value for this slash command parameter. /// [JsonProperty("max_value", NullValueHandling = NullValueHandling.Ignore)] public object MaximumValue { get; internal set; } /// /// Creates a new instance of a . /// /// The name of this parameter. /// The description of the parameter. /// The type of this parameter. /// Whether the parameter is required. /// The optional choice selection for this parameter. /// The optional subcommands for this parameter. /// If the option is a channel type, the channels shown will be restricted to these types. /// Whether this option provides autocompletion. /// The minimum value for this parameter. Only valid for types or . /// The maximum value for this parameter. Only valid for types or . /// The localizations of the parameter name. /// The localizations of the parameter description. - public DiscordApplicationCommandOption(string name, string description, ApplicationCommandOptionType type, bool? required = null, IEnumerable choices = null, IEnumerable options = null, IEnumerable channelTypes = null, bool? autocomplete = null, object minimumValue = null, object maximumValue = null, DiscordApplicationCommandLocalization nameLocalizations = null, DiscordApplicationCommandLocalization descriptionLocalizations = null) + /// The minimum allowed length of the string. (Min 0) + /// The maximum allowed length of the string. (Min 1) + public DiscordApplicationCommandOption(string name, string description, ApplicationCommandOptionType type, bool? required = null, IEnumerable choices = null, IEnumerable options = null, IEnumerable channelTypes = null, bool? autocomplete = null, object minimumValue = null, object maximumValue = null, DiscordApplicationCommandLocalization nameLocalizations = null, DiscordApplicationCommandLocalization descriptionLocalizations = null, int? minimumLength = null, int? maximumLength = null) { if (!Utilities.IsValidSlashCommandName(name)) throw new ArgumentException("Invalid application command option name specified. It must be below 32 characters and not contain any whitespace.", nameof(name)); if (name.Any(char.IsUpper)) throw new ArgumentException("Application command option name cannot have any upper case characters.", nameof(name)); if (description.Length > 100) throw new ArgumentException("Application command option description cannot exceed 100 characters.", nameof(description)); if ((autocomplete ?? false) && (choices?.Any() ?? false)) throw new InvalidOperationException("Auto-complete slash command options cannot provide choices."); this.Name = name; this.Description = description; this.Type = type; this.Required = required; this.Choices = choices != null ? new ReadOnlyCollection(choices.ToList()) : null; this.Options = options != null ? new ReadOnlyCollection(options.ToList()) : null; this.ChannelTypes = channelTypes != null ? new ReadOnlyCollection(channelTypes.ToList()) : null; this.AutoComplete = autocomplete; this.MinimumValue = minimumValue; this.MaximumValue = maximumValue; + this.MinimumLength = minimumLength; + this.MaximumLength = maximumLength; this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs(); this.RawDescriptionLocalizations = descriptionLocalizations?.GetKeyValuePairs(); } } diff --git a/README.md b/README.md index 33e3f81dd..24f4c3ee2 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,118 @@ # DisCatSharp ![Stable](https://img.shields.io/nuget/v/DisCatSharp?color=%23ebb34b&label=Stable&style=flat-square&logo=nuget) ![Nightly](https://img.shields.io/nuget/vpre/DisCatSharp?color=%23ff1493&label=Nightly&style=flat-square&logo=nuget) ## A Discord Bot Library written in C# for .NET ![DisCatSharp Logo](https://github.com/Aiko-IT-Systems/DisCatSharp/blob/main/DisCatSharp.Logos/android-chrome-192x192.png?raw=true) ---- [![Build](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/dotnet.yml) [![Documentation](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/docs.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/docs.yml) [![Typos](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/typos.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/typos.yml) [![CodeQL](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/codeql.yml/badge.svg)](https://github.com/Aiko-IT-Systems/DisCatSharp/actions/workflows/codeql.yml) ![Wakatime](https://wakatime.com/badge/github/Aiko-IT-Systems/DisCatSharp.svg) [![AppVeyor](https://img.shields.io/appveyor/build/AITSYS/DisCatSharp?label=Appveyor&logo=appveyor&style=flat-square)](https://ci.appveyor.com/project/AITSYS/discatsharp) [![GitHub last commit](https://img.shields.io/github/last-commit/Aiko-IT-Systems/DisCatSharp?label=Last%20Commit&style=flat-square&logo=github)](https://aitsys.dev/source/DisCatSharp/history/) [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/Aiko-IT-Systems/DisCatSharp?label=Commit%20Activity&style=flat-square&logo=github)](https://github.com/Aiko-IT-Systems/DisCatSharp/commits/main) [![GitHub pull requests](https://img.shields.io/github/issues-pr/Aiko-IT-Systems/DisCatSharp?label=PRs&style=flat-square&logo=github&logo=gitub)](https://github.com/Aiko-IT-Systems/DisCatSharp/pulls) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/Aiko-IT-Systems/DisCatSharp?label=Size&style=flat-square&logo=github) ---- ## Why DisCatSharp? -- You want a library where you get kind and efficient help? -- You would like to have and use the most recent features of the Discord API? -- You are ready to build great things? + +If you: +- want a library where you get kind and efficient help +- would like to have and use the most recent features of the Discord API +- are ready to build great things Then this is the right place for you! ## Installing + You can install the library from following source: [NuGet](https://www.nuget.org/profiles/DisCatSharp). ## Documentation + The documentation of the nightly versions is available at [docs.dcs.aitsys.dev](https://docs.discatsharp.tech). Fallback docs are available at [docs-alt.dcs.aitsys.dev](https://docs-alt.dcs.aitsys.dev). ## Bugs or Feature requests? + Write a mail to [bugs@aitsys.dev](mailto:bugs@aitsys.dev). All requests are tracked at [aitsys.dev](https://aitsys.dev). ## Tutorials + * [Howto](https://docs.discatsharp.tech/articles/basics/bot_account.html) * [Examples](https://examples.dcs.aitsys.dev) ## Snippets [Snippets for Visual Studio](https://github.com/Aiko-IT-Systems/DisCatSharp.Snippets) ---- ## NuGet Packages + ### Main + | Package | Stable | Nightly | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | DisCatSharp | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | | DisCatSharp.ApplicationCommands | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.ApplicationCommands.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.ApplicationCommands.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | | DisCatSharp.CommandsNext | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.CommandsNext.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.CommandsNext.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | | DisCatSharp.Interactivity | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Interactivity.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Interactivity.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | ### Voice + | Package | Stable | Nightly | | ----------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | DisCatSharp.Lavalink | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Lavalink.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Lavalink.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | | DisCatSharp.VoiceNext | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.VoiceNext.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.VoiceNext.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | | DisCatSharp.VoiceNext.Natives | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.VoiceNext.Natives.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.VoiceNext.Natives.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | ### Hosting + | Package | Stable | Nightly | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | DisCatSharp.Configuration | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Configuration.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Configuration.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) | | DisCatSharp.Hosting | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Hosting.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Hosting.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) | | DisCatSharp.Hosting.DependencyInjection | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Hosting.DependencyInjection.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Hosting.DependencyInjection.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) | ### Templates + | Package | Stable | Nightly | | ---------------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | DisCatSharp.ProjectTemplates | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.ProjectTemplates.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.ProjectTemplates.svg?label=&logo=nuget&color=%23ff1493&style=flat-square) | ### Development / Commons + | Package | Stable | Nightly | | ------------------ | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | DisCatSharp.Common | ![NuGet](https://img.shields.io/nuget/v/DisCatSharp.Common.svg?label=&logo=nuget&style=flat-square) | ![NuGet](https://img.shields.io/nuget/vpre/DisCatSharp.Common.svg?label=&logo=nuget&style=flat-square&color=%23ff1493) | ---- ## Sponsors [![Sponsors](https://img.shields.io/github/sponsors/Aiko-IT-Systems?label=&style=flat-square&logo=github)](https://github.com/sponsors/Aiko-IT-Systems) + - [Deividas Kazakevicius](https://github.com/DeividasKaza) - [Will](https://github.com/villChurch) ## Thanks + Big thanks goes to the following people who helped us ♥️ - [Auros Nexus](https://github.com/Auros) - [Lunar Starstrum](https://github.com/OoLunar) - [Johannes](https://github.com/JMLutra) - [Geferon](https://github.com/geferon) - [Alice](https://github.com/QuantuChi) - [Will](https://github.com/villChurch) ## Special Thanks + The special thanks goes to Nagisa. Make sure to check out her [instagram](https://www.instagram.com/nagisaarts_/) ♥️♥️