diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
index 927d3a333..34753d80a 100644
--- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
+++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs
@@ -1,1776 +1,1776 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DisCatSharp.ApplicationCommands.Attributes;
using DisCatSharp.ApplicationCommands.EventArgs;
using DisCatSharp.Common;
using DisCatSharp.Common.Utilities;
using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.EventArgs;
using DisCatSharp.Exceptions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace DisCatSharp.ApplicationCommands
{
///
/// A class that handles slash commands for a client.
///
public sealed class ApplicationCommandsExtension : BaseExtension
{
///
/// A list of methods for top level commands.
///
private static List s_commandMethods { get; set; } = new List();
///
/// List of groups.
///
private static List s_groupCommands { get; set; } = new List();
///
/// List of groups with subgroups.
///
private static List s_subGroupCommands { get; set; } = new List();
///
/// List of context menus.
///
private static List s_contextMenuCommands { get; set; } = new List();
///
/// List of global commands on discords backend.
///
internal static List GlobalDiscordCommands { get; set; } = null;
///
/// List of guild commands on discords backend.
///
internal static Dictionary> GuildDiscordCommands { get; set; } = null;
///
/// Singleton modules.
///
private static List s_singletonModules { get; set; } = new List();
///
/// List of modules to register.
///
private readonly List> _updateList = new();
///
/// Configuration for Discord.
///
internal static ApplicationCommandsConfiguration Configuration;
///
/// Discord client.
///
internal static DiscordClient ClientInternal;
///
/// Set to true if anything fails when registering.
///
private static bool s_errored { get; set; } = false;
///
/// Gets a list of registered commands. The key is the guild id (null if global).
///
public IReadOnlyList>> RegisteredCommands
- => s_registeredCommands;
- private static readonly List>> s_registeredCommands = new();
+ => _registeredCommands;
+ private static readonly List>> _registeredCommands = new();
///
/// Gets a list of registered global commands.
///
public IReadOnlyList GlobalCommands
=> GlobalCommandsInternal;
internal static readonly List GlobalCommandsInternal = new();
///
/// Gets a list of registered guild commands mapped by guild id.
///
public IReadOnlyDictionary> GuildCommands
=> GuildCommandsInternal;
internal static readonly Dictionary> GuildCommandsInternal = new();
///
/// Gets the registration count.
///
private static int s_registrationCount { get; set; } = 0;
///
/// Gets the expected count.
///
private static int s_expectedCount { get; set; } = 0;
///
/// Gets the guild ids where the applications.commands scope is missing.
///
private IReadOnlyList _missingScopeGuildIds;
///
/// Gets whether debug is enabled.
///
private static bool s_debugEnabled { get; set; }
///
/// Initializes a new instance of the class.
///
/// The configuration.
internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration)
{
Configuration = configuration;
s_debugEnabled = configuration.DebugStartupCounts;
}
///
/// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS.
///
/// The client to setup on.
protected internal override void Setup(DiscordClient client)
{
if (this.Client != null)
throw new InvalidOperationException("What did I tell you?");
this.Client = client;
ClientInternal = client;
this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null);
this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null);
this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null);
this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null);
this._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null);
this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null);
this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null);
this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null);
this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync();
this.Client.InteractionCreated += this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup;
}
private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e)
=> await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now."));
private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e)
=> await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now."));
private void FinishedRegistration()
{
this.Client.InteractionCreated -= this.CatchInteractionsOnStartup;
this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup;
this.Client.InteractionCreated += this.InteractionHandler;
this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler;
}
///
/// Registers a command class.
///
/// The command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T))));
}
///
/// Registers a command class.
///
/// The of the command class to register.
/// The guild id to register it on. If you want global commands, leave it null.
public void RegisterCommands(Type type, ulong? guildId = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type)));
}
///
/// Cleans all guild application commands.
///
public async Task CleanGuildCommandsAsync()
{
foreach (var guild in this.Client.Guilds.Values)
{
await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty());
}
}
///
/// Cleans the global application commands.
///
public async Task CleanGlobalCommandsAsync()
=> await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty());
///
/// Registers a command class with permission and translation setup.
///
/// The command class to register.
/// The guild id to register it on.
/// A callback to setup permissions with.
/// A callback to setup translations with.
public void RegisterCommands(ulong guildId, Action permissionSetup = null, Action translationSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup, translationSetup)));
}
///
/// Registers a command class with permission and translation setup.
///
/// The of the command class to register.
/// The guild id to register it on.
/// A callback to setup permissions with.
/// A callback to setup translations with.
public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = null, Action translationSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup, translationSetup)));
}
/*
///
/// Registers a command class with permission setup but without a guild id.
///
/// The command class to register.
/// A callback to setup permissions with.
public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule
{
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup)));
}
///
/// Registers a command class with permission setup but without a guild id.
///
/// The of the command class to register.
/// A callback to setup permissions with.
public void RegisterCommands(Type type, Action permissionSetup = null)
{
if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type))
throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type));
//If sharding, only register for shard 0
if (this.Client.ShardId == 0)
this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup)));
}
*/
///
/// Fired when the application commands module is ready.
///
public event AsyncEventHandler ApplicationCommandsModuleReady
{
add { this._applicationCommandsModuleReady.Register(value); }
remove { this._applicationCommandsModuleReady.Unregister(value); }
}
private AsyncEvent _applicationCommandsModuleReady;
///
/// Fired when the application commands modules startup is finished.
///
public event AsyncEventHandler ApplicationCommandsModuleStartupFinished
{
add { this._applicationCommandsModuleStartupFinished.Register(value); }
remove { this._applicationCommandsModuleStartupFinished.Unregister(value); }
}
private AsyncEvent _applicationCommandsModuleStartupFinished;
///
/// Fired when guild commands are registered on a guild.
///
public event AsyncEventHandler GuildApplicationCommandsRegistered
{
add { this._guildApplicationCommandsRegistered.Register(value); }
remove { this._guildApplicationCommandsRegistered.Unregister(value); }
}
private AsyncEvent _guildApplicationCommandsRegistered;
///
/// Fired when the global commands are registered.
///
public event AsyncEventHandler GlobalApplicationCommandsRegistered
{
add { this._globalApplicationCommandsRegistered.Register(value); }
remove { this._globalApplicationCommandsRegistered.Unregister(value); }
}
private AsyncEvent _globalApplicationCommandsRegistered;
///
/// Used for RegisterCommands and the event.
///
internal async Task UpdateAsync()
{
//Only update for shard 0
if (this.Client.ShardId == 0)
{
GlobalDiscordCommands = new();
GuildDiscordCommands = new();
var commandsPending = this._updateList.Select(x => x.Key).Distinct();
s_expectedCount = commandsPending.Count();
if (s_debugEnabled)
this.Client.Logger.LogDebug($"Expected Count: {s_expectedCount}");
List failedGuilds = new();
IEnumerable globalCommands = null;
globalCommands = await this.Client.GetGlobalApplicationCommandsAsync() ?? null;
foreach (var guild in this.Client.Guilds.Keys)
{
IEnumerable commands = null;
var unauthorized = false;
try
{
commands = await this.Client.GetGuildApplicationCommandsAsync(guild) ?? null;
}
catch (UnauthorizedException)
{
unauthorized = true;
}
finally
{
if (!unauthorized && commands != null && commands.Any())
GuildDiscordCommands.Add(guild, commands.ToList());
else if (!unauthorized)
GuildDiscordCommands.Add(guild, null);
else
failedGuilds.Add(guild);
}
}
//Default should be to add the help and slash commands can be added without setting any configuration
//so this should still add the default help
if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp))
{
foreach (var key in commandsPending.ToList())
{
this._updateList.Add(new KeyValuePair
(key, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule))));
}
}
if (globalCommands != null && globalCommands.Any())
GlobalDiscordCommands.AddRange(globalCommands);
foreach (var key in commandsPending.ToList())
{
this.Client.Logger.LogDebug(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands.");
this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key);
}
this._missingScopeGuildIds = failedGuilds;
await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
GuildsWithoutScope = failedGuilds
});
}
}
///
/// Method for registering commands for a target from modules.
///
/// The types.
/// The optional guild id.
private void RegisterCommands(IEnumerable types, ulong? guildid)
{
//Initialize empty lists to be added to the global ones at the end
var commandMethods = new List();
var groupCommands = new List();
var subGroupCommands = new List();
var contextMenuCommands = new List();
var updateList = new List();
var commandTypeSources = new List>();
_ = Task.Run(async () =>
{
//Iterates over all the modules
foreach (var config in types)
{
var type = config.Type;
try
{
var module = type.GetTypeInfo();
var classes = new List();
var ctx = new ApplicationCommandsTranslationContext(type, module.FullName);
config.Translations?.Invoke(ctx);
//Add module to classes list if it's a group
if (module.GetCustomAttribute() != null)
{
classes.Add(module);
}
else
{
//Otherwise add the nested groups
classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList();
}
List groupTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
var slashGroupsTulpe = NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildid, groupTranslations).Result;
if (slashGroupsTulpe.applicationCommands != null && slashGroupsTulpe.applicationCommands.Any())
updateList.AddRange(slashGroupsTulpe.applicationCommands);
if (slashGroupsTulpe.commandTypeSources != null && slashGroupsTulpe.commandTypeSources.Any())
commandTypeSources.AddRange(slashGroupsTulpe.commandTypeSources);
if (slashGroupsTulpe.singletonModules != null && slashGroupsTulpe.singletonModules.Any())
s_singletonModules.AddRange(slashGroupsTulpe.singletonModules);
if (slashGroupsTulpe.groupCommands != null && slashGroupsTulpe.groupCommands.Any())
groupCommands.AddRange(slashGroupsTulpe.groupCommands);
if (slashGroupsTulpe.subGroupCommands != null && slashGroupsTulpe.subGroupCommands.Any())
subGroupCommands.AddRange(slashGroupsTulpe.subGroupCommands);
//Handles methods and context menus, only if the module isn't a group itself
if (module.GetCustomAttribute() == null)
{
List commandTranslations = null;
if (!string.IsNullOrEmpty(ctx.Translations))
{
commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations);
}
//Slash commands
var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var slashCommands = CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildid, commandTranslations).Result;
if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any())
updateList.AddRange(slashCommands.applicationCommands);
if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any())
commandTypeSources.AddRange(slashCommands.commandTypeSources);
if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any())
commandMethods.AddRange(slashCommands.commandMethods);
//Context Menus
var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null);
var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations);
if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any())
updateList.AddRange(contextCommands.applicationCommands);
if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any())
commandTypeSources.AddRange(contextCommands.commandTypeSources);
if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any())
contextMenuCommands.AddRange(contextCommands.contextMenuCommands);
//Accounts for lifespans
if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton)
{
s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider));
}
}
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}");
else
this.Client.Logger.LogCritical(ex, $"There was an error registering application commands");
s_errored = true;
}
}
if (!s_errored)
{
try
{
List commands = new();
try
{
if (guildid == null)
{
if (updateList != null && updateList.Any())
{
var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(updateList);
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GlobalCommandsInternal.AddRange(actualCommands);
}
else
{
foreach (var cmd in GlobalDiscordCommands)
{
try
{
await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id);
}
catch (NotFoundException)
{
this.Client.Logger.LogError($"Could not delete global command {cmd.Id}. Please clean up manually");
}
}
}
}
else
{
if (updateList != null && updateList.Any())
{
var regCommands = await RegistrationWorker.RegisterGuilldCommandsAsync(guildid.Value, updateList);
var actualCommands = regCommands.Distinct().ToList();
commands.AddRange(actualCommands);
GuildCommandsInternal.Add(guildid.Value, actualCommands);
if (this.Client.Guilds.TryGetValue(guildid.Value, out var guild))
{
guild.InternalRegisteredApplicationCommands = new();
guild.InternalRegisteredApplicationCommands.AddRange(actualCommands);
}
}
else
{
foreach (var cmd in GuildDiscordCommands[guildid.Value])
{
try
{
await this.Client.DeleteGuildApplicationCommandAsync(guildid.Value, cmd.Id);
}
catch (NotFoundException)
{
this.Client.Logger.LogError($"Could not delete guild command {cmd.Id} in guild {guildid.Value}. Please clean up manually");
}
}
}
}
}
catch (UnauthorizedException ex)
{
this.Client.Logger.LogError($"Could not register application commands for guild {guildid}.\nError: {ex.JsonMessage}");
return;
}
//Creates a guild command if a guild id is specified, otherwise global
//Checks against the ids and adds them to the command method lists
foreach (var command in commands)
{
if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com))
{
com.CommandId = command.Id;
var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType);
await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, command.Id, com.Name, source.Value, source.Key);
}
else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom))
{
groupCom.CommandId = command.Id;
foreach (var gCom in groupCom.Methods)
{
var source = commandTypeSources.FirstOrDefault(f => f.Key == gCom.Value.DeclaringType);
await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, groupCom.CommandId, gCom.Key, source.Key, source.Value);
}
}
else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom))
{
subCom.CommandId = command.Id;
foreach (var groupComs in subCom.SubCommands)
{
foreach (var gCom in groupComs.Methods)
{
var source = commandTypeSources.FirstOrDefault(f => f.Key == gCom.Value.DeclaringType);
await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, subCom.CommandId, gCom.Key, source.Key, source.Value);
}
}
}
else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom))
{
cmCom.CommandId = command.Id;
var source = commandTypeSources.First(f => f.Key == cmCom.Method.DeclaringType);
await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, command.Id, cmCom.Name, source.Value, source.Key);
}
}
//Adds to the global lists finally
s_commandMethods.AddRange(commandMethods);
s_groupCommands.AddRange(groupCommands);
s_subGroupCommands.AddRange(subGroupCommands);
s_contextMenuCommands.AddRange(contextMenuCommands);
- s_registeredCommands.Add(new KeyValuePair>(guildid, commands.ToList()));
+ _registeredCommands.Add(new KeyValuePair>(guildid, commands.ToList()));
foreach (var command in commandMethods)
{
var app = types.First(t => t.Type == command.Method.DeclaringType);
}
s_registrationCount++;
if (s_debugEnabled)
this.Client.Logger.LogDebug($"Expected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}");
if (guildid.HasValue)
{
await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
GuildId = guildid.Value,
RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildid.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildid.Value).Value : null
});
}
else
{
await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider)
{
Handled = true,
RegisteredCommands = GlobalCommandsInternal
});
}
this.CheckRegistrationStartup();
}
catch (Exception ex)
{
if (ex is BadRequestException brex)
this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}");
else
this.Client.Logger.LogCritical(ex, $"There was an error registering application commands");
s_errored = true;
}
}
});
}
private async void CheckRegistrationStartup()
{
if (s_debugEnabled)
this.Client.Logger.LogDebug($"Checking counts...\n\nExpected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}");
if (s_registrationCount == s_expectedCount)
{
await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider)
{
RegisteredGlobalCommands = GlobalCommandsInternal,
RegisteredGuildCommands = GuildCommandsInternal,
GuildsWithoutScope = this._missingScopeGuildIds
});
this.FinishedRegistration();
}
}
///
/// Interaction handler.
///
/// The client.
/// The event args.
private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e)
{
_ = Task.Run(async () =>
{
if (e.Interaction.Type == InteractionType.ApplicationCommand)
{
//Creates the context
var context = new InteractionContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Guild = e.Interaction.Guild,
User = e.Interaction.User,
Client = client,
ApplicationCommandsExtension = this,
CommandName = e.Interaction.Data.Name,
InteractionId = e.Interaction.Id,
Token = e.Interaction.Token,
Services = Configuration?.ServiceProvider,
ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(),
ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(),
ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(),
ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(),
Type = ApplicationCommandType.ChatInput,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale
};
try
{
if (s_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
throw new InvalidOperationException("A slash command was executed, but no command was registered for it.");
if (methods.Any())
{
var method = methods.First().Method;
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options);
await this.RunCommandAsync(context, method, args);
}
else if (groups.Any())
{
var command = e.Interaction.Data.Options.First();
var method = groups.First().Methods.First(x => x.Key == command.Name).Value;
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
else if (subgroups.Any())
{
var command = e.Interaction.Data.Options.First();
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name);
var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value;
var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options);
await this.RunCommandAsync(context, method, args);
}
await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
}
else if (e.Interaction.Type == InteractionType.AutoComplete)
{
if (s_errored)
throw new InvalidOperationException("Slash commands failed to register properly on startup.");
var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id);
var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id);
if (!methods.Any() && !groups.Any() && !subgroups.Any())
throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it.");
try
{
if (methods.Any())
{
var focusedOption = e.Interaction.Data.Options.First(o => o.Focused);
var method = methods.First().Method;
var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Client = this.Client,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = e.Interaction.Data.Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
else if (groups.Any())
{
var command = e.Interaction.Data.Options.First();
var group = groups.First().Methods.First(x => x.Key == command.Name).Value;
var focusedOption = command.Options.First(o => o.Focused);
var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name);
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Services = Configuration?.ServiceProvider,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.ToList(),
FocusedOption = focusedOption,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}
/*else if (subgroups.Any())
{
var command = e.Interaction.Data.Options.First();
var method = methods.First().Method;
var group = subgroups.First().SubCommands.First(x => x.Name == command.Name);
var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused);
this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue);
var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value;
var provider = option.GetCustomAttribute().ProviderType;
var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider));
var providerInstance = Activator.CreateInstance(provider);
var context = new AutocompleteContext
{
Interaction = e.Interaction,
Services = this._configuration?.Services,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
Channel = e.Interaction.Channel,
User = e.Interaction.User,
Options = command.Options.First(x => x.Name == group.Name).Options.ToList(),
FocusedOption = focusedOption
};
var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context });
await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices));
}*/
}
catch (Exception ex)
{
this.Client.Logger.LogError(ex, "Error in autocomplete interaction");
}
}
});
return Task.CompletedTask;
}
///
/// Context menu handler.
///
/// The client.
/// The event args.
private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e)
{
_ = Task.Run(async () =>
{
//Creates the context
var context = new ContextMenuContext
{
Interaction = e.Interaction,
Channel = e.Interaction.Channel,
Client = client,
Services = Configuration?.ServiceProvider,
CommandName = e.Interaction.Data.Name,
ApplicationCommandsExtension = this,
Guild = e.Interaction.Guild,
InteractionId = e.Interaction.Id,
User = e.Interaction.User,
Token = e.Interaction.Token,
TargetUser = e.TargetUser,
TargetMessage = e.TargetMessage,
Type = e.Type,
Locale = e.Interaction.Locale,
GuildLocale = e.Interaction.GuildLocale
};
try
{
if (s_errored)
throw new InvalidOperationException("Context menus failed to register properly on startup.");
//Gets the method for the command
var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id);
if (method == null)
throw new InvalidOperationException("A context menu was executed, but no command was registered for it.");
await this.RunCommandAsync(context, method.Method, new[] { context });
await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context });
}
catch (Exception ex)
{
await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex });
}
});
return Task.CompletedTask;
}
///
/// Runs a command.
///
/// The base context.
/// The method info.
/// The arguments.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")]
internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args)
{
object classInstance;
//Accounts for lifespans
var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient;
switch (moduleLifespan)
{
case ApplicationCommandModuleLifespan.Scoped:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider);
break;
case ApplicationCommandModuleLifespan.Transient:
//Accounts for static methods and adds DI
classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider);
break;
//If singleton, gets it from the singleton list
case ApplicationCommandModuleLifespan.Singleton:
classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType));
break;
default:
throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}");
}
ApplicationCommandsModule module = null;
if (classInstance is ApplicationCommandsModule mod)
module = mod;
// Slash commands
if (context is InteractionContext slashContext)
{
await this.RunPreexecutionChecksAsync(method, slashContext);
var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true));
if (shouldExecute)
{
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask);
}
}
// Context menus
if (context is ContextMenuContext contextMenuContext)
{
await this.RunPreexecutionChecksAsync(method, contextMenuContext);
var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true));
if (shouldExecute)
{
await (Task)method.Invoke(classInstance, args.ToArray());
await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask);
}
}
}
///
/// Property injection
///
/// The type.
/// The services.
internal static object CreateInstance(Type t, IServiceProvider services)
{
var ti = t.GetTypeInfo();
var constructors = ti.DeclaredConstructors
.Where(xci => xci.IsPublic)
.ToArray();
if (constructors.Length != 1)
throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor.");
var constructor = constructors[0];
var constructorArgs = constructor.GetParameters();
var args = new object[constructorArgs.Length];
if (constructorArgs.Length != 0 && services == null)
throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors.");
// inject via constructor
if (constructorArgs.Length != 0)
for (var i = 0; i < args.Length; i++)
args[i] = services.GetRequiredService(constructorArgs[i].ParameterType);
var moduleInstance = Activator.CreateInstance(t, args);
// inject into properties
var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic);
foreach (var prop in props)
{
if (prop.GetCustomAttribute() != null)
continue;
var service = services.GetService(prop.PropertyType);
if (service == null)
continue;
prop.SetValue(moduleInstance, service);
}
// inject into fields
var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic);
foreach (var field in fields)
{
if (field.GetCustomAttribute() != null)
continue;
var service = services.GetService(field.FieldType);
if (service == null)
continue;
field.SetValue(moduleInstance, service);
}
return moduleInstance;
}
///
/// Resolves the slash command parameters.
///
/// The event arguments.
/// The interaction context.
/// The method info.
/// The options.
private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options)
{
var args = new List { context };
var parameters = method.GetParameters().Skip(1);
for (var i = 0; i < parameters.Count(); i++)
{
var parameter = parameters.ElementAt(i);
//Accounts for optional arguments without values given
if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true)))
args.Add(parameter.DefaultValue);
else
{
var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower());
if (parameter.ParameterType == typeof(string))
args.Add(option.Value.ToString());
else if (parameter.ParameterType.IsEnum)
args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value));
else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?))
args.Add((long?)option.Value);
else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?))
args.Add((bool?)option.Value);
else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?))
args.Add((double?)option.Value);
else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?))
args.Add((int?)option.Value);
else if (parameter.ParameterType == typeof(DiscordAttachment))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Attachments != null &&
e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment))
args.Add(attachment);
else
args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord });
}
else if (parameter.ParameterType == typeof(DiscordUser))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Members != null &&
e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member))
args.Add(member);
else if (e.Interaction.Data.Resolved.Users != null &&
e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user))
args.Add(user);
else
args.Add(await this.Client.GetUserAsync((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(DiscordChannel))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Channels != null &&
e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel))
args.Add(channel);
else
args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(DiscordRole))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Roles != null &&
e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role))
args.Add(role);
else
args.Add(e.Interaction.Guild.GetRole((ulong)option.Value));
}
else if (parameter.ParameterType == typeof(SnowflakeObject))
{
//Checks through resolved
if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role))
args.Add(role);
else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member))
args.Add(member);
else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user))
args.Add(user);
else if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment))
args.Add(attachment);
else
throw new ArgumentException("Error resolving mentionable option.");
}
else
throw new ArgumentException($"Error resolving interaction.");
}
}
return args;
}
///
/// Runs the preexecution checks.
///
/// The method info.
/// The base context.
private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context)
{
if (context is InteractionContext ctx)
{
//Gets all attributes from parent classes as well and stuff
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes());
}
}
var dict = new Dictionary();
foreach (var att in attributes)
{
//Runs the check and adds the result to a list
var result = await att.ExecuteChecksAsync(ctx);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value == false))
throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() };
}
if (context is ContextMenuContext cMctx)
{
var attributes = new List();
attributes.AddRange(method.GetCustomAttributes(true));
attributes.AddRange(method.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes());
if (method.DeclaringType.DeclaringType.DeclaringType != null)
{
attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes());
}
}
var dict = new Dictionary();
foreach (var att in attributes)
{
//Runs the check and adds the result to a list
var result = await att.ExecuteChecksAsync(cMctx);
dict.Add(att, result);
}
//Checks if any failed, and throws an exception
if (dict.Any(x => x.Value == false))
throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() };
}
}
///
/// Gets the choice attributes from choice provider.
///
/// The custom attributes.
/// The optional guild id
private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null)
{
var choices = new List();
foreach (var choiceProviderAttribute in customAttributes)
{
var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider));
if (method == null)
throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider.");
else
{
var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType);
// Abstract class offers more properties that can be set
if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider)))
{
choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId))
?.SetValue(instance, guildId);
choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services))
?.SetValue(instance, Configuration.ServiceProvider);
}
//Gets the choices from the method
var result = await (Task>)method.Invoke(instance, null);
if (result.Any())
{
choices.AddRange(result);
}
}
}
return choices;
}
///
/// Gets the choice attributes from enum parameter.
///
/// The enum parameter.
private static List GetChoiceAttributesFromEnumParameter(Type enumParam)
{
var choices = new List();
foreach (Enum enumValue in Enum.GetValues(enumParam))
{
choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString()));
}
return choices;
}
///
/// Gets the parameter type.
///
/// The type.
private static ApplicationCommandOptionType GetParameterType(Type type)
{
var parametertype = type == typeof(string)
? ApplicationCommandOptionType.String
: type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?)
? ApplicationCommandOptionType.Integer
: type == typeof(bool) || type == typeof(bool?)
? ApplicationCommandOptionType.Boolean
: type == typeof(double) || type == typeof(double?)
? ApplicationCommandOptionType.Number
: type == typeof(DiscordChannel)
? ApplicationCommandOptionType.Channel
: type == typeof(DiscordUser)
? ApplicationCommandOptionType.User
: type == typeof(DiscordRole)
? ApplicationCommandOptionType.Role
: type == typeof(SnowflakeObject)
? ApplicationCommandOptionType.Mentionable
: type == typeof(DiscordAttachment)
? ApplicationCommandOptionType.Attachment
: type.IsEnum
? ApplicationCommandOptionType.String
: throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum.");
return parametertype;
}
///
/// Gets the choice attributes from parameter.
///
/// The choice attributes.
private static List GetChoiceAttributesFromParameter(IEnumerable choiceattributes)
{
return !choiceattributes.Any()
? null
: choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList();
}
///
/// Parses the parameters.
///
/// The parameters.
/// The optional guild id.
internal static async Task> ParseParametersAsync(ParameterInfo[] parameters, ulong? guildId)
{
var options = new List();
foreach (var parameter in parameters)
{
//Gets the attribute
var optionattribute = parameter.GetCustomAttribute();
if (optionattribute == null)
throw new ArgumentException("Arguments must have the Option attribute!");
var minimumValue = parameter.GetCustomAttribute()?.Value ?? null;
var maximumValue = parameter.GetCustomAttribute()?.Value ?? null;
var autocompleteAttribute = parameter.GetCustomAttribute();
if (optionattribute.Autocomplete && autocompleteAttribute == null)
throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!");
if (!optionattribute.Autocomplete && autocompleteAttribute != null)
throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!");
//Sets the type
var type = parameter.ParameterType;
var parametertype = GetParameterType(type);
//Handles choices
//From attributes
var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes());
//From enums
if (parameter.ParameterType.IsEnum)
{
choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType);
}
//From choice provider
var choiceProviders = parameter.GetCustomAttributes();
if (choiceProviders.Any())
{
choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId);
}
var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null;
options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes, optionattribute.Autocomplete, minimumValue, maximumValue));
}
return options;
}
///
/// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client.
/// Should only be run on the slash command extension linked to shard 0 if sharding.
/// Not recommended and should be avoided since it can make slash commands be unresponsive for a while.
///
public async Task RefreshCommandsAsync()
{
s_commandMethods.Clear();
s_groupCommands.Clear();
s_subGroupCommands.Clear();
- s_registeredCommands.Clear();
+ _registeredCommands.Clear();
s_contextMenuCommands.Clear();
GlobalDiscordCommands.Clear();
GuildDiscordCommands.Clear();
GlobalDiscordCommands = null;
GuildDiscordCommands = null;
GuildCommandsInternal.Clear();
GlobalCommandsInternal.Clear();
await this.UpdateAsync();
}
///
/// Fires when the execution of a slash command fails.
///
public event AsyncEventHandler SlashCommandErrored
{
add { this._slashError.Register(value); }
remove { this._slashError.Unregister(value); }
}
private AsyncEvent _slashError;
///
/// Fires when the execution of a slash command is successful.
///
public event AsyncEventHandler SlashCommandExecuted
{
add { this._slashExecuted.Register(value); }
remove { this._slashExecuted.Unregister(value); }
}
private AsyncEvent _slashExecuted;
///
/// Fires when the execution of a context menu fails.
///
public event AsyncEventHandler ContextMenuErrored
{
add { this._contextMenuErrored.Register(value); }
remove { this._contextMenuErrored.Unregister(value); }
}
private AsyncEvent _contextMenuErrored;
///
/// Fire when the execution of a context menu is successful.
///
public event AsyncEventHandler ContextMenuExecuted
{
add { this._contextMenuExecuted.Register(value); }
remove { this._contextMenuExecuted.Unregister(value); }
}
private AsyncEvent _contextMenuExecuted;
}
///
/// Holds configuration data for setting up an application command.
///
internal class ApplicationCommandsModuleConfiguration
{
///
/// The type of the command module.
///
public Type Type { get; }
///
/// The permission setup.
///
public Action Setup { get; }
public Action Translations { get; }
///
/// Creates a new command configuration.
///
/// The type of the command module.
/// The permission setup callback.
/// The translation setup callback.
public ApplicationCommandsModuleConfiguration(Type type, Action setup = null, Action translations = null)
{
this.Type = type;
this.Setup = setup;
this.Translations = translations;
}
}
///
/// Links a command to its original command module.
///
internal class ApplicationCommandSourceLink
{
///
/// The command.
///
public DiscordApplicationCommand ApplicationCommand { get; set; }
///
/// The base/root module the command is contained in.
///
public Type RootCommandContainerType { get; set; }
///
/// The direct group the command is contained in.
///
public Type CommandContainerType { get; set; }
}
///
/// The command method.
///
internal class CommandMethod
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
///
/// The group command.
///
internal class GroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the methods.
///
public List> Methods { get; set; } = null;
}
///
/// The sub group command.
///
internal class SubGroupCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the sub commands.
///
public List SubCommands { get; set; } = new List();
}
///
/// The context menu command.
///
internal class ContextMenuCommand
{
///
/// Gets or sets the command id.
///
public ulong CommandId { get; set; }
///
/// Gets or sets the name.
///
public string Name { get; set; }
///
/// Gets or sets the method.
///
public MethodInfo Method { get; set; }
}
#region Default Help
///
/// Represents the default help module.
///
public class DefaultHelpModule : ApplicationCommandsModule
{
public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
var guildCommandsTask= context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
var 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();
foreach (var sc in slashCommands.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim()));
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
var guildCommandsTask= context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
var slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase));
if (command is null || command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand
&& c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider
{
public async Task> Provider(AutocompleteContext context)
{
var options = new List();
var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync();
var guildCommandsTask= context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
var slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First());
var command = slashCommands.FirstOrDefault(ac =>
ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (command.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
return options.AsEnumerable();
}
var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase));
if (foundCommand is null || foundCommand.Options is null)
{
options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command"));
}
else
{
var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand &&
x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList();
foreach (var option in opt.Take(25))
{
options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim()));
}
}
return options.AsEnumerable();
}
}
[SlashCommand("help", "Displays command help")]
public async Task DefaultHelpAsync(InteractionContext ctx,
[Autocomplete(typeof(DefaultHelpAutoCompleteProvider))]
[Option("option_one", "top level command to provide help for", true)] string commandName,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))]
[Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null,
[Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))]
[Option("option_three", "command to provide help for", true)] string commandTwoName = null)
{
var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync();
var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id);
await Task.WhenAll(globalCommandsTask, guildCommandsTask);
var applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result)
.Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase))
.GroupBy(ac => ac.Name).Select(x => x.First())
.ToList();
if (applicationCommands.Count < 1)
{
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"There are no slash commands for guild {ctx.Guild.Name}").AsEphemeral(true));
return;
}
if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command"))
{
var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup));
var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options
.FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase));
var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase));
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{Formatter.InlineCode(cmd.Name)}: {cmd.Description ?? "No description provided."}"
};
if (cmd.Options is not null)
{
var commandOptions = cmd.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField("Arguments", sb.ToString().Trim(), false);
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command"))
{
var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand));
var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase));
var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase));
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{Formatter.InlineCode(subCommand.Name)}: {subCommand.Description ?? "No description provided."}"
};
if (subCommand.Options is not null)
{
var commandOptions = subCommand.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField("Arguments", sb.ToString().Trim(), false);
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
else
{
var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
if (command is null)
{
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder()
.WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true));
return;
}
var discordEmbed = new DiscordEmbedBuilder
{
Title = "Help",
Description = $"{Formatter.InlineCode(command.Name)}: {command.Description ?? "No description provided."}"
};
if (command.Options is not null)
{
var commandOptions = command.Options.ToList();
var sb = new StringBuilder();
foreach (var option in commandOptions)
sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n');
sb.Append('\n');
discordEmbed.AddField("Arguments", sb.ToString().Trim(), false);
}
await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource,
new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true));
}
}
}
#endregion
}
diff --git a/DisCatSharp.Lavalink/LavalinkUtil.cs b/DisCatSharp.Lavalink/LavalinkUtil.cs
index 86aae4fc5..e0598ffd2 100644
--- a/DisCatSharp.Lavalink/LavalinkUtil.cs
+++ b/DisCatSharp.Lavalink/LavalinkUtil.cs
@@ -1,285 +1,285 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.IO;
using System.Text;
using DisCatSharp.Lavalink.EventArgs;
namespace DisCatSharp.Lavalink
{
///
/// Various utilities for Lavalink.
///
public static class LavalinkUtilities
{
///
/// Indicates whether a new track should be started after reciving this TrackEndReason. If this is false, either this event is
/// already triggered because another track started (REPLACED) or because the player is stopped (STOPPED, CLEANUP).
///
public static bool MayStartNext(this TrackEndReason reason)
=> reason == TrackEndReason.Finished || reason == TrackEndReason.LoadFailed;
///
/// Decodes a Lavalink track string.
///
/// Track string to decode.
/// Decoded Lavalink track.
public static LavalinkTrack DecodeTrack(string track)
{
// https://github.com/sedmelluq/lavaplayer/blob/804cd1038229230052d9b1dee5e6d1741e30e284/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java#L63-L64
const int TRACK_INFO_VERSIONED = 1;
//const int TRACK_INFO_VERSION = 2;
var raw = Convert.FromBase64String(track);
var decoded = new LavalinkTrack
{
TrackString = track
};
using (var ms = new MemoryStream(raw))
using (var br = new JavaBinaryReader(ms))
{
// https://github.com/sedmelluq/lavaplayer/blob/b0c536098c4f92e6d03b00f19221021f8f50b19b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageInput.java#L37-L39
var messageHeader = br.ReadInt32();
var messageFlags = (int) ((messageHeader & 0xC0000000L) >> 30);
var messageSize = messageHeader & 0x3FFFFFFF;
//if (messageSize != raw.Length)
// Warn($"Size conflict: {messageSize} but was {raw.Length}");
// https://github.com/sedmelluq/lavaplayer/blob/804cd1038229230052d9b1dee5e6d1741e30e284/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java#L268
// java bytes are signed
// https://docs.oracle.com/javase/7/docs/api/java/io/DataInput.html#readByte()
var version = (messageFlags & TRACK_INFO_VERSIONED) != 0 ? (br.ReadSByte() & 0xFF) : 1;
//if (version != TRACK_INFO_VERSION)
// Warn($"Version conflict: Expected {TRACK_INFO_VERSION} but got {version}");
decoded.Title = br.ReadJavaUtf8();
decoded.Author = br.ReadJavaUtf8();
decoded.LengthInternal = br.ReadInt64();
decoded.Identifier = br.ReadJavaUtf8();
decoded.IsStream = br.ReadBoolean();
var uri = br.ReadNullableString();
decoded.Uri = uri != null && version >= 2 ? new Uri(uri) : null;
}
return decoded;
}
}
///
///
/// Java's DataOutputStream always uses big-endian, while BinaryReader always uses little-endian.
/// This class converts a big-endian stream to little-endian, and includes some helper methods
/// for interacting with Lavaplayer/Lavalink.
///
internal class JavaBinaryReader : BinaryReader
{
- private static readonly Encoding s_utf8NoBom = new UTF8Encoding();
+ private static readonly Encoding _utf8NoBom = new UTF8Encoding();
///
/// Initializes a new instance of the class.
///
/// The ms.
- public JavaBinaryReader(Stream ms) : base(ms, s_utf8NoBom)
+ public JavaBinaryReader(Stream ms) : base(ms, _utf8NoBom)
{
}
// https://docs.oracle.com/javase/7/docs/api/java/io/DataInput.html#readUTF()
///
/// Reads the java utf8.
///
/// A string.
public string ReadJavaUtf8()
{
var length = this.ReadUInt16(); // string size in bytes
var bytes = new byte[length];
var amountRead = this.Read(bytes, 0, length);
if (amountRead < length)
throw new InvalidDataException("EOS unexpected");
var output = new char[length];
var strlen = 0;
// i'm gonna blindly assume here that the javadocs had the correct endianness
for (var i = 0; i < length; i++)
{
var value1 = bytes[i];
if ((value1 & 0b10000000) == 0) // highest bit 1 is false
{
output[strlen++] = (char)value1;
continue;
}
// remember to skip one byte for every extra byte
var value2 = bytes[++i];
if ((value1 & 0b00100000) == 0 && // highest bit 3 is false
(value1 & 0b11000000) != 0 && // highest bit 1 and 2 are true
(value2 & 0b01000000) == 0 && // highest bit 2 is false
(value2 & 0b10000000) != 0) // highest bit 1 is true
{
var value1Chop = (value1 & 0b00011111) << 6;
var value2Chop = value2 & 0b00111111;
output[strlen++] = (char)(value1Chop | value2Chop);
continue;
}
var value3 = bytes[++i];
if ((value1 & 0b00010000) == 0 && // highest bit 4 is false
(value1 & 0b11100000) != 0 && // highest bit 1,2,3 are true
(value2 & 0b01000000) == 0 && // highest bit 2 is false
(value2 & 0b10000000) != 0 && // highest bit 1 is true
(value3 & 0b01000000) == 0 && // highest bit 2 is false
(value3 & 0b10000000) != 0) // highest bit 1 is true
{
var value1Chop = (value1 & 0b00001111) << 12;
var value2Chop = (value2 & 0b00111111) << 6;
var value3Chop = value3 & 0b00111111;
output[strlen++] = (char)(value1Chop | value2Chop | value3Chop);
continue;
}
}
return new string(output, 0, strlen);
}
// https://github.com/sedmelluq/lavaplayer/blob/b0c536098c4f92e6d03b00f19221021f8f50b19b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DataFormatTools.java#L114-L125
///
/// Reads the nullable string.
///
/// A string.
public string ReadNullableString() => this.ReadBoolean() ? this.ReadJavaUtf8() : null;
// swap endianness
///
/// Reads the decimal.
///
/// A decimal.
public override decimal ReadDecimal() => throw new MissingMethodException("This method does not have a Java equivalent");
// from https://github.com/Zoltu/Zoltu.EndianAwareBinaryReaderWriter under CC0
///
/// Reads the single.
///
/// A float.
public override float ReadSingle() => this.Read(4, BitConverter.ToSingle);
///
/// Reads the double.
///
/// A double.
public override double ReadDouble() => this.Read(8, BitConverter.ToDouble);
///
/// Reads the int16.
///
/// A short.
public override short ReadInt16() => this.Read(2, BitConverter.ToInt16);
///
/// Reads the int32.
///
/// An int.
public override int ReadInt32() => this.Read(4, BitConverter.ToInt32);
///
/// Reads the int64.
///
/// A long.
public override long ReadInt64() => this.Read(8, BitConverter.ToInt64);
///
/// Reads the u int16.
///
/// An ushort.
public override ushort ReadUInt16() => this.Read(2, BitConverter.ToUInt16);
///
/// Reads the u int32.
///
/// An uint.
public override uint ReadUInt32() => this.Read(4, BitConverter.ToUInt32);
///
/// Reads the u int64.
///
/// An ulong.
public override ulong ReadUInt64() => this.Read(8, BitConverter.ToUInt64);
///
/// Reads the.
///
/// The size.
/// The converter.
/// A T.
private T Read(int size, Func converter) where T : struct
{
//Contract.Requires(size >= 0);
//Contract.Requires(converter != null);
var bytes = this.GetNextBytesNativeEndian(size);
return converter(bytes, 0);
}
///
/// Gets the next bytes native endian.
///
/// The count.
/// An array of byte.
private byte[] GetNextBytesNativeEndian(int count)
{
//Contract.Requires(count >= 0);
//Contract.Ensures(Contract.Result() != null);
//Contract.Ensures(Contract.Result().Length == count);
var bytes = this.GetNextBytes(count);
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return bytes;
}
///
/// Gets the next bytes.
///
/// The count.
/// An array of byte.
private byte[] GetNextBytes(int count)
{
//Contract.Requires(count >= 0);
//Contract.Ensures(Contract.Result() != null);
//Contract.Ensures(Contract.Result().Length == count);
var buffer = new byte[count];
var bytesRead = this.BaseStream.Read(buffer, 0, count);
return bytesRead != count ? throw new EndOfStreamException() : buffer;
}
}
}
diff --git a/DisCatSharp/Entities/Color/DiscordColor.cs b/DisCatSharp/Entities/Color/DiscordColor.cs
index 3fe0a8892..a8d7fe46d 100644
--- a/DisCatSharp/Entities/Color/DiscordColor.cs
+++ b/DisCatSharp/Entities/Color/DiscordColor.cs
@@ -1,130 +1,130 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Globalization;
using System.Linq;
namespace DisCatSharp.Entities
{
///
/// Represents a color used in Discord API.
///
public partial struct DiscordColor
{
- private static readonly char[] s_hexAlphabet = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+ private static readonly char[] _hexAlphabet = new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
///
/// Gets the integer representation of this color.
///
public int Value { get; }
///
/// Gets the red component of this color as an 8-bit integer.
///
public byte R
=> (byte)((this.Value >> 16) & 0xFF);
///
/// Gets the green component of this color as an 8-bit integer.
///
public byte G
=> (byte)((this.Value >> 8) & 0xFF);
///
/// Gets the blue component of this color as an 8-bit integer.
///
public byte B
=> (byte)(this.Value & 0xFF);
///
/// Creates a new color with specified value.
///
/// Value of the color.
public DiscordColor(int color)
{
this.Value = color;
}
///
/// Creates a new color with specified values for red, green, and blue components.
///
/// Value of the red component.
/// Value of the green component.
/// Value of the blue component.
public DiscordColor(byte r, byte g, byte b)
{
this.Value = (r << 16) | (g << 8) | b;
}
///
/// Creates a new color with specified values for red, green, and blue components.
///
/// Value of the red component.
/// Value of the green component.
/// Value of the blue component.
public DiscordColor(float r, float g, float b)
{
if (r < 0 || r > 1 || g < 0 || g > 1 || b < 0 || b > 1)
throw new ArgumentOutOfRangeException("Each component must be between 0.0 and 1.0 inclusive.");
var rb = (byte)(r * 255);
var gb = (byte)(g * 255);
var bb = (byte)(b * 255);
this.Value = (rb << 16) | (gb << 8) | bb;
}
///
/// Creates a new color from specified string representation.
///
/// String representation of the color. Must be 6 hexadecimal characters, optionally with # prefix.
public DiscordColor(string color)
{
if (string.IsNullOrWhiteSpace(color))
throw new ArgumentNullException(nameof(color), "Null or empty values are not allowed!");
if (color.Length != 6 && color.Length != 7)
throw new ArgumentException(nameof(color), "Color must be 6 or 7 characters in length.");
color = color.ToUpper();
if (color.Length == 7 && color[0] != '#')
throw new ArgumentException(nameof(color), "7-character colors must begin with #.");
else if (color.Length == 7)
color = color[1..];
- if (color.Any(xc => !s_hexAlphabet.Contains(xc)))
+ if (color.Any(xc => !_hexAlphabet.Contains(xc)))
throw new ArgumentException(nameof(color), "Colors must consist of hexadecimal characters only.");
this.Value = int.Parse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
}
///
/// Gets a string representation of this color.
///
/// String representation of this color.
public override string ToString() => $"#{this.Value:X6}";
public static implicit operator DiscordColor(int value)
=> new(value);
}
}
diff --git a/DisCatSharp/Logging/DefaultLogger.cs b/DisCatSharp/Logging/DefaultLogger.cs
index 2584cd6ae..0994f996b 100644
--- a/DisCatSharp/Logging/DefaultLogger.cs
+++ b/DisCatSharp/Logging/DefaultLogger.cs
@@ -1,148 +1,148 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using Microsoft.Extensions.Logging;
namespace DisCatSharp
{
///
/// Represents a default logger.
///
public class DefaultLogger : ILogger
{
- private static readonly object s_lock = new();
+ private static readonly object _lock = new();
///
/// Gets the minimum log level.
///
private readonly LogLevel _minimumLevel;
///
/// Gets the timestamp format.
///
private readonly string _timestampFormat;
///
/// Initializes a new instance of the class.
///
/// The client.
internal DefaultLogger(BaseDiscordClient client)
: this(client.Configuration.MinimumLogLevel, client.Configuration.LogTimestampFormat)
{ }
///
/// Initializes a new instance of the class.
///
/// The min level.
/// The timestamp format.
internal DefaultLogger(LogLevel minLevel = LogLevel.Information, string timestampFormat = "yyyy-MM-dd HH:mm:ss zzz")
{
this._minimumLevel = minLevel;
this._timestampFormat = timestampFormat;
}
///
/// Logs an event.
///
/// The log level.
/// The event id.
/// The state.
/// The exception.
/// The formatter.
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
{
if (!this.IsEnabled(logLevel))
return;
- lock (s_lock)
+ lock (_lock)
{
var ename = eventId.Name;
ename = ename?.Length > 12 ? ename?[..12] : ename;
Console.Write($"[{DateTimeOffset.Now.ToString(this._timestampFormat)}] [{eventId.Id,-4}/{ename,-12}] ");
switch (logLevel)
{
case LogLevel.Trace:
Console.ForegroundColor = ConsoleColor.Gray;
break;
case LogLevel.Debug:
Console.ForegroundColor = ConsoleColor.DarkMagenta;
break;
case LogLevel.Information:
Console.ForegroundColor = ConsoleColor.DarkCyan;
break;
case LogLevel.Warning:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
case LogLevel.Error:
Console.ForegroundColor = ConsoleColor.Red;
break;
case LogLevel.Critical:
Console.BackgroundColor = ConsoleColor.Red;
Console.ForegroundColor = ConsoleColor.Black;
break;
}
Console.Write(logLevel switch
{
LogLevel.Trace => "[Trace] ",
LogLevel.Debug => "[Debug] ",
LogLevel.Information => "[Info ] ",
LogLevel.Warning => "[Warn ] ",
LogLevel.Error => "[Error] ",
LogLevel.Critical => "[Crit ]",
LogLevel.None => "[None ] ",
_ => "[?????] "
});
Console.ResetColor();
//The foreground color is off.
if (logLevel == LogLevel.Critical)
Console.Write(" ");
var message = formatter(state, exception);
Console.WriteLine(message);
if (exception != null)
Console.WriteLine(exception);
}
}
///
/// Whether the logger is enabled.
///
/// The log level.
public bool IsEnabled(LogLevel logLevel)
=> logLevel >= this._minimumLevel;
///
/// Begins the scope.
///
/// The state.
/// An IDisposable.
public IDisposable BeginScope(TState state) => throw new NotImplementedException();
}
}
diff --git a/DisCatSharp/Net/Rest/RateLimitBucket.cs b/DisCatSharp/Net/Rest/RateLimitBucket.cs
index d3027623d..ec4dff4ba 100644
--- a/DisCatSharp/Net/Rest/RateLimitBucket.cs
+++ b/DisCatSharp/Net/Rest/RateLimitBucket.cs
@@ -1,291 +1,291 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace DisCatSharp.Net
{
///
/// Represents a rate limit bucket.
///
internal class RateLimitBucket : IEquatable
{
///
/// Gets the Id of the guild bucket.
///
public string GuildId { get; internal set; }
///
/// Gets the Id of the channel bucket.
///
public string ChannelId { get; internal set; }
///
/// Gets the ID of the webhook bucket.
///
public string WebhookId { get; internal set; }
///
/// Gets the Id of the ratelimit bucket.
///
public volatile string BucketId;
///
/// Gets or sets the ratelimit hash of this bucket.
///
public string Hash
{
get => Volatile.Read(ref this.HashInternal);
internal set
{
- this.IsUnlimited = value.Contains(s_unlimitedHash);
+ this.IsUnlimited = value.Contains(_unlimitedHash);
if (this.BucketId != null && !this.BucketId.StartsWith(value))
{
var id = GenerateBucketId(value, this.GuildId, this.ChannelId, this.WebhookId);
this.BucketId = id;
this.RouteHashes.Add(id);
}
Volatile.Write(ref this.HashInternal, value);
}
}
internal string HashInternal;
///
/// Gets the past route hashes associated with this bucket.
///
public ConcurrentBag RouteHashes { get; }
///
/// Gets when this bucket was last called in a request.
///
public DateTimeOffset LastAttemptAt { get; internal set; }
///
/// Gets the number of uses left before pre-emptive rate limit is triggered.
///
public int Remaining
=> this.RemainingInternal;
///
/// Gets the maximum number of uses within a single bucket.
///
public int Maximum { get; set; }
///
/// Gets the timestamp at which the rate limit resets.
///
public DateTimeOffset Reset { get; internal set; }
///
/// Gets the time interval to wait before the rate limit resets.
///
public TimeSpan? ResetAfter { get; internal set; } = null;
///
/// Gets a value indicating whether the ratelimit global.
///
public bool IsGlobal { get; internal set; } = false;
///
/// Gets the ratelimit scope.
///
public string Scope { get; internal set; } = "user";
///
/// Gets the time interval to wait before the rate limit resets as offset
///
internal DateTimeOffset ResetAfterOffset { get; set; }
internal volatile int RemainingInternal;
///
/// Gets whether this bucket has it's ratelimit determined.
/// This will be if the ratelimit is determined.
///
internal volatile bool IsUnlimited;
///
/// If the initial request for this bucket that is deterternining the rate limits is currently executing
/// This is a int because booleans can't be accessed atomically
/// 0 => False, all other values => True
///
internal volatile int LimitTesting;
///
/// Task to wait for the rate limit test to finish
///
internal volatile Task LimitTestFinished;
///
/// If the rate limits have been determined
///
internal volatile bool LimitValid;
///
/// Rate limit reset in ticks, UTC on the next response after the rate limit has been reset
///
internal long NextReset;
///
/// If the rate limit is currently being reset.
/// This is a int because booleans can't be accessed atomically.
/// 0 => False, all other values => True
///
internal volatile int LimitResetting;
- private static readonly string s_unlimitedHash = "unlimited";
+ private static readonly string _unlimitedHash = "unlimited";
///
/// Initializes a new instance of the class.
///
/// The hash.
/// The guild_id.
/// The channel_id.
/// The webhook_id.
internal RateLimitBucket(string hash, string guildId, string channelId, string webhookId)
{
this.Hash = hash;
this.ChannelId = channelId;
this.GuildId = guildId;
this.WebhookId = webhookId;
this.BucketId = GenerateBucketId(hash, guildId, channelId, webhookId);
this.RouteHashes = new ConcurrentBag();
}
///
/// Generates an ID for this request bucket.
///
/// Hash for this bucket.
/// Guild Id for this bucket.
/// Channel Id for this bucket.
/// Webhook Id for this bucket.
/// Bucket Id.
public static string GenerateBucketId(string hash, string guildId, string channelId, string webhookId)
=> $"{hash}:{guildId}:{channelId}:{webhookId}";
///
/// Generates the hash key.
///
/// The method.
/// The route.
/// A string.
public static string GenerateHashKey(RestRequestMethod method, string route)
=> $"{method}:{route}";
///
/// Generates the unlimited hash.
///
/// The method.
/// The route.
/// A string.
public static string GenerateUnlimitedHash(RestRequestMethod method, string route)
- => $"{GenerateHashKey(method, route)}:{s_unlimitedHash}";
+ => $"{GenerateHashKey(method, route)}:{_unlimitedHash}";
///
/// Returns a string representation of this bucket.
///
/// String representation of this bucket.
public override string ToString()
{
var guildId = this.GuildId != string.Empty ? this.GuildId : "guild_id";
var channelId = this.ChannelId != string.Empty ? this.ChannelId : "channel_id";
var webhookId = this.WebhookId != string.Empty ? this.WebhookId : "webhook_id";
return $"{this.Scope} rate limit bucket [{this.Hash}:{guildId}:{channelId}:{webhookId}] [{this.Remaining}/{this.Maximum}] {(this.ResetAfter.HasValue ? this.ResetAfterOffset : this.Reset)}";
}
///
/// Checks whether this is equal to another object.
///
/// Object to compare to.
/// Whether the object is equal to this .
public override bool Equals(object obj)
=> this.Equals(obj as RateLimitBucket);
///
/// Checks whether this is equal to another .
///
/// to compare to.
/// Whether the is equal to this .
public bool Equals(RateLimitBucket e) => e is not null && (ReferenceEquals(this, e) || this.BucketId == e.BucketId);
///
/// Gets the hash code for this .
///
/// The hash code for this .
public override int GetHashCode()
=> this.BucketId.GetHashCode();
///
/// Sets remaining number of requests to the maximum when the ratelimit is reset
///
///
internal async Task TryResetLimitAsync(DateTimeOffset now)
{
if (this.ResetAfter.HasValue)
this.ResetAfter = this.ResetAfterOffset - now;
if (this.NextReset == 0)
return;
if (this.NextReset > now.UtcTicks)
return;
while (Interlocked.CompareExchange(ref this.LimitResetting, 1, 0) != 0)
#pragma warning restore 420
await Task.Yield();
if (this.NextReset != 0)
{
this.RemainingInternal = this.Maximum;
this.NextReset = 0;
}
this.LimitResetting = 0;
}
///
/// Sets the initial values.
///
/// The max.
/// The uses left.
/// The new reset.
internal void SetInitialValues(int max, int usesLeft, DateTimeOffset newReset)
{
this.Maximum = max;
this.RemainingInternal = usesLeft;
this.NextReset = newReset.UtcTicks;
this.LimitValid = true;
this.LimitTestFinished = null;
this.LimitTesting = 0;
}
}
}
diff --git a/DisCatSharp/Net/Serialization/DiscordJson.cs b/DisCatSharp/Net/Serialization/DiscordJson.cs
index 2454cd149..77716a742 100644
--- a/DisCatSharp/Net/Serialization/DiscordJson.cs
+++ b/DisCatSharp/Net/Serialization/DiscordJson.cs
@@ -1,82 +1,82 @@
// This file is part of the DisCatSharp project, based off DSharpPlus.
//
// Copyright (c) 2021 AITSYS
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System;
using System.Globalization;
using System.IO;
using System.Text;
using DisCatSharp.Entities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace DisCatSharp.Net.Serialization
{
///
/// Represents discord json.
///
public static class DiscordJson
{
- private static readonly JsonSerializer s_serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings
+ private static readonly JsonSerializer _serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings
{
ContractResolver = new OptionalJsonContractResolver()
});
/// Serializes the specified object to a JSON string.
/// The object to serialize.
/// A JSON string representation of the object.
- public static string SerializeObject(object value) => SerializeObjectInternal(value, null, s_serializer);
+ public static string SerializeObject(object value) => SerializeObjectInternal(value, null, _serializer);
/// Populates an object with the values from a JSON node.
/// The token to populate the object with.
/// The object to populate.
public static void PopulateObject(JToken value, object target)
{
using var reader = value.CreateReader();
- s_serializer.Populate(reader, target);
+ _serializer.Populate(reader, target);
}
///
/// Converts this token into an object, passing any properties through extra s if needed.
///
/// The token to convert
/// Type to convert to
/// The converted token
- public static T ToDiscordObject(this JToken token) => token.ToObject(s_serializer);
+ public static T ToDiscordObject(this JToken token) => token.ToObject(_serializer);
///
/// Serializes the object.
///
/// The value.
/// The type.
/// The json serializer.
private static string SerializeObjectInternal(object value, Type type, JsonSerializer jsonSerializer)
{
var stringWriter = new StringWriter(new StringBuilder(256), CultureInfo.InvariantCulture);
using (var jsonTextWriter = new JsonTextWriter(stringWriter))
{
jsonTextWriter.Formatting = jsonSerializer.Formatting;
jsonSerializer.Serialize(jsonTextWriter, value, type);
}
return stringWriter.ToString();
}
}
}