diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 095dea55b..5fd6d1d52 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,2091 +1,2091 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.Context; using DisCatSharp.ApplicationCommands.Entities; using DisCatSharp.ApplicationCommands.Enums; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.ApplicationCommands.Exceptions; using DisCatSharp.ApplicationCommands.Workers; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands; /// /// A class that handles slash commands for a client. /// public sealed class ApplicationCommandsExtension : BaseExtension { /// /// A list of methods for top level commands. /// private static List s_commandMethods { get; set; } = new(); /// /// List of groups. /// private static List s_groupCommands { get; set; } = new(); /// /// List of groups with subgroups. /// private static List s_subGroupCommands { get; set; } = new(); /// /// List of context menus. /// private static List s_contextMenuCommands { get; set; } = new(); /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = new(); /// /// List of modules to register. /// private readonly List> _updateList = new(); /// /// Configuration for Discord. /// internal static ApplicationCommandsConfiguration Configuration; /// /// Set to true if anything fails when registering. /// private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => s_registeredCommands; private static List>> s_registeredCommands = new(); /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => GlobalCommandsInternal; internal static List GlobalCommandsInternal = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; internal static Dictionary> GuildCommandsInternal = new(); /// /// Gets the registration count. /// private static int s_registrationCount { get; set; } /// /// Gets the expected count. /// private static int s_expectedCount { get; set; } /// /// Gets the guild ids where the applications.commands scope is missing. /// private List _missingScopeGuildIds; /// /// Gets whether debug is enabled. /// internal static bool DebugEnabled { get; set; } internal static LogLevel ApplicationCommandsLogLevel => DebugEnabled ? LogLevel.Debug : LogLevel.Trace; /// /// Gets whether check through all guilds is enabled. /// internal static bool CheckAllGuilds { get; set; } /// /// Gets whether the registration check should be manually overridden. /// internal static bool ManOr { get; set; } /// /// Gets whether interactions should be automatically deffered. /// internal static bool AutoDeferEnabled { get; set; } /// /// Whether this module finished the startup. /// internal bool StartupFinished { get; set; } = false; /// /// Gets the service provider this module was configured with. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] public IServiceProvider Services => Configuration.ServiceProvider; /// /// Gets a list of handled interactions. Fix for double interaction execution bug. /// internal static List HandledInteractions = new(); /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration = null) { Configuration = configuration; DebugEnabled = configuration?.DebugStartup ?? false; CheckAllGuilds = configuration?.CheckAllGuilds ?? false; ManOr = configuration?.ManualOverride ?? false; AutoDeferEnabled = configuration?.AutoDefer ?? false; } /// /// Runs setup. /// DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. /// /// The client to setup on. protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null); this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null); this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null); this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null); this._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null); this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null); this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null); this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null); this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync(); this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e) { if (!this.StartupFinished) await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")); else await Task.Delay(1); } private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) { if (!this.StartupFinished) await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")); else await Task.Delay(1); } private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.StartupFinished = true; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Cleans the module for a new start of the bot. /// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES. /// public void CleanModule() { this._updateList.Clear(); s_singletonModules.Clear(); s_errored = false; s_expectedCount = 0; s_registrationCount = 0; s_commandMethods.Clear(); s_groupCommands.Clear(); s_contextMenuCommands.Clear(); s_subGroupCommands.Clear(); s_singletonModules.Clear(); s_registeredCommands.Clear(); GlobalCommandsInternal.Clear(); GuildCommandsInternal.Clear(); } /// /// Cleans all guild application commands. /// You normally don't need to execute it. /// internal async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } /// /// Cleans the global application commands. /// You normally don't need to execute it. /// internal async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with optional translation setup for a guild. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup for a guild. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Registers a command class with optional translation setup globally. /// /// The command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup globally. /// /// The of the command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Type type, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add => this._applicationCommandsModuleReady.Register(value); remove => this._applicationCommandsModuleReady.Unregister(value); } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add => this._applicationCommandsModuleStartupFinished.Register(value); remove => this._applicationCommandsModuleStartupFinished.Unregister(value); } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add => this._guildApplicationCommandsRegistered.Register(value); remove => this._guildApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add => this._globalApplicationCommandsRegistered.Register(value); remove => this._globalApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Request to register commands on shard {shard}", this.Client.ShardId); if (this.StartupFinished) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} already setup, skipping", this.Client.ShardId); this.FinishedRegistration(); return; } GlobalDiscordCommands = new(); GuildDiscordCommands = new(); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {count}", s_expectedCount); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} has {guilds} guilds.", this.Client.ShardId, this.Client.Guilds?.Count); List failedGuilds = new(); List globalCommands = null; globalCommands = (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false)).ToList() ?? null; var updateList = this._updateList; var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys.ToList() : updateList.Where(x => x.Key != null)?.Select(x => x.Key.Value).Distinct().ToList(); var wrongShards = guilds.Where(x => !this.Client.Guilds.ContainsKey(x)).ToList(); if (wrongShards.Any()) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Some guilds are not on the same shard as the client. Removing them from the update list."); foreach (var guild in wrongShards) { updateList.RemoveAll(x => x.Key == guild); guilds.Remove(guild); } } var commandsPending = updateList.Select(x => x.Key).Distinct().ToList(); s_expectedCount = commandsPending.Count; foreach (var guild in guilds) { List commands = null; var unauthorized = false; try { commands = (await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false)).ToList() ?? null; } catch (UnauthorizedException) { unauthorized = true; } finally { if (!unauthorized && commands != null && commands.Any()) GuildDiscordCommands.Add(guild, commands.ToList()); else if (unauthorized) failedGuilds.Add(guild); } } //Default should be to add the help and slash commands can be added without setting any configuration //so this should still add the default help if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp)) { updateList.Add(new KeyValuePair (null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule)))); commandsPending = updateList.Select(x => x.Key).Distinct().ToList(); } if (globalCommands != null && globalCommands.Any()) GlobalDiscordCommands.AddRange(globalCommands); foreach (var key in commandsPending) { this.Client.Logger.Log(ApplicationCommandsLogLevel, key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); if (key.HasValue) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering"); } await this.RegisterCommands(updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key); } this._missingScopeGuildIds = new(failedGuilds); await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider) { GuildsWithoutScope = failedGuilds }); this.Client.GuildDownloadCompleted -= async (c, e) => await this.UpdateAsync(); } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private async Task RegisterCommands(List types, ulong? guildId) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering commands on shard {shard}", this.Client.ShardId); //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); var groupTranslation = new List(); var translation = new List(); //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); //Add module to classes list if it's a group var extremeNestedGroup = false; if (module.GetCustomAttribute() != null) { classes.Add(module); } else if (module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Any(x => x.IsDefined(typeof(SlashCommandGroupAttribute)))) { //Otherwise add the extreme nested groups classes = module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) .Where(x => x.IsDefined(typeof(SlashCommandGroupAttribute))) .Select(x => module.GetNestedType(x.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetTypeInfo()).ToList(); extremeNestedGroup = true; } else { //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); } if (module.GetCustomAttribute() != null || extremeNestedGroup) { List groupTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations); if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any()) { updateList.AddRange(slashGroupsTuple.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cgwsgs = new List(); var cgs2 = new List(); foreach (var cmd in slashGroupsTuple.applicationCommands) { if (cmd.Type == ApplicationCommandType.ChatInput) { if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommandGroup) { var cgs = new List(); foreach (var scg in cmd.Options) { var cs = new List(); foreach (var sc in scg.Options) { if (sc.Options == null || !sc.Options.Any()) cs.Add(new Command(sc.Name, sc.Description, null, null)); else cs.Add(new Command(sc.Name, sc.Description, sc.Options.ToList(), null)); } cgs.Add(new CommandGroup(scg.Name, scg.Description, cs, null)); } cgwsgs.Add(new CommandGroupWithSubGroups(cmd.Name, cmd.Description, cgs, ApplicationCommandType.ChatInput)); } else if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommand) { var cs2 = new List(); foreach (var sc2 in cmd.Options) { if (sc2.Options == null || !sc2.Options.Any()) cs2.Add(new Command(sc2.Name, sc2.Description, null, null)); else cs2.Add(new Command(sc2.Name, sc2.Description, sc2.Options.ToList(), null)); } cgs2.Add(new CommandGroup(cmd.Name, cmd.Description, cs2, ApplicationCommandType.ChatInput)); } } } if (cgwsgs.Any()) foreach (var cgwsg in cgwsgs) groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg))); if (cgs2.Any()) foreach (var cg2 in cgs2) groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cg2))); } } if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any()) commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources); if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any()) s_singletonModules.AddRange(slashGroupsTuple.singletonModules); if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any()) groupCommands.AddRange(slashGroupsTuple.groupCommands); if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any()) subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands); } //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { List commandTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } //Slash commands var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations); if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any()) { updateList.AddRange(slashCommands.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cs = new List(); foreach (var cmd in slashCommands.applicationCommands) if (cmd.Type == ApplicationCommandType.ChatInput && (cmd.Options == null || !cmd.Options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup))) { if (cmd.Options == null || !cmd.Options.Any()) cs.Add(new Command(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput)); else cs.Add(new Command(cmd.Name, cmd.Description, cmd.Options.ToList(), ApplicationCommandType.ChatInput)); } if (cs.Any()) foreach (var c in cs) translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))); } } if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any()) commandTypeSources.AddRange(slashCommands.commandTypeSources); if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any()) commandMethods.AddRange(slashCommands.commandMethods); //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations); if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any()) { updateList.AddRange(contextCommands.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cs = new List(); foreach (var cmd in contextCommands.applicationCommands) if (cmd.Type == ApplicationCommandType.Message || cmd.Type == ApplicationCommandType.User) cs.Add(new Command(cmd.Name, null, null, cmd.Type)); if (cs.Any()) foreach (var c in cs) translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))); } } if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any()) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any()) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response); else this.Client.Logger.LogCritical(ex, @"There was an error parsing the application commands"); } s_errored = true; } } if (!s_errored) { updateList = updateList.DistinctBy(x => x.Name).ToList(); if (Configuration.GenerateTranslationFilesOnly) { s_registrationCount++; this.CheckRegistrationStartup(ManOr, translation, groupTranslation); } else { try { List commands = new(); try { if (guildId == null) { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } else { foreach (var cmd in GlobalDiscordCommands) { try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete global command {cmdId}. Please clean up manually", cmd.Id); } } } } else { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildId.Value, actualCommands); if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild)) guild.InternalRegisteredApplicationCommands.AddRange(actualCommands); } else { foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value) { try { await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete guild command {cmdId} in guild {guildId}. Please clean up manually", cmd.Id, guildId.Value); } } } } } catch (UnauthorizedException ex) { this.Client.Logger.LogError("Could not register application commands for guild {guildId}.\nError: {exc}", guildId, ex.JsonMessage); return; } //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in commands) { if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com)) com.CommandId = command.Id; else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) groupCom.CommandId = command.Id; else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom)) subCom.CommandId = command.Id; else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) cmCom.CommandId = command.Id; } //Adds to the global lists finally s_commandMethods.AddRange(commandMethods.DistinctBy(x => x.Name)); s_groupCommands.AddRange(groupCommands.DistinctBy(x => x.Name)); s_subGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name)); s_contextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name)); s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, s_registrationCount); if (guildId.HasValue) { await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null }); } else { await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }); } s_registrationCount++; this.CheckRegistrationStartup(ManOr); } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response); else this.Client.Logger.LogCritical(ex, @"There was an general error registering application commands"); } s_errored = true; } } } } private async void CheckRegistrationStartup(bool man = false, List translation = null, List groupTranslation = null) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Checking counts...\n\nExpected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, s_registrationCount); if ((s_registrationCount == s_expectedCount) || man) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredGlobalCommands = GlobalCommandsInternal, RegisteredGuildCommands = GuildCommandsInternal, GuildsWithoutScope = this._missingScopeGuildIds }); if (Configuration.GenerateTranslationFilesOnly) { try { if (translation != null && translation.Any()) { - var file_name = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{s_registrationCount}_of_{s_expectedCount}.json"; - var fs = File.Create(file_name); + var fileName = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{s_registrationCount}_of_{s_expectedCount}.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)); await writer.FlushAsync(); ms.Position = 0; await ms.CopyToAsync(fs); await fs.FlushAsync(); fs.Close(); await fs.DisposeAsync(); ms.Close(); await ms.DisposeAsync(); - this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name); + this.Client.Logger.LogInformation("Exported base translation to {exppath}", fileName); } if (groupTranslation != null && groupTranslation.Any()) { - var file_name = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{s_registrationCount}_of_{s_expectedCount}.json"; - var fs = File.Create(file_name); + var fileName = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{s_registrationCount}_of_{s_expectedCount}.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)); await writer.FlushAsync(); ms.Position = 0; await ms.CopyToAsync(fs); await fs.FlushAsync(); fs.Close(); await fs.DisposeAsync(); ms.Close(); await ms.DisposeAsync(); - this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name); + 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); } this.FinishedRegistration(); await this.Client.DisconnectAsync(); } else { this.FinishedRegistration(); } } } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got slash interaction on shard {shard}", this.Client.ShardId); if (HandledInteractions.Contains(e.Interaction.Id)) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received"); return Task.FromResult(true); } else HandledInteractions.Add(e.Interaction.Id); _ = Task.Run(async () => { if (e.Interaction.Type == InteractionType.ApplicationCommand) { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = Configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(), Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup.")); throw new InvalidOperationException("Application commands failed to register properly on startup."); } var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A application command was executed, but no command was registered for it.")); throw new InvalidOperationException("A application command was executed, but no command was registered for it."); } if (methods.Any()) { var method = methods.First().Method; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); await this.RunCommandAsync(context, method, args); } else if (groups.Any()) { var command = e.Interaction.Data.Options[0]; var method = groups.First().Methods.First(x => x.Key == command.Name).Value; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options); await this.RunCommandAsync(context, method, args); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options[0]; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var method = group.Methods.First(x => x.Key == command.Options[0].Name).Value; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options[0].Options); await this.RunCommandAsync(context, method, args); } await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup.")); throw new InvalidOperationException("Application commands failed to register properly on startup."); } var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("An autocomplete interaction was created, but no command was registered for it")); throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it"); } try { if (methods.Any()) { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = client, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (groups.Any()) { var command = e.Interaction.Data.Options[0]; var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options[0]; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options[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 }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got context menu interaction on shard {shard}", this.Client.ShardId); if (HandledInteractions.Contains(e.Interaction.Id)) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received"); return Task.FromResult(true); } else HandledInteractions.Add(e.Interaction.Id); _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = Configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Context menus failed to register properly on startup.")); throw new InvalidOperationException("Context menus failed to register properly on startup."); } //Gets the method for the command var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A context menu command was executed, but no command was registered for it.")); throw new InvalidOperationException("A context menu command was executed, but no command was registered for it."); } await this.RunCommandAsync(context, method.Method, new[] { context }); await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { object classInstance; this.Client.Logger.Log(ApplicationCommandsLogLevel, "Executing {cmd}", method.Name); //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; switch (moduleLifespan) { case ApplicationCommandModuleLifespan.Scoped: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); break; default: throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); } ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; // Slash commands if (context is InteractionContext slashContext) { await this.RunPreexecutionChecksAsync(method, slashContext); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection /// /// The type. /// The services. internal static object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1); foreach (var parameter in parameters) { //Accounts for optional arguments without values given if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(ulong) || parameter.ParameterType == typeof(ulong?)) args.Add((ulong?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) if (option.Value == null) args.Add(null); else args.Add(Convert.ToInt64(option.Value)); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the pre-execution checks. /// /// The method info. /// The base context. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext cMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(cMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null) { var choices = new List(); foreach (var choiceProviderAttribute in customAttributes) { var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); if (method == null) throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); else { var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); // Abstract class offers more properties that can be set if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) { choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) ?.SetValue(instance, guildId); choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) ?.SetValue(instance, Configuration.ServiceProvider); } //Gets the choices from the method var result = await (Task>)method.Invoke(instance, null); if (result.Any()) { choices.AddRange(result); } } } return choices; } /// /// Gets the choice attributes from enum parameter. /// /// The enum parameter. private static List GetChoiceAttributesFromEnumParameter(Type enumParam) { var choices = new List(); foreach (Enum enumValue in Enum.GetValues(enumParam)) { choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); } return choices; } /// /// Gets the parameter type. /// /// The type. private static ApplicationCommandOptionType GetParameterType(Type type) { var parameterType = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parameterType; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) => !choiceAttributes.Any() ? null : choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); /// /// Parses the parameters. /// /// The parameters. /// The optional guild id. internal static async Task> ParseParametersAsync(IEnumerable parameters, string commandName, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionAttribute = parameter.GetCustomAttribute(); if (optionAttribute == null) 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(); if (optionAttribute.Autocomplete && autocompleteAttribute == null) throw new ArgumentException($"The command '{commandName}' has autocomplete enabled but is missing an autocomplete attribute!"); if (!optionAttribute.Autocomplete && 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); if (parameterType == ApplicationCommandOptionType.String) { minimumValue = null; maximumValue = null; } else if (parameterType == ApplicationCommandOptionType.Integer || parameterType == ApplicationCommandOptionType.Number) { minimumLength = null; maximumLength = null; } if (parameterType != ApplicationCommandOptionType.Channel) channelTypes = null; //Handles choices //From attributes var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); //From enums if (parameter.ParameterType.IsEnum) { choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); } //From choice provider var choiceProviders = parameter.GetCustomAttributes(); if (choiceProviders.Any()) { choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId); } options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength)); } return options; } /* /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. /// public async Task RefreshCommandsAsync() { s_commandMethods.Clear(); s_groupCommands.Clear(); s_subGroupCommands.Clear(); s_registeredCommands.Clear(); s_contextMenuCommands.Clear(); GlobalDiscordCommands.Clear(); GuildDiscordCommands.Clear(); GuildCommandsInternal.Clear(); GlobalCommandsInternal.Clear(); GlobalDiscordCommands = null; GuildDiscordCommands = null; s_errored = false; /*if (Configuration != null && Configuration.EnableDefaultHelp) { this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule)); }*/ /* await this.UpdateAsync(); }*/ /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { add => this._slashError.Register(value); remove => this._slashError.Unregister(value); } private AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add => this._slashExecuted.Register(value); remove => this._slashExecuted.Unregister(value); } private AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add => this._contextMenuErrored.Register(value); remove => this._contextMenuErrored.Unregister(value); } private AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add => this._contextMenuExecuted.Register(value); remove => this._contextMenuExecuted.Unregister(value); } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The translation setup. /// public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action translations = null) { this.Type = type; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = new(); } /// /// The context menu command. /// internal class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } #region Default Help /// /// Represents the default help module. /// internal class DefaultHelpModule : ApplicationCommandsModule { public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } foreach (var sc in slashCommands.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())); } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase)); if (command is null || command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand && c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); return options.AsEnumerable(); } var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (foundCommand is null || foundCommand.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand && x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } [SlashCommand("help", "Displays command help")] internal async Task DefaultHelpAsync(InteractionContext ctx, [Autocomplete(typeof(DefaultHelpAutoCompleteProvider))] [Option("option_one", "top level command to provide help for", true)] string commandName, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))] [Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))] [Option("option_three", "command to provide help for", true)] string commandTwoName = null) { List applicationCommands = null; var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); if (ctx.Guild != null) { var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } else { await Task.WhenAll(globalCommandsTask); applicationCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } if (applicationCommands.Count < 1) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"There are no slash commands")); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands").AsEphemeral(true)); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var subCommandParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options .FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {cmdParent.Name} {cmd.Name}")}: {cmd.Description ?? "No description provided."}" }; if (cmd.Options is not null) { var commandOptions = cmd.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .AddEmbed(discordEmbed)); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}")); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true)); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{command.Mention}: {command.Description ?? "No description provided."}" }.AddField(new DiscordEmbedField("Command is NSFW", command.IsNsfw.ToString())); if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } } } #endregion diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs index 21faddcde..3cd88e4ca 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandCooldownAttribute.cs @@ -1,157 +1,157 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Context; using DisCatSharp.ApplicationCommands.Entities; using DisCatSharp.ApplicationCommands.Enums; namespace DisCatSharp.ApplicationCommands.Attributes; /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public sealed class SlashCommandCooldownAttribute : ApplicationCommandCheckBaseAttribute, ICooldown { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// public int MaxUses { get; } /// /// Gets the time after which the cooldown is reset. /// public TimeSpan Reset { get; } /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// public CooldownBucketType BucketType { get; } /// /// Gets the cooldown buckets for this command. /// - internal readonly ConcurrentDictionary _buckets; + internal readonly ConcurrentDictionary Buckets; /// /// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again. /// /// Number of times the command can be used before triggering a cooldown. /// Number of seconds after which the cooldown is reset. /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally. public SlashCommandCooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) { this.MaxUses = maxUses; this.Reset = TimeSpan.FromSeconds(resetAfter); this.BucketType = bucketType; - this._buckets = new ConcurrentDictionary(); + this.Buckets = new ConcurrentDictionary(); } /// /// Gets a cooldown bucket for given command context. /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. public SlashCommandCooldownBucket GetBucket(BaseContext ctx) { var bid = this.GetBucketId(ctx, out _, out _, out _); - this._buckets.TryGetValue(bid, out var bucket); + this.Buckets.TryGetValue(bid, out var bucket); return bucket; } /// /// Calculates the cooldown remaining for given command context. /// /// Context for which to calculate the cooldown. /// Remaining cooldown, or zero if no cooldown is active. public TimeSpan GetRemainingCooldown(BaseContext ctx) { var bucket = this.GetBucket(ctx); return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow; } /// /// Calculates bucket ID for given command context. /// /// Context for which to calculate bucket ID for. /// ID of the user with which this bucket is associated. /// ID of the channel with which this bucket is associated. /// ID of the guild with which this bucket is associated. /// Calculated bucket ID. private string GetBucketId(BaseContext ctx, out ulong userId, out ulong channelId, out ulong guildId) { userId = 0ul; if ((this.BucketType & CooldownBucketType.User) != 0) userId = ctx.User.Id; channelId = 0ul; if ((this.BucketType & CooldownBucketType.Channel) != 0) channelId = ctx.Channel.Id; if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null) channelId = ctx.Channel.Id; guildId = 0ul; if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0) guildId = ctx.Guild.Id; var bid = CooldownBucket.MakeId(userId, channelId, guildId); return bid; } /// /// Executes a check. /// /// The command context. public override async Task ExecuteChecksAsync(BaseContext ctx) { var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld); - if (!this._buckets.TryGetValue(bid, out var bucket)) + if (!this.Buckets.TryGetValue(bid, out var bucket)) { bucket = new SlashCommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld); - this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); + this.Buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); } return await bucket.DecrementUseAsync().ConfigureAwait(false); } } /// /// Represents a cooldown bucket for commands. /// public sealed class SlashCommandCooldownBucket : CooldownBucket { /// /// Returns a string representation of this command cooldown bucket. /// /// String representation of this command cooldown bucket. public override string ToString() => $"Slash Command bucket {this.BucketId}"; internal SlashCommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) : base(maxUses, resetAfter, userId, channelId, guildId) { } } diff --git a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs index 3e380d725..45010f23f 100644 --- a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs +++ b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs @@ -1,329 +1,329 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using System.Linq; using DisCatSharp.Entities; using DisCatSharp.Enums; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands.Checks; internal static class ApplicationCommandEqualityChecks { /// /// Whether two application commands are equal. /// /// Source command. /// Command to check against. /// The discord client. - /// Whether the equal check is performed for a guild command. - internal static bool IsEqualTo(this DiscordApplicationCommand ac1, DiscordApplicationCommand targetApplicationCommand, DiscordClient client, bool IsGuild) + /// Whether the equal check is performed for a guild command. + internal static bool IsEqualTo(this DiscordApplicationCommand ac1, DiscordApplicationCommand targetApplicationCommand, DiscordClient client, bool isGuild) { if (targetApplicationCommand is null || ac1 is null) return false; DiscordApplicationCommand sourceApplicationCommand = new( ac1.Name, ac1.Description, ac1.Options, ac1.Type, ac1.NameLocalizations, ac1.DescriptionLocalizations, ac1.DefaultMemberPermissions, ac1.DmPermission ?? true//, ac1.IsNsfw ); if (sourceApplicationCommand.DefaultMemberPermissions == Permissions.None && targetApplicationCommand.DefaultMemberPermissions == null) sourceApplicationCommand.DefaultMemberPermissions = null; - if (IsGuild) + if (isGuild) { sourceApplicationCommand.DmPermission = null; targetApplicationCommand.DmPermission = null; } client.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, "[AC Change Check] Command {name}\n\n[{jsonOne},{jsontwo}]\n\n", ac1.Name, JsonConvert.SerializeObject(sourceApplicationCommand), JsonConvert.SerializeObject(targetApplicationCommand)); - return ac1.Type == targetApplicationCommand.Type && sourceApplicationCommand.SoftEqual(targetApplicationCommand, ac1.Type, ApplicationCommandsExtension.Configuration?.EnableLocalization ?? false, IsGuild); + return ac1.Type == targetApplicationCommand.Type && sourceApplicationCommand.SoftEqual(targetApplicationCommand, ac1.Type, ApplicationCommandsExtension.Configuration?.EnableLocalization ?? false, isGuild); } /// /// Checks softly whether two s are the same. /// Excluding id, application id and version here. /// /// Source application command. /// Application command to check against. /// The application command type. /// Whether localization is enabled. /// Whether the equal check is performed for a guild command. internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApplicationCommand target, ApplicationCommandType type, bool localizationEnabled = false, bool guild = false) { bool? sDmPerm = source.DmPermission ?? true; bool? tDmPerm = target.DmPermission ?? true; if (guild) { sDmPerm = null; tDmPerm = null; } return localizationEnabled ? type switch { ApplicationCommandType.ChatInput => DeepEqual(source, target, true, sDmPerm, tDmPerm), _ => source.Name == target.Name && source.Type == target.Type && source.NameLocalizations == target.NameLocalizations && source.DefaultMemberPermissions == target.DefaultMemberPermissions && sDmPerm == tDmPerm //&& (source.IsNsfw == target.IsNsfw) } : type switch { ApplicationCommandType.ChatInput => DeepEqual(source, target, false, sDmPerm, tDmPerm), _ => source.Name == target.Name && source.Type == target.Type && source.DefaultMemberPermissions == target.DefaultMemberPermissions && sDmPerm == tDmPerm //&& (source.IsNsfw == target.IsNsfw) }; } /// /// Checks deeply whether two s are the same. /// Excluding id, application id and version here. /// /// Source application command. /// Application command to check against. /// Whether localization is enabled. /// The source dm permission. /// The target dm permission. internal static bool DeepEqual(DiscordApplicationCommand source, DiscordApplicationCommand target, bool localizationEnabled = false, bool? sDmPerm = null, bool? tDmPerm = null) { var rootCheck = true; /*Console.WriteLine($"{source.Name == target.Name}"); Console.WriteLine($"{source.Description == target.Description}"); Console.WriteLine($"{source.Type == target.Type}"); Console.WriteLine($"{source.DefaultMemberPermissions == target.DefaultMemberPermissions} - {source.DefaultMemberPermissions} == {target.DefaultMemberPermissions}"); Console.WriteLine($"{sDmPerm == tDmPerm}");*/ rootCheck = source.Name == target.Name && source.Description == target.Description && source.Type == target.Type && source.DefaultMemberPermissions == target.DefaultMemberPermissions && sDmPerm == tDmPerm; if (localizationEnabled) rootCheck = rootCheck && source.NameLocalizations == target.NameLocalizations && source.DescriptionLocalizations == target.DescriptionLocalizations; //Console.WriteLine($"{rootCheck}"); if (source.Options == null && target.Options == null) return rootCheck; else if ((source.Options != null && target.Options == null) || (source.Options == null && target.Options != null)) return false; else if (source.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommandGroup) && target.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommandGroup)) { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) { List minimalSubSourceOptions = new(); if (option.Options != null) { foreach (var subOption in option.Options) { List minimalSubSubSourceOptions = null; if (subOption.Options != null) { minimalSubSubSourceOptions = new(); foreach (var subSubOption in subOption.Options) minimalSubSubSourceOptions.Add(new DiscordApplicationCommandOption( subSubOption.Name, subSubOption.Description, subSubOption.Type, subSubOption.Required, subSubOption.Choices, null, subSubOption.ChannelTypes, subSubOption.AutoComplete, subSubOption.MinimumValue, subSubOption.MaximumValue, localizationEnabled ? subSubOption.NameLocalizations : null, localizationEnabled ? subSubOption.DescriptionLocalizations : null, subSubOption.MinimumLength, subSubOption.MaximumLength )); minimalSubSourceOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, options: minimalSubSubSourceOptions, nameLocalizations: localizationEnabled ? subOption.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? subOption.DescriptionLocalizations : null )); } } } minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubSourceOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } foreach (var option in target.Options) { List minimalSubTargetOptions = new(); foreach (var subOption in option.Options) { List minimalSubSubTargetOptions = null; if (subOption.Options != null && subOption.Options.Any()) { minimalSubSubTargetOptions = new(); foreach (var subSubOption in subOption.Options) minimalSubSubTargetOptions.Add(new DiscordApplicationCommandOption( subSubOption.Name, subSubOption.Description, subSubOption.Type, subSubOption.Required, subSubOption.Choices, null, subSubOption.ChannelTypes, subSubOption.AutoComplete, subSubOption.MinimumValue, subSubOption.MaximumValue, localizationEnabled ? subSubOption.NameLocalizations : null, localizationEnabled ? subSubOption.DescriptionLocalizations : null, subSubOption.MinimumLength, subSubOption.MaximumLength )); minimalSubTargetOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, options: minimalSubSubTargetOptions, nameLocalizations: localizationEnabled ? subOption.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubTargetOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } var sOpt = JsonConvert.SerializeObject(minimalSourceOptions, Formatting.None); var tOpt = JsonConvert.SerializeObject(minimalTargetOptions, Formatting.None); //Console.WriteLine("Checking equality subcommandgroup"); //Console.WriteLine($"{rootCheck}"); //Console.WriteLine($"{sOpt}"); //Console.WriteLine($"{tOpt}"); return rootCheck && sOpt == tOpt; } else if (source.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommand) && target.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommand)) { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) { List minimalSubSourceOptions =null; if (option.Options != null) { minimalSubSourceOptions = new(); foreach (var subOption in option.Options) minimalSubSourceOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, subOption.Required, subOption.Choices, null, subOption.ChannelTypes, subOption.AutoComplete, subOption.MinimumValue, subOption.MaximumValue, localizationEnabled ? subOption.NameLocalizations : null, localizationEnabled ? subOption.DescriptionLocalizations : null, subOption.MinimumLength, subOption.MaximumLength )); } minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubSourceOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } foreach (var option in target.Options) { List minimalSubTargetOptions = null; if (option.Options != null && option.Options.Any()) { minimalSubTargetOptions = new(); foreach (var subOption in option.Options) minimalSubTargetOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, subOption.Required, subOption.Choices, null, subOption.ChannelTypes, subOption.AutoComplete, subOption.MinimumValue, subOption.MaximumValue, localizationEnabled ? subOption.NameLocalizations : null, localizationEnabled ? subOption.DescriptionLocalizations : null, subOption.MinimumLength, subOption.MaximumLength )); } minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubTargetOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } var sOpt = JsonConvert.SerializeObject(minimalSourceOptions, Formatting.None); var tOpt = JsonConvert.SerializeObject(minimalTargetOptions, Formatting.None); //Console.WriteLine("Checking equality subcommand"); //Console.WriteLine($"{rootCheck}"); //Console.WriteLine($"{sOpt}"); //Console.WriteLine($"{tOpt}"); return rootCheck && sOpt == tOpt; } else { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, option.Required, option.Choices, null, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, localizationEnabled ? option.NameLocalizations : null, localizationEnabled ? option.DescriptionLocalizations : null, option.MinimumLength, option.MaximumLength )); foreach (var option in target.Options) minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, option.Required, option.Choices, null, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, localizationEnabled ? option.NameLocalizations : null, localizationEnabled ? option.DescriptionLocalizations : null, option.MinimumLength, option.MaximumLength )); var sOpt = JsonConvert.SerializeObject(minimalSourceOptions, Formatting.None); var tOpt = JsonConvert.SerializeObject(minimalTargetOptions, Formatting.None); //Console.WriteLine("Checking equality other"); //Console.WriteLine($"{rootCheck}"); //Console.WriteLine($"{sOpt}"); //Console.WriteLine($"{tOpt}"); return rootCheck && sOpt == tOpt; } } } diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs index 0fd7a4134..4a94b5515 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs @@ -1,1321 +1,1322 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; +using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp.Entities; public partial class DiscordGuild { // TODO: Rework audit logs! /// /// Gets audit log entries for this guild. /// /// Maximum number of entries to fetch. /// Filter by member responsible. /// Filter by action type. /// A collection of requested audit log entries. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public async Task> GetAuditLogsAsync(int? limit = null, DiscordMember byMember = null, AuditLogActionType? actionType = null) { var alrs = new List(); int ac = 1, tc = 0, rmn = 100; var last = 0ul; while (ac > 0) { rmn = limit != null ? limit.Value - tc : 100; rmn = Math.Min(100, rmn); if (rmn <= 0) break; var alr = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, rmn, null, last == 0 ? null : last, byMember?.Id, (int?)actionType).ConfigureAwait(false); ac = alr.Entries.Count; tc += ac; if (ac > 0) { last = alr.Entries[alr.Entries.Count - 1].Id; alrs.Add(alr); } } var amr = alrs.SelectMany(xa => xa.Users) .GroupBy(xu => xu.Id) .Select(xgu => xgu.First()); foreach (var xau in amr) { if (this.Discord.UserCache.ContainsKey(xau.Id)) continue; var xtu = new TransportUser { Id = xau.Id, Username = xau.Username, Discriminator = xau.Discriminator, AvatarHash = xau.AvatarHash }; var xu = new DiscordUser(xtu) { Discord = this.Discord }; xu = this.Discord.UserCache.AddOrUpdate(xu.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); } var atgse = alrs.SelectMany(xa => xa.ScheduledEvents) .GroupBy(xse => xse.Id) .Select(xgse => xgse.First()); var ath = alrs.SelectMany(xa => xa.Threads) .GroupBy(xt => xt.Id) .Select(xgt => xgt.First()); var aig = alrs.SelectMany(xa => xa.Integrations) .GroupBy(xi => xi.Id) .Select(xgi => xgi.First()); var ahr = alrs.SelectMany(xa => xa.Webhooks) .GroupBy(xh => xh.Id) .Select(xgh => xgh.First()); var ams = amr.Select(xau => this.MembersInternal != null && this.MembersInternal.TryGetValue(xau.Id, out var member) ? member : new DiscordMember { Discord = this.Discord, Id = xau.Id, GuildId = this.Id }); var amd = ams.ToDictionary(xm => xm.Id, xm => xm); #pragma warning disable CS0219 Dictionary dtc = null; Dictionary di = null; Dictionary dse = null; #pragma warning restore Dictionary ahd = null; if (ahr.Any()) { var whr = await this.GetWebhooksAsync().ConfigureAwait(false); var whs = whr.ToDictionary(xh => xh.Id, xh => xh); var amh = ahr.Select(xah => whs.TryGetValue(xah.Id, out var webhook) ? webhook : new DiscordWebhook { Discord = this.Discord, Name = xah.Name, Id = xah.Id, AvatarHash = xah.AvatarHash, ChannelId = xah.ChannelId, GuildId = xah.GuildId, Token = xah.Token }); ahd = amh.ToDictionary(xh => xh.Id, xh => xh); } var acs = alrs.SelectMany(xa => xa.Entries).OrderByDescending(xa => xa.Id); var entries = new List(); foreach (var xac in acs) { DiscordAuditLogEntry entry = null; ulong t1, t2; int t3, t4; long t5, t6; bool p1, p2; switch (xac.ActionType) { case AuditLogActionType.Invalid: break; case AuditLogActionType.GuildUpdate: entry = new DiscordAuditLogGuildEntry { Target = this }; var entrygld = entry as DiscordAuditLogGuildEntry; foreach (var xc in xac.Changes) { PropertyChange GetChannelChange() { ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); return new PropertyChange { Before = this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } }; } switch (xc.Key.ToLowerInvariant()) { case "name": entrygld.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "owner_id": entrygld.OwnerChange = new PropertyChange { Before = this.MembersInternal != null && this.MembersInternal.TryGetValue(xc.OldValueUlong, out var oldMember) ? oldMember : await this.GetMemberAsync(xc.OldValueUlong).ConfigureAwait(false), After = this.MembersInternal != null && this.MembersInternal.TryGetValue(xc.NewValueUlong, out var newMember) ? newMember : await this.GetMemberAsync(xc.NewValueUlong).ConfigureAwait(false) }; break; case "icon_hash": entrygld.IconChange = new PropertyChange { Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.OldValueString}.webp" : null, After = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.NewValueString}.webp" : null }; break; case "verification_level": entrygld.VerificationLevelChange = new PropertyChange { Before = (VerificationLevel)(long)xc.OldValue, After = (VerificationLevel)(long)xc.NewValue }; break; case "afk_channel_id": entrygld.AfkChannelChange = GetChannelChange(); break; case "system_channel_flags": entrygld.SystemChannelFlagsChange = new PropertyChange() { Before = (SystemChannelFlags)(long)xc.OldValue, After = (SystemChannelFlags)(long)xc.NewValue }; break; case "widget_channel_id": entrygld.WidgetChannelChange = GetChannelChange(); break; case "rules_channel_id": entrygld.RulesChannelChange = GetChannelChange(); break; case "public_updates_channel_id": entrygld.PublicUpdatesChannelChange = GetChannelChange(); break; case "splash_hash": entrygld.SplashChange = new PropertyChange { Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.OldValueString}.webp?size=2048" : null, After = xc.NewValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.NewValueString}.webp?size=2048" : null }; break; case "default_message_notifications": entrygld.NotificationSettingsChange = new PropertyChange { Before = (DefaultMessageNotifications)(long)xc.OldValue, After = (DefaultMessageNotifications)(long)xc.NewValue }; break; case "system_channel_id": entrygld.SystemChannelChange = GetChannelChange(); break; case "explicit_content_filter": entrygld.ExplicitContentFilterChange = new PropertyChange { Before = (ExplicitContentFilter)(long)xc.OldValue, After = (ExplicitContentFilter)(long)xc.NewValue }; break; case "mfa_level": entrygld.MfaLevelChange = new PropertyChange { Before = (MfaLevel)(long)xc.OldValue, After = (MfaLevel)(long)xc.NewValue }; break; case "region": entrygld.RegionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "vanity_url_code": entrygld.VanityUrlCodeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "premium_progress_bar_enabled": entrygld.PremiumProgressBarChange = new PropertyChange { Before = (bool)xc.OldValue, After = (bool)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in guild update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ChannelCreate: case AuditLogActionType.ChannelDelete: case AuditLogActionType.ChannelUpdate: entry = new DiscordAuditLogChannelEntry { Target = this.GetChannel(xac.TargetId.Value) ?? new DiscordChannel { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; var entrychn = entry as DiscordAuditLogChannelEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrychn.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "type": p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrychn.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "flags": entrychn.ChannelFlagsChange = new PropertyChange() { Before = (ChannelFlags)(long)(xc.OldValue ?? 0L), After = (ChannelFlags)(long)(xc.NewValue ?? 0L) }; break; case "permission_overwrites": var olds = xc.OldValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); var news = xc.NewValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); entrychn.OverwriteChange = new PropertyChange> { Before = olds != null ? new ReadOnlyCollection(new List(olds)) : null, After = news != null ? new ReadOnlyCollection(new List(news)) : null }; break; case "topic": entrychn.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "nsfw": entrychn.NsfwChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "rtc_region": entrychn.RtcRegionIdChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "bitrate": entrychn.BitrateChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "user_limit": entrychn.UserLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "rate_limit_per_user": entrychn.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "default_auto_archive_duration": entrychn.DefaultAutoArchiveDurationChange = new PropertyChange { Before = (ThreadAutoArchiveDuration?)(long?)xc.OldValue, After = (ThreadAutoArchiveDuration?)(long?)xc.NewValue }; break; case "available_tags": - var old_tags = xc.OldValues?.OfType() + var oldTags = xc.OldValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); - var new_tags = xc.NewValues?.OfType() + var newTags = xc.NewValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); entrychn.AvailableTagsChange = new PropertyChange> { - Before = old_tags != null ? new List(new List(old_tags)) : null, - After = new_tags != null ? new List(new List(new_tags)) : null + Before = oldTags != null ? new List(new List(oldTags)) : null, + After = newTags != null ? new List(new List(newTags)) : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in channel update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.OverwriteCreate: case AuditLogActionType.OverwriteDelete: case AuditLogActionType.OverwriteUpdate: entry = new DiscordAuditLogOverwriteEntry { Target = this.GetChannel(xac.TargetId.Value)?.PermissionOverwrites.FirstOrDefault(xo => xo.Id == xac.Options.Id), Channel = this.GetChannel(xac.TargetId.Value) }; var entryovr = entry as DiscordAuditLogOverwriteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "deny": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.DenyChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "allow": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.AllowChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "type": entryovr.TypeChange = new PropertyChange { Before = xc.OldValue != null ? (OverwriteType)(long)xc.OldValue : null, After = xc.NewValue != null ? (OverwriteType)(long)xc.NewValue : null }; break; case "id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.TargetIdChange = new PropertyChange { Before = p1 ? t1 : null, After = p2 ? t2 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in overwrite update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.Kick: entry = new DiscordAuditLogKickEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var kickMember) ? kickMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; break; case AuditLogActionType.Prune: entry = new DiscordAuditLogPruneEntry { Days = xac.Options.DeleteMemberDays, Toll = xac.Options.MembersRemoved }; break; case AuditLogActionType.Ban: case AuditLogActionType.Unban: entry = new DiscordAuditLogBanEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var unbanMember) ? unbanMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; break; case AuditLogActionType.MemberUpdate: case AuditLogActionType.MemberRoleUpdate: entry = new DiscordAuditLogMemberUpdateEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var roleUpdMember) ? roleUpdMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; var entrymbu = entry as DiscordAuditLogMemberUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "nick": entrymbu.NicknameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "deaf": entrymbu.DeafenChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "mute": entrymbu.MuteChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "communication_disabled_until": entrymbu.CommunicationDisabledUntilChange = new PropertyChange { Before = (DateTime?)xc.OldValue, After = (DateTime?)xc.NewValue }; break; case "$add": entrymbu.AddedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; case "$remove": entrymbu.RemovedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in member update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.RoleCreate: case AuditLogActionType.RoleDelete: case AuditLogActionType.RoleUpdate: entry = new DiscordAuditLogRoleUpdateEntry { Target = this.GetRole(xac.TargetId.Value) ?? new DiscordRole { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryrol = entry as DiscordAuditLogRoleUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryrol.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "color": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryrol.ColorChange = new PropertyChange { Before = p1 ? t3 : null, After = p2 ? t4 : null }; break; case "permissions": entryrol.PermissionChange = new PropertyChange { Before = xc.OldValue != null ? (Permissions?)long.Parse((string)xc.OldValue) : null, After = xc.NewValue != null ? (Permissions?)long.Parse((string)xc.NewValue) : null }; break; case "position": entryrol.PositionChange = new PropertyChange { Before = xc.OldValue != null ? (int?)(long)xc.OldValue : null, After = xc.NewValue != null ? (int?)(long)xc.NewValue : null, }; break; case "mentionable": entryrol.MentionableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "hoist": entryrol.HoistChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "icon_hash": entryrol.IconHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in role update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.InviteCreate: case AuditLogActionType.InviteDelete: case AuditLogActionType.InviteUpdate: entry = new DiscordAuditLogInviteEntry(); var inv = new DiscordInvite { Discord = this.Discord, Guild = new DiscordInviteGuild { Discord = this.Discord, Id = this.Id, Name = this.Name, SplashHash = this.SplashHash } }; var entryinv = entry as DiscordAuditLogInviteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "max_age": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxAgeChange = new PropertyChange { Before = p1 ? t3 : null, After = p2 ? t4 : null }; break; case "code": inv.Code = xc.OldValueString ?? xc.NewValueString; entryinv.CodeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "temporary": entryinv.TemporaryChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "inviter_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.InviterChange = new PropertyChange { Before = amd.TryGetValue(t1, out var propBeforeMember) ? propBeforeMember : new DiscordMember { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = amd.TryGetValue(t2, out var propAfterMember) ? propAfterMember : new DiscordMember { Id = t1, Discord = this.Discord, GuildId = this.Id }, }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; var ch = entryinv.ChannelChange.Before ?? entryinv.ChannelChange.After; var cht = ch?.Type; inv.Channel = new DiscordInviteChannel { Discord = this.Discord, Id = p1 ? t1 : t2, Name = ch?.Name, Type = cht != null ? cht.Value : ChannelType.Unknown }; break; case "uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.UsesChange = new PropertyChange { Before = p1 ? t3 : null, After = p2 ? t4 : null }; break; case "max_uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxUsesChange = new PropertyChange { Before = p1 ? t3 : null, After = p2 ? t4 : null }; break; // TODO: Add changes for target application default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in invite update: {0} - this should be reported to library developers", xc.Key); break; } } entryinv.Target = inv; break; case AuditLogActionType.WebhookCreate: case AuditLogActionType.WebhookDelete: case AuditLogActionType.WebhookUpdate: entry = new DiscordAuditLogWebhookEntry { Target = ahd.TryGetValue(xac.TargetId.Value, out var webhook) ? webhook : new DiscordWebhook { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrywhk = entry as DiscordAuditLogWebhookEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "application_id": // ??? p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.IdChange = new PropertyChange { Before = p1 ? t1 : null, After = p2 ? t2 : null }; break; case "name": entrywhk.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; break; case "type": // ??? p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entrywhk.TypeChange = new PropertyChange { Before = p1 ? t3 : null, After = p2 ? t4 : null }; break; case "avatar_hash": entrywhk.AvatarHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in webhook update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.EmojiCreate: case AuditLogActionType.EmojiDelete: case AuditLogActionType.EmojiUpdate: entry = new DiscordAuditLogEmojiEntry { Target = this.EmojisInternal.TryGetValue(xac.TargetId.Value, out var target) ? target : new DiscordEmoji { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryemo = entry as DiscordAuditLogEmojiEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryemo.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in emote update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StageInstanceCreate: case AuditLogActionType.StageInstanceDelete: case AuditLogActionType.StageInstanceUpdate: entry = new DiscordAuditLogStageEntry { Target = this.StageInstancesInternal.TryGetValue(xac.TargetId.Value, out var stage) ? stage : new DiscordStageInstance { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysta = entry as DiscordAuditLogStageEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "topic": entrysta.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": entrysta.PrivacyLevelChange = new PropertyChange { Before = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5) ? (StagePrivacyLevel?)t5 : null, After = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6) ? (StagePrivacyLevel?)t6 : null, }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in stage instance update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StickerCreate: case AuditLogActionType.StickerDelete: case AuditLogActionType.StickerUpdate: entry = new DiscordAuditLogStickerEntry { Target = this.StickersInternal.TryGetValue(xac.TargetId.Value, out var sticker) ? sticker : new DiscordSticker { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysti = entry as DiscordAuditLogStickerEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrysti.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entrysti.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "tags": entrysti.TagsChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "guild_id": entrysti.GuildIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "available": entrysti.AvailabilityChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue, }; break; case "asset": entrysti.AssetChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "id": entrysti.IdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var oid) ? oid : null, After = ulong.TryParse(xc.NewValueString, out var nid) ? nid : null }; break; case "type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.TypeChange = new PropertyChange { Before = p1 ? (StickerType?)t5 : null, After = p2 ? (StickerType?)t6 : null }; break; case "format_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.FormatChange = new PropertyChange { Before = p1 ? (StickerFormat?)t5 : null, After = p2 ? (StickerFormat?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in sticker update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.MessageDelete: case AuditLogActionType.MessageBulkDelete: { entry = new DiscordAuditLogMessageEntry(); var entrymsg = entry as DiscordAuditLogMessageEntry; if (xac.Options != null) { entrymsg.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrymsg.MessageCount = xac.Options.Count; } if (entrymsg.Channel != null) { entrymsg.Target = this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == xac.TargetId.Value && xm.ChannelId == entrymsg.Channel.Id, out var msg) ? msg : new DiscordMessage { Discord = this.Discord, Id = xac.TargetId.Value }; } break; } case AuditLogActionType.MessagePin: case AuditLogActionType.MessageUnpin: { entry = new DiscordAuditLogMessagePinEntry(); var entrypin = entry as DiscordAuditLogMessagePinEntry; if (this.Discord is not DiscordClient dc) { break; } if (xac.Options != null) { DiscordMessage message = default; dc.MessageCache?.TryGet(x => x.Id == xac.Options.MessageId && x.ChannelId == xac.Options.ChannelId, out message); entrypin.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrypin.Message = message ?? new DiscordMessage { Id = xac.Options.MessageId, Discord = this.Discord }; } if (xac.TargetId.HasValue) { dc.UserCache.TryGetValue(xac.TargetId.Value, out var user); entrypin.Target = user ?? new DiscordUser { Id = user.Id, Discord = this.Discord }; } break; } case AuditLogActionType.BotAdd: { entry = new DiscordAuditLogBotAddEntry(); if (!(this.Discord is DiscordClient dc && xac.TargetId.HasValue)) { break; } dc.UserCache.TryGetValue(xac.TargetId.Value, out var bot); (entry as DiscordAuditLogBotAddEntry).TargetBot = bot ?? new DiscordUser { Id = xac.TargetId.Value, Discord = this.Discord }; break; } case AuditLogActionType.MemberMove: entry = new DiscordAuditLogMemberMoveEntry(); if (xac.Options == null) { break; } var moveentry = entry as DiscordAuditLogMemberMoveEntry; moveentry.UserCount = xac.Options.Count; moveentry.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; break; case AuditLogActionType.MemberDisconnect: entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = xac.Options?.Count ?? 0 }; break; case AuditLogActionType.IntegrationCreate: case AuditLogActionType.IntegrationDelete: case AuditLogActionType.IntegrationUpdate: entry = new DiscordAuditLogIntegrationEntry(); var integentry = entry as DiscordAuditLogIntegrationEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "type": integentry.Type = new PropertyChange() { Before = xc.OldValueString, After = xc.NewValueString }; break; case "enable_emoticons": integentry.EnableEmoticons = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "expire_behavior": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; case "expire_grace_period": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in integration update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ThreadCreate: case AuditLogActionType.ThreadDelete: case AuditLogActionType.ThreadUpdate: entry = new DiscordAuditLogThreadEntry { Target = this.ThreadsInternal.TryGetValue(xac.TargetId.Value, out var thread) ? thread : new DiscordThreadChannel { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrythr = entry as DiscordAuditLogThreadEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrythr.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "type": p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrythr.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "archived": entrythr.ArchivedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "locked": entrythr.LockedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "invitable": entrythr.InvitableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "auto_archive_duration": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrythr.AutoArchiveDurationChange = new PropertyChange { Before = p1 ? (ThreadAutoArchiveDuration?)t5 : null, After = p2 ? (ThreadAutoArchiveDuration?)t6 : null }; break; case "rate_limit_per_user": entrythr.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in thread update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.GuildScheduledEventCreate: case AuditLogActionType.GuildScheduledEventDelete: case AuditLogActionType.GuildScheduledEventUpdate: entry = new DiscordAuditLogGuildScheduledEventEntry { Target = this.ScheduledEventsInternal.TryGetValue(xac.TargetId.Value, out var scheduledEvent) ? scheduledEvent : new DiscordScheduledEvent { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryse = entry as DiscordAuditLogGuildScheduledEventEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "channel_id": entryse.ChannelIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "name": entryse.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entryse.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "location": entryse.LocationChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.PrivacyLevelChange = new PropertyChange { Before = p1 ? (ScheduledEventPrivacyLevel?)t5 : null, After = p2 ? (ScheduledEventPrivacyLevel?)t6 : null }; break; case "entity_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.EntityTypeChange = new PropertyChange { Before = p1 ? (ScheduledEventEntityType?)t5 : null, After = p2 ? (ScheduledEventEntityType?)t6 : null }; break; case "status": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.StatusChange = new PropertyChange { Before = p1 ? (ScheduledEventStatus?)t5 : null, After = p2 ? (ScheduledEventStatus?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in scheduled event update: {0} - this should be reported to library developers", xc.Key); break; } } break; // TODO: Handle ApplicationCommandPermissionUpdate case AuditLogActionType.ApplicationCommandPermissionUpdate: break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown audit log action type: {0} - this should be reported to library developers", (int)xac.ActionType); break; } if (entry == null) continue; entry.ActionCategory = xac.ActionType switch { AuditLogActionType.ChannelCreate or AuditLogActionType.EmojiCreate or AuditLogActionType.InviteCreate or AuditLogActionType.OverwriteCreate or AuditLogActionType.RoleCreate or AuditLogActionType.WebhookCreate or AuditLogActionType.IntegrationCreate or AuditLogActionType.StickerCreate or AuditLogActionType.StageInstanceCreate or AuditLogActionType.ThreadCreate or AuditLogActionType.GuildScheduledEventCreate => AuditLogActionCategory.Create, AuditLogActionType.ChannelDelete or AuditLogActionType.EmojiDelete or AuditLogActionType.InviteDelete or AuditLogActionType.MessageDelete or AuditLogActionType.MessageBulkDelete or AuditLogActionType.OverwriteDelete or AuditLogActionType.RoleDelete or AuditLogActionType.WebhookDelete or AuditLogActionType.IntegrationDelete or AuditLogActionType.StickerDelete or AuditLogActionType.StageInstanceDelete or AuditLogActionType.ThreadDelete or AuditLogActionType.GuildScheduledEventDelete => AuditLogActionCategory.Delete, AuditLogActionType.ChannelUpdate or AuditLogActionType.EmojiUpdate or AuditLogActionType.InviteUpdate or AuditLogActionType.MemberRoleUpdate or AuditLogActionType.MemberUpdate or AuditLogActionType.OverwriteUpdate or AuditLogActionType.RoleUpdate or AuditLogActionType.WebhookUpdate or AuditLogActionType.IntegrationUpdate or AuditLogActionType.StickerUpdate or AuditLogActionType.StageInstanceUpdate or AuditLogActionType.ThreadUpdate or AuditLogActionType.GuildScheduledEventUpdate => AuditLogActionCategory.Update, _ => AuditLogActionCategory.Other, }; entry.Discord = this.Discord; entry.ActionType = xac.ActionType; entry.Id = xac.Id; entry.Reason = xac.Reason; entry.UserResponsible = amd[xac.UserId]; entries.Add(entry); } return new ReadOnlyCollection(entries); } } diff --git a/DisCatSharp/Enums/Guild/Stage/StagePrivacyLevel.cs b/DisCatSharp/Enums/Guild/Stage/StagePrivacyLevel.cs index 704cc713b..79adda857 100644 --- a/DisCatSharp/Enums/Guild/Stage/StagePrivacyLevel.cs +++ b/DisCatSharp/Enums/Guild/Stage/StagePrivacyLevel.cs @@ -1,42 +1,42 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; namespace DisCatSharp.Enums; /// /// Represents the privacy level for a stage. /// -[Obsolete("Not uses anymore")] +[Obsolete("Not used anymore")] public enum StagePrivacyLevel : int { /// /// Indicates that the stage is public visible. /// Public = 1, /// /// Indicates that the stage is only visible to guild members. /// GuildOnly = 2 }