diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index 48a64b181..52a94cbe6 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,1847 +1,1871 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.EventArgs;
using DisCatSharp.Common;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace DisCatSharp.ApplicationCommands;
///
/// A class that handles slash commands for a client.
///
public sealed class ApplicationCommandsExtension : BaseExtension
{
///
/// A list of methods for top level commands.
///
private static List s_commandMethods { get; set; } = new();
///
/// List of groups.
///
private static List s_groupCommands { get; set; } = new();
///
/// List of groups with subgroups.
///
private static List s_subGroupCommands { get; set; } = new();
///
/// List of context menus.
///
private static List s_contextMenuCommands { get; set; } = new();
///
/// List of global commands on discords backend.
///
internal static List GlobalDiscordCommands { get; set; }
///
/// List of guild commands on discords backend.
///
internal static Dictionary> GuildDiscordCommands { get; set; }
///
/// Singleton modules.
///
private static List s_singletonModules { get; set; } = new();
///
/// List of modules to register.
///
private readonly List> _updateList = new();
///
/// Configuration for Discord.
///
internal static ApplicationCommandsConfiguration Configuration;
///
/// Set to true if anything fails when registering.
///
private static bool s_errored { get; set; }
///
/// Gets a list of registered commands. The key is the guild id (null if global).
///
public IReadOnlyList>> RegisteredCommands
=> s_registeredCommands;
private static readonly List>> s_registeredCommands = new();
///
/// Gets a list of registered global commands.
///
public IReadOnlyList GlobalCommands
=> GlobalCommandsInternal;
internal static readonly List GlobalCommandsInternal = new();
///
/// Gets a list of registered guild commands mapped by guild id.
///
public IReadOnlyDictionary> GuildCommands
=> GuildCommandsInternal;
internal static readonly Dictionary> GuildCommandsInternal = new();
///
/// Gets the registration count.
///
private static int s_registrationCount { get; set; }
///
/// Gets the expected count.
///
private static int s_expectedCount { get; set; }
///
/// Gets the guild ids where the applications.commands scope is missing.
///
private IReadOnlyList _missingScopeGuildIds;
///
/// Gets whether debug is enabled.
///
internal static bool DebugEnabled { get; set; }
internal static LogLevel ApplicationCommandsLogLevel
=> DebugEnabled ? LogLevel.Debug : LogLevel.Trace;
///
/// Gets whether check through all guilds is enabled.
///
internal static bool CheckAllGuilds { get; set; }
///
/// Gets whether the registration check should be manually overridden.
///
internal static bool ManOr { get; set; }
///
/// Gets whether interactions should be automatically deffered.
///
internal static bool AutoDeferEnabled { get; set; }
+ ///
+ /// Whether this module finished the startup.
+ ///
+ internal bool StartupFinished { get; set; }
+
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration = null)
{
Configuration = configuration;
DebugEnabled = configuration?.DebugStartup ?? false;
CheckAllGuilds = configuration?.CheckAllGuilds ?? false;
ManOr = configuration?.ManualOverride ?? false;
AutoDeferEnabled = configuration?.AutoDefer ?? false;
}
///
/// Runs setup.
/// DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS.
///
/// The client to setup on.
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
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.StartupFinished = false;
this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync();
this.Client.InteractionCreated += this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup;
}
private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e)
- => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now."));
+ {
+ if (!this.StartupFinished)
+ await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now."));
+ else
+ await Task.Delay(1);
+ }
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."));
+ {
+ if (!this.StartupFinished)
+ 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."));
+ else
+ await Task.Delay(1);
+ }
private void FinishedRegistration()
{
this.Client.InteractionCreated -= this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup;
+ this.StartupFinished = true;
+
this.Client.InteractionCreated += this.InteractionHandler;
this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler;
}
///
/// Cleans the module for a new start of the bot.
/// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES.
///
public void CleanModule()
{
this._updateList.Clear();
s_singletonModules.Clear();
s_errored = false;
s_expectedCount = 0;
s_registrationCount = 0;
s_commandMethods.Clear();
s_groupCommands.Clear();
s_contextMenuCommands.Clear();
s_subGroupCommands.Clear();
s_singletonModules.Clear();
s_registeredCommands.Clear();
GlobalCommandsInternal.Clear();
GuildCommandsInternal.Clear();
}
///
/// Cleans all guild application commands.
/// You normally don't need to execute it.
///
public async Task CleanGuildCommandsAsync()
{
foreach (var guild in this.Client.Guilds.Values)
await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty());
}
///
/// Cleans the global application commands.
/// You normally don't need to execute it.
///
public async Task CleanGlobalCommandsAsync()
=> await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty());
///
/// Registers a command class with optional translation setup for a guild.
///
/// The command class to register.
/// The guild id to register it on.
/// A callback to setup translations with.
public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule
=> this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup)));
///
/// Registers a command class with optional translation setup for a guild.
///
/// The of the command class to register.
/// The guild id to register it on.
/// A callback to setup translations with.
public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup)));
}
///
/// Registers a command class with optional translation setup globally.
///
/// The command class to register.
/// A callback to setup translations with.
public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule
=> this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup)));
///
/// Registers a command class with optional translation setup globally.
///
/// The of the command class to register.
/// A callback to setup translations with.
public void RegisterGlobalCommands(Type type, Action translationSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, translationSetup)));
}
///
/// Fired when the application commands module is ready.
///
public event AsyncEventHandler ApplicationCommandsModuleReady
{
add => this._applicationCommandsModuleReady.Register(value);
remove => this._applicationCommandsModuleReady.Unregister(value);
}
private AsyncEvent _applicationCommandsModuleReady;
///
/// Fired when the application commands modules startup is finished.
///
public event AsyncEventHandler ApplicationCommandsModuleStartupFinished
{
add => this._applicationCommandsModuleStartupFinished.Register(value);
remove => this._applicationCommandsModuleStartupFinished.Unregister(value);
}
private AsyncEvent _applicationCommandsModuleStartupFinished;
///
/// Fired when guild commands are registered on a guild.
///
public event AsyncEventHandler GuildApplicationCommandsRegistered
{
add => this._guildApplicationCommandsRegistered.Register(value);
remove => this._guildApplicationCommandsRegistered.Unregister(value);
}
private AsyncEvent _guildApplicationCommandsRegistered;
///
/// Fired when the global commands are registered.
///
public event AsyncEventHandler GlobalApplicationCommandsRegistered
{
add => this._globalApplicationCommandsRegistered.Register(value);
remove => this._globalApplicationCommandsRegistered.Unregister(value);
}
private AsyncEvent _globalApplicationCommandsRegistered;
///
/// Used for RegisterCommands and the event.
///
internal async Task UpdateAsync()
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Request to register commands on shard {shard}", this.Client.ShardId);
GlobalDiscordCommands = new();
GuildDiscordCommands = new();
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {count}", s_expectedCount);
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} has {guilds} guilds.", this.Client.ShardId, this.Client.Guilds?.Count);
List failedGuilds = new();
List globalCommands = null;
globalCommands = (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false)).ToList() ?? null;
var updateList = this._updateList;
var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys.ToList() : updateList.Where(x => x.Key != null)?.Select(x => x.Key.Value).Distinct().ToList();
var wrongShards = guilds.Where(x => !this.Client.Guilds.ContainsKey(x)).ToList();
if (wrongShards.Any())
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Some guilds are not on the same shard as the client. Removing them from the update list.");
foreach (var guild in wrongShards)
{
updateList.RemoveAll(x => x.Key == guild);
guilds.Remove(guild);
}
}
var commandsPending = updateList.Select(x => x.Key).Distinct().ToList();
s_expectedCount = commandsPending.Count();
foreach (var guild in guilds)
{
List commands = null;
var unauthorized = false;
try
{
commands = (await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false)).ToList() ?? null;
}
catch (UnauthorizedException)
{
unauthorized = true;
}
finally
{
if (!unauthorized && commands != null && commands.Any())
GuildDiscordCommands.Add(guild, commands.ToList());
else if (unauthorized)
failedGuilds.Add(guild);
}
}
//Default should be to add the help and slash commands can be added without setting any configuration
//so this should still add the default help
if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp))
{
updateList.Add(new KeyValuePair
(null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule))));
commandsPending = updateList.Select(x => x.Key).Distinct().ToList();
}
if (globalCommands != null && globalCommands.Any())
GlobalDiscordCommands.AddRange(globalCommands);
foreach (var key in commandsPending)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands.");
if (key.HasValue)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId);
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering");
}
await this.RegisterCommands(updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key);
}
this._missingScopeGuildIds = failedGuilds;
await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider)
{
GuildsWithoutScope = failedGuilds
});
this.Client.GuildDownloadCompleted -= async (c, e) => await this.UpdateAsync();
}
///
/// Method for registering commands for a target from modules.
///
/// The types.
/// The optional guild id.
private async Task RegisterCommands(List types, ulong? guildId)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering commands on shard {shard}", this.Client.ShardId);
//Initialize empty lists to be added to the global ones at the end
var commandMethods = new List();
var groupCommands = new List();
var subGroupCommands = new List();
var contextMenuCommands = new List();
var updateList = new List();
var commandTypeSources = new List>();
//Iterates over all the modules
foreach (var config in types)
{
var type = config.Type;
try
{
var module = type.GetTypeInfo();
var classes = new List();
var ctx = new ApplicationCommandsTranslationContext(type, module.FullName);
config.Translations?.Invoke(ctx);
//Add module to classes list if it's a group
if (module.GetCustomAttribute() != null)
{
classes.Add(module);
}
else
{
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
}
List groupTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations);
if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any())
updateList.AddRange(slashGroupsTuple.applicationCommands);
if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any())
commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources);
if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any())
s_singletonModules.AddRange(slashGroupsTuple.singletonModules);
if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any())
groupCommands.AddRange(slashGroupsTuple.groupCommands);
if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any())
subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands);
//Handles methods and context menus, only if the module isn't a group itself
if (module.GetCustomAttribute() == null)
{
List commandTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
//Slash commands
var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations);
if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any())
updateList.AddRange(slashCommands.applicationCommands);
if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any())
commandTypeSources.AddRange(slashCommands.commandTypeSources);
if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any())
commandMethods.AddRange(slashCommands.commandMethods);
//Context Menus
var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations);
if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any())
updateList.AddRange(contextCommands.applicationCommands);
if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any())
commandTypeSources.AddRange(contextCommands.commandTypeSources);
if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any())
contextMenuCommands.AddRange(contextCommands.contextMenuCommands);
//Accounts for lifespans
if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider));
}
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
{
this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.WebResponse.Response}");
}
else
{
if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1)
this.Client.Logger.LogCritical(brex1, $"There was an error registering application commands: {brex1.WebResponse.Response}");
else
this.Client.Logger.LogCritical(ex, $"There was an error parsing the application commands");
}
s_errored = true;
}
}
if (!s_errored)
{
try
{
List commands = new();
try
{
if (guildId == null)
{
if (updateList != null && updateList.Any())
{
var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList);
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GlobalCommandsInternal.AddRange(actualCommands);
}
else
{
foreach (var cmd in GlobalDiscordCommands)
{
try
{
await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id);
}
catch (NotFoundException)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete global command {cmd.Id}. Please clean up manually");
}
}
}
}
else
{
if (updateList != null && updateList.Any())
{
var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList);
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GuildCommandsInternal.Add(guildId.Value, actualCommands);
/*
if (client.Guilds.TryGetValue(guildId.Value, out var guild))
{
guild.InternalRegisteredApplicationCommands = new();
guild.InternalRegisteredApplicationCommands.AddRange(actualCommands);
}
*/
}
else
{
foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value)
{
try
{
await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id);
}
catch (NotFoundException)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete guild command {cmd.Id} in guild {guildId.Value}. Please clean up manually");
}
}
}
}
}
catch (UnauthorizedException ex)
{
this.Client.Logger.LogError($"Could not register application commands for guild {guildId}.\nError: {ex.JsonMessage}");
return;
}
//Creates a guild command if a guild id is specified, otherwise global
//Checks against the ids and adds them to the command method lists
foreach (var command in commands)
{
if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com))
com.CommandId = command.Id;
else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom))
groupCom.CommandId = command.Id;
else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom))
subCom.CommandId = command.Id;
else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom))
cmCom.CommandId = command.Id;
}
//Adds to the global lists finally
s_commandMethods.AddRange(commandMethods);
s_groupCommands.AddRange(groupCommands);
s_subGroupCommands.AddRange(subGroupCommands);
s_contextMenuCommands.AddRange(contextMenuCommands);
s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList()));
foreach (var command in commandMethods)
{
var app = types.First(t => t.Type == command.Method.DeclaringType);
}
this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}");
if (guildId.HasValue)
{
await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
GuildId = guildId.Value,
RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null
});
}
else
{
await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredCommands = GlobalCommandsInternal
});
}
s_registrationCount++;
this.CheckRegistrationStartup(ManOr);
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
{
this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.WebResponse.Response}");
}
else
{
if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1)
this.Client.Logger.LogCritical(brex1, $"There was an error registering application commands: {brex1.WebResponse.Response}");
else
this.Client.Logger.LogCritical(ex, $"There was an general error registering application commands");
}
s_errored = true;
}
}
}
private async void CheckRegistrationStartup(bool man = false)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Checking counts...\n\n" +
$"Expected Count: {s_expectedCount}\n" +
$"Current Count: {s_registrationCount}");
if ((s_registrationCount == s_expectedCount) || man)
{
await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredGlobalCommands = GlobalCommandsInternal,
RegisteredGuildCommands = GuildCommandsInternal,
GuildsWithoutScope = this._missingScopeGuildIds
});
this.FinishedRegistration();
}
}
///
/// Interaction handler.
///
/// The client.
/// The event args.
private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got interaction on shard {shard}", this.Client.ShardId);
_ = Task.Run(async () =>
{
if (e.Interaction.Type == InteractionType.ApplicationCommand)
{
//Creates the context
var context = new InteractionContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Guild = e.Interaction.Guild,
User = e.Interaction.User,
Client = client,
ApplicationCommandsExtension = this,
CommandName = e.Interaction.Data.Name,
InteractionId = e.Interaction.Id,
Token = e.Interaction.Token,
Services = Configuration?.ServiceProvider,
ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(),
ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(),
ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(),
ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(),
Type = ApplicationCommandType.ChatInput,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
try
{
if (s_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
throw new InvalidOperationException("A slash command was executed, but no command was registered for it.");
if (methods.Any())
{
var method = methods.First().Method;
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
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;
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
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;
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
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)
{
this.Client.Logger.LogError(ex.Message);
this.Client.Logger.LogError(ex.StackTrace);
await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
}
else if (e.Interaction.Type == InteractionType.AutoComplete)
{
if (s_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it.");
try
{
if (methods.Any())
{
var focusedOption = e.Interaction.Data.Options.First(o => o.Focused);
var method = methods.First().Method;
var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Client = client,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = e.Interaction.Data.Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
else if (groups.Any())
{
var command = e.Interaction.Data.Options.First();
var group = groups.First().Methods.First(x => x.Key == command.Name).Value;
var focusedOption = command.Options.First(o => o.Focused);
var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Client = client,
Interaction = e.Interaction,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options.First();
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value;
var focusedOption = command.Options.First().Options.First(o => o.Focused);
var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Client = client,
Interaction = e.Interaction,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.First().Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
}
catch (Exception ex)
{
this.Client.Logger.LogError(ex, "Error in autocomplete interaction");
}
}
});
return Task.CompletedTask;
}
///
/// Context menu handler.
///
/// The client.
/// The event args.
private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e)
{
_ = Task.Run(async () =>
{
//Creates the context
var context = new ContextMenuContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Client = client,
Services = Configuration?.ServiceProvider,
CommandName = e.Interaction.Data.Name,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
InteractionId = e.Interaction.Id,
User = e.Interaction.User,
Token = e.Interaction.Token,
TargetUser = e.TargetUser,
TargetMessage = e.TargetMessage,
Type = e.Type,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions
};
try
{
if (s_errored)
throw new InvalidOperationException("Context menus failed to register properly on startup.");
//Gets the method for the command
var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id);
if (method == null)
throw new InvalidOperationException("A context menu was executed, but no command was registered for it.");
await this.RunCommandAsync(context, method.Method, new[] { context });
await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
});
return Task.CompletedTask;
}
///
/// Runs a command.
///
/// The base context.
/// The method info.
/// The arguments.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")]
internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args)
{
object classInstance;
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Executing {cmd}", method.Name);
//Accounts for lifespans
var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient;
switch (moduleLifespan)
{
case ApplicationCommandModuleLifespan.Scoped:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider);
break;
case ApplicationCommandModuleLifespan.Transient:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider);
break;
//If singleton, gets it from the singleton list
case ApplicationCommandModuleLifespan.Singleton:
classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType));
break;
default:
throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}");
}
ApplicationCommandsModule module = null;
if (classInstance is ApplicationCommandsModule mod)
module = mod;
// Slash commands
if (context is InteractionContext slashContext)
{
await this.RunPreexecutionChecksAsync(method, slashContext);
var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true));
if (shouldExecute)
{
if (AutoDeferEnabled)
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource);
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask);
}
}
// Context menus
if (context is ContextMenuContext contextMenuContext)
{
await this.RunPreexecutionChecksAsync(method, contextMenuContext);
var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true));
if (shouldExecute)
{
if (AutoDeferEnabled)
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource);
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask);
}
}
}
///
/// Property injection
///
/// The type.
/// The services.
internal static object CreateInstance(Type t, IServiceProvider services)
{
var ti = t.GetTypeInfo();
var constructors = ti.DeclaredConstructors
.Where(xci => xci.IsPublic)
.ToArray();
if (constructors.Length != 1)
throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor.");
var constructor = constructors[0];
var constructorArgs = constructor.GetParameters();
var args = new object[constructorArgs.Length];
if (constructorArgs.Length != 0 && services == null)
throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors.");
// inject via constructor
if (constructorArgs.Length != 0)
for (var i = 0; i < args.Length; i++)
args[i] = services.GetRequiredService(constructorArgs[i].ParameterType);
var moduleInstance = Activator.CreateInstance(t, args);
// inject into properties
var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic);
foreach (var prop in props)
{
if (prop.GetCustomAttribute() != null)
continue;
var service = services.GetService(prop.PropertyType);
if (service == null)
continue;
prop.SetValue(moduleInstance, service);
}
// inject into fields
var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic);
foreach (var field in fields)
{
if (field.GetCustomAttribute() != null)
continue;
var service = services.GetService(field.FieldType);
if (service == null)
continue;
field.SetValue(moduleInstance, service);
}
return moduleInstance;
}
///
/// Resolves the slash command parameters.
///
/// The event arguments.
/// The interaction context.
/// The method info.
/// The options.
private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options)
{
var args = new List { context };
var parameters = method.GetParameters().Skip(1);
foreach (var parameter in parameters)
{
//Accounts for optional arguments without values given
if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true)))
args.Add(parameter.DefaultValue);
else
{
var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower());
if (parameter.ParameterType == typeof(string))
args.Add(option.Value.ToString());
else if (parameter.ParameterType.IsEnum)
args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value));
else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?))
args.Add((long?)option.Value);
else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?))
args.Add((bool?)option.Value);
else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?))
args.Add((double?)option.Value);
else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?))
args.Add((int?)option.Value);
else if (parameter.ParameterType == typeof(DiscordAttachment))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Attachments != null &&
e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment))
args.Add(attachment);
else
args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord });
}
else if (parameter.ParameterType == typeof(DiscordUser))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Members != null &&
e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member))
args.Add(member);
else if (e.Interaction.Data.Resolved.Users != null &&
e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user))
args.Add(user);
else
args.Add(await this.Client.GetUserAsync((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(DiscordChannel))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Channels != null &&
e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel))
args.Add(channel);
else
args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(DiscordRole))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Roles != null &&
e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role))
args.Add(role);
else
args.Add(e.Interaction.Guild.GetRole((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(SnowflakeObject))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role))
args.Add(role);
else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member))
args.Add(member);
else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user))
args.Add(user);
else
throw new ArgumentException("Error resolving mentionable option.");
}
else
throw new ArgumentException($"Error resolving interaction.");
}
}
return args;
}
///
/// Runs the pre-execution checks.
///
/// The method info.
/// The base context.
private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context)
{
if (context is InteractionContext ctx)
{
//Gets all attributes from parent classes as well and stuff
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes());
}
}
var dict = new Dictionary();
foreach (var att in attributes)
{
//Runs the check and adds the result to a list
var result = await att.ExecuteChecksAsync(ctx);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value == false))
throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() };
}
if (context is ContextMenuContext cMctx)
{
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes());
}
}
var dict = new Dictionary();
foreach (var att in attributes)
{
//Runs the check and adds the result to a list
var result = await att.ExecuteChecksAsync(cMctx);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value == false))
throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() };
}
}
///
/// Gets the choice attributes from choice provider.
///
/// The custom attributes.
/// The optional guild id
private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null)
{
var choices = new List();
foreach (var choiceProviderAttribute in customAttributes)
{
var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider));
if (method == null)
throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider.");
else
{
var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType);
// Abstract class offers more properties that can be set
if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider)))
{
choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId))
?.SetValue(instance, guildId);
choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services))
?.SetValue(instance, Configuration.ServiceProvider);
}
//Gets the choices from the method
var result = await (Task>)method.Invoke(instance, null);
if (result.Any())
{
choices.AddRange(result);
}
}
}
return choices;
}
///
/// Gets the choice attributes from enum parameter.
///
/// The enum parameter.
private static List GetChoiceAttributesFromEnumParameter(Type enumParam)
{
var choices = new List();
foreach (Enum enumValue in Enum.GetValues(enumParam))
{
choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString()));
}
return choices;
}
///
/// Gets the parameter type.
///
/// The type.
private static ApplicationCommandOptionType GetParameterType(Type type)
{
var parameterType = type == typeof(string)
? ApplicationCommandOptionType.String
: type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?)
? ApplicationCommandOptionType.Integer
: type == typeof(bool) || type == typeof(bool?)
? ApplicationCommandOptionType.Boolean
: type == typeof(double) || type == typeof(double?)
? ApplicationCommandOptionType.Number
: type == typeof(DiscordAttachment)
? ApplicationCommandOptionType.Attachment
: type == typeof(DiscordChannel)
? ApplicationCommandOptionType.Channel
: type == typeof(DiscordUser)
? ApplicationCommandOptionType.User
: type == typeof(DiscordRole)
? ApplicationCommandOptionType.Role
: type == typeof(SnowflakeObject)
? ApplicationCommandOptionType.Mentionable
: type.IsEnum
? ApplicationCommandOptionType.String
: throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum.");
return parameterType;
}
///
/// Gets the choice attributes from parameter.
///
/// The choice attributes.
private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) =>
!choiceAttributes.Any()
? null
: choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList();
///
/// Parses the parameters.
///
/// The parameters.
/// The optional guild id.
internal static async Task> ParseParametersAsync(IEnumerable parameters, ulong? guildId)
{
var options = new List();
foreach (var parameter in parameters)
{
//Gets the attribute
var optionAttribute = parameter.GetCustomAttribute();
if (optionAttribute == null)
throw new ArgumentException("Arguments must have the Option attribute!");
var minimumValue = parameter.GetCustomAttribute()?.Value ?? null;
var maximumValue = parameter.GetCustomAttribute()?.Value ?? null;
var minimumLength = parameter.GetCustomAttribute()?.Value ?? null;
var maximumLength = parameter.GetCustomAttribute()?.Value ?? null;
var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null;
var autocompleteAttribute = parameter.GetCustomAttribute();
if (optionAttribute.Autocomplete && autocompleteAttribute == null)
throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!");
if (!optionAttribute.Autocomplete && autocompleteAttribute != null)
throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!");
//Sets the type
var type = parameter.ParameterType;
var parameterType = GetParameterType(type);
if (parameterType == ApplicationCommandOptionType.String)
{
minimumValue = null;
maximumValue = null;
}
else if (parameterType == ApplicationCommandOptionType.Integer || parameterType == ApplicationCommandOptionType.Number)
{
minimumLength = null;
maximumLength = null;
}
if (parameterType != ApplicationCommandOptionType.Channel)
channelTypes = null;
//Handles choices
//From attributes
var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes());
//From enums
if (parameter.ParameterType.IsEnum)
{
choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType);
}
//From choice provider
var choiceProviders = parameter.GetCustomAttributes();
if (choiceProviders.Any())
{
choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId);
}
options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength));
}
return options;
}
-
+ /*
///
- /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client.
- /// Should only be run on the slash command extension linked to shard 0 if sharding.
+ /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client.
/// Not recommended and should be avoided since it can make slash commands be unresponsive for a while.
///
public async Task RefreshCommandsAsync()
{
s_commandMethods.Clear();
s_groupCommands.Clear();
s_subGroupCommands.Clear();
s_registeredCommands.Clear();
s_contextMenuCommands.Clear();
GlobalDiscordCommands.Clear();
GuildDiscordCommands.Clear();
GuildCommandsInternal.Clear();
GlobalCommandsInternal.Clear();
GlobalDiscordCommands = null;
GuildDiscordCommands = null;
s_errored = false;
/*if (Configuration != null && Configuration.EnableDefaultHelp)
{
this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule));
}*/
-
+ /*
await this.UpdateAsync();
- }
+ }*/
///
/// Fires when the execution of a slash command fails.
///
public event AsyncEventHandler SlashCommandErrored
{
add => this._slashError.Register(value);
remove => this._slashError.Unregister(value);
}
private AsyncEvent _slashError;
///
/// Fires when the execution of a slash command is successful.
///
public event AsyncEventHandler SlashCommandExecuted
{
add => this._slashExecuted.Register(value);
remove => this._slashExecuted.Unregister(value);
}
private AsyncEvent _slashExecuted;
///
/// Fires when the execution of a context menu fails.
///
public event AsyncEventHandler ContextMenuErrored
{
add => this._contextMenuErrored.Register(value);
remove => this._contextMenuErrored.Unregister(value);
}
private AsyncEvent _contextMenuErrored;
///
/// Fire when the execution of a context menu is successful.
///
public event AsyncEventHandler ContextMenuExecuted
{
add => this._contextMenuExecuted.Register(value);
remove => this._contextMenuExecuted.Unregister(value);
}
private AsyncEvent _contextMenuExecuted;
+
+ public override void Dispose()
+ {
+ this.Client.InteractionCreated -= this.InteractionHandler;
+ this.Client.ContextMenuInteractionCreated -= this.ContextMenuHandler;
+ GC.SuppressFinalize(this);
+ }
}
///
/// Holds configuration data for setting up an application command.
///
internal class ApplicationCommandsModuleConfiguration
{
///
/// The type of the command module.
///
public Type Type { get; }
///
/// The translation setup.
///
public Action Translations { get; }
///
/// Creates a new command configuration.
///
/// The type of the command module.
/// The translation setup callback.
public ApplicationCommandsModuleConfiguration(Type type, Action translations = null)
{
this.Type = type;
this.Translations = translations;
}
}
///
/// Links a command to its original command module.
///
internal class ApplicationCommandSourceLink
{
///
/// The command.
///
public DiscordApplicationCommand ApplicationCommand { get; set; }
///
/// The base/root module the command is contained in.
///
public Type RootCommandContainerType { get; set; }
///
/// The direct group the command is contained in.
///
public Type CommandContainerType { get; set; }
}
///
/// The command method.
///
internal class CommandMethod
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
///
/// The group command.
///
internal class GroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the methods.
///
public List> Methods { get; set; } = null;
}
///
/// The sub group command.
///
internal class SubGroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the sub commands.
///
public List SubCommands { get; set; } = new();
}
///
/// The context menu command.
///
internal class ContextMenuCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
#region Default Help
///
/// Represents the default help module.
///
internal class DefaultHelpModule : ApplicationCommandsModule
{
public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
foreach (var sc in slashCommands.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim()));
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase));
if (command is null || command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand
&& c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
return options.AsEnumerable();
}
var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (foundCommand is null || foundCommand.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand &&
x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
[SlashCommand("help", "Displays command help")]
internal async Task DefaultHelpAsync(InteractionContext ctx,
[Autocomplete(typeof(DefaultHelpAutoCompleteProvider))]
[Option("option_one", "top level command to provide help for", true)] string commandName,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))]
[Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))]
[Option("option_three", "command to provide help for", true)] string commandTwoName = null)
{
List applicationCommands = null;
var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync();
if (ctx.Guild != null)
{
var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.ToList();
}
else
{
await Task.WhenAll(globalCommandsTask);
applicationCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.ToList();
}
if (applicationCommands.Count < 1)
{
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"There are no slash commands").AsEphemeral(true));
return;
}
if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command"))
{
var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup));
var subCommandParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase));
var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options
.FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase));
var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase));
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {cmdParent.Name} {cmd.Name}")}: {cmd.Description ?? "No description provided."}"
};
if (cmd.Options is not null)
{
var commandOptions = cmd.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim()));
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command"))
{
var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand));
var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase));
var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase));
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}"
};
if (subCommand.Options is not null)
{
var commandOptions = subCommand.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim()));
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
else
{
var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
if (command is null)
{
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true));
return;
}
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{command.Mention}: {command.Description ?? "No description provided."}"
}.AddField(new DiscordEmbedField("Command is NSFW", command.IsNsfw.ToString()));
if (command.Options is not null)
{
var commandOptions = command.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim()));
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
}
}
#endregion
diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
index e9447807a..b7c101900 100644
--- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs
+++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs
@@ -1,1086 +1,1094 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.CommandsNext.Attributes;
using DisCatSharp.CommandsNext.Builders;
using DisCatSharp.CommandsNext.Converters;
using DisCatSharp.CommandsNext.Entities;
using DisCatSharp.CommandsNext.Exceptions;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.EventArgs;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace DisCatSharp.CommandsNext;
///
/// This is the class which handles command registration, management, and execution.
///
public class CommandsNextExtension : BaseExtension
{
///
/// Gets the config.
///
private readonly CommandsNextConfiguration _config;
///
/// Gets the help formatter.
///
private readonly HelpFormatterFactory _helpFormatter;
///
/// Gets the convert generic.
///
private readonly MethodInfo _convertGeneric;
///
/// Gets the user friendly type names.
///
private readonly Dictionary _userFriendlyTypeNames;
///
/// Gets the argument converters.
///
internal Dictionary ArgumentConverters { get; }
///
/// Gets the service provider this CommandsNext module was configured with.
///
public IServiceProvider Services
=> this._config.ServiceProvider;
///
/// Initializes a new instance of the class.
///
/// The cfg.
internal CommandsNextExtension(CommandsNextConfiguration cfg)
{
this._config = new CommandsNextConfiguration(cfg);
this._topLevelCommands = new Dictionary();
this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this._topLevelCommands));
this._helpFormatter = new HelpFormatterFactory();
this._helpFormatter.SetFormatterType();
this.ArgumentConverters = new Dictionary
{
[typeof(string)] = new StringConverter(),
[typeof(bool)] = new BoolConverter(),
[typeof(sbyte)] = new Int8Converter(),
[typeof(byte)] = new Uint8Converter(),
[typeof(short)] = new Int16Converter(),
[typeof(ushort)] = new Uint16Converter(),
[typeof(int)] = new Int32Converter(),
[typeof(uint)] = new Uint32Converter(),
[typeof(long)] = new Int64Converter(),
[typeof(ulong)] = new Uint64Converter(),
[typeof(float)] = new Float32Converter(),
[typeof(double)] = new Float64Converter(),
[typeof(decimal)] = new Float128Converter(),
[typeof(DateTime)] = new DateTimeConverter(),
[typeof(DateTimeOffset)] = new DateTimeOffsetConverter(),
[typeof(TimeSpan)] = new TimeSpanConverter(),
[typeof(Uri)] = new UriConverter(),
[typeof(DiscordUser)] = new DiscordUserConverter(),
[typeof(DiscordMember)] = new DiscordMemberConverter(),
[typeof(DiscordRole)] = new DiscordRoleConverter(),
[typeof(DiscordChannel)] = new DiscordChannelConverter(),
[typeof(DiscordGuild)] = new DiscordGuildConverter(),
[typeof(DiscordMessage)] = new DiscordMessageConverter(),
[typeof(DiscordEmoji)] = new DiscordEmojiConverter(),
[typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(),
[typeof(DiscordInvite)] = new DiscordInviteConverter(),
[typeof(DiscordColor)] = new DiscordColorConverter(),
[typeof(DiscordScheduledEvent)] = new DiscordScheduledEventConverter(),
};
this._userFriendlyTypeNames = new Dictionary()
{
[typeof(string)] = "string",
[typeof(bool)] = "boolean",
[typeof(sbyte)] = "signed byte",
[typeof(byte)] = "byte",
[typeof(short)] = "short",
[typeof(ushort)] = "unsigned short",
[typeof(int)] = "int",
[typeof(uint)] = "unsigned int",
[typeof(long)] = "long",
[typeof(ulong)] = "unsigned long",
[typeof(float)] = "float",
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(DateTime)] = "date and time",
[typeof(DateTimeOffset)] = "date and time",
[typeof(TimeSpan)] = "time span",
[typeof(Uri)] = "URL",
[typeof(DiscordUser)] = "user",
[typeof(DiscordMember)] = "member",
[typeof(DiscordRole)] = "role",
[typeof(DiscordChannel)] = "channel",
[typeof(DiscordGuild)] = "guild",
[typeof(DiscordMessage)] = "message",
[typeof(DiscordEmoji)] = "emoji",
[typeof(DiscordThreadChannel)] = "thread",
[typeof(DiscordInvite)] = "invite",
[typeof(DiscordColor)] = "color",
[typeof(DiscordScheduledEvent)] = "event"
};
foreach (var xt in this.ArgumentConverters.Keys.ToArray())
{
var xti = xt.GetTypeInfo();
if (!xti.IsValueType)
continue;
var xcvt = typeof(NullableConverter<>).MakeGenericType(xt);
var xnt = typeof(Nullable<>).MakeGenericType(xt);
if (this.ArgumentConverters.ContainsKey(xcvt))
continue;
var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter;
this.ArgumentConverters[xnt] = xcv;
this._userFriendlyTypeNames[xnt] = this._userFriendlyTypeNames[xt];
}
var t = this.GetType();
var ms = t.GetTypeInfo().DeclaredMethods;
var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgumentToObj" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPrivate);
this._convertGeneric = m;
}
///
/// Sets the help formatter to use with the default help command.
///
/// Type of the formatter to use.
public void SetHelpFormatter() where T : BaseHelpFormatter => this._helpFormatter.SetFormatterType();
#region DiscordClient Registration
///
/// DO NOT USE THIS MANUALLY.
///
/// DO NOT USE THIS MANUALLY.
///
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler);
this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler);
if (this._config.UseDefaultCommandHandler)
this.Client.MessageCreated += this.HandleCommandsAsync;
else
this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message");
if (this._config.EnableDefaultHelp)
{
this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds);
if (this._config.DefaultHelpChecks != null)
{
var checks = this._config.DefaultHelpChecks.ToArray();
foreach (var cb in tcmds)
cb.WithExecutionChecks(checks);
}
if (tcmds != null)
foreach (var xc in tcmds)
this.AddToCommandDictionary(xc.Build(null));
}
}
#endregion
#region Command Handling
///
/// Handles the commands async.
///
/// The sender.
/// The e.
/// A Task.
private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e)
{
if (e.Author.IsBot) // bad bot
return;
if (!this._config.EnableDms && e.Channel.IsPrivate)
return;
var mpos = -1;
if (this._config.EnableMentionPrefix)
mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser);
if (this._config.StringPrefixes?.Any() == true)
foreach (var pfix in this._config.StringPrefixes)
if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix))
mpos = e.Message.GetStringPrefixLength(pfix, this._config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase);
if (mpos == -1 && this._config.PrefixResolver != null)
mpos = await this._config.PrefixResolver(e.Message).ConfigureAwait(false);
if (mpos == -1)
return;
var pfx = e.Message.Content[..mpos];
var cnt = e.Message.Content[mpos..];
var __ = 0;
var fname = cnt.ExtractNextArgument(ref __);
var cmd = this.FindCommand(cnt, out var args);
var ctx = this.CreateContext(e.Message, pfx, cmd, args);
if (cmd == null)
{
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false);
return;
}
_ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false));
}
///
/// Finds a specified command by its qualified name, then separates arguments.
///
/// Qualified name of the command, optionally with arguments.
/// Separated arguments.
/// Found command or null if none was found.
public Command FindCommand(string commandString, out string rawArguments)
{
rawArguments = null;
var ignoreCase = !this._config.CaseSensitive;
var pos = 0;
var next = commandString.ExtractNextArgument(ref pos);
if (next == null)
return null;
if (!this.RegisteredCommands.TryGetValue(next, out var cmd))
{
if (!ignoreCase)
return null;
next = next.ToLowerInvariant();
var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next);
if (cmdKvp.Value == null)
return null;
cmd = cmdKvp.Value;
}
if (cmd is not CommandGroup)
{
rawArguments = commandString[pos..].Trim();
return cmd;
}
while (cmd is CommandGroup)
{
var cm2 = cmd as CommandGroup;
var oldPos = pos;
next = commandString.ExtractNextArgument(ref pos);
if (next == null)
break;
if (ignoreCase)
{
next = next.ToLowerInvariant();
cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true);
}
else
{
cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true);
}
if (cmd == null)
{
cmd = cm2;
pos = oldPos;
break;
}
}
rawArguments = commandString[pos..].Trim();
return cmd;
}
///
/// Creates a command execution context from specified arguments.
///
/// Message to use for context.
/// Command prefix, used to execute commands.
/// Command to execute.
/// Raw arguments to pass to command.
/// Created command execution context.
public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null)
{
var ctx = new CommandContext
{
Client = this.Client,
Command = cmd,
Message = msg,
Config = this._config,
RawArgumentString = rawArguments ?? "",
Prefix = prefix,
CommandsNext = this,
Services = this.Services
};
if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null))
{
var scope = ctx.Services.CreateScope();
ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope);
ctx.Services = scope.ServiceProvider;
}
return ctx;
}
///
/// Executes specified command from given context.
///
/// Context to execute command from.
///
public async Task ExecuteCommandAsync(CommandContext ctx)
{
try
{
var cmd = ctx.Command;
await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false);
var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false);
if (res.IsSuccessful)
await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false);
else
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false);
}
catch (Exception ex)
{
await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false);
}
finally
{
if (ctx.ServiceScopeContext.IsInitialized)
ctx.ServiceScopeContext.Dispose();
}
}
///
/// Runs the all checks async.
///
/// The cmd.
/// The ctx.
/// A Task.
private async Task RunAllChecksAsync(Command cmd, CommandContext ctx)
{
if (cmd.Parent != null)
await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false);
var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false);
if (fchecks.Any())
throw new ChecksFailedException(cmd, ctx, fchecks);
}
#endregion
#region Command Registration
///
/// Gets a dictionary of registered top-level commands.
///
public IReadOnlyDictionary RegisteredCommands
=> this._registeredCommandsLazy.Value;
///
/// Gets or sets the top level commands.
///
private readonly Dictionary _topLevelCommands;
private readonly Lazy> _registeredCommandsLazy;
///
/// Registers all commands from a given assembly. The command classes need to be public to be considered for registration.
///
/// Assembly to register commands from.
public void RegisterCommands(Assembly assembly)
{
var types = assembly.ExportedTypes.Where(xt =>
{
var xti = xt.GetTypeInfo();
return xti.IsModuleCandidateType() && !xti.IsNested;
});
foreach (var xt in types)
this.RegisterCommands(xt);
}
///
/// Registers all commands from a given command class.
///
/// Class which holds commands to register.
public void RegisterCommands() where T : BaseCommandModule
{
var t = typeof(T);
this.RegisterCommands(t);
}
///
/// Registers all commands from a given command class.
///
/// Type of the class which holds commands to register.
public void RegisterCommands(Type t)
{
if (t == null)
throw new ArgumentNullException(nameof(t), "Type cannot be null.");
if (!t.IsModuleCandidateType())
throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static.");
this.RegisterCommands(t, null, null, out var tempCommands);
if (tempCommands != null)
foreach (var command in tempCommands)
this.AddToCommandDictionary(command.Build(null));
}
///
/// Registers the commands.
///
/// The type.
/// The current parent.
/// The inherited checks.
/// The found commands.
private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands)
{
var ti = t.GetTypeInfo();
var lifespan = ti.GetCustomAttribute();
var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton;
var module = new CommandModuleBuilder()
.WithType(t)
.WithLifespan(moduleLifespan)
.Build(this.Services);
// restrict parent lifespan to more or equally restrictive
if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient)
throw new InvalidOperationException("In a transient module, child modules can only be transient.");
// check if we are anything
var groupBuilder = new CommandGroupBuilder(module);
var isModule = false;
var moduleAttributes = ti.GetCustomAttributes();
var moduleHidden = false;
var moduleChecks = new List();
foreach (var xa in moduleAttributes)
{
switch (xa)
{
case GroupAttribute g:
isModule = true;
var moduleName = g.Name;
if (moduleName == null)
{
moduleName = ti.Name;
if (moduleName.EndsWith("Group") && moduleName != "Group")
moduleName = moduleName[0..^5];
else if (moduleName.EndsWith("Module") && moduleName != "Module")
moduleName = moduleName[0..^6];
else if (moduleName.EndsWith("Commands") && moduleName != "Commands")
moduleName = moduleName[0..^8];
}
if (!this._config.CaseSensitive)
moduleName = moduleName.ToLowerInvariant();
groupBuilder.WithName(moduleName);
if (inheritedChecks != null)
foreach (var chk in inheritedChecks)
groupBuilder.WithExecutionCheck(chk);
foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null))
groupBuilder.WithOverload(new CommandOverloadBuilder(mi));
break;
case AliasesAttribute a:
foreach (var xalias in a.Aliases)
groupBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant());
break;
case HiddenAttribute h:
groupBuilder.WithHiddenStatus(true);
moduleHidden = true;
break;
case DescriptionAttribute d:
groupBuilder.WithDescription(d.Description);
break;
case CheckBaseAttribute c:
moduleChecks.Add(c);
groupBuilder.WithExecutionCheck(c);
break;
default:
groupBuilder.WithCustomAttribute(xa);
break;
}
}
if (!isModule)
{
groupBuilder = null;
if (inheritedChecks != null)
moduleChecks.AddRange(inheritedChecks);
}
// candidate methods
var methods = ti.DeclaredMethods;
var commands = new List();
var commandBuilders = new Dictionary();
foreach (var m in methods)
{
if (!m.IsCommandCandidate(out _))
continue;
var attrs = m.GetCustomAttributes();
if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr)
continue;
var commandName = cattr.Name;
if (commandName == null)
{
commandName = m.Name;
if (commandName.EndsWith("Async") && commandName != "Async")
commandName = commandName[0..^5];
}
if (!this._config.CaseSensitive)
commandName = commandName.ToLowerInvariant();
if (!commandBuilders.TryGetValue(commandName, out var commandBuilder))
{
commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName));
if (!isModule)
if (currentParent != null)
currentParent.WithChild(commandBuilder);
else
commands.Add(commandBuilder);
else
groupBuilder.WithChild(commandBuilder);
}
commandBuilder.WithOverload(new CommandOverloadBuilder(m));
if (!isModule && moduleChecks.Any())
foreach (var chk in moduleChecks)
commandBuilder.WithExecutionCheck(chk);
foreach (var xa in attrs)
{
switch (xa)
{
case AliasesAttribute a:
foreach (var xalias in a.Aliases)
commandBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant());
break;
case CheckBaseAttribute p:
commandBuilder.WithExecutionCheck(p);
break;
case DescriptionAttribute d:
commandBuilder.WithDescription(d.Description);
break;
case HiddenAttribute h:
commandBuilder.WithHiddenStatus(true);
break;
default:
commandBuilder.WithCustomAttribute(xa);
break;
}
}
if (!isModule && moduleHidden)
commandBuilder.WithHiddenStatus(true);
}
// candidate types
var types = ti.DeclaredNestedTypes
.Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic));
foreach (var type in types)
{
this.RegisterCommands(type.AsType(),
groupBuilder,
!isModule ? moduleChecks : null,
out var tempCommands);
if (isModule)
foreach (var chk in moduleChecks)
groupBuilder.WithExecutionCheck(chk);
if (isModule && tempCommands != null)
foreach (var xtcmd in tempCommands)
groupBuilder.WithChild(xtcmd);
else if (tempCommands != null)
commands.AddRange(tempCommands);
}
if (isModule && currentParent == null)
commands.Add(groupBuilder);
else if (isModule)
currentParent.WithChild(groupBuilder);
foundCommands = commands;
}
///
/// Builds and registers all supplied commands.
///
/// Commands to build and register.
public void RegisterCommands(params CommandBuilder[] cmds)
{
foreach (var cmd in cmds)
this.AddToCommandDictionary(cmd.Build(null));
}
///
/// Unregister specified commands from CommandsNext.
///
/// Commands to unregister.
public void UnregisterCommands(params Command[] cmds)
{
if (cmds.Any(x => x.Parent != null))
throw new InvalidOperationException("Cannot unregister nested commands.");
var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList();
foreach (var key in keys)
this._topLevelCommands.Remove(key);
}
///
/// Adds the to command dictionary.
///
/// The cmd.
private void AddToCommandDictionary(Command cmd)
{
if (cmd.Parent != null)
return;
if (this._topLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this._topLevelCommands.ContainsKey(xs))))
throw new DuplicateCommandException(cmd.QualifiedName);
this._topLevelCommands[cmd.Name] = cmd;
if (cmd.Aliases != null)
foreach (var xs in cmd.Aliases)
this._topLevelCommands[xs] = cmd;
}
#endregion
#region Default Help
///
/// Represents the default help module.
///
[ModuleLifespan(ModuleLifespan.Transient)]
public class DefaultHelpModule : BaseCommandModule
{
///
/// Defaults the help async.
///
/// The ctx.
/// The command.
/// A Task.
[Command("help"), Description("Displays command help.")]
public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command)
{
var topLevel = ctx.CommandsNext._topLevelCommands.Values.Distinct();
var helpBuilder = ctx.CommandsNext._helpFormatter.Create(ctx);
if (command != null && command.Any())
{
Command cmd = null;
var searchIn = topLevel;
foreach (var c in command)
{
if (searchIn == null)
{
cmd = null;
break;
}
cmd = ctx.Config.CaseSensitive
? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c)))
: searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant())));
if (cmd == null)
break;
var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (failedChecks.Any())
throw new ChecksFailedException(cmd, ctx, failedChecks);
searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null;
}
if (cmd == null)
throw new CommandNotFoundException(string.Join(" ", command));
helpBuilder.WithCommand(cmd);
if (cmd is CommandGroup group)
{
var commandsToSearch = group.Children.Where(xc => !xc.IsHidden);
var eligibleCommands = new List();
foreach (var candidateCommand in commandsToSearch)
{
if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any())
{
eligibleCommands.Add(candidateCommand);
continue;
}
var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (!candidateFailedChecks.Any())
eligibleCommands.Add(candidateCommand);
}
if (eligibleCommands.Any())
helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
}
else
{
var commandsToSearch = topLevel.Where(xc => !xc.IsHidden);
var eligibleCommands = new List();
foreach (var sc in commandsToSearch)
{
if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any())
{
eligibleCommands.Add(sc);
continue;
}
var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false);
if (!candidateFailedChecks.Any())
eligibleCommands.Add(sc);
}
if (eligibleCommands.Any())
helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name));
}
var helpMessage = helpBuilder.Build();
var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed);
if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null)
await ctx.RespondAsync(builder).ConfigureAwait(false);
else
await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false);
}
}
#endregion
#region Sudo
///
/// Creates a fake command context to execute commands with.
///
/// The user or member to use as message author.
/// The channel the message is supposed to appear from.
/// Contents of the message.
/// Command prefix, used to execute commands.
/// Command to execute.
/// Raw arguments to pass to command.
/// Created fake context.
public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null)
{
var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero);
var now = DateTimeOffset.UtcNow;
var timeSpan = (ulong)(now - epoch).TotalMilliseconds;
// create fake message
var msg = new DiscordMessage
{
Discord = this.Client,
Author = actor,
ChannelId = channel.Id,
Content = messageContents,
Id = timeSpan << 22,
Pinned = false,
MentionEveryone = messageContents.Contains("@everyone"),
IsTts = false,
AttachmentsInternal = new List(),
EmbedsInternal = new List(),
TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"),
ReactionsInternal = new List()
};
var mentionedUsers = new List();
var mentionedRoles = msg.Channel.Guild != null ? new List() : null;
var mentionedChannels = msg.Channel.Guild != null ? new List() : null;
if (!string.IsNullOrWhiteSpace(msg.Content))
{
if (msg.Channel.Guild != null)
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild.MembersInternal.TryGetValue(xid, out var member) ? member : null).Cast().ToList();
mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList();
mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList();
}
else
{
mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList();
}
}
msg.MentionedUsersInternal = mentionedUsers;
msg.MentionedRolesInternal = mentionedRoles;
msg.MentionedChannelsInternal = mentionedChannels;
var ctx = new CommandContext
{
Client = this.Client,
Command = cmd,
Message = msg,
Config = this._config,
RawArgumentString = rawArguments ?? "",
Prefix = prefix,
CommandsNext = this,
Services = this.Services
};
if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null))
{
var scope = ctx.Services.CreateScope();
ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope);
ctx.Services = scope.ServiceProvider;
}
return ctx;
}
#endregion
#region Type Conversion
///
/// Converts a string to specified type.
///
/// Type to convert to.
/// Value to convert.
/// Context in which to convert to.
/// Converted object.
public async Task ConvertArgument(string value, CommandContext ctx)
{
var t = typeof(T);
if (!this.ArgumentConverters.ContainsKey(t))
throw new ArgumentException("There is no converter specified for given type.", nameof(T));
if (this.ArgumentConverters[t] is not IArgumentConverter cv)
throw new ArgumentException("Invalid converter registered for this type.", nameof(T));
var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false);
return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value;
}
///
/// Converts a string to specified type.
///
/// Value to convert.
/// Context in which to convert to.
/// Type to convert to.
/// Converted object.
public async Task ConvertArgument(string value, CommandContext ctx, Type type)
{
var m = this._convertGeneric.MakeGenericMethod(type);
try
{
return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false);
}
catch (TargetInvocationException ex)
{
throw ex.InnerException;
}
}
///
/// Registers an argument converter for specified type.
///
/// Type for which to register the converter.
/// Converter to register.
public void RegisterConverter(IArgumentConverter converter)
{
if (converter == null)
throw new ArgumentNullException(nameof(converter), "Converter cannot be null.");
var t = typeof(T);
var ti = t.GetTypeInfo();
this.ArgumentConverters[t] = converter;
if (!ti.IsValueType)
return;
var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t);
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (this.ArgumentConverters.ContainsKey(nullableType))
return;
var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter;
this.ArgumentConverters[nullableType] = nullableConverter;
}
///
/// Unregister an argument converter for specified type.
///
/// Type for which to unregister the converter.
public void UnregisterConverter()
{
var t = typeof(T);
var ti = t.GetTypeInfo();
if (this.ArgumentConverters.ContainsKey(t))
this.ArgumentConverters.Remove(t);
if (this._userFriendlyTypeNames.ContainsKey(t))
this._userFriendlyTypeNames.Remove(t);
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
if (!this.ArgumentConverters.ContainsKey(nullableType))
return;
this.ArgumentConverters.Remove(nullableType);
this._userFriendlyTypeNames.Remove(nullableType);
}
///
/// Registers a user-friendly type name.
///
/// Type to register the name for.
/// Name to register.
public void RegisterUserFriendlyTypeName(string value)
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentNullException(nameof(value), "Name cannot be null or empty.");
var t = typeof(T);
var ti = t.GetTypeInfo();
if (!this.ArgumentConverters.ContainsKey(t))
throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter.");
this._userFriendlyTypeNames[t] = value;
if (!ti.IsValueType)
return;
var nullableType = typeof(Nullable<>).MakeGenericType(t);
this._userFriendlyTypeNames[nullableType] = value;
}
///
/// Converts a type into user-friendly type name.
///
/// Type to convert.
/// User-friendly type name.
public string GetUserFriendlyTypeName(Type t)
{
if (this._userFriendlyTypeNames.ContainsKey(t))
return this._userFriendlyTypeNames[t];
var ti = t.GetTypeInfo();
if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>))
{
var tn = ti.GenericTypeArguments[0];
return this._userFriendlyTypeNames.ContainsKey(tn) ? this._userFriendlyTypeNames[tn] : tn.Name;
}
return t.Name;
}
#endregion
#region Helpers
///
/// Allows easier interoperability with reflection by turning the returned by
/// into a task containing , using the provided generic type information.
///
private async Task ConvertArgumentToObj(string value, CommandContext ctx)
=> await this.ConvertArgument(value, ctx).ConfigureAwait(false);
///
/// Gets the configuration-specific string comparer. This returns or ,
/// depending on whether is set to or .
///
/// A string comparer.
internal IEqualityComparer GetStringComparer()
=> this._config.CaseSensitive
? StringComparer.Ordinal
: StringComparer.OrdinalIgnoreCase;
#endregion
#region Events
///
/// Triggered whenever a command executes successfully.
///
public event AsyncEventHandler CommandExecuted
{
add => this._executed.Register(value);
remove => this._executed.Unregister(value);
}
private AsyncEvent _executed;
///
/// Triggered whenever a command throws an exception during execution.
///
public event AsyncEventHandler CommandErrored
{
add => this._error.Register(value);
remove => this._error.Unregister(value);
}
private AsyncEvent _error;
///
/// Fires when a command gets executed.
///
/// The command execution event arguments.
private Task OnCommandExecuted(CommandExecutionEventArgs e)
=> this._executed.InvokeAsync(this, e);
///
/// Fires when a command fails.
///
/// The command error event arguments.
private Task OnCommandErrored(CommandErrorEventArgs e)
=> this._error.InvokeAsync(this, e);
#endregion
+
+
+
+ public override void Dispose()
+ {
+ this.Client.MessageCreated -= this.HandleCommandsAsync;
+ GC.SuppressFinalize(this);
+ }
}
diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs
index 42cfda735..b54a663c2 100644
--- a/DisCatSharp.Interactivity/InteractivityExtension.cs
+++ b/DisCatSharp.Interactivity/InteractivityExtension.cs
@@ -1,971 +1,973 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Interactivity.Enums;
using DisCatSharp.Interactivity.EventHandling;
namespace DisCatSharp.Interactivity;
///
/// Extension class for DisCatSharp.Interactivity
///
public class InteractivityExtension : BaseExtension
{
///
/// 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;
///
/// 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 ComponentPaginator(this.Client, this.Config);
this._componentEventWaiter = new ComponentEventWaiter(this.Client, this.Config);
this._modalEventWaiter = new ModalEventWaiter(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 ComponentMatchRequest(message,
c =>
c.Interaction.Data.ComponentType == ComponentType.Button &&
buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false);
return new InteractivityResult(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 customId, TimeSpan? timeoutOverride = null)
=> this.WaitForModalAsync(customId, 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 customId, CancellationToken token)
{
var result =
await this
._modalEventWaiter
.WaitForModalMatchAsync(new ModalMatchRequest(customId, c => c.Interaction.Type == InteractionType.ModalSubmit, token))
.ConfigureAwait(false);
return new InteractivityResult(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 ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token))
.ConfigureAwait(false);
return new InteractivityResult(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 ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token))
.ConfigureAwait(false);
return new InteractivityResult(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 ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token))
.ConfigureAwait(false);
return new InteractivityResult(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 ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token))
.ConfigureAwait(false);
return new InteractivityResult(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));
///
/// Waits for any dropdown to be interacted with.
///
/// The message to wait for.
/// A filter predicate.
/// A token that can be used to cancel interactivity. Pass to wait indefinitely.
/// Thrown when the Provided message does not contain any dropdowns
public async Task> WaitForSelectAsync(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.Select))
throw new ArgumentException("Message does not contain any select components.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a dropdown to be interacted with.
///
/// This is here for backwards-compatibility and will internally create a cancellation token.
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with.
///
/// The message to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public async Task> WaitForSelectAsync(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.Select))
throw new ArgumentException("Message does not contain any select components.");
if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select component with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token))
.ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// Override the timeout period specified in .
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null)
=> this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride));
///
/// Waits for a dropdown to be interacted with by a specific user.
///
/// The message to wait on.
/// The user to wait on.
/// The Id of the dropdown to wait on.
/// A custom cancellation token that can be cancelled at any point.
/// Thrown when the message does not have any dropdowns or any dropdown with the specified Id.
public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, 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.Select))
throw new ArgumentException("Message does not contain any select components.");
if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id))
throw new ArgumentException($"Message does not contain select with Id of '{id}'.");
var result = await this
._componentEventWaiter
.WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false);
return new InteractivityResult(result is null, result);
}
///
/// Waits for a specific message.
///
/// Predicate to match.
/// Override timeout period.
public async Task> WaitForMessageAsync(Func predicate,
TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No message intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._messageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns?.Message);
}
///
/// Wait for a specific reaction.
///
/// Predicate to match.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No reaction intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._messageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false);
return new InteractivityResult(returns == null, returns);
}
///
/// Wait for a specific reaction.
/// For this Event you need the intent specified in
///
/// Message reaction was added to.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user,
TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// Predicate to match.
/// Message reaction was added to.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a specific reaction.
/// For this Event you need the intent specified in
///
/// predicate to match.
/// User that made the reaction.
/// Override timeout period.
public async Task> WaitForReactionAsync(Func predicate,
DiscordUser user, TimeSpan? timeoutOverride = null)
=> await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutOverride).ConfigureAwait(false);
///
/// Waits for a user to start typing.
///
/// User that starts typing.
/// Channel the user is typing in.
/// Override timeout period.
public async Task> WaitForUserTypingAsync(DiscordUser user,
DiscordChannel channel, TimeSpan? timeoutOverride = null)
{
if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents))
throw new InvalidOperationException("No typing intents are enabled.");
var timeout = timeoutOverride ?? this.Config.Timeout;
var returns = await this._typingStartWaiter.WaitForMatchAsync(
new MatchRequest