diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index 973012622..3bfdcf870 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,2622 +1,2622 @@
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.Enums.Core;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
// ReSharper disable HeuristicUnreachableCode
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.
///
internal static List CommandMethods { get; set; } = [];
///
/// List of groups.
///
internal static List GroupCommands { get; set; } = [];
///
/// List of groups with subgroups.
///
internal static List SubGroupCommands { get; set; } = [];
///
/// List of context menus.
///
internal static List ContextMenuCommands { get; set; } = [];
///
/// 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; } = [];
///
/// List of modules to register.
///
private readonly List> _updateList = [];
///
/// 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.Select(guild =>
new KeyValuePair>(guild.Key, guild.Value
.Select(parent => new RegisteredDiscordApplicationCommand(parent)).ToList())).ToList().AsReadOnly();
///
/// Sets a list of registered commands. The key is the guild id (null if global).
///
private static readonly List>> s_registeredCommands = [];
///
/// Gets a list of registered global commands.
///
public IReadOnlyList GlobalCommands
=> GlobalCommandsInternal;
///
/// Sets a list of registered global commands.
///
internal static readonly List GlobalCommandsInternal = [];
///
/// Gets a list of registered guild commands mapped by guild id.
///
public IReadOnlyDictionary> GuildCommands
=> GuildCommandsInternal;
///
/// Sets a list of registered guild commands mapped by guild id.
///
internal static readonly Dictionary> GuildCommandsInternal = [];
///
/// Gets the guild ids where the applications.commands scope is missing.
///
private List MISSING_SCOPE_GUILD_IDS { get; set; } = [];
///
/// Gets the guild ids where the applications.commands scope is missing.
///
private static List s_missingScopeGuildIdsGlobal { get; set; } = [];
///
/// Gets whether debug is enabled.
///
internal static bool DebugEnabled { get; set; }
///
/// Gets the debug level for the logs.
///
internal static LogLevel ApplicationCommandsLogLevel
=> DebugEnabled ? LogLevel.Debug : LogLevel.Trace;
///
/// Gets the logger.
///
internal static ILogger Logger { get; set; }
///
/// 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 ShardStartupFinished { get; set; } = false;
///
/// Whether this module finished the startup.
///
internal static bool StartupFinished { get; set; } = false;
///
/// Gets the service provider this module was configured with.
///
public IServiceProvider Services
=> Configuration.ServiceProvider;
///
/// Whether this module is called by an unit test.
///
internal static bool IsCalledByUnitTest { get; set; } = false;
///
/// Gets a list of handled interactions. Fix for double interaction execution bug.
///
internal static readonly List HandledInteractions = [];
///
/// Gets the shard count.
///
internal static int ShardCount { get; set; } = 1;
///
/// Gets the count of shards who finished initializing the module.
///
internal static int FinishedShardCount { get; set; } = 0;
///
/// Gets whether the finish event was fired.
///
public static bool FinishFired { get; set; } = false;
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal ApplicationCommandsExtension(ApplicationCommandsConfiguration? configuration = null)
{
configuration ??= new();
Configuration = configuration;
DebugEnabled = configuration?.DebugStartup ?? false;
CheckAllGuilds = configuration?.CheckAllGuilds ?? false;
AutoDeferEnabled = configuration?.AutoDefer ?? false;
IsCalledByUnitTest = configuration?.UnitTestMode ?? false;
this._slashError = new("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null!);
this._slashExecuted = new("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null!);
this._contextMenuErrored = new("CONTEXTMENU_ERRORED", TimeSpan.Zero, null!);
this._contextMenuExecuted = new("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null!);
this._applicationCommandsModuleReady = new("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null!);
this._applicationCommandsModuleStartupFinished = new("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null!);
this._globalApplicationCommandsRegistered = new("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null!);
this._guildApplicationCommandsRegistered = new("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null!);
this.ApplicationCommandsModuleStartupFinished += this.CheckStartupFinishAsync;
}
///
/// 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;
ShardCount = client.ShardCount;
Logger = client.Logger;
this.Client.Ready += (c, e) =>
{
if (!this.ShardStartupFinished)
_ = Task.Run(async () => await this.UpdateAsync().ConfigureAwait(false));
return Task.CompletedTask;
};
this.Client.InteractionCreated += this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup;
}
///
/// Catches all interactions during the startup.
///
/// The client.
/// The interaction create event args.
///
private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e)
{
if (!this.ShardStartupFinished)
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")).ConfigureAwait(false);
else
await Task.Delay(1).ConfigureAwait(false);
}
///
/// Catches all context menu interactions during the startup.
///
/// The client.
/// The context menu interaction create event args.
private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e)
{
if (!this.ShardStartupFinished)
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral().WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")).ConfigureAwait(false);
else
await Task.Delay(1).ConfigureAwait(false);
}
///
/// Fired when the startup is completed.
/// Switches the interaction handling from and to and .
///
private void FinishedRegistration()
{
this.Client.InteractionCreated -= this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup;
this.Client.InteractionCreated += this.InteractionHandler;
this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler;
}
///
/// Cleans the module for a new start of the bot.
/// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES.
///
public void CleanModule()
{
this._updateList.Clear();
s_singletonModules.Clear();
s_errored = false;
ShardCount = 1;
CommandMethods.Clear();
GroupCommands.Clear();
ContextMenuCommands.Clear();
SubGroupCommands.Clear();
s_singletonModules.Clear();
s_registeredCommands.Clear();
GlobalCommandsInternal.Clear();
GuildCommandsInternal.Clear();
s_missingScopeGuildIdsGlobal.Clear();
this.MISSING_SCOPE_GUILD_IDS.Clear();
HandledInteractions.Clear();
FinishedShardCount = 0;
FinishFired = false;
StartupFinished = false;
this.ShardStartupFinished = false;
this.Client.InteractionCreated += this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup;
}
///
/// Cleans all guild application commands.
/// You normally don't need to execute it.
///
public async Task CleanGuildCommandsAsync()
{
foreach (var guild in this.Client.Guilds.Values)
await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()).ConfigureAwait(false);
}
///
/// Cleans all global application commands.
/// You normally don't need to execute it.
///
public async Task CleanGlobalCommandsAsync()
=> await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()).ConfigureAwait(false);
///
/// Registers all commands from a given assembly. The command classes need to be public to be considered for registration.
///
/// Assembly to register commands from.
/// The guild id to register it on.
public void RegisterGuildCommands(Assembly assembly, ulong guildId)
{
var types = assembly.GetTypes().Where(xt =>
{
var xti = xt.GetTypeInfo();
return xti.IsModuleCandidateType() && !xti.IsNested;
});
foreach (var xt in types)
this.RegisterGuildCommands(xt, guildId, null);
}
///
/// Registers all commands from a given assembly. The command classes need to be public to be considered for registration.
///
/// Assembly to register commands from.
public void RegisterGlobalCommands(Assembly assembly)
{
var types = assembly.GetTypes().Where(xt =>
{
var xti = xt.GetTypeInfo();
return xti.IsModuleCandidateType() && !xti.IsNested;
});
foreach (var xt in types)
this.RegisterGlobalCommands(xt, null);
}
///
/// 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(guildId, new(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(guildId, new(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(null, new(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(null, new(type, translationSetup)));
}
///
/// Fired when the application commands module is ready.
///
public event AsyncEventHandler ApplicationCommandsModuleReady
{
add => this._applicationCommandsModuleReady.Register(value);
remove => this._applicationCommandsModuleReady.Unregister(value);
}
///
/// Fires the application command module ready event.
/// This is fired when the whole module for the finished the startup and registration.
///
private readonly 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);
}
///
/// Fires the application command module startup finished event.
///
private readonly 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);
}
///
/// Fires the guild application command registered event.
///
private readonly AsyncEvent _guildApplicationCommandsRegistered;
///
/// Fired when the global commands are registered.
///
public event AsyncEventHandler GlobalApplicationCommandsRegistered
{
add => this._globalApplicationCommandsRegistered.Register(value);
remove => this._globalApplicationCommandsRegistered.Unregister(value);
}
///
/// Fires the global application command registered event.
///
private readonly AsyncEvent _globalApplicationCommandsRegistered;
///
/// Used for RegisterCommands and the event.
///
internal async Task UpdateAsync()
{
this.Client.Logger.Log(LogLevel.Information, "Request to register commands on shard {shard}", this.Client.ShardId);
try
{
if (this.ShardStartupFinished)
{
this.Client.Logger.Log(LogLevel.Information, "Shard {shard} already setup, skipping", this.Client.ShardId);
return;
}
GlobalDiscordCommands = [];
GuildDiscordCommands = [];
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} has {guilds} guilds", this.Client.ShardId, this.Client.ReadyGuildIds.Count);
List failedGuilds = [];
var globalCommands = IsCalledByUnitTest ? null : (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false).ConfigureAwait(false))?.ToList() ?? null;
var guilds = CheckAllGuilds ? this.Client.ReadyGuildIds : this._updateList.Where(x => x.Key is not null)?.Select(x => x.Key.Value).Distinct().ToList();
var wrongShards = guilds is not null && this.Client.ReadyGuildIds.Count is not 0 ? guilds.Where(x => !this.Client.ReadyGuildIds.Contains(x)).ToList() : [];
if (wrongShards.Count is not 0)
{
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)
{
this._updateList.RemoveAll(x => x.Key == guild);
guilds?.Remove(guild);
}
}
var commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList();
if (guilds is not null && guilds.Count != 0)
foreach (var guild in guilds)
{
List? commands = null;
var unauthorized = false;
try
{
commands = (await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false).ConfigureAwait(false)).ToList() ?? null;
}
catch (UnauthorizedException)
{
unauthorized = true;
}
finally
{
switch (unauthorized)
{
case false when commands is not null && commands.Count is not 0:
GuildDiscordCommands.Add(guild, [.. commands]);
break;
case true:
failedGuilds.Add(guild);
break;
}
}
}
//Default should be to add the help and slash commands can be added without setting any configuration
//so this should still add the default help
if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp))
{
this._updateList.Add(new(null, new(typeof(DefaultHelpModule))));
commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList();
}
else if (Configuration is not null && Configuration.EnableDefaultUserAppsHelp)
{
this._updateList.Add(new(null, new(typeof(DefaultUserAppsHelpModule))));
commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList();
}
else
{
try
{
this._updateList.Remove(new(null, new(typeof(DefaultHelpModule))));
}
catch
{ }
commandsPending = this._updateList.Select(x => x.Key).Distinct().ToList();
}
if (globalCommands is not null && globalCommands.Count is not 0)
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(this._updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key).ConfigureAwait(false);
}
this.MISSING_SCOPE_GUILD_IDS = [..failedGuilds];
s_missingScopeGuildIdsGlobal.AddRange(failedGuilds);
this.ShardStartupFinished = true;
FinishedShardCount++;
StartupFinished = FinishedShardCount == ShardCount;
this.Client.Logger.Log(LogLevel.Information, "Application command setup finished for shard {ShardId}, enabling receiving", this.Client.ShardId);
await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new(Configuration?.ServiceProvider)
{
RegisteredGlobalCommands = GlobalCommandsInternal,
RegisteredGuildCommands = GuildCommandsInternal,
GuildsWithoutScope = this.MISSING_SCOPE_GUILD_IDS,
ShardId = this.Client.ShardId
}).ConfigureAwait(false);
this.FinishedRegistration();
}
catch (Exception ex)
{
this.Client.Logger.LogCritical(ex, "There was an error during the application commands setup");
this.Client.Logger.LogError(ex.Message);
this.Client.Logger.LogError(ex.StackTrace);
}
}
///
/// 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(LogLevel.Information, "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();
List unitTestCommands = [];
//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() is not null)
classes.Add(module);
else if (module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance).Any(x => x.IsDefined(typeof(SlashCommandGroupAttribute))))
{
//Otherwise add the extreme nested groups
classes = module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)
.Where(x => x.IsDefined(typeof(SlashCommandGroupAttribute)))
.Select(x => module.GetNestedType(x.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance).GetTypeInfo()).ToList();
extremeNestedGroup = true;
}
else
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
if (module.GetCustomAttribute() is not null || extremeNestedGroup)
{
List groupTranslations = null;
if (!string.IsNullOrEmpty(ctx.GroupTranslations))
groupTranslations = JsonConvert.DeserializeObject>(ctx.GroupTranslations)!;
var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations).ConfigureAwait(false);
if (slashGroupsTuple.applicationCommands is not null && slashGroupsTuple.applicationCommands.Count is not 0)
{
if (IsCalledByUnitTest)
unitTestCommands.AddRange(slashGroupsTuple.applicationCommands);
updateList.AddRange(slashGroupsTuple.applicationCommands);
if (Configuration.GenerateTranslationFilesOnly)
{
var cgwsgs = new List();
foreach (var cmd in slashGroupsTuple.applicationCommands)
if (cmd.Type is ApplicationCommandType.ChatInput)
{
var cgs = new List();
var cs2 = new List();
if (cmd.Options is not null)
{
foreach (var scg in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommandGroup))
{
var cs = new List();
if (scg.Options is not null)
foreach (var sc in scg.Options)
if (sc.Options is null || sc.Options.Count is 0)
cs.Add(new(sc.Name, sc.Description, null, null, sc.RawNameLocalizations, sc.RawDescriptionLocalizations));
else
cs.Add(new(sc.Name, sc.Description, [.. sc.Options], null, sc.RawNameLocalizations, sc.RawDescriptionLocalizations));
cgs.Add(new(scg.Name, scg.Description, cs, null, scg.RawNameLocalizations, scg.RawDescriptionLocalizations));
}
foreach (var sc2 in cmd.Options.Where(x => x.Type is ApplicationCommandOptionType.SubCommand))
if (sc2.Options == null || sc2.Options.Count == 0)
cs2.Add(new(sc2.Name, sc2.Description, null, null, sc2.RawNameLocalizations, sc2.RawDescriptionLocalizations));
else
cs2.Add(new(sc2.Name, sc2.Description, [.. sc2.Options], null, sc2.RawNameLocalizations, sc2.RawDescriptionLocalizations));
}
cgwsgs.Add(new(cmd.Name, cmd.Description, cgs, cs2, cmd.Type, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations));
}
if (cgwsgs.Count is not 0)
groupTranslation.AddRange(cgwsgs.Select(cgwsg => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg))!));
}
}
if (slashGroupsTuple.commandTypeSources is not null && slashGroupsTuple.commandTypeSources.Count is not 0)
commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources);
if (slashGroupsTuple.singletonModules is not null && slashGroupsTuple.singletonModules.Count is not 0)
s_singletonModules.AddRange(slashGroupsTuple.singletonModules);
if (slashGroupsTuple.groupCommands is not null && slashGroupsTuple.groupCommands.Count is not 0)
groupCommands.AddRange(slashGroupsTuple.groupCommands);
if (slashGroupsTuple.subGroupCommands is not null && slashGroupsTuple.subGroupCommands.Count is not 0)
subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands);
}
//Handles methods and context menus, only if the module isn't a group itself
if (module.GetCustomAttribute() is null)
{
List? commandTranslations = null;
if (!string.IsNullOrEmpty(ctx.SingleTranslations))
commandTranslations = JsonConvert.DeserializeObject>(ctx.SingleTranslations);
//Slash commands
var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() is not null);
var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations).ConfigureAwait(false);
if (slashCommands.applicationCommands is not null && slashCommands.applicationCommands.Count is not 0)
{
if (IsCalledByUnitTest)
unitTestCommands.AddRange(slashCommands.applicationCommands);
updateList.AddRange(slashCommands.applicationCommands);
if (Configuration.GenerateTranslationFilesOnly)
{
var cs = new List();
foreach (var cmd in slashCommands.applicationCommands.Where(cmd => cmd.Type is ApplicationCommandType.ChatInput && (cmd.Options is null || !cmd.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommand or ApplicationCommandOptionType.SubCommandGroup))))
if (cmd.Options == null || cmd.Options.Count == 0)
cs.Add(new(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations));
else
cs.Add(new(cmd.Name, cmd.Description, [.. cmd.Options], ApplicationCommandType.ChatInput, cmd.RawNameLocalizations, cmd.RawDescriptionLocalizations));
if (cs.Count is not 0)
//translation.AddRange(cs.Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))!));
{
foreach (var c in cs)
{
var json = JsonConvert.SerializeObject(c);
var obj = JsonConvert.DeserializeObject(json);
translation.Add(obj!);
}
}
}
}
if (slashCommands.commandTypeSources is not null && slashCommands.commandTypeSources.Count is not 0)
commandTypeSources.AddRange(slashCommands.commandTypeSources);
if (slashCommands.commandMethods is not null && slashCommands.commandMethods.Count is not 0)
commandMethods.AddRange(slashCommands.commandMethods);
//Context Menus
var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() is not null);
var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations).ConfigureAwait(false);
if (contextCommands.applicationCommands is not null && contextCommands.applicationCommands.Count is not 0)
{
if (IsCalledByUnitTest)
unitTestCommands.AddRange(contextCommands.applicationCommands);
updateList.AddRange(contextCommands.applicationCommands);
if (Configuration.GenerateTranslationFilesOnly)
{
var cs = new List();
foreach (var cmd in contextCommands.applicationCommands)
if (cmd.Type is ApplicationCommandType.Message or ApplicationCommandType.User)
cs.Add(new(cmd.Name, null, null, cmd.Type));
if (cs.Count != 0)
translation.AddRange(cs.Select(c => JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))!));
}
}
if (contextCommands.commandTypeSources is not null && contextCommands.commandTypeSources.Count is not 0)
commandTypeSources.AddRange(contextCommands.commandTypeSources);
if (contextCommands.contextMenuCommands is not null && contextCommands.contextMenuCommands.Count is not 0)
contextMenuCommands.AddRange(contextCommands.contextMenuCommands);
//Accounts for lifespans
if (module.GetCustomAttribute() is not null && module.GetCustomAttribute().Lifespan is ApplicationCommandModuleLifespan.Singleton)
s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider));
}
}
catch (NullReferenceException ex)
{
this.Client.Logger.LogCritical(ex, "NRE Exception thrown: {msg}\nStack: {stack}", ex.Message, ex.StackTrace);
}
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 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 && !IsCalledByUnitTest)
{
updateList = updateList.DistinctBy(x => x.Name).ToList();
if (Configuration.GenerateTranslationFilesOnly)
await this.CheckRegistrationStartup(translation, groupTranslation, guildId);
else
try
{
List commands = [];
try
{
if (guildId is null)
{
if (updateList.Count is not 0)
{
var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList).ConfigureAwait(false);
if (regCommands is not null)
{
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GlobalCommandsInternal.AddRange(actualCommands);
}
}
else if (GlobalDiscordCommands.Count is not 0)
foreach (var cmd in GlobalDiscordCommands)
try
{
await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id).ConfigureAwait(false);
}
catch (NotFoundException)
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete global command {cmdId}. Please clean up manually", cmd.Id);
}
}
else
{
if (updateList.Count is not 0)
{
var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList).ConfigureAwait(false);
if (regCommands is not null)
{
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GuildCommandsInternal.Add(guildId.Value, actualCommands);
try
{
if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild))
guild.InternalRegisteredApplicationCommands.AddRange(actualCommands);
}
catch (NullReferenceException)
{ }
}
}
else if (GuildDiscordCommands.Count is not 0)
foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value)
try
{
await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id).ConfigureAwait(false);
}
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!.TryGetFirstValueWhere(x => x.Name == command.Name, out var com))
com.CommandId = command.Id;
if (groupCommands!.TryGetFirstValueWhere(x => x.Name == command.Name, out var groupCom))
groupCom.CommandId = command.Id;
if (subGroupCommands!.TryGetFirstValueWhere(x => x.Name == command.Name, out var subCom))
subCom.CommandId = command.Id;
if (contextMenuCommands!.TryGetFirstValueWhere(x => x.Name == command.Name, out var cmCom))
cmCom.CommandId = command.Id;
}
//Adds to the global lists finally
CommandMethods.AddRange(commandMethods.DistinctBy(x => x.Name));
GroupCommands.AddRange(groupCommands.DistinctBy(x => x.Name));
SubGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name));
ContextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name));
s_registeredCommands.Add(new(guildId, commands.ToList()));
foreach (var app in commandMethods.Select(command => types.First(t => t.Type == command.Method.DeclaringType)))
{ }
if (guildId.HasValue)
await this._guildApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider)
{
Handled = true,
GuildId = guildId.Value,
RegisteredCommands = GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value ?? []
}).ConfigureAwait(false);
else
await this._globalApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredCommands = GlobalCommandsInternal
}).ConfigureAwait(false);
await this.CheckRegistrationStartup(translation, groupTranslation, guildId);
}
catch (NullReferenceException ex)
{
this.Client.Logger.LogCritical(ex, "NRE Exception thrown: {msg}\nStack: {stack}", ex.Message, ex.StackTrace);
}
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 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;
}
}
else if (IsCalledByUnitTest)
{
CommandMethods.AddRange(commandMethods.DistinctBy(x => x.Name));
GroupCommands.AddRange(groupCommands.DistinctBy(x => x.Name));
SubGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name));
ContextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name));
s_registeredCommands.Add(new(guildId, unitTestCommands.ToList()));
foreach (var app in commandMethods.Select(command => types.First(t => t.Type == command.Method.DeclaringType)))
{ }
if (guildId.HasValue)
await this._guildApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider)
{
Handled = true,
GuildId = guildId.Value,
RegisteredCommands = GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value ?? []
}).ConfigureAwait(false);
else
await this._globalApplicationCommandsRegistered.InvokeAsync(this, new(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredCommands = GlobalCommandsInternal
}).ConfigureAwait(false);
}
}
///
/// Checks the registration startup.
///
/// The optional translations.
/// The optional group translations.
/// The optional guild id.
private async Task CheckRegistrationStartup(List? translation = null, List? groupTranslation = null, ulong? guildId = null)
{
if (Configuration.GenerateTranslationFilesOnly)
{
try
{
if (translation is not null && translation.Count is not 0)
{
var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{(guildId.HasValue ? guildId.Value : "global")}.json";
var fs = File.Create(fileName);
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
await writer.WriteAsync(JsonConvert.SerializeObject(translation.DistinctBy(x => x.Name), Formatting.Indented)).ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false);
ms.Position = 0;
await ms.CopyToAsync(fs).ConfigureAwait(false);
await fs.FlushAsync().ConfigureAwait(false);
fs.Close();
await fs.DisposeAsync().ConfigureAwait(false);
ms.Close();
await ms.DisposeAsync().ConfigureAwait(false);
this.Client.Logger.LogInformation("Exported base translation to {exppath}", fileName);
}
if (groupTranslation is not null && groupTranslation.Count is not 0)
{
var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{(guildId.HasValue ? guildId.Value : "global")}.json";
var fs = File.Create(fileName);
var ms = new MemoryStream();
var writer = new StreamWriter(ms);
await writer.WriteAsync(JsonConvert.SerializeObject(groupTranslation.DistinctBy(x => x.Name), Formatting.Indented)).ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false);
ms.Position = 0;
await ms.CopyToAsync(fs).ConfigureAwait(false);
await fs.FlushAsync().ConfigureAwait(false);
fs.Close();
await fs.DisposeAsync().ConfigureAwait(false);
ms.Close();
await ms.DisposeAsync().ConfigureAwait(false);
this.Client.Logger.LogInformation("Exported base translation to {exppath}", fileName);
}
}
catch (Exception ex)
{
this.Client.Logger.LogError(@"{msg}", ex.Message);
this.Client.Logger.LogError(@"{stack}", ex.StackTrace);
}
await this.Client.DisconnectAsync().ConfigureAwait(false);
}
}
///
/// Checks whether we finished up the complete startup.
///
private async Task CheckStartupFinishAsync(ApplicationCommandsExtension sender, ApplicationCommandsModuleStartupFinishedEventArgs args)
{
if (StartupFinished)
if (!FinishFired)
{
await this._applicationCommandsModuleReady.InvokeAsync(sender, new(Configuration?.ServiceProvider)
{
GuildsWithoutScope = s_missingScopeGuildIdsGlobal
}).ConfigureAwait(false);
FinishFired = true;
if (Configuration.GenerateTranslationFilesOnly)
Environment.Exit(0);
}
args.Handled = false;
}
///
/// 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) || (e.Interaction is { GuildId: not null, AuthorizingIntegrationOwners.GuildInstallKey: not null } && !client.Guilds.ContainsKey(e.Interaction.GuildId.Value)))
{
this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received or wrong shard");
return Task.FromResult(true);
}
HandledInteractions.Add(e.Interaction.Id);
_ = Task.Run(async () =>
{
var type = GetInteractionType(e.Interaction.Data);
switch (e.Interaction.Type)
{
case 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,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds,
UserId = e.Interaction.User.Id,
GuildId = e.Interaction.GuildId,
MemberId = e.Interaction.GuildId is not null ? e.Interaction.User.Id : null,
ChannelId = e.Interaction.ChannelId
};
try
{
if (s_errored)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup.").AsEphemeral()).ConfigureAwait(false);
throw new InvalidOperationException("Application commands failed to register properly on startup.");
}
var methods = CommandMethods.Where(x => x.CommandId == e.Interaction.Data.Id).ToList();
var groups = GroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList();
var subgroups = SubGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList();
if (methods.Count is 0 && groups.Count is 0 && subgroups.Count is 0)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("An application command was executed, but no command was registered for it.").AsEphemeral()).ConfigureAwait(false);
throw new InvalidOperationException($"An application command was executed, but no command was registered for it.\n\tCommand name: {e.Interaction.Data.Name}\n\tCommand ID: {e.Interaction.Data.Id}");
}
switch (type)
{
case ApplicationCommandFinalType.Command when methods.Count is not 0:
{
var method = methods.First().Method;
context.SubCommandName = null;
context.SubSubCommandName = null;
if (DebugEnabled)
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options).ConfigureAwait(false);
await this.RunCommandAsync(context, method, args).ConfigureAwait(false);
break;
}
case ApplicationCommandFinalType.SubCommand when groups.Count is not 0:
{
var command = e.Interaction.Data.Options[0];
var method = groups.First().Methods.First(x => x.Key == command.Name).Value;
context.SubCommandName = command.Name;
context.SubSubCommandName = null;
if (DebugEnabled)
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options).ConfigureAwait(false);
await this.RunCommandAsync(context, method, args).ConfigureAwait(false);
break;
}
case ApplicationCommandFinalType.SubCommandGroup when subgroups.Count is not 0:
{
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[0].Name).Value;
context.SubCommandName = command.Name;
context.SubSubCommandName = command.Options[0].Name;
if (DebugEnabled)
this.Client.Logger.LogDebug("Executing {cmd}", method.Name);
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options[0].Options).ConfigureAwait(false);
await this.RunCommandAsync(context, method, args).ConfigureAwait(false);
break;
}
case ApplicationCommandFinalType.NotDetermined:
default:
throw new ArgumentOutOfRangeException(null, "Could not determine application command type");
}
await this._slashExecuted.InvokeAsync(this, new(this.Client.ServiceProvider)
{
Context = context
}).ConfigureAwait(false);
}
catch (Exception ex)
{
await this._slashError.InvokeAsync(this, new(this.Client.ServiceProvider)
{
Context = context,
Exception = ex
}).ConfigureAwait(false);
this.Client.Logger.LogError(ex, "Error in slash interaction");
}
break;
}
case InteractionType.AutoComplete when s_errored:
throw new InvalidOperationException("Application commands failed to register properly on startup.");
case InteractionType.AutoComplete:
{
var methods = CommandMethods.Where(x => x.CommandId == e.Interaction.Data.Id).ToList();
var groups = GroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList();
var subgroups = SubGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id).ToList();
if (methods.Count is 0 && groups.Count is 0 && subgroups.Count is 0)
throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it");
try
{
switch (type)
{
case ApplicationCommandFinalType.Command when methods.Count is not 0:
{
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,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds
};
var choices = await ((Task>)providerMethod.Invoke(providerInstance, [context])).ConfigureAwait(false);
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)).ConfigureAwait(false);
break;
}
case ApplicationCommandFinalType.SubCommand when groups.Count is not 0:
{
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,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds
};
var choices = await ((Task>)providerMethod.Invoke(providerInstance, [context])).ConfigureAwait(false);
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)).ConfigureAwait(false);
break;
}
case ApplicationCommandFinalType.SubCommandGroup when subgroups.Count is not 0:
{
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[0].Name).Value;
var focusedOption = command.Options[0].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[0].Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale,
AppPermissions = e.Interaction.AppPermissions,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds
};
var choices = await ((Task>)providerMethod.Invoke(providerInstance, [context])).ConfigureAwait(false);
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)).ConfigureAwait(false);
break;
}
}
}
catch (Exception ex)
{
this.Client.Logger.LogError(ex, "Error in autocomplete interaction");
}
break;
}
case InteractionType.Ping:
case InteractionType.Component:
case InteractionType.ModalSubmit:
break;
default:
throw new ArgumentOutOfRangeException(null, "Received out unknown interaction type");
}
});
return Task.CompletedTask;
}
///
/// Gets the interaction type from the interaction data.
///
///
///
private static ApplicationCommandFinalType GetInteractionType(DiscordInteractionData data)
{
var type = ApplicationCommandFinalType.NotDetermined;
if (data.Options.Count is 0)
return ApplicationCommandFinalType.Command;
if (data.Options.All(x =>
x.Type is not ApplicationCommandOptionType.SubCommand
and not ApplicationCommandOptionType.SubCommandGroup))
return ApplicationCommandFinalType.Command;
if (data.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommandGroup))
type = ApplicationCommandFinalType.SubCommandGroup;
else if (data.Options.Any(x => x.Type is ApplicationCommandOptionType.SubCommand))
type = ApplicationCommandFinalType.SubCommand;
return type;
}
///
/// 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);
}
HandledInteractions.Add(e.Interaction.Id);
_ = Task.Run(async () =>
{
//Creates the context
var context = new ContextMenuContext(e.Type switch
{
ApplicationCommandType.User => DisCatSharpCommandType.UserCommand,
ApplicationCommandType.Message => DisCatSharpCommandType.MessageCommand,
_ => throw new ArgumentOutOfRangeException(nameof(e.Type), "Unknown context menu type")
})
{
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,
Entitlements = e.Interaction.Entitlements,
EntitlementSkuIds = e.Interaction.EntitlementSkuIds,
UserId = e.Interaction.User.Id,
GuildId = e.Interaction.GuildId,
MemberId = e.Interaction.GuildId is not null ? e.Interaction.User.Id : null,
ChannelId = e.Interaction.ChannelId
};
try
{
if (s_errored)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Context menus failed to register properly on startup.").AsEphemeral()).ConfigureAwait(false);
throw new InvalidOperationException("Context menus failed to register properly on startup.");
}
//Gets the method for the command
var method = ContextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id);
if (method == null)
{
await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A context menu command was executed, but no command was registered for it.").AsEphemeral()).ConfigureAwait(false);
throw new InvalidOperationException("A context menu command was executed, but no command was registered for it.");
}
await this.RunCommandAsync(context, method.Method, new[] { context }).ConfigureAwait(false);
await this._contextMenuExecuted.InvokeAsync(this, new(this.Client.ServiceProvider)
{
Context = context
}).ConfigureAwait(false);
}
catch (Exception ex)
{
await this._contextMenuErrored.InvokeAsync(this, new(this.Client.ServiceProvider)
{
Context = context,
Exception = ex
}).ConfigureAwait(false);
}
});
return Task.CompletedTask;
}
///
/// Runs a command.
///
/// The base context.
/// The method info.
/// The arguments.
internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args)
{
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;
var classInstance = moduleLifespan switch
{
ApplicationCommandModuleLifespan.Scoped =>
//Accounts for static methods and adds DI
method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider),
ApplicationCommandModuleLifespan.Transient =>
//Accounts for static methods and adds DI
method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider),
//If singleton, gets it from the singleton list
ApplicationCommandModuleLifespan.Singleton => s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)),
_ => throw new($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}")
};
ApplicationCommandsModule module = null;
if (classInstance is ApplicationCommandsModule mod)
module = mod;
switch (context)
{
// Slash commands
case InteractionContext slashContext:
{
await RunPreexecutionChecksAsync(method, slashContext).ConfigureAwait(false);
var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)).ConfigureAwait(false);
if (shouldExecute)
{
if (AutoDeferEnabled)
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource).ConfigureAwait(false);
await ((Task)method.Invoke(classInstance, args.ToArray())).ConfigureAwait(false);
await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask).ConfigureAwait(false);
}
break;
}
// Context menus
case ContextMenuContext contextMenuContext:
{
await RunPreexecutionChecksAsync(method, contextMenuContext).ConfigureAwait(false);
var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)).ConfigureAwait(false);
if (shouldExecute)
{
if (AutoDeferEnabled)
await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource).ConfigureAwait(false);
await ((Task)method.Invoke(classInstance, args.ToArray())).ConfigureAwait(false);
await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask).ConfigureAwait(false);
}
break;
}
}
}
///
/// 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 is not 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 is { IsStatic: false, IsPublic: true });
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, MethodBase method, IReadOnlyList options)
{
var args = new List
{
context
};
var parameters = method.GetParameters().Skip(1).ToList();
foreach (var parameter in parameters)
//Accounts for optional arguments without values given
if (parameter.IsOptional && (options is 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 is 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).ConfigureAwait(false));
}
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 static async Task RunPreexecutionChecksAsync(MemberInfo method, BaseContext context)
{
switch (context)
{
case 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 is not 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).ConfigureAwait(false);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value is false))
throw new SlashExecutionChecksFailedException
{
FailedChecks = dict.Where(x => x.Value is false).Select(x => x.Key).ToList()
};
break;
}
case ContextMenuContext cMctx:
{
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType!.GetCustomAttributes());
if (method.DeclaringType?.DeclaringType is not 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).ConfigureAwait(false);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value is false))
throw new ContextMenuExecutionChecksFailedException
{
FailedChecks = dict.Where(x => x.Value is false).Select(x => x.Key).ToList()
};
break;
}
}
}
///
/// Gets the choice attributes from choice provider.
///
/// The custom attributes.
/// The optional guild id
private static async Task> GetChoiceAttributesFromProvider(List customAttributes, ulong? guildId = null)
{
var choices = new List();
foreach (var choiceProviderAttribute in customAttributes)
{
var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider));
if (method is 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)!).ConfigureAwait(false)).ToList();
if (result.Count is not 0)
choices.AddRange(result);
}
}
return choices;
}
///
/// Gets the choice attributes from enum parameter.
///
/// The enum parameter.
private static List GetChoiceAttributesFromEnumParameter(Type enumParam)
=> (from Enum enumValue in Enum.GetValues(enumParam) select new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())).ToList();
///
/// 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(List choiceAttributes) =>
choiceAttributes.Count is 0
? []
: choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList();
///
/// Parses the parameters.
///
/// The parameters.
/// The command name.
/// The optional guild id.
internal static async Task> ParseParametersAsync(IEnumerable parameters, string commandName, ulong? guildId)
{
var options = new List();
foreach (var parameter in parameters)
{
//Gets the attribute
var optionAttribute = parameter.GetCustomAttribute() ?? throw new ArgumentException($"One or more arguments of the command '{commandName}' are missing 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();
switch (optionAttribute.Autocomplete)
{
case true when autocompleteAttribute == null:
throw new ArgumentException($"The command '{commandName}' has autocomplete enabled but is missing an autocomplete attribute!");
case false when autocompleteAttribute != null:
throw new ArgumentException($"The command '{commandName}' has an autocomplete provider but the option to have autocomplete set to false!");
}
//Sets the type
var type = parameter.ParameterType;
var parameterType = GetParameterType(type);
switch (parameterType)
{
case ApplicationCommandOptionType.String:
minimumValue = null;
maximumValue = null;
break;
case ApplicationCommandOptionType.Integer:
case ApplicationCommandOptionType.Number:
minimumLength = null;
maximumLength = null;
break;
case not ApplicationCommandOptionType.Channel:
channelTypes = null;
break;
}
//Handles choices
//From attributes
var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes().ToList());
//From enums
if (parameter.ParameterType.IsEnum)
choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType);
//From choice provider
var choiceProviders = parameter.GetCustomAttributes().ToList();
if (choiceProviders.Count is not 0)
choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId).ConfigureAwait(false);
options.Add(new(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();
_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);
}
///
/// Fires the slash command error event.
///
private readonly 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);
}
///
/// Fires the slash command executed event.
///
private readonly 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);
}
///
/// Fires the context menu error event.
///
private readonly 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);
}
///
/// Fires the context menu executed event.
///
private readonly AsyncEvent _contextMenuExecuted;
}
///
/// Holds configuration data for setting up an application command.
///
internal sealed 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 sealed class CommandMethod
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; init; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; init; }
}
///
/// The group command.
///
internal sealed class GroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; init; }
///
/// Gets or sets the methods.
///
public List> Methods { get; init; } = [];
}
///
/// The sub group command.
///
internal sealed class SubGroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; init; }
///
/// Gets or sets the sub commands.
///
public List SubCommands { get; set; } = [];
}
///
/// The context menu command.
///
internal sealed class ContextMenuCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; init; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; init; }
}
#region Default Help
///
/// Represents the default help module.
///
internal sealed class DefaultHelpModule : ApplicationCommandsModule
{
public sealed class DefaultHelpAutoCompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
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).ConfigureAwait(false);
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).ConfigureAwait(false);
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();
}
var options = slashCommands.Take(25).Select(sc => new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())).ToList();
return options.AsEnumerable();
}
}
public sealed 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).ConfigureAwait(false);
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).ConfigureAwait(false);
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("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();
options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())));
}
return options.AsEnumerable();
}
}
public sealed 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).ConfigureAwait(false);
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).ConfigureAwait(false);
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("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?.Options is null)
options.Add(new("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();
options.AddRange(opt.Take(25).Select(option => 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).ConfigureAwait(false);
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).ConfigureAwait(false);
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")).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"There are no slash commands").AsEphemeral()).ConfigureAwait(false);
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("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.AddEmbed(discordEmbed)).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false);
}
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("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false);
}
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}")).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral()).ConfigureAwait(false);
return;
}
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{command.Mention}: {command.Description ?? "No description provided."}"
}.AddField(new("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("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false);
}
}
}
#endregion
#region Default User Apps Help
///
/// Represents the default user apps help module.
///
internal sealed class DefaultUserAppsHelpModule : ApplicationCommandsModule
{
public sealed class DefaultUserAppsHelpAutoCompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
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).ConfigureAwait(false);
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 globalCommandsTask.ConfigureAwait(false);
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();
}
var options = slashCommands.Take(25).Select(sc => new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())).ToList();
return options.AsEnumerable();
}
}
public sealed class DefaultUserAppsHelpAutoCompleteLevelOneProvider : 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).ConfigureAwait(false);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await globalCommandsTask.ConfigureAwait(false);
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("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();
options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())));
}
return options.AsEnumerable();
}
}
public sealed class DefaultUserAppsHelpAutoCompleteLevelTwoProvider : 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).ConfigureAwait(false);
slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
}
else
{
await globalCommandsTask.ConfigureAwait(false);
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("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?.Options is null)
options.Add(new("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();
options.AddRange(opt.Take(25).Select(option => new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())));
}
return options.AsEnumerable();
}
}
- [SlashCommand("help", "Displays command help", false, [ApplicationCommandContexts.Guild, ApplicationCommandContexts.BotDm, ApplicationCommandContexts.PrivateChannel], [ApplicationCommandIntegrationTypes.GuildInstall, ApplicationCommandIntegrationTypes.UserInstall])]
+ [SlashCommand("help", "Displays command help", false, [InteractionContextType.Guild, InteractionContextType.BotDm, InteractionContextType.PrivateChannel], [ApplicationCommandIntegrationTypes.GuildInstall, ApplicationCommandIntegrationTypes.UserInstall])]
internal async Task DefaulUserAppstHelpAsync(
InteractionContext ctx,
[Autocomplete(typeof(DefaultUserAppsHelpAutoCompleteProvider)), Option("option_one", "top level command to provide help for", true)]
string commandName,
[Autocomplete(typeof(DefaultUserAppsHelpAutoCompleteLevelOneProvider)), Option("option_two", "subgroup or command to provide help for", true)]
string commandOneName = null,
[Autocomplete(typeof(DefaultUserAppsHelpAutoCompleteLevelTwoProvider)), 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).ConfigureAwait(false);
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 globalCommandsTask.ConfigureAwait(false);
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")).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"There are no slash commands").AsEphemeral()).ConfigureAwait(false);
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("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder()
.AddEmbed(discordEmbed)).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false);
}
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("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false);
}
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}")).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral()).ConfigureAwait(false);
return;
}
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{command.Mention}: {command.Description ?? "No description provided."}"
}.AddField(new("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("Arguments", sb.ToString().Trim()));
}
if (ApplicationCommandsExtension.Configuration.AutoDefer)
await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)).ConfigureAwait(false);
else
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral()).ConfigureAwait(false);
}
}
}
#endregion
diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs
index 5632a4306..04e827421 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs
@@ -1,141 +1,141 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DisCatSharp.Enums;
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Represents a with the type of or .
///
[AttributeUsage(AttributeTargets.Method)]
public sealed class ContextMenuAttribute : Attribute
{
///
/// Gets the name of this context menu.
///
public string Name { get; internal set; }
///
/// Gets the type of this context menu.
///
public ApplicationCommandType Type { get; internal set; }
///
/// Gets this context menu's needed permissions.
///
public Permissions? DefaultMemberPermissions { get; internal set; }
///
/// Gets the allowed contexts of this context menu.
///
- public List? AllowedContexts { get; set; }
+ public List? AllowedContexts { get; set; }
///
/// Gets the allowed integration types of this context menu.
///
public List? IntegrationTypes { get; set; }
///
/// Gets whether this context menu can be used in direct messages.
///
public bool? DmPermission { get; set; }
///
/// Gets whether this context menu is marked as NSFW.
///
public bool IsNsfw { get; set; }
///
/// Marks this method as a context menu.
///
/// The type of the context menu.
/// The name of the context menu.
/// Whether the context menu is marked as NSFW.
/// The allowed contexts of the context menu.
/// The allowed integration types of the context menu.
- public ContextMenuAttribute(ApplicationCommandType type, string name, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public ContextMenuAttribute(ApplicationCommandType type, string name, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
if (type == ApplicationCommandType.ChatInput)
throw new ArgumentException("Context menus cannot be of type ChatInput (Slash).");
this.Type = type;
this.Name = name;
this.DefaultMemberPermissions = null;
this.DmPermission = null;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a context menu.
///
/// The type of the context menu.
/// The name of the context menu.
/// The default member permissions of the context menu.
/// Whether the context menu is marked as NSFW.
/// The allowed contexts of the context menu.
/// The allowed integration types of the context menu.
- public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
if (type == ApplicationCommandType.ChatInput)
throw new ArgumentException("Context menus cannot be of type ChatInput (Slash).");
this.Type = type;
this.Name = name;
this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions;
this.DmPermission = null;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as context menu.
///
/// The type of the context menu.
/// The name of the context menu.
/// The dm permission of the context menu.
/// Whether the context menu is marked as NSFW.
/// The allowed contexts of the context menu.
/// The allowed integration types of the context menu.
- public ContextMenuAttribute(ApplicationCommandType type, string name, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public ContextMenuAttribute(ApplicationCommandType type, string name, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
if (type == ApplicationCommandType.ChatInput)
throw new ArgumentException("Context menus cannot be of type ChatInput (Slash).");
this.Type = type;
this.Name = name;
this.DefaultMemberPermissions = null;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a context menu.
///
/// The type of the context menu.
/// The name of the context menu.
/// The default member permissions of the context menu.
/// The dm permission of the context menu.
/// Whether the context menu is marked as NSFW.
/// The allowed contexts of the context menu.
/// The allowed integration types of the context menu.
- public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
if (type == ApplicationCommandType.ChatInput)
throw new ArgumentException("Context menus cannot be of type ChatInput (Slash).");
this.Type = type;
this.Name = name;
this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs
index 7dec292a2..56633f0c9 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs
@@ -1,129 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DisCatSharp.Enums;
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Represents a .
///
[AttributeUsage(AttributeTargets.Method)]
public class SlashCommandAttribute : Attribute
{
///
/// Gets the name of this command.
///
public string Name { get; set; }
///
/// Gets the description of this command.
///
public string Description { get; set; }
///
/// Gets the needed permission of this command.
///
public Permissions? DefaultMemberPermissions { get; set; }
///
/// Gets the allowed contexts of this command.
///
- public List? AllowedContexts { get; set; }
+ public List? AllowedContexts { get; set; }
///
/// Gets the allowed integration types of this command.
///
public List? IntegrationTypes { get; set; }
///
/// Gets the dm permission of this command.
///
public bool? DmPermission { get; set; }
///
/// Gets whether this command is marked as NSFW.
///
public bool IsNsfw { get; set; }
///
/// Marks this method as a slash command.
///
/// The name of this slash command.
/// The description of this slash command.
/// Whether this command is marked as NSFW.
/// The allowed contexts of this slash command.
/// The allowed integration types.
- public SlashCommandAttribute(string name, string description, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandAttribute(string name, string description, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = null;
this.DmPermission = null;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a slash command.
///
/// The name of this slash command.
/// The description of this slash command.
/// The default member permissions.
/// Whether this command is marked as NSFW.
/// The allowed contexts of this slash command.
/// The allowed integration types.
- public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions;
this.DmPermission = null;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a slash command.
///
/// The name of this slash command.
/// The description of this slash command.
/// The dm permission.
/// Whether this command is marked as NSFW.
/// The allowed contexts of this slash command.
/// The allowed integration types.
- public SlashCommandAttribute(string name, string description, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandAttribute(string name, string description, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = null;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a slash command.
///
/// The name of this slash command.
/// The description of this slash command.
/// The default member permissions.
/// The dm permission.
/// Whether this command is marked as NSFW.
/// The allowed contexts of this slash command.
/// The allowed integration types.
- public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
}
diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs
index c0ac7557c..a13f89de5 100644
--- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs
+++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs
@@ -1,129 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DisCatSharp.Enums;
namespace DisCatSharp.ApplicationCommands.Attributes;
///
/// Represents a group.
///
[AttributeUsage(AttributeTargets.Class)]
public class SlashCommandGroupAttribute : Attribute
{
///
/// Gets the name of this slash command group.
///
public string Name { get; set; }
///
/// Gets the description of this slash command group.
///
public string Description { get; set; }
///
/// Gets the needed permission of this slash command group.
///
public Permissions? DefaultMemberPermissions { get; set; }
///
/// Gets the allowed contexts of this slash command group.
///
- public List? AllowedContexts { get; set; }
+ public List? AllowedContexts { get; set; }
///
/// Gets the allowed integration types of this slash command group.
///
public List? IntegrationTypes { get; set; }
///
/// Gets the dm permission of this slash command group.
///
public bool? DmPermission { get; set; }
///
/// Gets whether this slash command group is marked as NSFW.
///
public bool IsNsfw { get; set; }
///
/// Marks this class as a slash command group.
///
/// The name of the slash command group.
/// The description of the slash command group.
/// Whether the slash command group is marked as NSFW.
/// The allowed contexts of the slash command group.
/// The allowed integration types of the slash command group.
- public SlashCommandGroupAttribute(string name, string description, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandGroupAttribute(string name, string description, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = null;
this.DmPermission = null;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a slash command group.
///
/// The name of the slash command group.
/// The description of the slash command group.
/// The default member permissions of the slash command group.
/// Whether the slash command group is marked as NSFW.
/// The allowed contexts of the slash command group.
/// The allowed integration types of the slash command group.
- public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions;
this.DmPermission = null;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a slash command group.
///
/// The name of the slash command group.
/// The description of the slash command group.
/// The dm permission of the slash command group.
/// Whether the slash command group is marked as NSFW.
/// The allowed contexts of the slash command group.
/// The allowed integration types of the slash command group.
- public SlashCommandGroupAttribute(string name, string description, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandGroupAttribute(string name, string description, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = null;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
///
/// Marks this method as a slash command group.
///
/// The name of the slash command group.
/// The description of the slash command group.
/// The default member permissions of the slash command group.
/// The dm permission of the slash command group.
/// Whether the slash command group is marked as NSFW.
/// The allowed contexts of the slash command group.
/// The allowed integration types of the slash command group.
- public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, ApplicationCommandContexts[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
+ public SlashCommandGroupAttribute(string name, string description, long defaultMemberPermissions, bool dmPermission, bool isNsfw = false, InteractionContextType[]? allowedContexts = null, ApplicationCommandIntegrationTypes[]? integrationTypes = null)
{
this.Name = name.ToLower();
this.Description = description;
this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts?.ToList();
this.IntegrationTypes = integrationTypes?.ToList();
}
}
diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs
index 5ca8cfdd4..c03874269 100644
--- a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs
+++ b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs
@@ -1,231 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DisCatSharp.Attributes;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents a command that is registered to an application.
///
public class DiscordApplicationCommand : SnowflakeObject, IEquatable
{
///
/// Gets the type of this application command.
///
[JsonProperty("type")]
public ApplicationCommandType Type { get; internal set; }
///
/// Gets the unique ID of this command's application.
///
[JsonProperty("application_id")]
public ulong ApplicationId { get; internal set; }
///
/// Gets the name of this command.
///
[JsonProperty("name")]
public string Name { get; internal set; }
///
/// Sets the name localizations.
///
[JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary? RawNameLocalizations { get; set; }
///
/// Gets the name localizations.
///
[JsonIgnore]
public DiscordApplicationCommandLocalization? NameLocalizations
=> this.RawNameLocalizations != null ? new(this.RawNameLocalizations) : null;
///
/// Gets the description of this command.
///
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
public string? Description { get; internal set; }
///
/// Sets the description localizations.
///
[JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
internal Dictionary? RawDescriptionLocalizations { get; set; }
///
/// Gets the description localizations.
///
[JsonIgnore]
public DiscordApplicationCommandLocalization? DescriptionLocalizations
=> this.RawDescriptionLocalizations != null ? new(this.RawDescriptionLocalizations) : null;
///
/// Gets the potential parameters for this command.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
public List? Options { get; internal set; } = null;
///
/// Gets the commands needed permissions.
///
[JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)]
public Permissions? DefaultMemberPermissions { get; internal set; } = null;
///
/// Gets whether the command can be used in direct messages.
///
[JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)]
public bool? DmPermission { get; internal set; }
///
/// Gets where the application command can be used.
///
[JsonProperty("contexts", NullValueHandling = NullValueHandling.Ignore), DiscordUnreleased]
- public List? AllowedContexts { get; internal set; }
+ public List? AllowedContexts { get; internal set; }
///
/// Gets the application command allowed integration types.
///
[JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore), DiscordUnreleased]
public List? IntegrationTypes { get; internal set; }
///
/// Gets whether the command is marked as NSFW.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public bool IsNsfw { get; internal set; } = false;
///
/// Gets the version number for this command.
///
[JsonProperty("version")]
public ulong Version { get; internal set; }
///
/// Gets the mention for this command.
///
[JsonIgnore]
public string Mention
=> this.Type == ApplicationCommandType.ChatInput ? $"{this.Name}:{this.Id}>" : this.Name;
///
/// Creates a new instance of a .
///
/// The name of the command.
/// The description of the command.
/// Optional parameters for this command.
/// The type of the command. Defaults to ChatInput.
/// The localizations of the command name.
/// The localizations of the command description.
/// The default member permissions.
/// The dm permission.
/// Whether this command is NSFW.
/// Where the command can be used.
/// The allowed integration types.
public DiscordApplicationCommand(
string name,
string? description,
IEnumerable? options = null,
ApplicationCommandType type = ApplicationCommandType.ChatInput,
DiscordApplicationCommandLocalization? nameLocalizations = null,
DiscordApplicationCommandLocalization? descriptionLocalizations = null,
Permissions? defaultMemberPermissions = null,
bool? dmPermission = null,
bool isNsfw = false,
- List? allowedContexts = null,
+ List? allowedContexts = null,
List? integrationTypes = null
)
: base(["guild_id", "name_localizations", "description_localizations"])
{
if (type is ApplicationCommandType.ChatInput)
{
if (!Utilities.IsValidSlashCommandName(name))
throw new ArgumentException("Invalid slash command name specified. It must be below 32 characters and not contain any whitespace.", nameof(name));
if (name.Any(char.IsUpper))
throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name));
if (description?.Length > 100)
throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description));
if (string.IsNullOrWhiteSpace(description))
throw new ArgumentException("Slash commands need a description.", nameof(description));
this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs();
this.RawDescriptionLocalizations = descriptionLocalizations?.GetKeyValuePairs();
}
else
{
if (!string.IsNullOrWhiteSpace(description))
throw new ArgumentException("Context menus do not support descriptions.");
if (options?.Any() ?? false)
throw new ArgumentException("Context menus do not support options.");
description = string.Empty;
this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs();
}
var optionsList = options != null && options.Any() ? options.ToList() : null;
this.Type = type;
this.Name = name;
this.Description = description;
this.Options = optionsList;
this.DefaultMemberPermissions = defaultMemberPermissions;
this.DmPermission = dmPermission;
this.IsNsfw = isNsfw;
this.AllowedContexts = allowedContexts;
this.IntegrationTypes = integrationTypes;
}
///
/// Creates a new empty Discord Application Command.
///
internal DiscordApplicationCommand()
: base(["name_localizations", "description_localizations", "guild_id"]) // Why tf is that so inconsistent?!
{ }
///
/// Checks whether this object is equal to another object.
///
/// The command to compare to.
/// Whether the command is equal to this .
public bool Equals(DiscordApplicationCommand other)
=> this.Id == other.Id;
///
/// Determines if two objects are equal.
///
/// The first command object.
/// The second command object.
/// Whether the two objects are equal.
public static bool operator ==(DiscordApplicationCommand e1, DiscordApplicationCommand e2)
=> e1.Equals(e2);
///
/// Determines if two objects are not equal.
///
/// The first command object.
/// The second command object.
/// Whether the two objects are not equal.
public static bool operator !=(DiscordApplicationCommand e1, DiscordApplicationCommand e2)
=> !(e1 == e2);
///
/// Determines if a is equal to the current .
///
/// The object to compare to.
/// Whether the two objects are not equal.
public override bool Equals(object other)
=> other is DiscordApplicationCommand dac && this.Equals(dac);
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
=> this.Id.GetHashCode();
}
diff --git a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
index e5d3c1fb1..00a8c37fd 100644
--- a/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
+++ b/DisCatSharp/Entities/Interaction/DiscordInteraction.cs
@@ -1,253 +1,253 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DisCatSharp.Attributes;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Entities;
///
/// Represents an interaction that was invoked.
///
public sealed class DiscordInteraction : SnowflakeObject
{
///
/// Gets the type of interaction invoked.
///
[JsonProperty("type")]
public InteractionType Type { get; internal set; }
///
/// Gets the command data for this interaction.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionData Data { get; internal set; }
///
/// Gets the Id of the guild that invoked this interaction, if any.
///
[JsonIgnore]
public ulong? GuildId { get; internal set; }
///
/// Gets the guild that invoked this interaction.
///
[JsonIgnore] // TODO: Is now also "guild"
public DiscordGuild Guild
=> (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId);
///
/// Gets the Id of the channel that invoked this interaction.
///
[JsonIgnore]
public ulong ChannelId { get; internal set; }
///
/// Gets the channel that invoked this interaction.
///
[JsonIgnore] // TODO: Is now also partial "channel"
public DiscordChannel Channel
=> (this.Discord as DiscordClient).InternalGetCachedChannel(this.ChannelId) ?? (DiscordChannel)(this.Discord as DiscordClient).InternalGetCachedThread(this.ChannelId) ?? (this.Guild == null
? new DiscordDmChannel
{
Id = this.ChannelId,
Type = ChannelType.Private,
Discord = this.Discord
}
: new DiscordChannel()
{
Id = this.ChannelId,
Discord = this.Discord
});
///
/// Gets the user that invoked this interaction.
/// This can be cast to a if created in a guild.
///
[JsonIgnore]
public DiscordUser User { get; internal set; }
///
/// Gets the continuation token for responding to this interaction.
///
[JsonProperty("token")]
public string Token { get; internal set; }
///
/// Gets the version number for this interaction type.
///
[JsonProperty("version")]
public int Version { get; internal set; }
///
/// Gets the ID of the application that created this interaction.
///
[JsonProperty("application_id")]
public ulong ApplicationId { get; internal set; }
///
/// The message this interaction was created with, if any.
///
[JsonProperty("message")]
internal DiscordMessage Message { get; set; }
///
/// Gets the invoking user locale.
///
[JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)]
public string Locale { get; internal set; }
///
/// Gets the guild locale if applicable.
///
[JsonProperty("guild_locale", NullValueHandling = NullValueHandling.Ignore)]
public string GuildLocale { get; internal set; }
///
/// Gets the applications permissions.
///
[JsonProperty("app_permissions", NullValueHandling = NullValueHandling.Ignore)]
public Permissions AppPermissions { get; internal set; }
///
/// Gets the entitlements.
/// This is related to premium subscriptions for bots.
/// Can only be used if you have an associated application subscription sku.
///
[JsonProperty("entitlements", NullValueHandling = NullValueHandling.Ignore), DiscordInExperiment("Currently in closed beta."), Experimental("We provide this type but can't provide support.")]
public List Entitlements { get; internal set; } = [];
///
/// Gets the entitlement sku ids.
/// This is related to premium subscriptions for bots.
/// Can only be used if you have an associated application subscription sku.
///
[JsonProperty("entitlement_sku_ids", NullValueHandling = NullValueHandling.Ignore), DiscordInExperiment("Currently in closed beta."), Experimental("We provide this type but can't provide support.")]
public List EntitlementSkuIds { get; internal set; } = [];
///
/// Gets which integrations authorized the interaction.
///
[JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)]
public AuthorizingIntegrationOwners? AuthorizingIntegrationOwners { get; internal set; }
///
/// Gets the interaction's calling context.
///
[JsonProperty("context", NullValueHandling = NullValueHandling.Ignore)]
- public ApplicationCommandContexts Context { get; internal set; }
+ public InteractionContextType Context { get; internal set; }
///
/// Creates a response to this interaction.
///
/// The type of the response.
/// The data, if any, to send.
public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null)
=> this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder);
///
/// Creates a modal response to this interaction.
///
/// The data to send.
public Task CreateInteractionModalResponseAsync(DiscordInteractionModalBuilder builder)
=> this.Type != InteractionType.Ping && this.Type != InteractionType.ModalSubmit ? this.Discord.ApiClient.CreateInteractionModalResponseAsync(this.Id, this.Token, InteractionResponseType.Modal, builder) : throw new NotSupportedException("You can't respond to a PING with a modal.");
// TODO: Add hints support
///
/// Creates an iframe response to this interaction.
///
/// The custom id of the iframe.
/// The title of the iframe.
/// The size of the iframe.
/// The path of the iframe. Uses %application_id%.discordsays.com/:iframe_path .
public Task CreateInteractionIframeResponseAsync(string customId, string title, IframeModalSize modalSize = IframeModalSize.Normal, string? iFramePath = null)
=> this.Type != InteractionType.Ping ? this.Discord.ApiClient.CreateInteractionIframeResponseAsync(this.Id, this.Token, InteractionResponseType.Iframe, customId, title, modalSize, iFramePath) : throw new NotSupportedException("You can't respond to a PING with an iframe.");
///
/// Gets the original interaction response.
///
/// The original message that was sent. This does not work on ephemeral messages.
public Task GetOriginalResponseAsync()
=> this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token);
///
/// Edits the original interaction response.
///
/// The webhook builder.
/// The edited .
public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder)
{
builder.Validate(isInteractionResponse: true);
if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value)
{
var attachments = this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token).Result.Attachments;
if (attachments?.Count > 0)
builder.AttachmentsInternal.AddRange(attachments);
}
else if (builder.KeepAttachmentsInternal.HasValue)
builder.AttachmentsInternal.Clear();
return await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false);
}
///
/// Deletes the original interaction response.
/// >
public Task DeleteOriginalResponseAsync()
=> this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token);
///
/// Creates a follow up message to this interaction.
///
/// The webhook builder.
/// The created .
public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder)
{
builder.Validate();
return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder).ConfigureAwait(false);
}
///
/// Gets a follow up message.
///
/// The id of the follow up message.
public Task GetFollowupMessageAsync(ulong messageId)
=> this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId);
///
/// Edits a follow up message.
///
/// The id of the follow up message.
/// The webhook builder.
/// The edited .
public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder)
{
builder.Validate(isFollowup: true);
if (builder.KeepAttachmentsInternal.HasValue && builder.KeepAttachmentsInternal.Value)
{
var attachments = this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId).Result.Attachments;
if (attachments?.Count > 0)
builder.AttachmentsInternal.AddRange(attachments);
}
else if (builder.KeepAttachmentsInternal.HasValue)
builder.AttachmentsInternal.Clear();
return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder).ConfigureAwait(false);
}
///
/// Deletes a follow up message.
///
/// The id of the follow up message.
public Task DeleteFollowupMessageAsync(ulong messageId)
=> this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId);
internal DiscordInteraction()
: base(["member", "guild_id", "channel_id", "channel", "guild", "user"])
{ }
}
diff --git a/DisCatSharp/Enums/Application/ApplicationCommandContexts.cs b/DisCatSharp/Enums/Interaction/InteractionContextType.cs
similarity index 79%
rename from DisCatSharp/Enums/Application/ApplicationCommandContexts.cs
rename to DisCatSharp/Enums/Interaction/InteractionContextType.cs
index 20052cd08..03aae9926 100644
--- a/DisCatSharp/Enums/Application/ApplicationCommandContexts.cs
+++ b/DisCatSharp/Enums/Interaction/InteractionContextType.cs
@@ -1,22 +1,22 @@
namespace DisCatSharp.Enums;
///
-/// Represents where application commands can be used.
+/// Represents the interaction context type.
///
-public enum ApplicationCommandContexts
+public enum InteractionContextType
{
///
/// Command can be used in guilds.
///
Guild = 0,
///
/// Command can be used in direct messages with the bot.
///
BotDm = 1,
///
/// Command can be used in group direct messages and direct messages.
///
PrivateChannel = 2
}
diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs
index 2c5d079cc..3e93f0eb6 100644
--- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs
+++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs
@@ -1,323 +1,323 @@
using System.Collections.Generic;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using Newtonsoft.Json;
namespace DisCatSharp.Net.Abstractions;
///
/// Represents a application command create payload.
///
internal sealed class RestApplicationCommandCreatePayload : ObservableApiObject
{
///
/// Gets the type.
///
[JsonProperty("type")]
public ApplicationCommandType Type { get; set; }
///
/// Gets the name.
///
[JsonProperty("name")]
public string Name { get; set; }
///
/// Gets the name localizations.
///
[JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Optional?> NameLocalizations { get; set; }
///
/// Gets the description.
///
[JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)]
public string? Description { get; set; }
///
/// Gets the description localizations.
///
[JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Optional?> DescriptionLocalizations { get; set; }
///
/// Gets the options.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable? Options { get; set; }
///
/// Whether the command is allowed for everyone.
///
[JsonProperty("default_permission", NullValueHandling = NullValueHandling.Include)]
public bool? DefaultPermission { get; set; } = null;
///
/// The command needed permissions.
///
[JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)]
public Permissions? DefaultMemberPermission { get; set; }
///
/// Whether the command is allowed for dms.
///
[JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)]
public bool? DmPermission { get; set; }
///
/// Whether the command is marked as NSFW.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public bool Nsfw { get; set; }
///
/// Gets where the command is allowed at.
///
[JsonProperty("contexts", NullValueHandling = NullValueHandling.Include)]
- public List? AllowedContexts { get; set; }
+ public List? AllowedContexts { get; set; }
///
/// Gets the allowed integration types.
///
[JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)]
public List? IntegrationTypes { get; set; }
}
///
/// Represents a application command edit payload.
///
internal sealed class RestApplicationCommandEditPayload : ObservableApiObject
{
///
/// Gets the name.
///
[JsonProperty("name")]
public Optional Name { get; set; }
///
/// Gets the name localizations.
///
[JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Optional?> NameLocalizations { get; set; }
///
/// Gets the description.
///
[JsonProperty("description")]
public Optional Description { get; set; }
///
/// Gets the description localizations.
///
[JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Optional?> DescriptionLocalizations { get; set; }
///
/// Gets the options.
///
[JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)]
public Optional?> Options { get; set; }
///
/// The command needed permissions.
///
[JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Include)]
public Optional DefaultMemberPermission { get; set; }
///
/// Whether the command is allowed for dms.
///
[JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Include)]
public Optional DmPermission { get; set; }
///
/// Whether the command is marked as NSFW.
///
[JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)]
public Optional Nsfw { get; set; }
///
/// Gets where the command is allowed at.
///
[JsonProperty("contexts", NullValueHandling = NullValueHandling.Include)]
- public Optional?> AllowedContexts { get; set; }
+ public Optional?> AllowedContexts { get; set; }
///
/// Gets the allowed integration types.
///
[JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)]
public Optional?> IntegrationTypes { get; set; }
}
///
/// Represents an interaction response payload.
///
internal sealed class RestInteractionResponsePayload : ObservableApiObject
{
///
/// Gets the type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public InteractionResponseType Type { get; set; }
///
/// Gets the data.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionApplicationCommandCallbackData Data { get; set; }
///
/// Gets the attachments.
///
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
public List Attachments { get; set; }
// TODO: Implement if it gets added to the api
///
/// Gets the callback hints.
///
[JsonProperty("hints", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList? CallbackHints { get; set; }
}
///
/// Represents an interaction response modal payload.
///
internal sealed class RestInteractionModalResponsePayload : ObservableApiObject
{
///
/// Gets the type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public InteractionResponseType Type { get; set; }
///
/// Gets the data.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionApplicationCommandModalCallbackData Data { get; set; }
// TODO: Implement if it gets added to the api
///
/// Gets the callback hints.
///
[JsonProperty("hints", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList? CallbackHints { get; set; }
}
///
/// Represents an interaction response iFrame payload.
///
internal sealed class RestInteractionIframeResponsePayload : ObservableApiObject
{
///
/// Gets the type.
///
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
public InteractionResponseType Type { get; set; }
///
/// Gets the data.
///
[JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)]
public DiscordInteractionApplicationCommandIframeCallbackData Data { get; set; }
// TODO: Implement if it gets added to the api
///
/// Gets the callback hints.
///
[JsonProperty("hints", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyList? CallbackHints { get; set; }
}
///
/// Represents a followup message create payload.
///
internal sealed class RestFollowupMessageCreatePayload : ObservableApiObject
{
///
/// Gets the content.
///
[JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)]
public string Content { get; set; }
///
/// Get whether the message is tts.
///
[JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)]
public bool? IsTts { get; set; }
///
/// Gets the embeds.
///
[JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)]
public IEnumerable Embeds { get; set; }
///
/// Gets the mentions.
///
[JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)]
public DiscordMentions Mentions { get; set; }
///
/// Gets the flags.
///
[JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)]
public MessageFlags? Flags { get; set; }
///
/// Gets the components.
///
[JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)]
public IReadOnlyCollection Components { get; set; }
///
/// Gets attachments.
///
[JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)]
public List Attachments { get; set; }
}
///
/// Represents a role connection metadata payload.
///
internal sealed class RestApplicationRoleConnectionMetadataPayload : ObservableApiObject
{
///
/// Gets the metadata type.
///
[JsonProperty("type")]
public ApplicationRoleConnectionMetadataType Type { get; set; }
///
/// Gets the metadata key.
///
[JsonProperty("key")]
public string Key { get; set; }
///
/// Gets the metadata name.
///
[JsonProperty("name")]
public string Name { get; set; }
///
/// Gets the metadata description.
///
[JsonProperty("description")]
public string Description { get; set; }
///
/// Gets the metadata name translations.
///
[JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary NameLocalizations { get; set; }
///
/// Gets the metadata description localizations.
///
[JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)]
public Dictionary DescriptionLocalizations { get; set; }
}
diff --git a/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs b/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs
index 49b93b54a..e8907d36c 100644
--- a/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs
+++ b/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs
@@ -1,87 +1,87 @@
using System;
using System.Collections.Generic;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
namespace DisCatSharp.Net.Models;
///
/// Represents a application command edit model.
///
public class ApplicationCommandEditModel : ObservableApiObject
{
///
/// Sets the command's new name.
///
public Optional Name
{
internal get => this._name;
set
{
if (value.Value.Length > 32)
throw new ArgumentException("Application command name cannot exceed 32 characters.", nameof(value));
this._name = value;
}
}
private Optional _name;
///
/// Sets the command's new description
///
public Optional Description
{
internal get => this._description;
set
{
if (value.Value.Length > 100)
throw new ArgumentException("Application command description cannot exceed 100 characters.", nameof(value));
this._description = value;
}
}
private Optional _description;
///
/// Sets the command's name localizations.
///
public Optional NameLocalizations { internal get; set; }
///
/// Sets the command's description localizations.
///
public Optional DescriptionLocalizations { internal get; set; }
///
/// Sets the command's new options.
///
public Optional?> Options { internal get; set; }
///
/// Sets the command's needed permissions.
///
public Optional DefaultMemberPermissions { internal get; set; }
///
/// Sets the command's allowed contexts.
///
- public Optional?> AllowedContexts { internal get; set; }
+ public Optional?> AllowedContexts { internal get; set; }
///
/// Sets the command's allowed integration types.
///
public Optional?> IntegrationTypes { internal get; set; }
///
/// Sets whether the command can be used in direct messages.
///
public Optional DmPermission { internal get; set; }
///
/// Sets whether the command is marked as NSFW.
///
public Optional IsNsfw { internal get; set; }
}
diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs
index dc4120b27..6c91eb040 100644
--- a/DisCatSharp/Net/Rest/DiscordApiClient.cs
+++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs
@@ -1,7254 +1,7254 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.Entities;
using DisCatSharp.Entities.OAuth2;
using DisCatSharp.Enums;
using DisCatSharp.Net.Abstractions;
using DisCatSharp.Net.Abstractions.Rest;
using DisCatSharp.Net.Serialization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Net;
///
/// Represents a discord api client.
///
public sealed class DiscordApiClient
{
///
/// The audit log reason header name.
///
private const string REASON_HEADER_NAME = CommonHeaders.AUDIT_LOG_REASON_HEADER;
///
/// Gets the discord client.
///
internal BaseDiscordClient Discord { get; }
///
/// Gets the oauth2 client.
///
internal DiscordOAuth2Client OAuth2Client { get; }
///
/// Gets the rest client.
///
internal RestClient Rest { get; }
///
/// Initializes a new instance of the class.
///
/// The base discord client.
internal DiscordApiClient(BaseDiscordClient client)
{
this.Discord = client;
this.OAuth2Client = null!;
this.Rest = new(client);
}
///
/// Initializes a new instance of the class.
///
/// The oauth2 client.
/// The proxy.
/// The timeout.
/// If true, use relative rate limit.
/// The logger.
internal DiscordApiClient(DiscordOAuth2Client client, IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger)
{
this.OAuth2Client = client;
this.Discord = null!;
this.Rest = new(proxy, timeout, useRelativeRateLimit, logger);
}
///
/// Initializes a new instance of the class.
///
/// The proxy.
/// The timeout.
/// If true, use relative rate limit.
/// The logger.
internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger)
{
this.Discord = null!;
this.OAuth2Client = null!;
this.Rest = new(proxy, timeout, useRelativeRateLimit, logger);
}
///
/// Builds the query string.
///
/// The values.
/// Whether this query will be transmitted via POST.
private static string BuildQueryString(IDictionary values, bool post = false)
{
if (values == null || values.Count == 0)
return string.Empty;
var valsCollection = values.Select(xkvp =>
$"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}");
var vals = string.Join("&", valsCollection);
return !post ? $"?{vals}" : vals;
}
///
/// Prepares the message.
///
/// The raw message.
private DiscordMessage PrepareMessage(JToken msgRaw)
{
var author = msgRaw["author"].ToObject();
var ret = msgRaw.ToDiscordObject();
ret.Discord = this.Discord;
this.PopulateMessage(author, ret);
var referencedMsg = msgRaw["referenced_message"];
if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString()))
{
author = referencedMsg["author"].ToObject();
ret.ReferencedMessage.Discord = this.Discord;
this.PopulateMessage(author, ret.ReferencedMessage);
}
if (ret.Channel != null)
return ret;
var channel = !ret.GuildId.HasValue
? new DiscordDmChannel
{
Id = ret.ChannelId,
Discord = this.Discord,
Type = ChannelType.Private
}
: new DiscordChannel
{
Id = ret.ChannelId,
GuildId = ret.GuildId,
Discord = this.Discord
};
ret.Channel = channel;
return ret;
}
///
/// Populates the message.
///
/// The author.
/// The message.
private void PopulateMessage(TransportUser author, DiscordMessage ret)
{
var guild = ret.Channel?.Guild;
//If this is a webhook, it shouldn't be in the user cache.
if (author.IsBot && int.Parse(author.Discriminator) == 0)
ret.Author = new(author)
{
Discord = this.Discord
};
else
{
if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr))
this.Discord.UserCache[author.Id] = usr = new(author)
{
Discord = this.Discord
};
if (guild != null)
{
if (!guild.Members.TryGetValue(author.Id, out var mbr))
mbr = new(usr)
{
Discord = this.Discord,
GuildId = guild.Id
};
ret.Author = mbr;
}
else
ret.Author = usr;
}
ret.PopulateMentions();
ret.ReactionsInternal ??= [];
foreach (var xr in ret.ReactionsInternal)
xr.Emoji.Discord = this.Discord;
}
///
/// Executes a rest request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The payload.
/// The ratelimit wait override.
/// Enables a possible breakpoint in the rest client for debugging purposes.
internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary? headers = null, string? payload = null, double? ratelimitWaitOverride = null, bool targetDebug = false)
{
var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride);
if (this.Discord is not null)
this.Rest.ExecuteRequestAsync(req, targetDebug).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, $"Error while executing request. Url: {url.AbsoluteUri}");
else
_ = this.Rest.ExecuteRequestAsync(req, targetDebug);
return req.WaitForCompletionAsync();
}
///
/// Executes a rest form data request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The form data.
/// The ratelimit wait override.
/// Enables a possible breakpoint in the rest client for debugging purposes.
internal Task DoFormRequestAsync(DiscordOAuth2Client client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, Dictionary formData, Dictionary? headers = null, double? ratelimitWaitOverride = null, bool targetDebug = false)
{
var req = new RestFormRequest(client, bucket, url, method, route, formData, headers, ratelimitWaitOverride);
this.Rest.ExecuteFormRequestAsync(req, targetDebug).LogTaskFault(this.OAuth2Client.Logger, LogLevel.Error, LoggerEvents.RestError, $"Error while executing request. Url: {url.AbsoluteUri}");
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart rest request for stickers.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The sticker name.
/// The sticker tag.
/// The sticker description.
/// The headers.
/// The file.
/// The ratelimit wait override.
/// Enables a possible breakpoint in the rest client for debugging purposes.
private Task DoStickerMultipartAsync(
BaseDiscordClient client,
RateLimitBucket bucket,
Uri url,
RestRequestMethod method,
string route,
string name,
string tags,
string? description = null,
IReadOnlyDictionary? headers = null,
DiscordMessageFile? file = null,
double? ratelimitWaitOverride = null,
bool targetDebug = false
)
{
var req = new MultipartStickerWebRequest(client, bucket, url, method, route, name, tags, description, headers, file, ratelimitWaitOverride);
if (this.Discord is not null)
this.Rest.ExecuteRequestAsync(req, targetDebug).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req, targetDebug);
return req.WaitForCompletionAsync();
}
///
/// Executes a multipart request.
///
/// The client.
/// The bucket.
/// The url.
/// The method.
/// The route.
/// The headers.
/// The values.
/// The files.
/// The ratelimit wait override.
/// Enables a possible breakpoint in the rest client for debugging purposes.
private Task DoMultipartAsync(
BaseDiscordClient client,
RateLimitBucket bucket,
Uri url,
RestRequestMethod method,
string route,
IReadOnlyDictionary? headers = null,
IReadOnlyDictionary? values = null,
IEnumerable? files = null,
double? ratelimitWaitOverride = null,
bool targetDebug = false
)
{
var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride);
if (this.Discord is not null)
this.Rest.ExecuteRequestAsync(req, targetDebug).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request");
else
_ = this.Rest.ExecuteRequestAsync(req, targetDebug);
return req.WaitForCompletionAsync();
}
// begin todo
#region Guild
///
/// Gets the guild async.
///
/// The guild id.
/// If true, with_counts.
internal async Task GetGuildAsync(ulong guildId, bool? withCounts)
{
var urlParams = new Dictionary();
if (withCounts.HasValue)
urlParams["with_counts"] = withCounts?.ToString();
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new
{
guild_id = guildId
}, out var path);
var url = Utilities.GetApiUriFor(path, urlParams.Count != 0 ? BuildQueryString(urlParams) : "", this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, urlParams).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guildRest = DiscordJson.DeserializeObject(res.Response, this.Discord);
foreach (var r in guildRest.RolesInternal.Values)
r.GuildId = guildRest.Id;
if (this.Discord is DiscordClient dc)
{
await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false);
return dc.GuildsInternal[guildRest.Id];
}
else
{
guildRest.Discord = this.Discord;
return guildRest;
}
}
///
/// Searches the members async.
///
/// The guild_id.
/// The name.
/// The limit.
internal async Task> SearchMembersAsync(ulong guildId, string name, int? limit)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new
{
guild_id = guildId
}, out var path);
var querydict = new Dictionary
{
["query"] = name,
["limit"] = limit.ToString()
};
var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JArray.Parse(res.Response);
var tms = json.ToObject>();
var mbrs = new List();
foreach (var xtm in tms)
{
var usr = new DiscordUser(xtm.User)
{
Discord = this.Discord
};
this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) =>
{
old.Username = usr.Username;
old.Discriminator = usr.Discriminator;
old.AvatarHash = usr.AvatarHash;
old.BannerHash = usr.BannerHash;
old.BannerColorInternal = usr.BannerColorInternal;
old.AvatarDecorationData = usr.AvatarDecorationData;
old.ThemeColorsInternal = usr.ThemeColorsInternal;
old.Pronouns = usr.Pronouns;
old.Locale = usr.Locale;
old.GlobalName = usr.GlobalName;
return old;
});
mbrs.Add(new(xtm)
{
Discord = this.Discord,
GuildId = guildId
});
}
return mbrs;
}
///
/// Gets the guild ban async.
///
/// The guild_id.
/// The user_id.
internal async Task GetGuildBanAsync(ulong guildId, ulong userId)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new
{
guild_id = guildId,
user_id = userId
}, out var path);
var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var ban = json.ToObject();
return ban;
}
///
/// Creates the guild async.
///
/// The name.
/// The region_id.
/// The iconb64.
/// The verification_level.
/// The default_message_notifications.
/// The system_channel_flags.
internal async Task CreateGuildAsync(
string name,
string regionId,
Optional iconb64,
VerificationLevel? verificationLevel,
DefaultMessageNotifications? defaultMessageNotifications,
SystemChannelFlags? systemChannelFlags
)
{
var pld = new RestGuildCreatePayload
{
Name = name,
RegionId = regionId,
DefaultMessageNotifications = defaultMessageNotifications,
VerificationLevel = verificationLevel,
IconBase64 = iconb64,
SystemChannelFlags = systemChannelFlags
};
var route = $"{Endpoints.GUILDS}";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new
{ }, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false);
return guild;
}
///
/// Creates the guild from template async.
///
/// The template_code.
/// The name.
/// The iconb64.
internal async Task CreateGuildFromTemplateAsync(string templateCode, string name, Optional iconb64)
{
var pld = new RestGuildCreateFromTemplatePayload
{
Name = name,
IconBase64 = iconb64
};
var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code";
var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new
{
template_code = templateCode
}, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);
var json = JObject.Parse(res.Response);
var rawMembers = (JArray)json["members"];
var guild = json.ToDiscordObject();
if (this.Discord is DiscordClient dc)
await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false);
return guild;
}
///
/// Deletes the guild async.
///
/// The guild_id.
internal async Task DeleteGuildAsync(ulong guildId)
{
var route = $"{Endpoints.GUILDS}/:guild_id";
var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new
{
guild_id = guildId
}, out var path);
var url = Utilities.GetApiUriFor(path, this.Discord.Configuration);
await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false);
if (this.Discord is DiscordClient dc)
{
var gld = dc.GuildsInternal[guildId];
await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false);
}
}
///
/// Modifies the guild.
///
/// The guild id.
/// The name.
/// The verification level.
/// The default message notifications.
/// The mfa level.
/// The explicit content filter.
/// The afk channel id.
/// The afk timeout.
/// The iconb64.
/// The owner id.
/// The splashb64.
/// The system channel id.
/// The system channel flags.
/// The public updates channel id.
/// The rules channel id.
/// The description.
/// The banner base64.
/// The discovery base64.
/// The home header base64.
/// The preferred locale.
/// Whether the premium progress bar should be enabled.
/// The reason.
internal async Task ModifyGuildAsync(
ulong guildId,
Optional name,
Optional verificationLevel,
Optional defaultMessageNotifications,
Optional mfaLevel,
Optional explicitContentFilter,
Optional