diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index d32a33448..71eb61fb2 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,2095 +1,2094 @@
// 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.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
-using DisCatSharp.ApplicationCommands.Enums;
using DisCatSharp.ApplicationCommands.EventArgs;
using DisCatSharp.ApplicationCommands.Exceptions;
using DisCatSharp.ApplicationCommands.Workers;
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; } = false;
///
/// Gets the service provider this module was configured with.
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")]
public IServiceProvider Services
=> Configuration.ServiceProvider;
///
/// Gets a list of handled interactions. Fix for double interaction execution bug.
///
internal static List HandledInteractions = new();
///
/// 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.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)
{
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)
{
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.
///
internal 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.
///
internal 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);
if (this.StartupFinished)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} already setup, skipping", this.Client.ShardId);
this.FinishedRegistration();
return;
}
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>();
var groupTranslation = new List();
var translation = 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
var extremeNestedGroup = false;
if (module.GetCustomAttribute() != null)
{
classes.Add(module);
}
else if (module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Any(x => x.IsDefined(typeof(SlashCommandGroupAttribute))))
{
//Otherwise add the extreme nested groups
classes = module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
.Where(x => x.IsDefined(typeof(SlashCommandGroupAttribute)))
.Select(x => module.GetNestedType(x.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetTypeInfo()).ToList();
extremeNestedGroup = true;
}
else
{
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
}
if (module.GetCustomAttribute() != null || extremeNestedGroup)
{
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 (Configuration.GenerateTranslationFilesOnly)
{
var cgwsgs = new List();
var cgs2 = new List();
foreach (var cmd in slashGroupsTuple.applicationCommands)
{
if (cmd.Type == ApplicationCommandType.ChatInput)
{
if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommandGroup)
{
var cgs = new List();
foreach (var scg in cmd.Options)
{
var cs = new List();
foreach (var sc in scg.Options)
{
if (sc.Options == null || !sc.Options.Any())
cs.Add(new Command(sc.Name, sc.Description, null, null));
else
cs.Add(new Command(sc.Name, sc.Description, sc.Options.ToList(), null));
}
cgs.Add(new CommandGroup(scg.Name, scg.Description, cs, null));
}
cgwsgs.Add(new CommandGroupWithSubGroups(cmd.Name, cmd.Description, cgs, ApplicationCommandType.ChatInput));
}
else if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommand)
{
var cs2 = new List();
foreach (var sc2 in cmd.Options)
{
if (sc2.Options == null || !sc2.Options.Any())
cs2.Add(new Command(sc2.Name, sc2.Description, null, null));
else
cs2.Add(new Command(sc2.Name, sc2.Description, sc2.Options.ToList(), null));
}
cgs2.Add(new CommandGroup(cmd.Name, cmd.Description, cs2, ApplicationCommandType.ChatInput));
}
}
}
if (cgwsgs.Any())
foreach (var cgwsg in cgwsgs)
groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg)));
if (cgs2.Any())
foreach (var cg2 in cgs2)
groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cg2)));
}
}
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 (Configuration.GenerateTranslationFilesOnly)
{
var cs = new List();
foreach (var cmd in slashCommands.applicationCommands)
if (cmd.Type == ApplicationCommandType.ChatInput && (cmd.Options == null || !cmd.Options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup)))
{
if (cmd.Options == null || !cmd.Options.Any())
cs.Add(new Command(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput));
else
cs.Add(new Command(cmd.Name, cmd.Description, cmd.Options.ToList(), ApplicationCommandType.ChatInput));
}
if (cs.Any())
foreach (var c in cs)
translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c)));
}
}
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 (Configuration.GenerateTranslationFilesOnly)
{
var cs = new List();
foreach (var cmd in contextCommands.applicationCommands)
if (cmd.Type == ApplicationCommandType.Message || cmd.Type == ApplicationCommandType.User)
cs.Add(new Command(cmd.Name, null, null, cmd.Type));
if (cs.Any())
foreach (var c in cs)
translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c)));
}
}
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: {res}", 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: {res}", brex1.WebResponse.Response);
else
this.Client.Logger.LogCritical(ex, @"There was an error parsing the application commands");
}
s_errored = true;
}
}
if (!s_errored)
{
updateList = updateList.DistinctBy(x => x.Name).ToList();
if (Configuration.GenerateTranslationFilesOnly)
{
s_registrationCount++;
this.CheckRegistrationStartup(ManOr, translation, groupTranslation);
}
else
{
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 {cmdId}. Please clean up manually", cmd.Id);
}
}
}
}
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 {cmdId} in guild {guildId}. Please clean up manually", cmd.Id, guildId.Value);
}
}
}
}
}
catch (UnauthorizedException ex)
{
this.Client.Logger.LogError("Could not register application commands for guild {guildId}.\nError: {exc}", guildId, 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.DistinctBy(x => x.Name));
s_groupCommands.AddRange(groupCommands.DistinctBy(x => x.Name));
s_subGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name));
s_contextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name));
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: {exp}\nCurrent Count: {cur}", s_expectedCount, 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: {res}", 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: {res}", 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, List translation = null, List groupTranslation = null)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Checking counts...\n\nExpected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, 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
});
if (Configuration.GenerateTranslationFilesOnly)
{
try
{
if (translation != null && translation.Any())
{
var file_name = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{s_registrationCount}_of_{s_expectedCount}.json";
var fs = File.Create(file_name);
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
await writer.WriteAsync(JsonConvert.SerializeObject(translation.DistinctBy(x => x.Name), Formatting.Indented));
await writer.FlushAsync();
ms.Position = 0;
await ms.CopyToAsync(fs);
await fs.FlushAsync();
fs.Close();
await fs.DisposeAsync();
ms.Close();
await ms.DisposeAsync();
this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name);
}
if (groupTranslation != null && groupTranslation.Any())
{
var file_name = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{s_registrationCount}_of_{s_expectedCount}.json";
var fs = File.Create(file_name);
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
await writer.WriteAsync(JsonConvert.SerializeObject(groupTranslation.DistinctBy(x => x.Name), Formatting.Indented));
await writer.FlushAsync();
ms.Position = 0;
await ms.CopyToAsync(fs);
await fs.FlushAsync();
fs.Close();
await fs.DisposeAsync();
ms.Close();
await ms.DisposeAsync();
this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name);
}
}
catch (Exception ex)
{
this.Client.Logger.LogError(@"{msg}", ex.Message);
this.Client.Logger.LogError(@"{stack}", ex.StackTrace);
}
this.FinishedRegistration();
await this.Client.DisconnectAsync();
}
else
{
this.FinishedRegistration();
}
}
}
///
/// Interaction handler.
///
/// The client.
/// The event args.
private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got slash interaction on shard {shard}", this.Client.ShardId);
if (HandledInteractions.Contains(e.Interaction.Id))
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received");
return Task.FromResult(true);
}
else
HandledInteractions.Add(e.Interaction.Id);
_ = 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)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup."));
throw new InvalidOperationException("Application 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())
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A application command was executed, but no command was registered for it."));
throw new InvalidOperationException("A application 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[0];
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[0].Options);
await this.RunCommandAsync(context, method, args);
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options[0];
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[0].Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
}
else if (e.Interaction.Type == InteractionType.AutoComplete)
{
if (s_errored)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup."));
throw new InvalidOperationException("Application 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())
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("An autocomplete interaction was created, but no command was registered for it"));
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[0];
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[0];
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)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got context menu interaction on shard {shard}", this.Client.ShardId);
if (HandledInteractions.Contains(e.Interaction.Id))
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received");
return Task.FromResult(true);
}
else
HandledInteractions.Add(e.Interaction.Id);
_ = 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)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Context menus failed to register properly on startup."));
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)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A context menu command was executed, but no command was registered for it."));
throw new InvalidOperationException("A context menu command 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(ulong) || parameter.ParameterType == typeof(ulong?))
args.Add((ulong?)option.Value);
else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?))
args.Add((int?)option.Value);
else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?))
if (option.Value == null)
args.Add(null);
else
args.Add(Convert.ToInt64(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.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel))
args.Add(channel);
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.
/// Not recommended and should be avoided since it can make slash commands be unresponsive for a while.
///
public async Task RefreshCommandsAsync()
{
s_commandMethods.Clear();
s_groupCommands.Clear();
s_subGroupCommands.Clear();
s_registeredCommands.Clear();
s_contextMenuCommands.Clear();
GlobalDiscordCommands.Clear();
GuildDiscordCommands.Clear();
GuildCommandsInternal.Clear();
GlobalCommandsInternal.Clear();
GlobalDiscordCommands = null;
GuildDiscordCommands = null;
s_errored = false;
/*if (Configuration != null && Configuration.EnableDefaultHelp)
{
this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule));
}*/
/*
await this.UpdateAsync();
}*/
///
/// Fires when the execution of a slash command fails.
///
public event AsyncEventHandler SlashCommandErrored
{
add => this._slashError.Register(value);
remove => this._slashError.Unregister(value);
}
private AsyncEvent _slashError;
///
/// Fires when the execution of a slash command is successful.
///
public event AsyncEventHandler SlashCommandExecuted
{
add => this._slashExecuted.Register(value);
remove => this._slashExecuted.Unregister(value);
}
private AsyncEvent _slashExecuted;
///
/// Fires when the execution of a context menu fails.
///
public event AsyncEventHandler ContextMenuErrored
{
add => this._contextMenuErrored.Register(value);
remove => this._contextMenuErrored.Unregister(value);
}
private AsyncEvent _contextMenuErrored;
///
/// Fire when the execution of a context menu is successful.
///
public event AsyncEventHandler ContextMenuExecuted
{
add => this._contextMenuExecuted.Register(value);
remove => this._contextMenuExecuted.Unregister(value);
}
private AsyncEvent _contextMenuExecuted;
}
///
/// Holds configuration data for setting up an application command.
///
internal class ApplicationCommandsModuleConfiguration
{
///
/// The type of the command module.
///
public Type Type { get; }
///
/// The translation setup.
///
public Action Translations { get; }
///
/// Creates a new command configuration.
///
/// The type of the command module.
/// The translation setup callback.
public ApplicationCommandsModuleConfiguration(Type type, Action translations = null)
{
this.Type = type;
this.Translations = translations;
}
}
///
/// Links a command to its original command module.
///
internal class ApplicationCommandSourceLink
{
///
/// The command.
///
public DiscordApplicationCommand ApplicationCommand { get; set; }
///
/// The base/root module the command is contained in.
///
public Type RootCommandContainerType { get; set; }
///
/// The direct group the command is contained in.
///
public Type CommandContainerType { get; set; }
}
///
/// The command method.
///
internal class CommandMethod
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
///
/// The group command.
///
internal class GroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the methods.
///
public List> Methods { get; set; } = null;
}
///
/// The sub group command.
///
internal class SubGroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the sub commands.
///
public List SubCommands { get; set; } = new();
}
///
/// The context menu command.
///
internal class ContextMenuCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
#region Default Help
///
/// Represents the default help module.
///
internal class DefaultHelpModule : ApplicationCommandsModule
{
public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase))
.ToList();
}
foreach (var sc in slashCommands.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim()));
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase));
if (command is null || command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand
&& c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
IEnumerable slashCommands = null;
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
if (context.Guild != null)
{
var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await Task.WhenAll(globalCommandsTask);
slashCommands = globalCommandsTask.Result
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
return options.AsEnumerable();
}
var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (foundCommand is null || foundCommand.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand &&
x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
[SlashCommand("help", "Displays command help")]
internal async Task DefaultHelpAsync(InteractionContext ctx,
[Autocomplete(typeof(DefaultHelpAutoCompleteProvider))]
[Option("option_one", "top level command to provide help for", true)] string commandName,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))]
[Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))]
[Option("option_three", "command to provide help for", true)] string commandTwoName = null)
{
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)
{
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.WithContent($"There are no slash commands"));
else
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()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.AddEmbed(discordEmbed));
else
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()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed));
else
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)
{
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}"));
else
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()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed));
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
}
}
#endregion
diff --git a/DisCatSharp.ApplicationCommands/Attributes/ApplicationCommandModuleLifespanAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ApplicationCommandModuleLifespanAttribute.cs
index 94243b6a6..1b16ffb15 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/ApplicationCommandModuleLifespanAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/ApplicationCommandModuleLifespanAttribute.cs
@@ -1,48 +1,67 @@
// 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 DisCatSharp.ApplicationCommands.Enums;
-
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Defines this application command module's lifespan. Module lifespans are transient by default.
///
[AttributeUsage(AttributeTargets.Class)]
public class ApplicationCommandModuleLifespanAttribute : Attribute
{
///
/// Gets the lifespan.
///
public ApplicationCommandModuleLifespan Lifespan { get; }
///
/// Defines this application command module's lifespan.
///
/// The lifespan of the module. Module lifespans are transient by default.
public ApplicationCommandModuleLifespanAttribute(ApplicationCommandModuleLifespan lifespan)
{
this.Lifespan = lifespan;
}
}
+
+///
+/// Represents a application command module lifespan.
+///
+public enum ApplicationCommandModuleLifespan
+{
+ ///
+ /// Whether this module should be initiated every time a command is run, with dependencies injected from a scope.
+ ///
+ Scoped,
+
+ ///
+ /// Whether this module should be initiated every time a command is run.
+ ///
+ Transient,
+
+ ///
+ /// Whether this module should be initiated at startup.
+ ///
+ Singleton
+}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
deleted file mode 100644
index a5475c6f9..000000000
--- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuCooldownAttribute.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-// 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.Concurrent;
-using System.Threading.Tasks;
-
-using DisCatSharp.ApplicationCommands.Context;
-using DisCatSharp.ApplicationCommands.Entities;
-using DisCatSharp.ApplicationCommands.Enums;
-
-namespace DisCatSharp.ApplicationCommands.Attributes;
-
-///
-/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command
-///
-[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
-public sealed class ContextMenuCooldownAttribute : ContextMenuCheckBaseAttribute, ICooldown
-{
- ///
- /// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
- ///
- public int MaxUses { get; }
-
- ///
- /// Gets the time after which the cooldown is reset.
- ///
- public TimeSpan Reset { get; }
-
- ///
- /// Gets the type of the cooldown bucket. This determines how cooldowns are applied.
- ///
- public CooldownBucketType BucketType { get; }
-
- ///
- /// Gets the cooldown buckets for this command.
- ///
- internal readonly ConcurrentDictionary _buckets;
-
- ///
- /// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again.
- ///
- /// Number of times the command can be used before triggering a cooldown.
- /// Number of seconds after which the cooldown is reset.
- /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally.
- public ContextMenuCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType)
- {
- this.MaxUses = maxUses;
- this.Reset = TimeSpan.FromSeconds(resetAfter);
- this.BucketType = bucketType;
- this._buckets = new ConcurrentDictionary();
- }
-
- ///
- /// Gets a cooldown bucket for given command context.
- ///
- /// Command context to get cooldown bucket for.
- /// Requested cooldown bucket, or null if one wasn't present.
- public ContextMenuCooldownBucket GetBucket(ContextMenuContext ctx)
- {
- var bid = this.GetBucketId(ctx, out _, out _, out _);
- this._buckets.TryGetValue(bid, out var bucket);
- return bucket;
- }
-
- ///
- /// Calculates the cooldown remaining for given command context.
- ///
- /// Context for which to calculate the cooldown.
- /// Remaining cooldown, or zero if no cooldown is active.
- public TimeSpan GetRemainingCooldown(ContextMenuContext ctx)
- {
- var bucket = this.GetBucket(ctx);
- return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow;
- }
-
- ///
- /// Calculates bucket ID for given command context.
- ///
- /// Context for which to calculate bucket ID for.
- /// ID of the user with which this bucket is associated.
- /// ID of the channel with which this bucket is associated.
- /// ID of the guild with which this bucket is associated.
- /// Calculated bucket ID.
- private string GetBucketId(ContextMenuContext ctx, out ulong userId, out ulong channelId, out ulong guildId)
- {
- userId = 0ul;
- if ((this.BucketType & CooldownBucketType.User) != 0)
- userId = ctx.User.Id;
-
- channelId = 0ul;
- if ((this.BucketType & CooldownBucketType.Channel) != 0)
- channelId = ctx.Channel.Id;
- if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null)
- channelId = ctx.Channel.Id;
-
- guildId = 0ul;
- if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0)
- guildId = ctx.Guild.Id;
-
- var bid = CooldownBucket.MakeId(userId, channelId, guildId);
- return bid;
- }
-
- ///
- /// Executes a check.
- ///
- /// The command context.
- public override async Task ExecuteChecksAsync(ContextMenuContext ctx)
- {
- var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld);
- if (!this._buckets.TryGetValue(bid, out var bucket))
- {
- bucket = new ContextMenuCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld);
- this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
- }
-
- return await bucket.DecrementUseAsync().ConfigureAwait(false);
- }
-}
-
-///
-/// Represents a cooldown bucket for commands.
-///
-public sealed class ContextMenuCooldownBucket : CooldownBucket
-{
- internal ContextMenuCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
- : base(maxUses, resetAfter, userId, channelId, guildId)
- {
- }
-
- ///
- /// Returns a string representation of this command cooldown bucket.
- ///
- /// String representation of this command cooldown bucket.
- public override string ToString() => $"Context Menu Command bucket {this.BucketId}";
-}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
deleted file mode 100644
index 2d679d87f..000000000
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-// 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.Concurrent;
-using System.Threading.Tasks;
-
-using DisCatSharp.ApplicationCommands.Context;
-using DisCatSharp.ApplicationCommands.Entities;
-using DisCatSharp.ApplicationCommands.Enums;
-
-namespace DisCatSharp.ApplicationCommands.Attributes;
-
-///
-/// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command
-///
-[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
-public sealed class SlashCommandCooldownAttribute : SlashCheckBaseAttribute, ICooldown
-{
- ///
- /// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
- ///
- public int MaxUses { get; }
-
- ///
- /// Gets the time after which the cooldown is reset.
- ///
- public TimeSpan Reset { get; }
-
- ///
- /// Gets the type of the cooldown bucket. This determines how cooldowns are applied.
- ///
- public CooldownBucketType BucketType { get; }
-
- ///
- /// Gets the cooldown buckets for this command.
- ///
- internal readonly ConcurrentDictionary _buckets;
-
- ///
- /// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again.
- ///
- /// Number of times the command can be used before triggering a cooldown.
- /// Number of seconds after which the cooldown is reset.
- /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally.
- public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType)
- {
- this.MaxUses = maxUses;
- this.Reset = TimeSpan.FromSeconds(resetAfter);
- this.BucketType = bucketType;
- this._buckets = new ConcurrentDictionary();
- }
-
- ///
- /// Gets a cooldown bucket for given command context.
- ///
- /// Command context to get cooldown bucket for.
- /// Requested cooldown bucket, or null if one wasn't present.
- public SlashCommandCooldownBucket GetBucket(InteractionContext ctx)
- {
- var bid = this.GetBucketId(ctx, out _, out _, out _);
- this._buckets.TryGetValue(bid, out var bucket);
- return bucket;
- }
-
- ///
- /// Calculates the cooldown remaining for given command context.
- ///
- /// Context for which to calculate the cooldown.
- /// Remaining cooldown, or zero if no cooldown is active.
- public TimeSpan GetRemainingCooldown(InteractionContext ctx)
- {
- var bucket = this.GetBucket(ctx);
- return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow;
- }
-
- ///
- /// Calculates bucket ID for given command context.
- ///
- /// Context for which to calculate bucket ID for.
- /// ID of the user with which this bucket is associated.
- /// ID of the channel with which this bucket is associated.
- /// ID of the guild with which this bucket is associated.
- /// Calculated bucket ID.
- private string GetBucketId(InteractionContext ctx, out ulong userId, out ulong channelId, out ulong guildId)
- {
- userId = 0ul;
- if ((this.BucketType & CooldownBucketType.User) != 0)
- userId = ctx.User.Id;
-
- channelId = 0ul;
- if ((this.BucketType & CooldownBucketType.Channel) != 0)
- channelId = ctx.Channel.Id;
- if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null)
- channelId = ctx.Channel.Id;
-
- guildId = 0ul;
- if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0)
- guildId = ctx.Guild.Id;
-
- var bid = CooldownBucket.MakeId(userId, channelId, guildId);
- return bid;
- }
-
- ///
- /// Executes a check.
- ///
- /// The command context.
- public override async Task ExecuteChecksAsync(InteractionContext ctx)
- {
- var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld);
- if (!this._buckets.TryGetValue(bid, out var bucket))
- {
- bucket = new SlashCommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld);
- this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket);
- }
-
- return await bucket.DecrementUseAsync().ConfigureAwait(false);
- }
-}
-
-///
-/// Represents a cooldown bucket for commands.
-///
-public sealed class SlashCommandCooldownBucket : CooldownBucket
-{
- ///
- /// Returns a string representation of this command cooldown bucket.
- ///
- /// String representation of this command cooldown bucket.
- public override string ToString() => $"Slash Command bucket {this.BucketId}";
-
- internal SlashCommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
- : base(maxUses, resetAfter, userId, channelId, guildId)
- {
-
- }
-}
diff --git a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs b/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs
deleted file mode 100644
index b43cef26e..000000000
--- a/DisCatSharp.ApplicationCommands/Entities/CooldownBucket.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-// 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.Globalization;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace DisCatSharp.ApplicationCommands.Entities;
-
-public class CooldownBucket : IBucket, IEquatable
-{
- ///
- /// The user id for this bucket.
- ///
- public ulong UserId { get; }
-
- ///
- /// The channel id for this bucket.
- ///
- public ulong ChannelId { get; }
-
- ///
- /// The guild id for this bucket.
- ///
- public ulong GuildId { get; }
-
- ///
- /// The id for this bucket.
- ///
- public string BucketId { get; }
-
- ///
- /// The remaining uses for this bucket.
- ///
- public int RemainingUses => Volatile.Read(ref this._remainingUses);
-
- ///
- /// The max uses for this bucket.
- ///
- public int MaxUses { get; }
-
- ///
- /// The datetime offset when this bucket resets.
- ///
- public DateTimeOffset ResetsAt { get; internal set; }
-
- ///
- /// The timespan when this bucket resets.
- ///
- public TimeSpan Reset { get; internal set; }
-
- ///
- /// Gets the semaphore used to lock the use value.
- ///
- internal readonly SemaphoreSlim _usageSemaphore;
-
- internal int _remainingUses;
-
- ///
- /// Creates a new command cooldown bucket.
- ///
- /// Maximum number of uses for this bucket.
- /// Time after which this bucket resets.
- /// ID of the user with which this cooldown is associated.
- /// ID of the channel with which this cooldown is associated.
- /// ID of the guild with which this cooldown is associated.
- internal CooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
- {
- this._remainingUses = maxUses;
- this.MaxUses = maxUses;
- this.ResetsAt = DateTimeOffset.UtcNow + resetAfter;
- this.Reset = resetAfter;
- this.UserId = userId;
- this.ChannelId = channelId;
- this.GuildId = guildId;
- this.BucketId = MakeId(userId, channelId, guildId);
- this._usageSemaphore = new(1, 1);
- }
-
- ///
- /// Decrements the remaining use counter.
- ///
- /// Whether decrement succeeded or not.
- internal async Task DecrementUseAsync()
- {
- await this._usageSemaphore.WaitAsync().ConfigureAwait(false);
- Console.WriteLine($"[DecrementUseAsync]: Remaining: {this.RemainingUses}/{this.MaxUses} Resets: {this.ResetsAt} Now: {DateTimeOffset.UtcNow} Vars[u,c,g]: {this.UserId} {this.ChannelId} {this.GuildId} Id: {this.BucketId}");
-
- // if we're past reset time...
- var now = DateTimeOffset.UtcNow;
- if (now >= this.ResetsAt)
- {
- // ...do the reset and set a new reset time
- Interlocked.Exchange(ref this._remainingUses, this.MaxUses);
- this.ResetsAt = now + this.Reset;
- }
-
- // check if we have any uses left, if we do...
- var success = false;
- if (this.RemainingUses > 0)
- {
- // ...decrement, and return success...
- Interlocked.Decrement(ref this._remainingUses);
- success = true;
- }
- Console.WriteLine($"[DecrementUseAsync]: Remaining: {this.RemainingUses}/{this.MaxUses} Resets: {this.ResetsAt} Now: {DateTimeOffset.UtcNow} Vars[u,c,g]: {this.UserId} {this.ChannelId} {this.GuildId} Id: {this.BucketId}");
- // ...otherwise just fail
- this._usageSemaphore.Release();
- return success;
- }
-
- ///
- /// Checks whether this is equal to another object.
- ///
- /// Object to compare to.
- /// Whether the object is equal to this .
- public override bool Equals(object obj) => this.Equals(obj as CooldownBucket);
-
- ///
- /// Checks whether this is equal to another .
- ///
- /// to compare to.
- /// Whether the is equal to this .
- public bool Equals(CooldownBucket other) => other is not null && (ReferenceEquals(this, other) || (this.UserId == other.UserId && this.ChannelId == other.ChannelId && this.GuildId == other.GuildId));
-
- ///
- /// Gets the hash code for this .
- ///
- /// The hash code for this .
- public override int GetHashCode() => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId);
-
- ///
- /// Gets whether the two objects are equal.
- ///
- /// First bucket to compare.
- /// Second bucket to compare.
- /// Whether the two buckets are equal.
- public static bool operator ==(CooldownBucket bucket1, CooldownBucket bucket2)
- {
- var null1 = bucket1 is null;
- var null2 = bucket2 is null;
-
- return (null1 && null2) || (null1 == null2 && null1.Equals(null2));
- }
-
- ///
- /// Gets whether the two objects are not equal.
- ///
- /// First bucket to compare.
- /// Second bucket to compare.
- /// Whether the two buckets are not equal.
- public static bool operator !=(CooldownBucket bucket1, CooldownBucket bucket2)
- => !(bucket1 == bucket2);
-
- ///
- /// Creates a bucket ID from given bucket parameters.
- ///
- /// ID of the user with which this cooldown is associated.
- /// ID of the channel with which this cooldown is associated.
- /// ID of the guild with which this cooldown is associated.
- /// Generated bucket ID.
- public static string MakeId(ulong userId = 0, ulong channelId = 0, ulong guildId = 0)
- => $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}";
-
-
-}
diff --git a/DisCatSharp.ApplicationCommands/Entities/IBucket.cs b/DisCatSharp.ApplicationCommands/Entities/IBucket.cs
deleted file mode 100644
index 5ef9aee60..000000000
--- a/DisCatSharp.ApplicationCommands/Entities/IBucket.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// This file is part of the DisCatSharp project, based off DSharpPlus.
-//
-// Copyright (c) 2021-2022 AITSYS
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-using System;
-
-namespace DisCatSharp.ApplicationCommands.Entities;
-
-///
-/// Defines the standard contract for bucket feature
-///
-public interface IBucket
-{
- ///
- /// Gets the ID of the user whom this cooldown is associated
- ///
- ulong UserId { get; }
-
- ///
- /// Gets the ID of the channel with which this cooldown is associated
- ///
- ulong ChannelId { get; }
-
- ///
- /// Gets the ID of the guild with which this cooldown is associated
- ///
- ulong GuildId { get; }
-
- ///
- /// Gets the ID of the bucket. This is used to distinguish between cooldown buckets
- ///
- string BucketId { get; }
-
- ///
- /// Gets the remaining number of uses before the cooldown is triggered
- ///
- int RemainingUses { get; }
-
- ///
- /// Gets the maximum number of times this command can be used in a given timespan
- ///
- int MaxUses { get; }
-
- ///
- /// Gets the date and time at which the cooldown resets
- ///
- DateTimeOffset ResetsAt { get; }
-
- ///
- /// Get the time after which this cooldown resets
- ///
- TimeSpan Reset { get; }
-}
diff --git a/DisCatSharp.ApplicationCommands/Entities/ICooldown.cs b/DisCatSharp.ApplicationCommands/Entities/ICooldown.cs
deleted file mode 100644
index 17097417d..000000000
--- a/DisCatSharp.ApplicationCommands/Entities/ICooldown.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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 DisCatSharp.ApplicationCommands.Context;
-using DisCatSharp.ApplicationCommands.Enums;
-
-namespace DisCatSharp.ApplicationCommands.Entities;
-
-///
-/// Cooldown feature contract
-///
-/// Type of in which this cooldown handles
-/// Type of Cooldown bucket
-public interface ICooldown
- where TContextType : BaseContext
- where TBucketType : CooldownBucket
-{
- ///
- /// Gets the maximum number of uses before this command triggers a cooldown for its bucket.
- ///
- int MaxUses { get; }
-
- ///
- /// Gets the time after which the cooldown is reset.
- ///
- TimeSpan Reset { get; }
-
- ///
- /// Gets the type of the cooldown bucket. This determines how a cooldown is applied.
- ///
- CooldownBucketType BucketType { get; }
-
- ///
- /// Calculates the cooldown remaining for given context.
- ///
- /// Context for which to calculate the cooldown.
- /// Remaining cooldown, or zero if no cooldown is active
- TimeSpan GetRemainingCooldown(TContextType ctx);
-
- ///
- /// Gets a cooldown bucket for given context
- ///
- /// Command context to get cooldown bucket for.
- /// Requested cooldown bucket, or null if one wasn't present
- TBucketType GetBucket(TContextType ctx);
-}
diff --git a/DisCatSharp.ApplicationCommands/Enums/ApplicationCommandModuleLifespan.cs b/DisCatSharp.ApplicationCommands/Enums/ApplicationCommandModuleLifespan.cs
deleted file mode 100644
index c8b2ac181..000000000
--- a/DisCatSharp.ApplicationCommands/Enums/ApplicationCommandModuleLifespan.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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.
-
-namespace DisCatSharp.ApplicationCommands.Enums;
-
-///
-/// Represents a application command module lifespan.
-///
-public enum ApplicationCommandModuleLifespan
-{
- ///
- /// Whether this module should be initiated every time a command is run, with dependencies injected from a scope.
- ///
- Scoped,
-
- ///
- /// Whether this module should be initiated every time a command is run.
- ///
- Transient,
-
- ///
- /// Whether this module should be initiated at startup.
- ///
- Singleton
-}
diff --git a/DisCatSharp.ApplicationCommands/Enums/CooldownBucketType.cs b/DisCatSharp.ApplicationCommands/Enums/CooldownBucketType.cs
deleted file mode 100644
index 3b1330bc8..000000000
--- a/DisCatSharp.ApplicationCommands/Enums/CooldownBucketType.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// 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.
-
-namespace DisCatSharp.ApplicationCommands.Enums;
-
-///
-/// Defines how are command cooldowns applied.
-///
-public enum CooldownBucketType : int
-{
-
- ///
- /// Denotes that the command will have its cooldown applied globally.
- ///
- Global = 0,
-
- ///
- /// Denotes that the command will have its cooldown applied per-user.
- ///
- User = 1,
-
- ///
- /// Denotes that the command will have its cooldown applied per-channel.
- ///
- Channel = 2,
-
- ///
- /// Denotes that the command will have its cooldown applied per-guild. In DMs, this applies the cooldown per-channel.
- ///
- Guild = 4
-}
diff --git a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs
index 1e2c43377..ca14337c4 100644
--- a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs
+++ b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs
@@ -1,371 +1,370 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021-2022 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.Context;
using DisCatSharp.ApplicationCommands.Entities;
-using DisCatSharp.ApplicationCommands.Enums;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
namespace DisCatSharp.ApplicationCommands.Workers;
///
/// Represents a .
///
internal class CommandWorker
{
///
/// Parses context menu application commands.
///
/// The type.
/// List of method infos.
/// The optional command translations.
/// Too much.
internal static Task<
(
List applicationCommands,
List> commandTypeSources,
List contextMenuCommands,
bool withLocalization
)
> ParseContextMenuCommands(Type type, IEnumerable methods, List translator = null)
{
List commands = new();
List> commandTypeSources = new();
List contextMenuCommands = new();
foreach (var contextMethod in methods)
{
var contextAttribute = contextMethod.GetCustomAttribute();
DiscordApplicationCommandLocalization nameLocalizations = null;
var commandTranslation = translator?.Single(c => c.Name == contextAttribute.Name && c.Type == contextAttribute.Type);
if (commandTranslation != null)
nameLocalizations = commandTranslation.NameTranslations;
var command = new DiscordApplicationCommand(contextAttribute.Name, null, null, contextAttribute.Type, nameLocalizations, null, contextAttribute.DefaultMemberPermissions, contextAttribute.DmPermission ?? true);
var parameters = contextMethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext)))
throw new ArgumentException($"The first argument must be a ContextMenuContext!");
if (parameters.Length > 1)
throw new ArgumentException($"A context menu cannot have parameters!");
contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name });
commands.Add(command);
commandTypeSources.Add(new KeyValuePair(type, type));
}
return Task.FromResult((commands, commandTypeSources, contextMenuCommands, translator != null));
}
///
/// Parses single application commands.
///
/// The type.
/// List of method infos.
/// The optional guild id.
/// The optional command translations.
/// Too much.
internal static async Task<
(
List applicationCommands,
List> commandTypeSources,
List commandMethods,
bool withLocalization
)
> ParseBasicSlashCommandsAsync(Type type, IEnumerable methods, ulong? guildId = null, List translator = null)
{
List commands = new();
List> commandTypeSources = new();
List commandMethods = new();
foreach (var method in methods)
{
var commandAttribute = method.GetCustomAttribute();
var parameters = method.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
var options = await ApplicationCommandsExtension.ParseParametersAsync(parameters.Skip(1), guildId);
commandMethods.Add(new CommandMethod { Method = method, Name = commandAttribute.Name });
DiscordApplicationCommandLocalization nameLocalizations = null;
DiscordApplicationCommandLocalization descriptionLocalizations = null;
List localizedOptions = null;
var commandTranslation = translator?.Single(c => c.Name == commandAttribute.Name && c.Type == ApplicationCommandType.ChatInput);
if (commandTranslation != null && commandTranslation.Options != null)
{
localizedOptions = new List(options.Count);
foreach (var option in options)
{
var choices = option.Choices != null ? new List(option.Choices.Count) : null;
if (option.Choices != null)
foreach (var choice in option.Choices)
choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, commandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations));
localizedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required,
choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue,
commandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, commandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations,
option.MinimumLength, option.MaximumLength
));
}
nameLocalizations = commandTranslation.NameTranslations;
descriptionLocalizations = commandTranslation.DescriptionTranslations;
}
var payload = new DiscordApplicationCommand(commandAttribute.Name, commandAttribute.Description, (localizedOptions != null && localizedOptions.Any() ? localizedOptions : null) ?? (options != null && options.Any() ? options : null), ApplicationCommandType.ChatInput, nameLocalizations, descriptionLocalizations, commandAttribute.DefaultMemberPermissions, commandAttribute.DmPermission ?? true);
commands.Add(payload);
commandTypeSources.Add(new KeyValuePair(type, type));
}
return (commands, commandTypeSources, commandMethods, translator != null);
}
}
///
/// Represents a .
///
internal class NestedCommandWorker
{
///
/// Parses application command groups.
///
/// The type.
/// List of type infos.
/// The optional guild id.
/// The optional group translations.
/// Too much.
internal static async Task<
(
List applicationCommands,
List> commandTypeSources,
List singletonModules,
List groupCommands,
List subGroupCommands,
bool withLocalization
)
> ParseSlashGroupsAsync(Type type, List types, ulong? guildId = null, List translator = null)
{
List commands = new();
List> commandTypeSources = new();
List groupCommands = new();
List subGroupCommands = new();
List singletonModules = new();
//Handles groups
foreach (var subclassInfo in types)
{
//Gets the attribute and methods in the group
var groupAttribute = subclassInfo.GetCustomAttribute();
var submethods = subclassInfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null).ToList();
var subclasses = subclassInfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
if (subclasses.Any() && submethods.Any())
throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!");
DiscordApplicationCommandLocalization nameLocalizations = null;
DiscordApplicationCommandLocalization descriptionLocalizations = null;
if (translator != null)
{
var commandTranslation = translator.Single(c => c.Name == groupAttribute.Name);
if (commandTranslation != null)
{
nameLocalizations = commandTranslation.NameTranslations;
descriptionLocalizations = commandTranslation.DescriptionTranslations;
}
}
//Initializes the command
var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, nameLocalizations: nameLocalizations, descriptionLocalizations: descriptionLocalizations, defaultMemberPermissions: groupAttribute.DefaultMemberPermissions, dmPermission: groupAttribute.DmPermission ?? true);
commandTypeSources.Add(new KeyValuePair(type, type));
var commandMethods = new List>();
//Handles commands in the group
foreach (var submethod in submethods)
{
var commandAttribute = submethod.GetCustomAttribute();
//Gets the parameters and accounts for InteractionContext
var parameters = submethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
var options = await ApplicationCommandsExtension.ParseParametersAsync(parameters.Skip(1), guildId);
DiscordApplicationCommandLocalization subNameLocalizations = null;
DiscordApplicationCommandLocalization subDescriptionLocalizations = null;
List localizedOptions = null;
var commandTranslation = translator?.Single(c => c.Name == payload.Name);
if (commandTranslation?.Commands != null)
{
var subCommandTranslation = commandTranslation.Commands.Single(sc => sc.Name == commandAttribute.Name);
if (subCommandTranslation.Options != null)
{
localizedOptions = new List(options.Count);
foreach (var option in options)
{
var choices = option.Choices != null ? new List(option.Choices.Count) : null;
if (option.Choices != null)
foreach (var choice in option.Choices)
choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations));
localizedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required,
choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue,
subCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations,
option.MinimumLength, option.MaximumLength
));
}
}
subNameLocalizations = subCommandTranslation.NameTranslations;
subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations;
}
//Creates the subcommand and adds it to the main command
var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, false, null, localizedOptions ?? options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations);
payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations, defaultMemberPermissions: payload.DefaultMemberPermissions, dmPermission: payload.DmPermission ?? true);
commandTypeSources.Add(new KeyValuePair(subclassInfo, type));
//Adds it to the method lists
commandMethods.Add(new KeyValuePair(commandAttribute.Name, submethod));
groupCommands.Add(new GroupCommand { Name = groupAttribute.Name, Methods = commandMethods });
}
var command = new SubGroupCommand { Name = groupAttribute.Name };
//Handles subgroups
foreach (var subclass in subclasses)
{
var subgroupAttribute = subclass.GetCustomAttribute();
var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var options = new List();
var currentMethods = new List>();
DiscordApplicationCommandLocalization subNameLocalizations = null;
DiscordApplicationCommandLocalization subDescriptionLocalizations = null;
if (translator != null)
{
var commandTranslation = translator.Single(c => c.Name == payload.Name);
if (commandTranslation != null && commandTranslation.SubGroups != null)
{
var subCommandTranslation = commandTranslation.SubGroups.Single(sc => sc.Name == subgroupAttribute.Name);
if (subCommandTranslation != null)
{
subNameLocalizations = subCommandTranslation.NameTranslations;
subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations;
}
}
}
//Similar to the one for regular groups
foreach (var subsubmethod in subsubmethods)
{
var suboptions = new List();
var commatt = subsubmethod.GetCustomAttribute();
var parameters = subsubmethod.GetParameters();
if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext)))
throw new ArgumentException($"The first argument must be an InteractionContext!");
suboptions = suboptions.Concat(await ApplicationCommandsExtension.ParseParametersAsync(parameters.Skip(1), guildId)).ToList();
DiscordApplicationCommandLocalization subSubNameLocalizations = null;
DiscordApplicationCommandLocalization subSubDescriptionLocalizations = null;
List localizedOptions = null;
var commandTranslation = translator?.Single(c => c.Name == payload.Name);
var subCommandTranslation = commandTranslation?.SubGroups?.Single(sc => sc.Name == commatt.Name);
var subSubCommandTranslation = subCommandTranslation?.Commands.Single(sc => sc.Name == commatt.Name);
if (subSubCommandTranslation != null && subSubCommandTranslation.Options != null)
{
localizedOptions = new List(suboptions.Count);
foreach (var option in suboptions)
{
var choices = option.Choices != null ? new List(option.Choices.Count) : null;
if (option.Choices != null)
foreach (var choice in option.Choices)
choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations));
localizedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required,
choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue,
subSubCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations,
option.MinimumLength, option.MaximumLength
));
}
subSubNameLocalizations = subSubCommandTranslation.NameTranslations;
subSubDescriptionLocalizations = subSubCommandTranslation.DescriptionTranslations;
}
var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, false, null, (localizedOptions != null && localizedOptions.Any() ? localizedOptions : null) ?? (suboptions != null && suboptions.Any() ? suboptions : null), nameLocalizations: subSubNameLocalizations, descriptionLocalizations: subSubDescriptionLocalizations);
options.Add(subsubpayload);
commandMethods.Add(new KeyValuePair(commatt.Name, subsubmethod));
currentMethods.Add(new KeyValuePair(commatt.Name, subsubmethod));
}
//Adds the group to the command and method lists
var subpayload = new DiscordApplicationCommandOption(subgroupAttribute.Name, subgroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, false, null, options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations);
command.SubCommands.Add(new GroupCommand { Name = subgroupAttribute.Name, Methods = currentMethods });
payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations, defaultMemberPermissions: payload.DefaultMemberPermissions, dmPermission: payload.DmPermission ?? true);
commandTypeSources.Add(new KeyValuePair(subclass, type));
//Accounts for lifespans for the sub group
if (subclass.GetCustomAttribute() != null && subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
singletonModules.Add(ApplicationCommandsExtension.CreateInstance(subclass, ApplicationCommandsExtension.Configuration?.ServiceProvider));
}
if (command.SubCommands.Any()) subGroupCommands.Add(command);
commands.Add(payload);
//Accounts for lifespans
if (subclassInfo.GetCustomAttribute() != null && subclassInfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
singletonModules.Add(ApplicationCommandsExtension.CreateInstance(subclassInfo, ApplicationCommandsExtension.Configuration?.ServiceProvider));
}
return (commands, commandTypeSources, singletonModules, groupCommands, subGroupCommands, translator != null);
}
}