diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index b28e47e69..44c777117 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,1961 +1,2144 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.EventArgs;
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 _commandMethods { get; set; } = new List();
///
/// List of groups.
///
private static List _groupCommands { get; set; } = new List();
///
/// List of groups with subgroups.
///
private static List _subGroupCommands { get; set; } = new List();
///
/// List of context menus.
///
private static List _contextMenuCommands { get; set; } = new List();
///
/// List of global commands on discords backend.
///
- private static List _globalDiscordCommands { get; set; } = new List();
+ private List _globalDiscordCommands { get; set; } = null;
///
/// List of guild commands on discords backend.
///
- private static Dictionary> _guildDiscordCommands { get; set; } = new Dictionary>();
+ private Dictionary> _guildDiscordCommands { get; set; } = null;
///
/// Singleton modules.
///
private static List _singletonModules { get; set; } = new List();
///
/// List of modules to register.
///
private List> _updateList { get; set; } = new List>();
///
/// Configuration for Discord.
///
private readonly ApplicationCommandsConfiguration _configuration;
///
/// Set to true if anything fails when registering.
///
private static bool _errored { get; set; } = false;
///
/// Gets a list of registered commands. The key is the guild id (null if global).
///
public IReadOnlyList>> RegisteredCommands
=> _registeredCommands;
private static List>> _registeredCommands = new();
///
/// Gets a list of registered global commands.
///
public IReadOnlyList GlobalCommands
=> _globalCommands;
private static readonly List _globalCommands = new();
///
/// Gets a list of registered guild commands mapped by guild id.
///
public IReadOnlyDictionary> GuildCommands
=> _guildCommands;
private static readonly Dictionary> _guildCommands = new();
///
/// Gets the registration count.
///
private static int RegistrationCount { get; set; } = 0;
///
/// Gets the expected count.
///
private static int ExpectedCount { get; set; } = 0;
///
/// Gets the guild ids where the applications.commands scope is missing.
///
private IReadOnlyList MissingScopeGuildIds { get; set; }
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration)
{
this._configuration = configuration;
}
///
/// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS.
///
/// The client to setup on.
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null);
this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null);
this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null);
this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null);
this._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.Ready += 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;
}
///
/// Registers a command class.
///
/// The command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T))));
}
///
/// Registers a command class.
///
/// The of the command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public void RegisterCommands(Type type, ulong? guildId = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type)));
}
///
/// Cleans all guild application commands.
///
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.
///
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 permissions with.
/// A callback to setup translations with.
public void RegisterCommands(ulong guildId, Action permissionSetup = null, Action translationSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup, 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 permissions with.
/// A callback to setup translations with.
public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = null, 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, permissionSetup, translationSetup)));
}
/*
///
/// Registers a command class with permission setup but without a guild id.
///
/// The command class to register.
/// A callback to setup permissions with.
public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup)));
}
///
/// Registers a command class with permission setup but without a guild id.
///
/// The of the command class to register.
/// A callback to setup permissions with.
public void RegisterCommands(Type type, Action permissionSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup)));
}
*/
///
/// 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;
///
/// To be run on ready.
///
/// The client.
/// The ready event args.
internal async Task UpdateAsync(DiscordClient client, ReadyEventArgs e)
=> await this.UpdateAsync();
///
/// Actual method for registering, used for RegisterCommands and on Ready.
///
internal async Task UpdateAsync()
{
//Only update for shard 0
if (this.Client.ShardId == 0)
{
+ this._globalDiscordCommands = new();
+ this._guildDiscordCommands = new();
+
var commands_pending = this._updateList.Select(x => x.Key).Distinct();
ExpectedCount = commands_pending.Count();
List FailedGuilds = new();
- _globalDiscordCommands = this.Client.GetGlobalApplicationCommandsAsync().Result.ToList();
+ IEnumerable GlobalCommands = null;
+ GlobalCommands = await this.Client.GetGlobalApplicationCommandsAsync() ?? null;
foreach(var guild in this.Client.Guilds.Keys)
{
IEnumerable Commands = null;
var unauthorized = false;
try
{
- Commands = await this.Client.GetGuildApplicationCommandsAsync(guild);
+ Commands = await this.Client.GetGuildApplicationCommandsAsync(guild) ?? null;
}
catch (UnauthorizedException)
{
unauthorized = true;
}
finally
{
- if (!unauthorized)
- _guildDiscordCommands.Add(guild, Commands.ToList());
+ if (!unauthorized && Commands != null && Commands.Any())
+ this._guildDiscordCommands.Add(guild, Commands.ToList());
+ else if (!unauthorized)
+ this._guildDiscordCommands.Add(guild, null);
else
FailedGuilds.Add(guild);
}
}
+ if (GlobalCommands != null && GlobalCommands.Any())
+ this._globalDiscordCommands.AddRange(GlobalCommands);
+
foreach (var key in commands_pending)
{
this.Client.Logger.LogDebug(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands.");
this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key);
}
this.MissingScopeGuildIds = FailedGuilds;
await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(this._configuration?.ServiceProvider)
{
Handled = true,
GuildsWithoutScope = FailedGuilds
});
}
}
///
/// Method for registering commands for a target from modules.
///
/// The types.
/// The optional guild id.
private void RegisterCommands(IEnumerable types, ulong? guildid)
{
//Initialize empty lists to be added to the global ones at the end
var commandMethods = new List();
var groupCommands = new List();
var subGroupCommands = new List();
var contextMenuCommands = new List();
var updateList = new List();
var commandTypeSources = new List>();
_ = Task.Run(async () =>
{
//Iterates over all the modules
foreach (var config in types)
{
var type = config.Type;
try
{
var module = type.GetTypeInfo();
var classes = new List();
//Add module to classes list if it's a group
if (module.GetCustomAttribute() != null)
{
classes.Add(module);
}
else
{
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
}
//Handles groups
foreach (var subclassinfo in classes)
{
var ctx = new ApplicationCommandsTranslationContext(type, module.FullName);
config.Translations?.Invoke(ctx);
List translations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
translations = JsonConvert.DeserializeObject>(ctx.Translations);
}
//Gets the attribute and methods in the group
var groupAttribute = subclassinfo.GetCustomAttribute();
var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null);
if (subclasses.Any() && submethods.Any())
{
throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!");
}
DiscordApplicationCommandLocalization NameLocalizations = null;
DiscordApplicationCommandLocalization DescriptionLocalizations = null;
if (translations != null)
{
var command_translation = translations.Single(c => c.Name == groupAttribute.Name);
if (command_translation != null)
{
NameLocalizations = command_translation.NameTranslations;
DescriptionLocalizations = command_translation.DescriptionTranslations;
}
}
//Initializes the command
var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, default_permission: groupAttribute.DefaultPermission, nameLocalizations: NameLocalizations, descriptionLocalizations: DescriptionLocalizations);
commandTypeSources.Add(new KeyValuePair(type, type));
var commandmethods = new List>();
//Handles commands in the group
foreach (var submethod in submethods)
{
var commandAttribute = submethod.GetCustomAttribute();
//Gets the paramaters and accounts for InteractionContext
var parameters = submethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
parameters = parameters.Skip(1).ToArray();
var options = await this.ParseParameters(parameters, guildid);
DiscordApplicationCommandLocalization SubNameLocalizations = null;
DiscordApplicationCommandLocalization SubDescriptionLocalizations = null;
List LocalizisedOptions = null;
if (translations != null)
{
var command_translation = translations.Single(c => c.Name == payload.Name);
if (command_translation.Commands != null)
{
var sub_command_translation = command_translation.Commands.Single(sc => sc.Name == commandAttribute.Name);
if (sub_command_translation.Options != null)
{
LocalizisedOptions = new(options.Count);
foreach (var option in options)
{
List choices = option.Choices != null ? new(option.Choices.Count()) : null;
if (option.Choices != null)
{
foreach (var choice in option.Choices)
{
choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, sub_command_translation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations));
}
}
LocalizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required,
choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue,
sub_command_translation.Options.Single(o => o.Name == option.Name).NameTranslations, sub_command_translation.Options.Single(o => o.Name == option.Name).DescriptionTranslations
));
}
}
SubNameLocalizations = sub_command_translation.NameTranslations;
SubDescriptionLocalizations = sub_command_translation.DescriptionTranslations;
}
}
//Creates the subcommand and adds it to the main command
var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, LocalizisedOptions ?? options, nameLocalizations: SubNameLocalizations, descriptionLocalizations: SubDescriptionLocalizations);
payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations);
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 (translations != null)
{
var command_translation = translations.Single(c => c.Name == payload.Name);
if (command_translation != null && command_translation.SubGroups != null)
{
var sub_command_translation = command_translation.SubGroups.Single(sc => sc.Name == subGroupAttribute.Name);
if (sub_command_translation != null)
{
SubNameLocalizations = sub_command_translation.NameTranslations;
SubDescriptionLocalizations = sub_command_translation.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!");
parameters = parameters.Skip(1).ToArray();
suboptions = suboptions.Concat(await this.ParseParameters(parameters, guildid)).ToList();
DiscordApplicationCommandLocalization SubSubNameLocalizations = null;
DiscordApplicationCommandLocalization SubSubDescriptionLocalizations = null;
List LocalizisedOptions = null;
if (translations != null)
{
var command_translation = translations.Single(c => c.Name == payload.Name);
if (command_translation.SubGroups != null)
{
var sub_command_translation = command_translation.SubGroups.Single(sc => sc.Name == commatt.Name);
if (sub_command_translation != null)
{
var sub_sub_command_translation = sub_command_translation.Commands.Single(sc => sc.Name == commatt.Name);
if(sub_sub_command_translation != null)
{
if(sub_sub_command_translation.Options != null)
{
LocalizisedOptions = new(suboptions.Count);
foreach (var option in suboptions)
{
List choices = option.Choices != null ? new(option.Choices.Count()) : null;
if (option.Choices != null)
{
foreach (var choice in option.Choices)
{
choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, sub_sub_command_translation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations));
}
}
LocalizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required,
choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue,
sub_sub_command_translation.Options.Single(o => o.Name == option.Name).NameTranslations, sub_sub_command_translation.Options.Single(o => o.Name == option.Name).DescriptionTranslations
));
}
}
SubSubNameLocalizations = sub_sub_command_translation.NameTranslations;
SubSubDescriptionLocalizations = sub_sub_command_translation.DescriptionTranslations;
}
}
}
}
var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, LocalizisedOptions ?? suboptions, 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 }, payload.DefaultPermission, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations);
commandTypeSources.Add(new KeyValuePair(subclass, type));
//Accounts for lifespans for the sub group
if (subclass.GetCustomAttribute() != null)
{
if (subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
_singletonModules.Add(this.CreateInstance(subclass, this._configuration?.ServiceProvider));
}
}
}
if (command.SubCommands.Any()) subGroupCommands.Add(command);
updateList.Add(payload);
//Accounts for lifespans
if (subclassinfo.GetCustomAttribute() != null)
{
if (subclassinfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
_singletonModules.Add(this.CreateInstance(subclassinfo, this._configuration?.ServiceProvider));
}
}
}
//Handles methods and context menus, only if the module isn't a group itself
if (module.GetCustomAttribute() == null)
{
var ctx = new ApplicationCommandsTranslationContext(type, module.FullName);
config.Translations?.Invoke(ctx);
List translations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
translations = JsonConvert.DeserializeObject>(ctx.Translations);
}
//Slash commands (again, similar to the one for groups)
var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
foreach (var method in methods)
{
var commandattribute = method.GetCustomAttribute();
var parameters = method.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
parameters = parameters.Skip(1).ToArray();
var options = await this.ParseParameters(parameters, guildid);
commandMethods.Add(new CommandMethod { Method = method, Name = commandattribute.Name });
DiscordApplicationCommandLocalization NameLocalizations = null;
DiscordApplicationCommandLocalization DescriptionLocalizations = null;
List LocalizisedOptions = null;
if (translations != null)
{
var command_translation = translations.Single(c => c.Name == commandattribute.Name && c.Type == ApplicationCommandType.ChatInput);
if (command_translation != null)
{
if (command_translation.Options != null)
{
LocalizisedOptions = new(options.Count);
foreach (var option in options)
{
List choices = option.Choices != null ? new(option.Choices.Count()) : null;
if (option.Choices != null)
{
foreach (var choice in option.Choices)
{
choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, command_translation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations));
}
}
LocalizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required,
choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue,
command_translation.Options.Single(o => o.Name == option.Name).NameTranslations, command_translation.Options.Single(o => o.Name == option.Name).DescriptionTranslations
));
}
}
NameLocalizations = command_translation.NameTranslations;
DescriptionLocalizations = command_translation.DescriptionTranslations;
}
}
var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, LocalizisedOptions ?? options, commandattribute.DefaultPermission, ApplicationCommandType.ChatInput, NameLocalizations, DescriptionLocalizations);
updateList.Add(payload);
commandTypeSources.Add(new KeyValuePair(type, type));
}
//Context Menus
var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
foreach (var contextMethod in contextMethods)
{
var contextAttribute = contextMethod.GetCustomAttribute();
DiscordApplicationCommandLocalization NameLocalizations = null;
if (translations != null)
{
var command_translation = translations.Single(c => c.Name == contextAttribute.Name && c.Type == contextAttribute.Type);
if (command_translation != null)
NameLocalizations = command_translation.NameTranslations;
}
var command = new DiscordApplicationCommand(contextAttribute.Name, null, null, contextAttribute.DefaultPermission, contextAttribute.Type, NameLocalizations);
var parameters = contextMethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext)))
throw new ArgumentException($"The first argument must be a ContextMenuContext!");
if (parameters.Length > 1)
throw new ArgumentException($"A context menu cannot have parameters!");
contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name });
updateList.Add(command);
commandTypeSources.Add(new KeyValuePair(type, type));
}
//Accounts for lifespans
if (module.GetCustomAttribute() != null)
{
if (module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
_singletonModules.Add(this.CreateInstance(module, this._configuration?.ServiceProvider));
}
}
}
}
catch (Exception ex)
{
//This isn't really much more descriptive but I added a separate case for it anyway
if (ex is BadRequestException brex)
this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}");
else
this.Client.Logger.LogCritical(ex, $"There was an error registering application commands");
_errored = true;
}
}
if (!_errored)
{
try
{
async Task UpdateCommandPermission(ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType)
{
if (!guildid.HasValue)
{
this.Client.Logger.LogTrace("You can't set global permissions till yet. See https://discord.com/developers/docs/interactions/application-commands#permissions");
}
else
{
var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName);
var conf = types.First(t => t.Type == commandRootType);
conf.Setup?.Invoke(ctx);
if (ctx.Permissions.Count == 0)
return;
await this.Client.OverwriteGuildApplicationCommandPermissionsAsync(guildid.Value, commandId, ctx.Permissions);
}
}
async Task UpdateCommandPermissionGroup(GroupCommand groupCommand)
{
foreach (var com in groupCommand.Methods)
{
var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Value.DeclaringType);
await UpdateCommandPermission(groupCommand.CommandId, com.Key, source.Key, source.Value);
}
}
List Commands = new();
try
{
if (guildid == null)
{
- var GlobalCommandsOverwriteList = this.BuildGlobalOverwriteList(updateList);
- var GlobalCommandsCreateList = this.BuildGlobalCreateList(updateList);
- var GlobalCommandsDeleteList = this.BuildGlobalDeleteList(updateList);
-
- if (GlobalCommandsCreateList.Any() && !GlobalCommandsOverwriteList.Any())
- {
- var cmds = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(GlobalCommandsCreateList);
- Commands.AddRange(cmds);
- }
- else if (!GlobalCommandsCreateList.Any() && GlobalCommandsOverwriteList.Any())
+ if (updateList != null && updateList.Any())
{
- List OverwriteList = new();
- foreach (var overwrite in GlobalCommandsOverwriteList)
+ var GlobalCommandsOverwriteList = this.BuildGlobalOverwriteList(updateList);
+ var GlobalCommandsCreateList = this.BuildGlobalCreateList(updateList);
+ var GlobalCommandsDeleteList = this.BuildGlobalDeleteList(updateList);
+
+ if (CheckAnyAndNotNull(GlobalCommandsCreateList) && CheckNotAnyOrNull(GlobalCommandsOverwriteList))
{
- var cmd = overwrite.Value;
- cmd.Id = overwrite.Key;
- OverwriteList.Add(cmd);
+ var cmds = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(GlobalCommandsCreateList);
+ Commands.AddRange(cmds);
}
- var discord_backend_commands = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(OverwriteList);
- Commands.AddRange(discord_backend_commands);
- }
- else if (GlobalCommandsCreateList.Any() && GlobalCommandsOverwriteList.Any())
- {
- foreach (var cmd in GlobalCommandsCreateList)
+ else if (CheckNotAnyOrNull(GlobalCommandsCreateList) && CheckAnyAndNotNull(GlobalCommandsOverwriteList))
{
- var discord_backend_command = await this.Client.CreateGlobalApplicationCommandAsync(cmd);
- Commands.Add(discord_backend_command);
+ List OverwriteList = new();
+ foreach (var overwrite in GlobalCommandsOverwriteList)
+ {
+ var cmd = overwrite.Value;
+ cmd.Id = overwrite.Key;
+ OverwriteList.Add(cmd);
+ }
+ var discord_backend_commands = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(OverwriteList);
+ Commands.AddRange(discord_backend_commands);
+ }
+ else if (CheckAnyAndNotNull(GlobalCommandsCreateList) && CheckAnyAndNotNull(GlobalCommandsOverwriteList))
+ {
+ foreach (var cmd in GlobalCommandsCreateList)
+ {
+ var discord_backend_command = await this.Client.CreateGlobalApplicationCommandAsync(cmd);
+ Commands.Add(discord_backend_command);
+ }
+
+ foreach (var cmd in GlobalCommandsOverwriteList)
+ {
+ var command = cmd.Value;
+
+ var discord_backend_command = await this.Client.EditGlobalApplicationCommandAsync(cmd.Key, action =>
+ {
+ action.Name = command.Name;
+ action.NameLocalizations = command.NameLocalizations;
+ action.Description = command.Description;
+ action.DescriptionLocalizations = command.DescriptionLocalizations;
+ if(command.Options != null && command.Options.Any())
+ action.Options = Optional.FromValue(command.Options);
+ action.DefaultPermission = command.DefaultPermission;
+ });
+ Commands.Add(discord_backend_command);
+ }
}
- foreach (var cmd in GlobalCommandsOverwriteList)
+ if (CheckAnyAndNotNull(GlobalCommandsDeleteList))
{
- var command = cmd.Value;
- var discord_backend_command = await this.Client.ApiClient.EditGlobalApplicationCommandAsync(this.Client.CurrentApplication.Id,
- cmd.Key, command.Name, command.Description, command.Options.Any() ? Optional.FromValue(command.Options) : null, command.DefaultPermission,
- command.NameLocalizations, command.DescriptionLocalizations
- );
- Commands.Add(discord_backend_command);
+ foreach (var cmdId in GlobalCommandsDeleteList)
+ {
+ await this.Client.DeleteGlobalApplicationCommandAsync(cmdId);
+ }
}
- }
- foreach (var cmdId in GlobalCommandsDeleteList)
- {
- await this.Client.DeleteGlobalApplicationCommandAsync(cmdId);
+ if (CheckAnyAndNotNull(Commands))
+ _globalCommands.AddRange(Commands);
+ else
+ return;
}
- _globalCommands.AddRange(Commands);
}
else
{
- var GuildCommandsOverwriteList = this.BuildGuildOverwriteList(guildid.Value, updateList);
- var GuildCommandsCreateList = this.BuildGuildCreateList(guildid.Value, updateList);
- var GuildCommandsDeleteList = this.BuildGuildDeleteList(guildid.Value, updateList);
-
- if (GuildCommandsCreateList.Any() && !GuildCommandsOverwriteList.Any())
- {
- var cmds = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, GuildCommandsCreateList);
- Commands.AddRange(cmds);
- }
- else if (!GuildCommandsCreateList.Any() && GuildCommandsOverwriteList.Any())
+ if (updateList != null && updateList.Any())
{
- List OverwriteList = new();
- foreach (var overwrite in GuildCommandsOverwriteList)
+ var GuildCommandsOverwriteList = this.BuildGuildOverwriteList(guildid.Value, updateList);
+ var GuildCommandsCreateList = this.BuildGuildCreateList(guildid.Value, updateList);
+ var GuildCommandsDeleteList = this.BuildGuildDeleteList(guildid.Value, updateList);
+
+ if (CheckAnyAndNotNull(GuildCommandsCreateList) && CheckNotAnyOrNull(GuildCommandsOverwriteList))
{
- var cmd = overwrite.Value;
- cmd.Id = overwrite.Key;
- OverwriteList.Add(cmd);
+ var cmds = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, GuildCommandsCreateList);
+ Commands.AddRange(cmds);
}
- var discord_backend_commands = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, OverwriteList);
- Commands.AddRange(discord_backend_commands);
- }
- else if (GuildCommandsCreateList.Any() && GuildCommandsOverwriteList.Any())
- {
- foreach (var cmd in GuildCommandsCreateList)
+ else if (CheckNotAnyOrNull(GuildCommandsCreateList) && CheckAnyAndNotNull(GuildCommandsOverwriteList))
{
- var discord_backend_command = await this.Client.CreateGuildApplicationCommandAsync(guildid.Value, cmd);
- Commands.Add(discord_backend_command);
+ List OverwriteList = new();
+ foreach (var overwrite in GuildCommandsOverwriteList)
+ {
+ var cmd = overwrite.Value;
+ cmd.Id = overwrite.Key;
+ OverwriteList.Add(cmd);
+ }
+ var discord_backend_commands = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, OverwriteList);
+ Commands.AddRange(discord_backend_commands);
}
+ else if (CheckAnyAndNotNull(GuildCommandsCreateList) && CheckAnyAndNotNull(GuildCommandsOverwriteList))
+ {
+ foreach (var cmd in GuildCommandsCreateList)
+ {
+ var discord_backend_command = await this.Client.CreateGuildApplicationCommandAsync(guildid.Value, cmd);
+ Commands.Add(discord_backend_command);
+ }
- foreach (var cmd in GuildCommandsOverwriteList)
+ foreach (var cmd in GuildCommandsOverwriteList)
+ {
+ var command = cmd.Value;
+ var discord_backend_command = await this.Client.EditGuildApplicationCommandAsync(guildid.Value, cmd.Key, action =>
+ {
+ action.Name = command.Name;
+ action.NameLocalizations = command.NameLocalizations;
+ action.Description = command.Description;
+ action.DescriptionLocalizations = command.DescriptionLocalizations;
+ if(command.Options != null && command.Options.Any())
+ action.Options = Optional.FromValue(command.Options);
+ action.DefaultPermission = command.DefaultPermission;
+ });
+
+ Commands.Add(discord_backend_command);
+ }
+ }
+
+ if(CheckAnyAndNotNull(GuildCommandsDeleteList))
{
- var command = cmd.Value;
- var discord_backend_command = await this.Client.ApiClient.EditGuildApplicationCommandAsync(this.Client.CurrentApplication.Id, guildid.Value,
- cmd.Key, command.Name, command.Description, command.Options.Any() ? Optional.FromValue(command.Options) : null, command.DefaultPermission,
- command.NameLocalizations, command.DescriptionLocalizations
- );
- Commands.Add(discord_backend_command);
+ foreach (var cmdId in GuildCommandsDeleteList)
+ {
+ await this.Client.DeleteGuildApplicationCommandAsync(guildid.Value, cmdId);
+ }
}
- }
- foreach (var cmdId in GuildCommandsDeleteList)
- {
- await this.Client.DeleteGuildApplicationCommandAsync(guildid.Value, cmdId);
+ if (CheckAnyAndNotNull(Commands))
+ _guildCommands.Add(guildid.Value, Commands);
+ else
+ return;
}
-
- _guildCommands.Add(guildid.Value, Commands);
}
}
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.Any(x => x.Name == command.Name))
{
var com = commandMethods.First(x => x.Name == command.Name);
com.CommandId = command.Id;
var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType);
await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key);
}
else if (groupCommands.Any(x => x.Name == command.Name))
{
var com = groupCommands.First(x => x.Name == command.Name);
com.CommandId = command.Id;
await UpdateCommandPermissionGroup(com);
}
else if (subGroupCommands.Any(x => x.Name == command.Name))
{
var com = subGroupCommands.First(x => x.Name == command.Name);
com.CommandId = command.Id;
foreach (var groupComs in com.SubCommands)
await UpdateCommandPermissionGroup(groupComs);
}
else if (contextMenuCommands.Any(x => x.Name == command.Name))
{
var com = contextMenuCommands.First(x => x.Name == command.Name);
com.CommandId = command.Id;
var source = commandTypeSources.First(f => f.Key == com.Method.DeclaringType);
await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key);
}
}
//Adds to the global lists finally
_commandMethods.AddRange(commandMethods);
_groupCommands.AddRange(groupCommands);
_subGroupCommands.AddRange(subGroupCommands);
_contextMenuCommands.AddRange(contextMenuCommands);
_registeredCommands.Add(new KeyValuePair>(guildid, Commands.ToList()));
foreach (var command in commandMethods)
{
var app = types.First(t => t.Type == command.Method.DeclaringType);
}
if (guildid.HasValue)
{
await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(this._configuration?.ServiceProvider)
{
Handled = true,
GuildId = guildid.Value,
RegisteredCommands = _guildCommands.Single(c => c.Key == guildid.Value).Value
});
}
else
{
await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(this._configuration?.ServiceProvider)
{
Handled = true,
RegisteredCommands = _globalCommands
});
}
RegistrationCount++;
this.CheckRegistrationStartup();
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}");
else
this.Client.Logger.LogCritical(ex, $"There was an error registering application commands");
_errored = true;
}
}
});
}
+ internal static bool CheckNotAnyOrNull(List list)
+ => list == null ? true : !list.Any();
+
+ internal static bool CheckNotAnyOrNull(List list)
+ => list == null ? true : !list.Any();
+
+ internal static bool CheckNotAnyOrNull(Dictionary dict)
+ => dict == null ? true : !dict.Any() ? true : dict.Keys == null ? true : !dict.Keys.Any();
+
+ internal static bool CheckAnyAndNotNull(List list)
+ => list == null ? false : list.Any();
+
+ internal static bool CheckAnyAndNotNull(List list)
+ => list == null ? false : list.Any();
+
+ internal static bool CheckAnyAndNotNull(Dictionary dict)
+ => dict == null ? false : !dict.Any() ? false : dict.Keys == null ? false : !dict.Keys.Any() ? false : true;
+
private async void CheckRegistrationStartup()
{
if (RegistrationCount == ExpectedCount)
{
await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(this._configuration?.ServiceProvider)
{
RegisteredGlobalCommands = _globalCommands,
RegisteredGuildCommands = _guildCommands,
GuildsWithoutScope = MissingScopeGuildIds
});
this.FinishedRegistration();
}
}
private List BuildGuildDeleteList(ulong guildId, List updateList)
{
- var discord = _guildDiscordCommands.Where(l => l.Key == guildId).First();
-
- List InvalidCommandIds = new();
- foreach (var cmd in discord.Value)
+ if(this._guildDiscordCommands == null || !this._guildDiscordCommands.Any())
{
- if (!updateList.Where(ul => ul.Name == cmd.Name).Any())
- InvalidCommandIds.Add(cmd.Id);
+ return null;
}
+ else if (updateList == null)
+ {
+ var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value;
+
+ if (discord != null)
+ {
+ List InvalidCommandIds = new();
+ foreach (var cmd in discord)
+ {
+ InvalidCommandIds.Add(cmd.Id);
+ }
+
+ return InvalidCommandIds;
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value;
- return InvalidCommandIds;
+ if (discord != null) {
+ List InvalidCommandIds = new();
+ foreach (var cmd in discord)
+ {
+ if (!updateList.Any(ul => ul.Name == cmd.Name))
+ InvalidCommandIds.Add(cmd.Id);
+ }
+
+ return InvalidCommandIds;
+ }
+ else
+ {
+ return null;
+ }
+ }
}
private List BuildGuildCreateList(ulong guildId, List updateList)
{
- var discord = _guildDiscordCommands.Where(l => l.Key == guildId).First().Value;
- List NewCommands = new();
- foreach(var cmd in updateList)
+ if (this._guildDiscordCommands == null || !this._guildDiscordCommands.Any())
{
- if(!discord.Where(d => d.Name == cmd.Name).Any())
+ return updateList;
+ }
+ else if (updateList == null)
+ {
+ return null;
+ }
+ else
+ {
+ var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value;
+ List NewCommands = new();
+ foreach (var cmd in updateList)
{
- NewCommands.Add(cmd);
+ if (discord == null || !discord.Any(d => d.Name == cmd.Name))
+ {
+ NewCommands.Add(cmd);
+ }
}
- }
- return NewCommands;
+ return NewCommands;
+ }
}
private Dictionary BuildGuildOverwriteList(ulong guildId, List updateList)
{
- var discord = _guildDiscordCommands.Where(l => l.Key == guildId).First().Value;
- Dictionary UpdateCommands = new();
- foreach (var cmd in updateList)
+ if (this._guildDiscordCommands == null || !this._guildDiscordCommands.Any() || !this._guildDiscordCommands.Any(l => l.Key == guildId))
+ {
+ return null;
+ }
+ else if (updateList == null)
+ {
+ return null;
+ }
+ else
{
- var dc = discord.Where(d => d.Name == cmd.Name);
- if (dc.Any())
+ var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value;
+ if (discord != null)
{
- UpdateCommands.Add(dc.First().Id, cmd);
+ Dictionary UpdateCommands = new();
+ foreach (var cmd in updateList)
+ {
+ if (discord.Any(d => d.Name == cmd.Name))
+ {
+ UpdateCommands.Add(discord.Where(d => d.Name == cmd.Name).First().Id, cmd);
+ }
+ }
+ return UpdateCommands;
+ }
+ else
+ {
+ return null;
}
}
-
- return UpdateCommands;
}
- private List BuildGlobalDeleteList(List updateList)
+ private List BuildGlobalDeleteList(List updateList = null)
{
- var discord = _globalDiscordCommands;
- List InvalidCommandIds = new();
- foreach(var cmd in discord)
+ if (this._globalDiscordCommands == null || !this._globalDiscordCommands.Any())
+ {
+ return null;
+ }
+ else if (updateList == null)
{
- if (!updateList.Where(ul => ul.Name == cmd.Name).Any())
- InvalidCommandIds.Add(cmd.Id);
+ var discord = this._globalDiscordCommands;
+
+ if (discord != null) {
+ List InvalidCommandIds = new();
+ foreach (var cmd in discord)
+ {
+ InvalidCommandIds.Add(cmd.Id);
+ }
+
+ return InvalidCommandIds;
+ }
+ else
+ {
+ return null;
+ }
}
+ else
+ {
+ var discord = this._globalDiscordCommands;
- return InvalidCommandIds;
+ if (discord != null)
+ {
+ List InvalidCommandIds = new();
+ foreach (var cmd in discord)
+ {
+ if (!updateList.Any(ul => ul.Name == cmd.Name))
+ InvalidCommandIds.Add(cmd.Id);
+ }
+
+ return InvalidCommandIds;
+ }
+ else
+ {
+ return null;
+ }
+ }
}
private List BuildGlobalCreateList(List updateList)
{
- var discord = _globalDiscordCommands;
- List NewCommands = new();
- foreach (var cmd in updateList)
+ if (this._globalDiscordCommands == null || !this._globalDiscordCommands.Any())
+ {
+ return updateList;
+ }
+ else if(updateList == null)
{
- if (!discord.Where(d => d.Name == cmd.Name).Any())
+ return null;
+ }
+ else
+ {
+ var discord = this._globalDiscordCommands;
+ List NewCommands = new();
+ foreach (var cmd in updateList)
{
- NewCommands.Add(cmd);
+ if (discord == null || !discord.Any(d => d.Name == cmd.Name))
+ {
+ NewCommands.Add(cmd);
+ }
}
- }
- return NewCommands;
+ return NewCommands;
+ }
}
private Dictionary BuildGlobalOverwriteList(List updateList)
{
- var discord = _globalDiscordCommands;
- Dictionary UpdateCommands = new();
- foreach (var cmd in updateList)
+ if (this._globalDiscordCommands == null || !this._globalDiscordCommands.Any())
+ {
+ return null;
+ }
+ else if (updateList == null)
{
- var dc = discord.Where(d => d.Name == cmd.Name);
- if (dc.Any())
+ return null;
+ }
+ else
+ {
+ var discord = this._globalDiscordCommands;
+ if (discord != null)
+ {
+ Dictionary UpdateCommands = new();
+ foreach (var cmd in updateList)
+ {
+ if (discord.Any(d => d.Name == cmd.Name))
+ {
+ UpdateCommands.Add(discord.Where(d => d.Name == cmd.Name).First().Id, cmd);
+ }
+ }
+
+ return UpdateCommands;
+ }
+ else
{
- UpdateCommands.Add(dc.First().Id, cmd);
+ return null;
}
}
-
- return UpdateCommands;
}
///
/// Interaction handler.
///
/// The client.
/// The event args.
private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e)
{
_ = Task.Run(async () =>
{
if (e.Interaction.Type == InteractionType.ApplicationCommand)
{
//Creates the context
var context = new InteractionContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Guild = e.Interaction.Guild,
User = e.Interaction.User,
Client = client,
ApplicationCommandsExtension = this,
CommandName = e.Interaction.Data.Name,
InteractionId = e.Interaction.Id,
Token = e.Interaction.Token,
Services = this._configuration?.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
};
try
{
if (_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
throw new InvalidOperationException("A slash command was executed, but no command was registered for it.");
if (methods.Any())
{
var method = methods.First().Method;
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options);
await this.RunCommandAsync(context, method, args);
}
else if (groups.Any())
{
var command = e.Interaction.Data.Options.First();
var method = groups.First().Methods.First(x => x.Key == command.Name).Value;
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options.First();
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name);
var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value;
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(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 (_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it.");
try
{
if (methods.Any())
{
var focusedOption = e.Interaction.Data.Options.First(o => o.Focused);
var method = methods.First().Method;
var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Client = this.Client,
Services = this._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
};
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 = this._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
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
/*else if (subgroups.Any())
{
var command = e.Interaction.Data.Options.First();
var method = methods.First().Method;
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name);
var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused);
this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue);
var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value;
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Services = this._configuration?.Services,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.First(x => x.Name == group.Name).Options.ToList(),
FocusedOption = focusedOption
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}*/
}
catch (Exception ex)
{
this.Client.Logger.LogError(ex, "Error in autocomplete interaction");
}
}
});
return Task.CompletedTask;
}
///
/// Context menu handler.
///
/// The client.
/// The event args.
private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e)
{
_ = Task.Run(async () =>
{
//Creates the context
var context = new ContextMenuContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Client = client,
Services = this._configuration?.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
};
try
{
if (_errored)
throw new InvalidOperationException("Context menus failed to register properly on startup.");
//Gets the method for the command
var method = _contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id);
if (method == null)
throw new InvalidOperationException("A context menu was executed, but no command was registered for it.");
await this.RunCommandAsync(context, method.Method, new[] { context });
await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(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(this._configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider.CreateScope().ServiceProvider);
break;
case ApplicationCommandModuleLifespan.Transient:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider);
break;
//If singleton, gets it from the singleton list
case ApplicationCommandModuleLifespan.Singleton:
classInstance = _singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType));
break;
default:
throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}");
}
ApplicationCommandsModule module = null;
if (classInstance is ApplicationCommandsModule mod)
module = mod;
// Slash commands
if (context is InteractionContext slashContext)
{
await this.RunPreexecutionChecksAsync(method, slashContext);
var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true));
if (shouldExecute)
{
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask);
}
}
// Context menus
if (context is ContextMenuContext contextMenuContext)
{
await this.RunPreexecutionChecksAsync(method, contextMenuContext);
var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true));
if (shouldExecute)
{
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask);
}
}
}
///
- /// Property injection copied over from CommandsNext
+ /// Property injection
///
/// The type.
/// The services.
internal object CreateInstance(Type t, IServiceProvider services)
{
var ti = t.GetTypeInfo();
var constructors = ti.DeclaredConstructors
.Where(xci => xci.IsPublic)
.ToArray();
if (constructors.Length != 1)
throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor.");
var constructor = constructors[0];
var constructorArgs = constructor.GetParameters();
var args = new object[constructorArgs.Length];
if (constructorArgs.Length != 0 && services == null)
throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors.");
// inject via constructor
if (constructorArgs.Length != 0)
for (var i = 0; i < args.Length; i++)
args[i] = services.GetRequiredService(constructorArgs[i].ParameterType);
var moduleInstance = Activator.CreateInstance(t, args);
// inject into properties
var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic);
foreach (var prop in props)
{
if (prop.GetCustomAttribute() != null)
continue;
var service = services.GetService(prop.PropertyType);
if (service == null)
continue;
prop.SetValue(moduleInstance, service);
}
// inject into fields
var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic);
foreach (var field in fields)
{
if (field.GetCustomAttribute() != null)
continue;
var service = services.GetService(field.FieldType);
if (service == null)
continue;
field.SetValue(moduleInstance, service);
}
return moduleInstance;
}
///
/// Resolves the slash command parameters.
///
/// The event arguments.
/// The interaction context.
/// The method info.
/// The options.
private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options)
{
var args = new List { context };
var parameters = method.GetParameters().Skip(1);
for (var i = 0; i < parameters.Count(); i++)
{
var parameter = parameters.ElementAt(i);
//Accounts for optional arguments without values given
- if (parameter.IsOptional && (options == null ||
- (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true)))
+ 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 if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment))
args.Add(attachment);
else
throw new ArgumentException("Error resolving mentionable option.");
}
else
throw new ArgumentException($"Error resolving interaction.");
}
}
return args;
}
///
/// Runs the preexecution checks.
///
/// The method info.
/// The 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 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 ApplicationCommandOptionType GetParameterType(Type type)
{
var parametertype = type == typeof(string)
? ApplicationCommandOptionType.String
: type == typeof(long) || type == typeof(long?)
? ApplicationCommandOptionType.Integer
: 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(DiscordChannel)
? ApplicationCommandOptionType.Channel
: type == typeof(DiscordUser)
? ApplicationCommandOptionType.User
: type == typeof(DiscordRole)
? ApplicationCommandOptionType.Role
: type == typeof(SnowflakeObject)
? ApplicationCommandOptionType.Mentionable
: type == typeof(DiscordAttachment)
? ApplicationCommandOptionType.Attachment
: 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 List GetChoiceAttributesFromParameter(IEnumerable choiceattributes)
{
return !choiceattributes.Any()
? null
: choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList();
}
///
/// Parses the parameters.
///
/// The parameters.
/// The optional guild id.
private async Task> ParseParameters(ParameterInfo[] parameters, ulong? guildId)
{
var options = new List();
foreach (var parameter in parameters)
{
//Gets the attribute
var optionattribute = parameter.GetCustomAttribute();
if (optionattribute == null)
throw new ArgumentException("Arguments must have the Option attribute!");
var minimumValue = parameter.GetCustomAttribute()?.Value ?? null;
var maximumValue = parameter.GetCustomAttribute()?.Value ?? 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 = this.GetParameterType(type);
//Handles choices
//From attributes
var choices = this.GetChoiceAttributesFromParameter(parameter.GetCustomAttributes());
//From enums
if (parameter.ParameterType.IsEnum)
{
choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType);
}
//From choice provider
var choiceProviders = parameter.GetCustomAttributes();
if (choiceProviders.Any())
{
choices = await this.GetChoiceAttributesFromProvider(choiceProviders, guildId);
}
var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null;
options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes, optionattribute.Autocomplete, minimumValue, maximumValue));
}
return options;
}
///
/// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client.
/// Should only be run on the slash command extension linked to shard 0 if sharding.
/// Not recommended and should be avoided since it can make slash commands be unresponsive for a while.
///
public async Task RefreshCommandsAsync()
{
_commandMethods.Clear();
_groupCommands.Clear();
_subGroupCommands.Clear();
_registeredCommands.Clear();
_contextMenuCommands.Clear();
- _globalDiscordCommands.Clear();
- _guildDiscordCommands.Clear();
+ this._globalDiscordCommands.Clear();
+ this._guildDiscordCommands.Clear();
+ this._globalDiscordCommands = null;
+ this._guildDiscordCommands = null;
_guildCommands.Clear();
_globalCommands.Clear();
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 permission setup.
///
public Action Setup { get; }
public Action Translations { get; }
///
/// Creates a new command configuration.
///
/// The type of the command module.
/// The permission setup callback.
/// The translation setup callback.
public ApplicationCommandsModuleConfiguration(Type type, Action setup = null, Action translations = null)
{
this.Type = type;
this.Setup = setup;
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 List();
}
///
/// The context menu command.
///
internal class ContextMenuCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs
index 29a221f6b..d4d38969c 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs
@@ -1,61 +1,71 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
namespace DisCatSharp.ApplicationCommands
{
///
/// Marks this method as a slash command
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class SlashCommandAttribute : Attribute
{
///
/// Gets the name of this command
///
- public string Name { get; }
+ public string Name { get; set; }
///
/// Gets the description of this command
///
- public string Description { get; }
+ public string Description { get; set; }
///
/// Gets the default permission of this command
///
public bool DefaultPermission { get; }
+ ///
+ /// Gets the needed permission of this command
+ ///
+ public Permissions? Permission { get; set; }
+
+ ///
+ /// Gets the dm permission of this command
+ ///
+ public bool? DmPermission { get; set; }
+
///
/// Marks this method as a slash command
///
/// The name of this slash command.
/// The description of this slash command.
/// Whether everyone can execute this command.
public SlashCommandAttribute(string name, string description, bool default_permission = true)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultPermission = default_permission;
}
}
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs
index aea710da2..b1fab1d93 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs
@@ -1,61 +1,71 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
namespace DisCatSharp.ApplicationCommands
{
///
/// Marks this class a slash command group
///
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class SlashCommandGroupAttribute : Attribute
{
///
/// Gets the name of this slash command group
///
public string Name { get; set; }
///
/// Gets the description of this slash command group
///
public string Description { get; set; }
///
/// Gets the default permission of this slash command group
///
public bool DefaultPermission { get; set; }
+ ///
+ /// Gets the needed permission of this slash command group
+ ///
+ public Permissions? Permission { get; set; }
+
+ ///
+ /// Gets the dm permission of this slash command group
+ ///
+ public bool? DmPermission { get; set; }
+
///
/// Marks this class as a slash command group
///
/// The name of this slash command group.
/// The description of this slash command group.
/// Whether everyone can execute this command.
public SlashCommandGroupAttribute(string name, string description, bool default_permission = true)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultPermission = default_permission;
}
}
}
diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs
index 7dd22f096..bf618c967 100644
--- a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs
+++ b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs
@@ -1,178 +1,181 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.Interactivity.EventHandling
{
///
/// The component paginator.
///
internal class ComponentPaginator : IPaginator
{
private readonly DiscordClient _client;
private readonly InteractivityConfiguration _config;
private readonly DiscordMessageBuilder _builder = new();
private readonly Dictionary _requests = new();
///
/// Initializes a new instance of the class.
///
/// The client.
/// The config.
public ComponentPaginator(DiscordClient client, InteractivityConfiguration config)
{
this._client = client;
this._client.ComponentInteractionCreated += this.Handle;
this._config = config;
}
///
/// Does the pagination async.
///
/// The request.
public async Task DoPaginationAsync(IPaginationRequest request)
{
var id = (await request.GetMessageAsync().ConfigureAwait(false)).Id;
this._requests.Add(id, request);
try
{
var tcs = await request.GetTaskCompletionSourceAsync().ConfigureAwait(false);
await tcs.Task.ConfigureAwait(false);
}
catch (Exception ex)
{
this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while paginating.");
}
finally
{
this._requests.Remove(id);
try
{
await request.DoCleanupAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while cleaning up pagination.");
}
}
}
///
/// Disposes the paginator.
///
public void Dispose() => this._client.ComponentInteractionCreated -= this.Handle;
///
/// Handles the pagination event.
///
/// The client.
/// The event arguments.
private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs e)
{
+ if (e.Interaction.Type == InteractionType.ModalSubmit)
+ return;
+
if (!this._requests.TryGetValue(e.Message.Id, out var req))
return;
if (this._config.AckPaginationButtons)
{
e.Handled = true;
await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate).ConfigureAwait(false);
}
if (await req.GetUserAsync().ConfigureAwait(false) != e.User)
{
if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond)
await e.Interaction.CreateFollowupMessageAsync(new() { Content = this._config.ResponseMessage, IsEphemeral = true }).ConfigureAwait(false);
return;
}
if (req is InteractionPaginationRequest ipr)
ipr.RegenerateCTS(e.Interaction); // Necessary to ensure we don't prematurely yeet the CTS //
await this.HandlePaginationAsync(req, e).ConfigureAwait(false);
}
///
/// Handles the pagination async.
///
/// The request.
/// The arguments.
private async Task HandlePaginationAsync(IPaginationRequest request, ComponentInteractionCreateEventArgs args)
{
var buttons = this._config.PaginationButtons;
var msg = await request.GetMessageAsync().ConfigureAwait(false);
var id = args.Id;
var tcs = await request.GetTaskCompletionSourceAsync().ConfigureAwait(false);
#pragma warning disable CS8846 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
var paginationTask = id switch
#pragma warning restore CS8846 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
{
_ when id == buttons.SkipLeft.CustomId => request.SkipLeftAsync(),
_ when id == buttons.SkipRight.CustomId => request.SkipRightAsync(),
_ when id == buttons.Stop.CustomId => Task.FromResult(tcs.TrySetResult(true)),
_ when id == buttons.Left.CustomId => request.PreviousPageAsync(),
_ when id == buttons.Right.CustomId => request.NextPageAsync(),
};
await paginationTask.ConfigureAwait(false);
if (id == buttons.Stop.CustomId)
return;
var page = await request.GetPageAsync().ConfigureAwait(false);
var bts = await request.GetButtonsAsync().ConfigureAwait(false);
if (request is InteractionPaginationRequest ipr)
{
var builder = new DiscordWebhookBuilder()
.WithContent(page.Content)
.AddEmbed(page.Embed)
.AddComponents(bts);
await args.Interaction.EditOriginalResponseAsync(builder).ConfigureAwait(false);
return;
}
this._builder.Clear();
this._builder
.WithContent(page.Content)
.AddEmbed(page.Embed)
.AddComponents(bts);
await this._builder.ModifyAsync(msg).ConfigureAwait(false);
}
}
}
diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs b/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs
new file mode 100644
index 000000000..dcb990f83
--- /dev/null
+++ b/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs
@@ -0,0 +1,112 @@
+// This file is part of the DisCatSharp project.
+//
+// Copyright (c) 2021 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading.Tasks;
+using ConcurrentCollections;
+using DisCatSharp.Entities;
+using DisCatSharp.EventArgs;
+using DisCatSharp.Interactivity.Enums;
+using Microsoft.Extensions.Logging;
+
+namespace DisCatSharp.Interactivity.EventHandling
+{
+ ///
+ /// A modal-based version of
+ ///
+ internal class ModalEventWaiter : IDisposable
+ {
+ private readonly DiscordClient _client;
+ private readonly ConcurrentHashSet _modalMatchRequests = new();
+
+ private readonly DiscordFollowupMessageBuilder _message;
+ private readonly InteractivityConfiguration _config;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The client.
+ /// The config.
+ public ModalEventWaiter(DiscordClient client, InteractivityConfiguration config)
+ {
+ this._client = client;
+ this._client.ComponentInteractionCreated += this.Handle;
+ this._config = config;
+
+ this._message = new() { Content = config.ResponseMessage ?? "This modal was not meant for you.", IsEphemeral = true };
+ }
+
+ ///
+ /// Waits for a specified 's predicate to be fufilled.
+ ///
+ /// The request to wait for.
+ /// The returned args, or null if it timed out.
+ public async Task WaitForModalMatchAsync(ModalMatchRequest request)
+ {
+ this._modalMatchRequests.Add(request);
+
+ try
+ {
+ return await request.Tcs.Task.ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ this._client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for modals.");
+ return null;
+ }
+ finally
+ {
+ this._modalMatchRequests.TryRemove(request);
+ }
+ }
+
+ ///
+ /// Handles the waiter.
+ ///
+ /// The client.
+ /// The args.
+ private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs args)
+ {
+ foreach (var mreq in this._modalMatchRequests.ToArray())
+ {
+ if (mreq.CustomId == args.Interaction.Data.CustomId && mreq.IsMatch(args))
+ mreq.Tcs.TrySetResult(args);
+
+ else if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond)
+ await args.Interaction.CreateFollowupMessageAsync(this._message).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Disposes the waiter.
+ ///
+ public void Dispose()
+ {
+ this._modalMatchRequests.Clear();
+ this._client.ComponentInteractionCreated -= this.Handle;
+ }
+ }
+}
diff --git a/DisCatSharp.Interactivity/EventHandling/Components/Requests/ModalMatchRequest.cs b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ModalMatchRequest.cs
new file mode 100644
index 000000000..ee3a69787
--- /dev/null
+++ b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ModalMatchRequest.cs
@@ -0,0 +1,69 @@
+// This file is part of the DisCatSharp project.
+//
+// Copyright (c) 2021 AITSYS
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using DisCatSharp.Entities;
+using DisCatSharp.EventArgs;
+
+namespace DisCatSharp.Interactivity.EventHandling
+{
+ ///
+ /// Represents a match that is being waited for.
+ ///
+ internal class ModalMatchRequest
+ {
+ ///
+ /// The id to wait on. This should be uniquely formatted to avoid collisions.
+ ///
+ public string CustomId { get; private set; }
+
+ ///
+ /// The completion source that represents the result of the match.
+ ///
+ public TaskCompletionSource Tcs { get; private set; } = new();
+
+ protected readonly CancellationToken _cancellation;
+ protected readonly Func _predicate;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The custom id.
+ /// The predicate.
+ /// The cancellation token.
+ public ModalMatchRequest(string custom_id, Func predicate, CancellationToken cancellation)
+ {
+ this.CustomId = custom_id;
+ this._predicate = predicate;
+ this._cancellation = cancellation;
+ this._cancellation.Register(() => this.Tcs.TrySetResult(null)); // TrySetCancelled would probably be better but I digress ~Velvet //
+ }
+
+ ///
+ /// Whether it is a match.
+ ///
+ /// The arguments.
+ public bool IsMatch(ComponentInteractionCreateEventArgs args) => this._predicate(args);
+ }
+}
diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs
index fc93fcfaf..73b64abef 100644
--- a/DisCatSharp.Interactivity/InteractivityExtension.cs
+++ b/DisCatSharp.Interactivity/InteractivityExtension.cs
@@ -1,930 +1,962 @@
// This file is part of the DisCatSharp project.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Enums;
using System.Threading;
using System.Globalization;
namespace DisCatSharp.Interactivity
{
///
/// Extension class for DisCatSharp.Interactivity
///
public class InteractivityExtension : BaseExtension
{
#pragma warning disable IDE1006 // Naming Styles
///
/// Gets the config.
///
internal InteractivityConfiguration Config { get; }
private EventWaiter MessageCreatedWaiter;
private EventWaiter MessageReactionAddWaiter;
private EventWaiter TypingStartWaiter;
+ private EventWaiter ModalInteractionWaiter;
+
private EventWaiter ComponentInteractionWaiter;
private ComponentEventWaiter ComponentEventWaiter;
+ private ModalEventWaiter ModalEventWaiter;
+
private ReactionCollector ReactionCollector;
private Poller Poller;
private Paginator Paginator;
private ComponentPaginator _compPaginator;
#pragma warning restore IDE1006 // Naming Styles
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal InteractivityExtension(InteractivityConfiguration cfg)
{
this.Config = new InteractivityConfiguration(cfg);
}
///
/// Setups the Interactivity Extension.
///
/// Discord client.
protected internal override void Setup(DiscordClient client)
{
this.Client = client;
this.MessageCreatedWaiter = new EventWaiter(this.Client);
this.MessageReactionAddWaiter = new EventWaiter(this.Client);
this.ComponentInteractionWaiter = new EventWaiter(this.Client);
+ this.ModalInteractionWaiter = new EventWaiter(this.Client);
this.TypingStartWaiter = new EventWaiter(this.Client);
this.Poller = new Poller(this.Client);
this.ReactionCollector = new ReactionCollector(this.Client);
this.Paginator = new Paginator(this.Client);
this._compPaginator = new(this.Client, this.Config);
this.ComponentEventWaiter = new(this.Client, this.Config);
+ this.ModalEventWaiter = new(this.Client, this.Config);
}
///
/// Makes a poll and returns poll results.
///
/// Message to create poll on.
/// Emojis to use for this poll.
/// What to do when the poll ends.
/// override timeout period.
///
public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
if (!emojis.Any())
throw new ArgumentException("You need to provide at least one emoji for a poll!");
foreach (var em in emojis)
await m.CreateReactionAsync(em).ConfigureAwait(false);
var res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false);
var pollbehaviour = behaviour ?? this.Config.PollBehaviour;
var thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false);
if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(Permissions.ManageMessages))
await m.DeleteAllReactionsAsync().ConfigureAwait(false);
return new ReadOnlyCollection(res.ToList());
}
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// Override the timeout period in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button in the specified collection to be pressed.
///
/// The message to wait on.
/// A collection of buttons to listen for.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!buttons.Any())
throw new ArgumentException("You must specify at least one button to listen for.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Provided Message does not contain any button components.");
var res = await this.ComponentEventWaiter
.WaitForMatchAsync(new(message,
c =>
c.Interaction.Data.ComponentType == ComponentType.Button &&
buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false);
return new(res is null, res);
}
+ ///
+ /// Waits for a user modal submit.
+ ///
+ /// The custom id of the modal to wait for.
+ /// Override the timeout period specified in .
+ /// A with the result of the modal.
+ public Task> WaitForModalAsync(string custom_id, TimeSpan? timeoutOverride = null)
+ => this.WaitForModalAsync(custom_id, this.GetCancellationToken(timeoutOverride));
+
+ ///
+ /// Waits for a user modal submit.
+ ///
+ /// The custom id of the modal to wait for.
+ /// A custom cancellation token that can be cancelled at any point.
+ /// A with the result of the modal.
+ public async Task> WaitForModalAsync(string custom_id, CancellationToken token)
+ {
+ var result =
+ await this
+ .ModalEventWaiter
+ .WaitForModalMatchAsync(new(custom_id, c => c.Interaction.Type == InteractionType.ModalSubmit, token))
+ .ConfigureAwait(false);
+
+ return new(result is null, result);
+ }
+
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed.
///
/// The message to wait for the button on.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId);
var result =
await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// Override the timeout period specified in .
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button on the specified message to be pressed by the specified user.
///
/// The message to wait for the button on.
/// The user to wait for the button press from.
/// A custom cancellation token that can be cancelled at any point.
/// A with the result of button that was pressed, if any.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a button with the specified Id to be pressed.
///
/// The message to wait for the button on.
/// The Id of the button to wait for.
/// Override the timeout period specified in .
/// A with the result of the operation.
/// Thrown when attempting to wait for a message that is not authored by the current user.
/// Thrown when the message does not contain a button with the specified Id, or any buttons at all.
public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id))
throw new ArgumentException($"Message does not contain button with Id of '{id}'.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// Override the timeout specified in
public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///
/// Waits for any button to be interacted with.
///
/// The message to wait on.
/// The predicate to filter interactions by.
/// A token to cancel interactivity with at any time. Pass to wait indefinitely.
public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token)
{
if (message.Author != this.Client.CurrentUser)
throw new InvalidOperationException("Interaction events are only sent to the application that created them.");
if (!message.Components.Any())
throw new ArgumentException("Provided message does not contain any components.");
if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button))
throw new ArgumentException("Message does not contain any button components.");
var result = await this
.ComponentEventWaiter
.WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token))
.ConfigureAwait(false);
return new(result is null, result);
}
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// Override the timeout period specified in .
/// Thrown when the Provided message does not contain any dropdowns
public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride));
///