diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 75e553264..395b2acbb 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1835 +1,1835 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands; /// /// A class that handles slash commands for a client. /// public sealed class ApplicationCommandsExtension : BaseExtension { /// /// A list of methods for top level commands. /// private static List s_commandMethods { get; set; } = new(); /// /// List of groups. /// private static List s_groupCommands { get; set; } = new(); /// /// List of groups with subgroups. /// private static List s_subGroupCommands { get; set; } = new(); /// /// List of context menus. /// private static List s_contextMenuCommands { get; set; } = new(); /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = new(); /// /// List of modules to register. /// private readonly List> _updateList = new(); /// /// Configuration for Discord. /// internal static ApplicationCommandsConfiguration Configuration; /// /// Set to true if anything fails when registering. /// private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => s_registeredCommands; private static readonly List>> s_registeredCommands = new(); /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => GlobalCommandsInternal; internal static readonly List GlobalCommandsInternal = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; internal static readonly Dictionary> GuildCommandsInternal = new(); /// /// Gets the registration count. /// private static int s_registrationCount { get; set; } /// /// Gets the expected count. /// private static int s_expectedCount { get; set; } /// /// Gets the guild ids where the applications.commands scope is missing. /// private IReadOnlyList _missingScopeGuildIds; /// /// Gets whether debug is enabled. /// internal static bool DebugEnabled { get; set; } internal static LogLevel ApplicationCommandsLogLevel => DebugEnabled ? LogLevel.Debug : LogLevel.Trace; /// /// Gets whether check through all guilds is enabled. /// internal static bool CheckAllGuilds { get; set; } /// /// Gets whether the registration check should be manually overridden. /// internal static bool ManOr { get; set; } /// /// Gets whether interactions should be automatically deffered. /// internal static bool AutoDeferEnabled { get; set; } /// /// 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) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")); private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")); private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Cleans the module for a new start of the bot. /// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES. /// public void CleanModule() { this._updateList.Clear(); s_singletonModules.Clear(); s_errored = false; s_expectedCount = 0; s_registrationCount = 0; s_commandMethods.Clear(); s_groupCommands.Clear(); s_contextMenuCommands.Clear(); s_subGroupCommands.Clear(); s_singletonModules.Clear(); s_registeredCommands.Clear(); GlobalCommandsInternal.Clear(); GuildCommandsInternal.Clear(); } /// /// Cleans all guild application commands. /// You normally don't need to execute it. /// public async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } /// /// Cleans the global application commands. /// You normally don't need to execute it. /// public async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with optional translation setup for a guild. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup for a guild. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Registers a command class with optional translation setup globally. /// /// The command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup globally. /// /// The of the command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Type type, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add => this._applicationCommandsModuleReady.Register(value); remove => this._applicationCommandsModuleReady.Unregister(value); } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add => this._applicationCommandsModuleStartupFinished.Register(value); remove => this._applicationCommandsModuleStartupFinished.Unregister(value); } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add => this._guildApplicationCommandsRegistered.Register(value); remove => this._guildApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add => this._globalApplicationCommandsRegistered.Register(value); remove => this._globalApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { - this.Client.Logger.LogInformation("Request to register commands on shard {shard}", this.Client.ShardId); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Request to register commands on shard {shard}", this.Client.ShardId); GlobalDiscordCommands = new(); GuildDiscordCommands = new(); - this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}"); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {count}", s_expectedCount); List failedGuilds = new(); List globalCommands = null; globalCommands = (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false)).ToList() ?? null; var updateList = this._updateList.DistinctBy(x => x.Key).ToList(); var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys.ToList() : updateList.Where(x => x.Key != null)?.Select(x => x.Key.Value).ToList(); var wrongShards = guilds.Where(x => !this.Client.Guilds.ContainsKey(x)).ToList(); if (wrongShards.Any()) { - this.Client.Logger.LogDebug("Some guilds are not on the same shard as the client. Removing them from the update list."); + 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); - this.Client.Logger.LogInformation("Test"); + foreach (var key in commandsPending) { - this.Client.Logger.LogInformation(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); + this.Client.Logger.Log(ApplicationCommandsLogLevel, key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); if (key.HasValue) { - this.Client.Logger.LogDebug("Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId); - this.Client.Logger.LogDebug("Registering"); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering"); } await this.RegisterCommands(updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key); } this._missingScopeGuildIds = failedGuilds; await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider) { GuildsWithoutScope = failedGuilds }); this.Client.GuildDownloadCompleted -= async (c, e) => await this.UpdateAsync(); } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private async Task RegisterCommands(List types, ulong? guildId) { - this.Client.Logger.LogInformation("Registering commands on shard {shard}", this.Client.ShardId); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering commands on shard {shard}", this.Client.ShardId); //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); //Add module to classes list if it's a group if (module.GetCustomAttribute() != null) { classes.Add(module); } else { //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); } List groupTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations); if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any()) updateList.AddRange(slashGroupsTuple.applicationCommands); if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any()) commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources); if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any()) s_singletonModules.AddRange(slashGroupsTuple.singletonModules); if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any()) groupCommands.AddRange(slashGroupsTuple.groupCommands); if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any()) subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands); //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { List commandTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } //Slash commands var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations); if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any()) updateList.AddRange(slashCommands.applicationCommands); if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any()) commandTypeSources.AddRange(slashCommands.commandTypeSources); if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any()) commandMethods.AddRange(slashCommands.commandMethods); //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations); if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any()) updateList.AddRange(contextCommands.applicationCommands); if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any()) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any()) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.WebResponse.Response}"); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, $"There was an error registering application commands: {brex1.WebResponse.Response}"); else this.Client.Logger.LogCritical(ex, $"There was an error parsing the application commands"); } s_errored = true; } } if (!s_errored) { try { List commands = new(); try { if (guildId == null) { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } else { foreach (var cmd in GlobalDiscordCommands) { try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete global command {cmd.Id}. Please clean up manually"); } } } } else { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildId.Value, actualCommands); /* if (client.Guilds.TryGetValue(guildId.Value, out var guild)) { guild.InternalRegisteredApplicationCommands = new(); guild.InternalRegisteredApplicationCommands.AddRange(actualCommands); } */ } else { foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value) { try { await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete guild command {cmd.Id} in guild {guildId.Value}. Please clean up manually"); } } } } } catch (UnauthorizedException ex) { this.Client.Logger.LogError($"Could not register application commands for guild {guildId}.\nError: {ex.JsonMessage}"); return; } //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in commands) { if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com)) com.CommandId = command.Id; else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) groupCom.CommandId = command.Id; else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom)) subCom.CommandId = command.Id; else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) cmCom.CommandId = command.Id; } //Adds to the global lists finally s_commandMethods.AddRange(commandMethods); s_groupCommands.AddRange(groupCommands); s_subGroupCommands.AddRange(subGroupCommands); s_contextMenuCommands.AddRange(contextMenuCommands); s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}"); if (guildId.HasValue) { await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null }); } else { await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }); } s_registrationCount++; this.CheckRegistrationStartup(ManOr); } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.WebResponse.Response}"); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, $"There was an error registering application commands: {brex1.WebResponse.Response}"); else this.Client.Logger.LogCritical(ex, $"There was an general error registering application commands"); } s_errored = true; } } } private async void CheckRegistrationStartup(bool man = false) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Checking counts...\n\n" + $"Expected Count: {s_expectedCount}\n" + $"Current Count: {s_registrationCount}"); if ((s_registrationCount == s_expectedCount) || man) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredGlobalCommands = GlobalCommandsInternal, RegisteredGuildCommands = GuildCommandsInternal, GuildsWithoutScope = this._missingScopeGuildIds }); this.FinishedRegistration(); } } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { - this.Client.Logger.LogInformation("Got interaction on shard {shard}", this.Client.ShardId); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got interaction on shard {shard}", this.Client.ShardId); _ = Task.Run(async () => { if (e.Interaction.Type == InteractionType.ApplicationCommand) { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = Configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(), Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("A slash command was executed, but no command was registered for it."); if (methods.Any()) { var method = methods.First().Method; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); await this.RunCommandAsync(context, method, args); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var method = groups.First().Methods.First(x => x.Key == command.Name).Value; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options); await this.RunCommandAsync(context, method, args); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options); await this.RunCommandAsync(context, method, args); } await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { this.Client.Logger.LogError(ex.Message); this.Client.Logger.LogError(ex.StackTrace); await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (s_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it."); try { if (methods.Any()) { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = client, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value; var focusedOption = command.Options.First().Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.First().Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = Configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) throw new InvalidOperationException("Context menus failed to register properly on startup."); //Gets the method for the command var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) throw new InvalidOperationException("A context menu was executed, but no command was registered for it."); await this.RunCommandAsync(context, method.Method, new[] { context }); await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { object classInstance; - this.Client.Logger.LogDebug("Executing {cmd}", method.Name); + this.Client.Logger.Log(ApplicationCommandsLogLevel, "Executing {cmd}", method.Name); //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; switch (moduleLifespan) { case ApplicationCommandModuleLifespan.Scoped: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); break; default: throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); } ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; // Slash commands if (context is InteractionContext slashContext) { await this.RunPreexecutionChecksAsync(method, slashContext); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection /// /// The type. /// The services. internal static object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1); foreach (var parameter in parameters) { //Accounts for optional arguments without values given if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) args.Add((long?)option.Value); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the pre-execution checks. /// /// The method info. /// The base context. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext cMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(cMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null) { var choices = new List(); foreach (var choiceProviderAttribute in customAttributes) { var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); if (method == null) throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); else { var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); // Abstract class offers more properties that can be set if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) { choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) ?.SetValue(instance, guildId); choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) ?.SetValue(instance, Configuration.ServiceProvider); } //Gets the choices from the method var result = await (Task>)method.Invoke(instance, null); if (result.Any()) { choices.AddRange(result); } } } return choices; } /// /// Gets the choice attributes from enum parameter. /// /// The enum parameter. private static List GetChoiceAttributesFromEnumParameter(Type enumParam) { var choices = new List(); foreach (Enum enumValue in Enum.GetValues(enumParam)) { choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); } return choices; } /// /// Gets the parameter type. /// /// The type. private static ApplicationCommandOptionType GetParameterType(Type type) { var parameterType = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parameterType; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) => !choiceAttributes.Any() ? null : choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); /// /// Parses the parameters. /// /// The parameters. /// The optional guild id. internal static async Task> ParseParametersAsync(IEnumerable parameters, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionAttribute = parameter.GetCustomAttribute(); if (optionAttribute == null) throw new ArgumentException("Arguments must have the Option attribute!"); var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; var minimumLength = parameter.GetCustomAttribute()?.Value ?? null; var maximumLength = parameter.GetCustomAttribute()?.Value ?? null; var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; var autocompleteAttribute = parameter.GetCustomAttribute(); if (optionAttribute.Autocomplete && autocompleteAttribute == null) throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!"); if (!optionAttribute.Autocomplete && autocompleteAttribute != null) throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!"); //Sets the type var type = parameter.ParameterType; var parameterType = GetParameterType(type); if (parameterType == ApplicationCommandOptionType.String) { minimumValue = null; maximumValue = null; } else if (parameterType == ApplicationCommandOptionType.Integer || parameterType == ApplicationCommandOptionType.Number) { minimumLength = null; maximumLength = null; } if (parameterType != ApplicationCommandOptionType.Channel) channelTypes = null; //Handles choices //From attributes var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); //From enums if (parameter.ParameterType.IsEnum) { choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); } //From choice provider var choiceProviders = parameter.GetCustomAttributes(); if (choiceProviders.Any()) { choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId); } options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength)); } return options; } /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Should only be run on the slash command extension linked to shard 0 if sharding. /// 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) { var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); var applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); if (applicationCommands.Count < 1) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands for guild {ctx.Guild.Name}").AsEphemeral(true)); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var 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(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true)); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{command.Mention}: {command.Description ?? "No description provided."}" }.AddField(new DiscordEmbedField("Command is NSFW", command.IsNsfw.ToString())); if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } } } #endregion diff --git a/DisCatSharp.ApplicationCommands/ExtensionMethods.cs b/DisCatSharp.ApplicationCommands/ExtensionMethods.cs index 366ed614d..16be1bed0 100644 --- a/DisCatSharp.ApplicationCommands/ExtensionMethods.cs +++ b/DisCatSharp.ApplicationCommands/ExtensionMethods.cs @@ -1,185 +1,180 @@ // 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.Globalization; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace DisCatSharp.ApplicationCommands; /// /// Defines various extension methods for application commands. /// public static class ExtensionMethods { /// /// Enables application commands on this . /// /// Client to enable application commands for. /// Configuration to use. /// Created . public static ApplicationCommandsExtension UseApplicationCommands(this DiscordClient client, ApplicationCommandsConfiguration config = null) { if (client.GetExtension() != null) throw new InvalidOperationException("Application commands are already enabled for that client."); var scomm = new ApplicationCommandsExtension(config); client.AddExtension(scomm); return scomm; } /// /// Gets the application commands module for this client. /// /// Client to get application commands for. /// The module, or null if not activated. public static ApplicationCommandsExtension GetApplicationCommands(this DiscordClient client) => client.GetExtension(); /// /// Gets the application commands from this . /// /// Client to get application commands from. /// A dictionary of current with the key being the shard id. public static async Task > GetApplicationCommandsAsync(this DiscordShardedClient client) { await client.InitializeShardsAsync().ConfigureAwait(false); var modules = new Dictionary(); foreach (var shard in client.ShardClients.Values) modules.Add(shard.ShardId, shard.GetExtension()); return modules; } /// /// Registers a command class with optional translation setup globally. /// /// Sharding extensions. /// The command class to register. /// A callback to setup translations with. public static void RegisterGlobalCommands(this IReadOnlyDictionary extensions, Action translationSetup = null) where T : ApplicationCommandsModule { foreach (var extension in extensions.Values) extension.RegisterGlobalCommands(translationSetup); } /// /// Registers a command class with optional translation setup globally. /// /// Sharding extensions. /// The of the command class to register. /// A callback to setup translations with. public static void RegisterGlobalCommands(this IReadOnlyDictionary extensions, Type type, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); foreach (var extension in extensions.Values) extension.RegisterGlobalCommands(type, translationSetup); } /// /// Registers a command class with optional translation setup for a guild. /// /// The command class to register. /// Sharding extensions. /// The guild id to register it on. /// A callback to setup translations with. public static void RegisterGuildCommands(this IReadOnlyDictionary extensions, ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule { foreach (var extension in extensions.Values) - { - extension.Client.Logger.LogInformation("Request to register guild commands on shard {shard} for guild {guild}!", extension.Client.ShardId, guildId); extension.RegisterGuildCommands(guildId, translationSetup); - } + } /// /// Registers a command class with optional translation setup for a guild. /// /// Sharding extensions. /// The of the command class to register. /// The guild id to register it on. /// A callback to setup translations with. public static void RegisterGuildCommands(this IReadOnlyDictionary extensions, Type type, ulong guildId, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); foreach (var extension in extensions.Values) - { - extension.Client.Logger.LogInformation("Request to register guild commands on shard {shard} for guild {guild}!", extension.Client.ShardId, guildId); extension.RegisterGuildCommands(type, guildId, translationSetup); - } } /// /// Enables application commands on this . /// /// Client to enable application commands on. /// Configuration to use. /// A dictionary of created with the key being the shard id. public static async Task> UseApplicationCommandsAsync(this DiscordShardedClient client, ApplicationCommandsConfiguration config = null) { var modules = new Dictionary(); await client.InitializeShardsAsync().ConfigureAwait(false); foreach (var shard in client.ShardClients.Values) { var scomm = shard.GetExtension(); if (scomm == null) scomm = shard.UseApplicationCommands(config); modules[shard.ShardId] = scomm; } return modules; } /// /// Gets the name from the for this enum value. /// /// The name. public static string GetName(this T e) where T : IConvertible { if (e is Enum) { var type = e.GetType(); var values = Enum.GetValues(type); foreach (int val in values) { if (val == e.ToInt32(CultureInfo.InvariantCulture)) { var memInfo = type.GetMember(type.GetEnumName(val)); return memInfo[0] .GetCustomAttributes(typeof(ChoiceNameAttribute), false) .FirstOrDefault() is ChoiceNameAttribute nameAttribute ? nameAttribute.Name : type.GetEnumName(val); } } } return null; } } diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index 755d3a77c..6c444bba8 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3416 +1,3416 @@ // 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.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp; /// /// Represents a discord client. /// public sealed partial class DiscordClient { #region Private Fields private string _sessionId; private bool _guildDownloadCompleted; private readonly Dictionary> _tempTimers = new(); /// /// Represents a timeout handler. /// internal class TimeoutHandler { /// /// Gets the member. /// internal readonly DiscordMember Member; /// /// Gets the guild. /// internal readonly DiscordGuild Guild; /// /// Gets the old timeout value. /// internal DateTime? TimeoutUntilOld; /// /// Gets the new timeout value. /// internal DateTime? TimeoutUntilNew; /// /// Constructs a new . /// /// The affected member. /// The affected guild. /// The old timeout value. /// The new timeout value. internal TimeoutHandler(DiscordMember mbr, DiscordGuild guild, DateTime? too, DateTime? ton) { this.Guild = guild; this.Member = mbr; this.TimeoutUntilOld = too; this.TimeoutUntilNew = ton; } } #endregion #region Dispatch Handler /// /// Handles the dispatch payloads. /// /// The payload. internal async Task HandleDispatchAsync(GatewayPayload payload) { if (payload.Data is not JObject dat) { this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {0} event: {1}; payload: {2}", payload.OpCode, payload.EventName, payload.Data); return; } await this._payloadReceived.InvokeAsync(this, new PayloadReceivedEventArgs(this.ServiceProvider) { EventName = payload.EventName, PayloadObject = dat }).ConfigureAwait(false); #region Default objects DiscordChannel chn; ulong gid; ulong cid; ulong uid; DiscordStageInstance stg = default; DiscordIntegration itg = default; DiscordThreadChannel trd = default; DiscordThreadChannelMember trdm = default; DiscordScheduledEvent gse = default; TransportUser usr = default; TransportMember mbr = default; TransportUser refUsr = default; TransportMember refMbr = default; JToken rawMbr = default; var rawRefMsg = dat["referenced_message"]; #endregion switch (payload.EventName.ToLowerInvariant()) { #region Gateway Status case "ready": var glds = (JArray)dat["guilds"]; await this.OnReadyEventAsync(dat.ToObject(), glds).ConfigureAwait(false); break; case "resumed": await this.OnResumedAsync().ConfigureAwait(false); break; #endregion #region Channel case "channel_create": chn = dat.ToObject(); await this.OnChannelCreateEventAsync(chn).ConfigureAwait(false); break; case "channel_update": await this.OnChannelUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "channel_delete": chn = dat.ToObject(); await this.OnChannelDeleteEventAsync(chn.IsPrivate ? dat.ToObject() : chn).ConfigureAwait(false); break; case "channel_pins_update": cid = (ulong)dat["channel_id"]; var ts = (string)dat["last_pin_timestamp"]; await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)).ConfigureAwait(false); break; #endregion #region Guild case "guild_create": await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_update": await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"]).ConfigureAwait(false); break; case "guild_delete": await this.OnGuildDeleteEventAsync(dat.ToDiscordObject()).ConfigureAwait(false); break; case "guild_sync": gid = (ulong)dat["id"]; await this.OnGuildSyncEventAsync(this.GuildsInternal[gid], (bool)dat["large"], (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_emojis_update": gid = (ulong)dat["guild_id"]; var ems = dat["emojis"].ToObject>(); await this.OnGuildEmojisUpdateEventAsync(this.GuildsInternal[gid], ems).ConfigureAwait(false); break; case "guild_stickers_update": gid = (ulong)dat["guild_id"]; var strs = dat["stickers"].ToDiscordObject>(); await this.OnStickersUpdatedAsync(strs, gid).ConfigureAwait(false); break; case "guild_integrations_update": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationsUpdateEventAsync(this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_join_request_create": break; case "guild_join_request_update": break; case "guild_join_request_delete": break; #endregion #region Guild Ban case "guild_ban_add": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanAddEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_ban_remove": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Guild Event case "guild_scheduled_event_create": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventCreateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_update": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventUpdateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_delete": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventDeleteEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_add": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserAddedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_remove": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserRemovedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Guild Integration case "integration_create": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationCreateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false); break; case "integration_update": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationUpdateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false); break; case "integration_delete": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationDeleteEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (ulong?)dat["application_id"]).ConfigureAwait(false); break; #endregion #region Guild Member case "guild_member_add": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberAddEventAsync(dat.ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_member_remove": gid = (ulong)dat["guild_id"]; usr = dat["user"].ToObject(); if (!this.GuildsInternal.ContainsKey(gid)) { // discord fires this event inconsistently if the current user leaves a guild. if (usr.Id != this.CurrentUser.Id) this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {0} in guild cache", gid); return; } await this.OnGuildMemberRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_member_update": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.GuildsInternal[gid], dat["roles"].ToObject>(), (string)dat["nick"], (bool?)dat["pending"]).ConfigureAwait(false); break; case "guild_members_chunk": await this.OnGuildMembersChunkEventAsync(dat).ConfigureAwait(false); break; #endregion #region Guild Role case "guild_role_create": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleCreateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_role_update": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_role_delete": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Invite case "invite_create": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteCreateEventAsync(cid, gid, dat.ToObject()).ConfigureAwait(false); break; case "invite_delete": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteDeleteEventAsync(cid, gid, dat).ConfigureAwait(false); break; #endregion #region Message case "message_ack": cid = (ulong)dat["channel_id"]; var mid = (ulong)dat["message_id"]; await this.OnMessageAckEventAsync(this.InternalGetCachedChannel(cid), mid).ConfigureAwait(false); break; case "message_create": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), dat["author"].ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; case "message_update": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; // delete event does *not* include message object case "message_delete": await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_delete_bulk": await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; #endregion #region Message Reaction case "message_reaction_add": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove": await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove_all": await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_reaction_remove_emoji": await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]).ConfigureAwait(false); break; #endregion #region Stage Instance case "stage_instance_create": stg = dat.ToObject(); await this.OnStageInstanceCreateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_update": stg = dat.ToObject(); await this.OnStageInstanceUpdateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_delete": stg = dat.ToObject(); await this.OnStageInstanceDeleteEventAsync(stg).ConfigureAwait(false); break; #endregion #region Thread case "thread_create": trd = dat.ToObject(); await this.OnThreadCreateEventAsync(trd).ConfigureAwait(false); break; case "thread_update": trd = dat.ToObject(); await this.OnThreadUpdateEventAsync(trd).ConfigureAwait(false); break; case "thread_delete": trd = dat.ToObject(); await this.OnThreadDeleteEventAsync(trd).ConfigureAwait(false); break; case "thread_list_sync": gid = (ulong)dat["guild_id"]; //get guild await this.OnThreadListSyncEventAsync(this.GuildsInternal[gid], dat["channel_ids"].ToObject>(), dat["threads"].ToObject>(), dat["members"].ToObject>()).ConfigureAwait(false); break; case "thread_member_update": trdm = dat.ToObject(); await this.OnThreadMemberUpdateEventAsync(trdm).ConfigureAwait(false); break; case "thread_members_update": gid = (ulong)dat["guild_id"]; await this.OnThreadMembersUpdateEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (JArray)dat["added_members"], (JArray)dat["removed_member_ids"], (int)dat["member_count"]).ConfigureAwait(false); break; #endregion #region Activities case "embedded_activity_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this.GuildsInternal[gid], cid, (JArray)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false); break; #endregion #region User/Presence Update case "presence_update": await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]).ConfigureAwait(false); break; case "user_settings_update": await this.OnUserSettingsUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "user_update": await this.OnUserUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; #endregion #region Voice case "voice_state_update": await this.OnVoiceStateUpdateEventAsync(dat).ConfigureAwait(false); break; case "voice_server_update": gid = (ulong)dat["guild_id"]; await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Interaction/Integration/Application case "interaction_create": rawMbr = dat["member"]; if (rawMbr != null) { mbr = dat["member"].ToObject(); usr = mbr.User; } else { usr = dat["user"].ToObject(); } cid = (ulong)dat["channel_id"]; // Console.WriteLine(dat.ToString()); // Get raw interaction payload. await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject(), dat.ToString()).ConfigureAwait(false); break; case "application_command_create": await this.OnApplicationCommandCreateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_update": await this.OnApplicationCommandUpdateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_delete": await this.OnApplicationCommandDeleteAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "guild_application_command_counts_update": var counts = dat["application_command_counts"]; await this.OnGuildApplicationCommandCountsUpdateAsync((int)counts["1"], (int)counts["2"], (int)counts["3"], (ulong)dat["guild_id"]).ConfigureAwait(false); break; case "guild_application_command_index_update": // TODO: Implement. break; case "application_command_permissions_update": var aid = (ulong)dat["application_id"]; if (aid != this.CurrentApplication.Id) return; var pms = dat["permissions"].ToObject>(); gid = (ulong)dat["guild_id"]; await this.OnApplicationCommandPermissionsUpdateAsync(pms, (ulong)dat["id"], gid, aid).ConfigureAwait(false); break; #endregion #region Misc case "gift_code_update": //Not supposed to be dispatched to bots break; case "typing_start": cid = (ulong)dat["channel_id"]; rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr).ConfigureAwait(false); break; case "webhooks_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnWebhooksUpdateAsync(this.GuildsInternal[gid].GetChannel(cid), this.GuildsInternal[gid]).ConfigureAwait(false); break; default: await this.OnUnknownEventAsync(payload).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, payload.Data); break; #endregion } } #endregion #region Events #region Gateway /// /// Handles the ready event. /// /// The ready payload. /// The raw guilds. internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds) { //ready.CurrentUser.Discord = this; var rusr = ready.CurrentUser; this.CurrentUser.Username = rusr.Username; this.CurrentUser.Discriminator = rusr.Discriminator; this.CurrentUser.AvatarHash = rusr.AvatarHash; this.CurrentUser.MfaEnabled = rusr.MfaEnabled; this.CurrentUser.Verified = rusr.Verified; this.CurrentUser.IsBot = rusr.IsBot; this.CurrentUser.Flags = rusr.Flags; this.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; var rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); this.GuildsInternal.Clear(); foreach (var guild in ready.Guilds) { guild.Discord = this; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); foreach (var xc in guild.Channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); foreach (var xr in guild.Roles.Values) { xr.Discord = this; xr.GuildId = guild.Id; } var rawGuild = rawGuildIndex[guild.Id]; var rawMembers = (JArray)rawGuild["members"]; if (guild.MembersInternal != null) guild.MembersInternal.Clear(); else guild.MembersInternal = new ConcurrentDictionary(); if (rawMembers != null) { foreach (var xj in rawMembers) { var xtm = xj.ToObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; xu = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id }; } } if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); foreach (var xe in guild.Emojis.Values) xe.Discord = this; if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); foreach (var xs in guild.Stickers.Values) xs.Discord = this; if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); foreach (var xvs in guild.VoiceStates.Values) xvs.Discord = this; if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); foreach (var xt in guild.ThreadsInternal.Values) xt.Discord = this; if (guild.StageInstancesInternal == null) guild.StageInstancesInternal = new ConcurrentDictionary(); foreach (var xsi in guild.StageInstancesInternal.Values) xsi.Discord = this; if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); foreach (var xse in guild.ScheduledEventsInternal.Values) xse.Discord = this; this.GuildsInternal[guild.Id] = guild; } await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the resumed event. /// internal Task OnResumedAsync() { this.Logger.LogInformation(LoggerEvents.SessionUpdate, "Session resumed"); return this._resumed.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)); } #endregion #region Channel /// /// Handles the channel create event. /// /// The channel. internal async Task OnChannelCreateEventAsync(DiscordChannel channel) { channel.Discord = this; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = channel.Id; } this.GuildsInternal[channel.GuildId.Value].ChannelsInternal[channel.Id] = channel; /*if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); }*/ await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = channel.Guild }).ConfigureAwait(false); } /// /// Handles the channel update event. /// /// The channel. internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; var gld = channel.Guild; var channelNew = this.InternalGetCachedChannel(channel.Id); DiscordChannel channelOld = null; if (channelNew != null) { channelOld = new DiscordChannel { Bitrate = channelNew.Bitrate, Discord = this, GuildId = channelNew.GuildId, Id = channelNew.Id, LastMessageId = channelNew.LastMessageId, Name = channelNew.Name, PermissionOverwritesInternal = new List(channelNew.PermissionOverwritesInternal), Position = channelNew.Position, Topic = channelNew.Topic, Type = channelNew.Type, UserLimit = channelNew.UserLimit, ParentId = channelNew.ParentId, IsNsfw = channelNew.IsNsfw, PerUserRateLimit = channelNew.PerUserRateLimit, RtcRegionId = channelNew.RtcRegionId, QualityMode = channelNew.QualityMode, DefaultAutoArchiveDuration = channelNew.DefaultAutoArchiveDuration }; channelNew.Bitrate = channel.Bitrate; channelNew.Name = channel.Name; channelNew.Position = channel.Position; channelNew.Topic = channel.Topic; channelNew.UserLimit = channel.UserLimit; channelNew.ParentId = channel.ParentId; channelNew.IsNsfw = channel.IsNsfw; channelNew.PerUserRateLimit = channel.PerUserRateLimit; channelNew.Type = channel.Type; channelNew.RtcRegionId = channel.RtcRegionId; channelNew.QualityMode = channel.QualityMode; channelNew.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration; channelNew.PermissionOverwritesInternal.Clear(); foreach (var po in channel.PermissionOverwritesInternal) { po.Discord = this; po.ChannelId = channel.Id; } channelNew.PermissionOverwritesInternal.AddRange(channel.PermissionOverwritesInternal); if (this.Configuration.AutoRefreshChannelCache && gld != null) { await this.RefreshChannelsAsync(channel.Guild.Id); } } else if (gld != null) { gld.ChannelsInternal[channel.Id] = channel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } } await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channelNew, Guild = gld, ChannelBefore = channelOld }).ConfigureAwait(false); } /// /// Handles the channel delete event. /// /// The channel. internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; //if (channel.IsPrivate) if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private) { var dmChannel = channel as DiscordDmChannel; await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs(this.ServiceProvider) { Channel = dmChannel }).ConfigureAwait(false); } else { var gld = channel.Guild; if (gld.ChannelsInternal.TryRemove(channel.Id, out var cachedChannel)) channel = cachedChannel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = gld }).ConfigureAwait(false); } } /// /// Refreshes the channels. /// /// The guild id. internal async Task RefreshChannelsAsync(ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); var channels = await this.ApiClient.GetGuildChannelsAsync(guildId); guild.ChannelsInternal.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = channel.Id; } guild.ChannelsInternal[channel.Id] = channel; } } /// /// Handles the channel pins update event. /// /// The optional guild id. /// The channel id. /// The optional last pin timestamp. internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var ea = new ChannelPinsUpdateEventArgs(this.ServiceProvider) { Guild = guild, Channel = channel, LastPinTimestamp = lastPinTimestamp }; await this._channelPinsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild /// /// Handles the guild create event. /// /// The guild. /// The raw members. /// The presences. internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) { if (presences != null) { foreach (var xp in presences) { xp.Discord = this; xp.GuildId = guild.Id; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp.InternalActivities = xp.RawActivities .Select(x => new DiscordActivity(x)).ToArray(); } this.PresencesInternal[xp.InternalUser.Id] = xp; } } var exists = this.GuildsInternal.TryGetValue(guild.Id, out var foundGuild); guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; if (exists) guild = foundGuild; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); if (guild.MembersInternal == null) guild.MembersInternal = new ConcurrentDictionary(); if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.MembersInternal.Count); guild.IsUnavailable = eventGuild.IsUnavailable; guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount; guild.PremiumTier = eventGuild.PremiumTier; guild.BannerHash = eventGuild.BannerHash; guild.VanityUrlCode = eventGuild.VanityUrlCode; guild.Description = eventGuild.Description; guild.IsNsfw = eventGuild.IsNsfw; foreach (var kvp in eventGuild.VoiceStatesInternal) guild.VoiceStatesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ChannelsInternal) guild.ChannelsInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.RolesInternal) guild.RolesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.EmojisInternal) guild.EmojisInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ThreadsInternal) guild.ThreadsInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.StickersInternal) guild.StickersInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.StageInstancesInternal) guild.StageInstancesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ScheduledEventsInternal) guild.ScheduledEventsInternal[kvp.Key] = kvp.Value; foreach (var xc in guild.ChannelsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } foreach (var xt in guild.ThreadsInternal.Values) { xt.GuildId = guild.Id; xt.Discord = this; } foreach (var xe in guild.EmojisInternal.Values) xe.Discord = this; foreach (var xs in guild.StickersInternal.Values) xs.Discord = this; foreach (var xvs in guild.VoiceStatesInternal.Values) xvs.Discord = this; foreach (var xsi in guild.StageInstancesInternal.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xr in guild.RolesInternal.Values) { xr.Discord = this; xr.GuildId = guild.Id; } foreach (var xse in guild.ScheduledEventsInternal.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } var old = Volatile.Read(ref this._guildDownloadCompleted); var dcompl = this.GuildsInternal.Values.All(xg => !xg.IsUnavailable); Volatile.Write(ref this._guildDownloadCompleted, dcompl); if (exists) await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); else await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); if (dcompl && !old) await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds, this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the guild update event. /// /// The guild. /// The raw members. internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) { DiscordGuild oldGuild; if (!this.GuildsInternal.ContainsKey(guild.Id)) { this.GuildsInternal[guild.Id] = guild; oldGuild = null; } else { var gld = this.GuildsInternal[guild.Id]; oldGuild = new DiscordGuild { Discord = gld.Discord, Name = gld.Name, AfkChannelId = gld.AfkChannelId, AfkTimeout = gld.AfkTimeout, ApplicationId = gld.ApplicationId, DefaultMessageNotifications = gld.DefaultMessageNotifications, ExplicitContentFilter = gld.ExplicitContentFilter, RawFeatures = gld.RawFeatures, IconHash = gld.IconHash, Id = gld.Id, IsLarge = gld.IsLarge, IsSynced = gld.IsSynced, IsUnavailable = gld.IsUnavailable, JoinedAt = gld.JoinedAt, MemberCount = gld.MemberCount, MaxMembers = gld.MaxMembers, MaxPresences = gld.MaxPresences, ApproximateMemberCount = gld.ApproximateMemberCount, ApproximatePresenceCount = gld.ApproximatePresenceCount, MaxVideoChannelUsers = gld.MaxVideoChannelUsers, DiscoverySplashHash = gld.DiscoverySplashHash, PreferredLocale = gld.PreferredLocale, MfaLevel = gld.MfaLevel, OwnerId = gld.OwnerId, SplashHash = gld.SplashHash, SystemChannelId = gld.SystemChannelId, SystemChannelFlags = gld.SystemChannelFlags, Description = gld.Description, WidgetEnabled = gld.WidgetEnabled, WidgetChannelId = gld.WidgetChannelId, VerificationLevel = gld.VerificationLevel, RulesChannelId = gld.RulesChannelId, PublicUpdatesChannelId = gld.PublicUpdatesChannelId, VoiceRegionId = gld.VoiceRegionId, IsNsfw = gld.IsNsfw, PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, PremiumSubscriptionCount = gld.PremiumSubscriptionCount, PremiumTier = gld.PremiumTier, ChannelsInternal = new ConcurrentDictionary(), ThreadsInternal = new ConcurrentDictionary(), EmojisInternal = new ConcurrentDictionary(), StickersInternal = new ConcurrentDictionary(), MembersInternal = new ConcurrentDictionary(), RolesInternal = new ConcurrentDictionary(), StageInstancesInternal = new ConcurrentDictionary(), VoiceStatesInternal = new ConcurrentDictionary(), ScheduledEventsInternal = new ConcurrentDictionary() }; foreach (var kvp in gld.ChannelsInternal) oldGuild.ChannelsInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.ThreadsInternal) oldGuild.ThreadsInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.EmojisInternal) oldGuild.EmojisInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.StickersInternal) oldGuild.StickersInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.RolesInternal) oldGuild.RolesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.VoiceStatesInternal) oldGuild.VoiceStatesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.MembersInternal) oldGuild.MembersInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.StageInstancesInternal) oldGuild.StageInstancesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.ScheduledEventsInternal) oldGuild.ScheduledEventsInternal[kvp.Key] = kvp.Value; } guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; guild = this.GuildsInternal[eventGuild.Id]; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); if (guild.StageInstancesInternal == null) guild.StageInstancesInternal = new ConcurrentDictionary(); if (guild.MembersInternal == null) guild.MembersInternal = new ConcurrentDictionary(); if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); foreach (var xc in guild.ChannelsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } foreach (var xc in guild.ThreadsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; } foreach (var xe in guild.EmojisInternal.Values) xe.Discord = this; foreach (var xs in guild.StickersInternal.Values) xs.Discord = this; foreach (var xvs in guild.VoiceStatesInternal.Values) xvs.Discord = this; foreach (var xr in guild.RolesInternal.Values) { xr.Discord = this; xr.GuildId = guild.Id; } foreach (var xsi in guild.StageInstancesInternal.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xse in guild.ScheduledEventsInternal.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs(this.ServiceProvider) { GuildBefore = oldGuild, GuildAfter = guild }).ConfigureAwait(false); } /// /// Handles the guild delete event. /// /// The guild. internal async Task OnGuildDeleteEventAsync(DiscordGuild guild) { if (guild.IsUnavailable) { if (!this.GuildsInternal.TryGetValue(guild.Id, out var gld)) return; gld.IsUnavailable = true; await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = guild, Unavailable = true }).ConfigureAwait(false); } else { if (!this.GuildsInternal.TryRemove(guild.Id, out var gld)) return; await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = gld }).ConfigureAwait(false); } } /// /// Handles the guild sync event. /// /// The guild. /// Whether the guild is a large guild.. /// The raw members. /// The presences. internal async Task OnGuildSyncEventAsync(DiscordGuild guild, bool isLarge, JArray rawMembers, IEnumerable presences) { presences = presences.Select(xp => { xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); return xp; }); foreach (var xp in presences) this.PresencesInternal[xp.InternalUser.Id] = xp; guild.IsSynced = true; guild.IsLarge = isLarge; this.UpdateCachedGuild(guild, rawMembers); await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild emojis update event. /// /// The guild. /// The new emojis. internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) { var oldEmojis = new ConcurrentDictionary(guild.EmojisInternal); guild.EmojisInternal.Clear(); foreach (var emoji in newEmojis) { emoji.Discord = this; guild.EmojisInternal[emoji.Id] = emoji; } var ea = new GuildEmojisUpdateEventArgs(this.ServiceProvider) { Guild = guild, EmojisAfter = guild.Emojis, EmojisBefore = new ReadOnlyConcurrentDictionary(oldEmojis) }; await this._guildEmojisUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the stickers updated. /// /// The new stickers. /// The guild id. internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); var oldStickers = new ConcurrentDictionary(guild.StickersInternal); guild.StickersInternal.Clear(); foreach (var nst in newStickers) { if (nst.User is not null) { nst.User.Discord = this; this.UserCache.AddOrUpdate(nst.User.Id, nst.User, (old, @new) => @new); } nst.Discord = this; guild.StickersInternal[nst.Id] = nst; } var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider) { Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false); } #endregion #region Guild Ban /// /// Handles the guild ban add event. /// /// The transport user. /// The guild. internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var ea = new GuildBanAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild ban remove event. /// /// The transport user. /// The guild. internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var ea = new GuildBanRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Scheduled Event /// /// Handles the scheduled event create event. /// /// The created event. /// The guild. internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { scheduledEvent.Discord = this; guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (old, newScheduledEvent) => newScheduledEvent); if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild }).ConfigureAwait(false); } /// /// Handles the scheduled event update event. /// /// The updated event. /// The guild. internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { if (guild == null) return; DiscordScheduledEvent oldEvent; if (!guild.ScheduledEventsInternal.ContainsKey(scheduledEvent.Id)) { oldEvent = null; } else { var ev = guild.ScheduledEventsInternal[scheduledEvent.Id]; oldEvent = new DiscordScheduledEvent { Id = ev.Id, ChannelId = ev.ChannelId, EntityId = ev.EntityId, EntityMetadata = ev.EntityMetadata, CreatorId = ev.CreatorId, Creator = ev.Creator, Discord = this, Description = ev.Description, EntityType = ev.EntityType, ScheduledStartTimeRaw = ev.ScheduledStartTimeRaw, ScheduledEndTimeRaw = ev.ScheduledEndTimeRaw, GuildId = ev.GuildId, Status = ev.Status, Name = ev.Name, UserCount = ev.UserCount, CoverImageHash = ev.CoverImageHash }; } if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } if (scheduledEvent.Status == ScheduledEventStatus.Completed) { guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false); } else if (scheduledEvent.Status == ScheduledEventStatus.Canceled) { guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); scheduledEvent.Status = ScheduledEventStatus.Canceled; await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false); } else { this.UpdateScheduledEvent(scheduledEvent, guild); await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = oldEvent, ScheduledEventAfter = scheduledEvent, Guild = guild }).ConfigureAwait(false); } } /// /// Handles the scheduled event delete event. /// /// The deleted event. /// The guild. internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { scheduledEvent.Discord = this; if (scheduledEvent.Status == ScheduledEventStatus.Scheduled) scheduledEvent.Status = ScheduledEventStatus.Canceled; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild, Reason = scheduledEvent.Status }).ConfigureAwait(false); guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); } /// /// Handles the scheduled event user add event. /// The event. /// The added user id. /// The guild. /// internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild) { var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guildScheduledEventId, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduledEvent.UserCount++; scheduledEvent.Discord = this; guild.Discord = this; var user = this.GetUserAsync(userId, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result; member.Discord = this; await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } /// /// Handles the scheduled event user remove event. /// The event. /// The removed user id. /// The guild. /// internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild) { var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guildScheduledEventId, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduledEvent.UserCount = scheduledEvent.UserCount == 0 ? 0 : scheduledEvent.UserCount - 1; scheduledEvent.Discord = this; guild.Discord = this; var user = this.GetUserAsync(userId, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result; member.Discord = this; await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } #endregion #region Guild Integration /// /// Handles the guild integration create event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationCreateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationCreated.InvokeAsync(this, new GuildIntegrationCreateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integration update event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationUpdateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationUpdated.InvokeAsync(this, new GuildIntegrationUpdateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integrations update event. /// /// The guild. internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) { var ea = new GuildIntegrationsUpdateEventArgs(this.ServiceProvider) { Guild = guild }; await this._guildIntegrationsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild integration delete event. /// /// The guild. /// The integration id. /// The optional application id. internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integrationId, ulong? applicationId) => await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integrationId, ApplicationId = applicationId }).ConfigureAwait(false); #endregion #region Guild Member /// /// Handles the guild member add event. /// /// The transport member. /// The guild. internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(member.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id }; guild.MembersInternal[mbr.Id] = mbr; guild.MemberCount++; var ea = new GuildMemberAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member remove event. /// /// The transport user. /// The guild. internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user); if (!guild.MembersInternal.TryRemove(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; guild.MemberCount--; _ = this.UserCache.AddOrUpdate(user.Id, usr, (old, @new) => @new); var ea = new GuildMemberRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member update event. /// /// The transport member. /// The guild. /// The roles. /// The nick. /// Whether the member is pending. internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild, IEnumerable roles, string nick, bool? pending) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(member.User.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var old = mbr; var gAvOld = old.GuildAvatarHash; var avOld = old.AvatarHash; var nickOld = mbr.Nickname; var pendingOld = mbr.IsPending; var rolesOld = new ReadOnlyCollection(new List(mbr.Roles)); var cduOld = mbr.CommunicationDisabledUntil; mbr.MemberFlags = member.MemberFlags; mbr.AvatarHashInternal = member.AvatarHash; mbr.GuildAvatarHash = member.GuildAvatarHash; mbr.Nickname = nick; mbr.GuildPronouns = member.GuildPronouns; mbr.IsPending = pending; mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil; mbr.RoleIdsInternal.Clear(); mbr.RoleIdsInternal.AddRange(roles); guild.MembersInternal.AddOrUpdate(member.User.Id, mbr, (id, oldMbr) => oldMbr); var timeoutUntil = member.CommunicationDisabledUntil; /*this.Logger.LogTrace($"Timeout:\nBefore - {cduOld}\nAfter - {timeoutUntil}"); if ((timeoutUntil.HasValue && cduOld.HasValue) || (timeoutUntil == null && cduOld.HasValue) || (timeoutUntil.HasValue && cduOld == null)) { // We are going to add a scheduled timer to assure that we get a auditlog entry. var id = $"tt-{mbr.Id}-{guild.Id}-{DateTime.Now.ToLongTimeString()}"; this._tempTimers.Add( id, new( new TimeoutHandler( mbr, guild, cduOld, timeoutUntil ), new Timer( this.TimeoutTimer, id, 2000, Timeout.Infinite ) ) ); this.Logger.LogTrace("Scheduling timeout event."); return; }*/ //this.Logger.LogTrace("No timeout detected. Continuing on normal operation."); var eargs = new GuildMemberUpdateEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr, NicknameAfter = mbr.Nickname, RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)), PendingAfter = mbr.IsPending, TimeoutAfter = mbr.CommunicationDisabledUntil, AvatarHashAfter = mbr.AvatarHash, GuildAvatarHashAfter = mbr.GuildAvatarHash, NicknameBefore = nickOld, RolesBefore = rolesOld, PendingBefore = pendingOld, TimeoutBefore = cduOld, AvatarHashBefore = avOld, GuildAvatarHashBefore = gAvOld }; await this._guildMemberUpdated.InvokeAsync(this, eargs).ConfigureAwait(false); } /// /// Handles timeout events. /// /// Internally used as uid for the timer data. private async void TimeoutTimer(object state) { var tid = (string)state; var data = this._tempTimers.First(x=> x.Key == tid).Value.Key; var timer = this._tempTimers.First(x=> x.Key == tid).Value.Value; IReadOnlyList auditlog = null; DiscordAuditLogMemberUpdateEntry filtered = null; try { auditlog = await data.Guild.GetAuditLogsAsync(10, null, AuditLogActionType.MemberUpdate); var preFiltered = auditlog.Select(x => x as DiscordAuditLogMemberUpdateEntry).Where(x => x.Target.Id == data.Member.Id); filtered = preFiltered.First(); } catch (UnauthorizedException) { } catch (Exception) { this.Logger.LogTrace("Failing timeout event."); await timer.DisposeAsync(); this._tempTimers.Remove(tid); return; } var actor = filtered?.UserResponsible as DiscordMember; this.Logger.LogTrace("Trying to execute timeout event."); if (data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue) { // A timeout was updated. if (filtered != null && auditlog == null) { this.Logger.LogTrace("Re-scheduling timeout event."); timer.Change(2000, Timeout.Infinite); return; } var ea = new GuildMemberTimeoutUpdateEventArgs(this.ServiceProvider) { Guild = data.Guild, Target = data.Member, TimeoutBefore = data.TimeoutUntilOld.Value, TimeoutAfter = data.TimeoutUntilNew.Value, Actor = actor, AuditLogId = filtered?.Id, AuditLogReason = filtered?.Reason }; await this._guildMemberTimeoutChanged.InvokeAsync(this, ea).ConfigureAwait(false); } else if (!data.TimeoutUntilOld.HasValue && data.TimeoutUntilNew.HasValue) { // A timeout was added. if (filtered != null && auditlog == null) { this.Logger.LogTrace("Re-scheduling timeout event."); timer.Change(2000, Timeout.Infinite); return; } var ea = new GuildMemberTimeoutAddEventArgs(this.ServiceProvider) { Guild = data.Guild, Target = data.Member, Timeout = data.TimeoutUntilNew.Value, Actor = actor, AuditLogId = filtered?.Id, AuditLogReason = filtered?.Reason }; await this._guildMemberTimeoutAdded.InvokeAsync(this, ea).ConfigureAwait(false); } else if (data.TimeoutUntilOld.HasValue && !data.TimeoutUntilNew.HasValue) { // A timeout was removed. if (filtered != null && auditlog == null) { this.Logger.LogTrace("Re-scheduling timeout event."); timer.Change(2000, Timeout.Infinite); return; } var ea = new GuildMemberTimeoutRemoveEventArgs(this.ServiceProvider) { Guild = data.Guild, Target = data.Member, TimeoutBefore = data.TimeoutUntilOld.Value, Actor = actor, AuditLogId = filtered?.Id, AuditLogReason = filtered?.Reason }; await this._guildMemberTimeoutRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } // Ending timer because it worked. this.Logger.LogTrace("Removing timeout event."); await timer.DisposeAsync(); this._tempTimers.Remove(tid); } /// /// Handles the guild members chunk event. /// /// The raw chunk data. internal async Task OnGuildMembersChunkEventAsync(JObject dat) { var guild = this.Guilds[(ulong)dat["guild_id"]]; var chunkIndex = (int)dat["chunk_index"]; var chunkCount = (int)dat["chunk_count"]; var nonce = (string)dat["nonce"]; var mbrs = new HashSet(); var pres = new HashSet(); var members = dat["members"].ToObject(); foreach (var member in members) { var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id }; if (!this.UserCache.ContainsKey(mbr.Id)) this.UserCache[mbr.Id] = new DiscordUser(member.User) { Discord = this }; guild.MembersInternal[mbr.Id] = mbr; mbrs.Add(mbr); } guild.MemberCount = guild.MembersInternal.Count; var ea = new GuildMembersChunkEventArgs(this.ServiceProvider) { Guild = guild, Members = new ReadOnlySet(mbrs), ChunkIndex = chunkIndex, ChunkCount = chunkCount, Nonce = nonce, }; if (dat["presences"] != null) { var presences = dat["presences"].ToObject(); var presCount = presences.Length; foreach (var presence in presences) { presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); if (presence.RawActivities != null) { presence.InternalActivities = presence.RawActivities .Select(x => new DiscordActivity(x)).ToArray(); } pres.Add(presence); } ea.Presences = new ReadOnlySet(pres); } if (dat["not_found"] != null) { var nf = dat["not_found"].ToObject>(); ea.NotFound = new ReadOnlySet(nf); } await this._guildMembersChunked.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Role /// /// Handles the guild role create event. /// /// The role. /// The guild. internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) { role.Discord = this; role.GuildId = guild.Id; guild.RolesInternal[role.Id] = role; var ea = new GuildRoleCreateEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role update event. /// /// The role. /// The guild. internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) { var newRole = guild.GetRole(role.Id); var oldRole = new DiscordRole { GuildId = guild.Id, ColorInternal = newRole.ColorInternal, Discord = this, IsHoisted = newRole.IsHoisted, Id = newRole.Id, IsManaged = newRole.IsManaged, IsMentionable = newRole.IsMentionable, Name = newRole.Name, Permissions = newRole.Permissions, Position = newRole.Position, IconHash = newRole.IconHash, Tags = newRole.Tags ?? null, UnicodeEmojiString = newRole.UnicodeEmojiString }; newRole.GuildId = guild.Id; newRole.ColorInternal = role.ColorInternal; newRole.IsHoisted = role.IsHoisted; newRole.IsManaged = role.IsManaged; newRole.IsMentionable = role.IsMentionable; newRole.Name = role.Name; newRole.Permissions = role.Permissions; newRole.Position = role.Position; newRole.IconHash = role.IconHash; newRole.Tags = role.Tags ?? null; newRole.UnicodeEmojiString = role.UnicodeEmojiString; var ea = new GuildRoleUpdateEventArgs(this.ServiceProvider) { Guild = guild, RoleAfter = newRole, RoleBefore = oldRole }; await this._guildRoleUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role delete event. /// /// The role id. /// The guild. internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) { if (!guild.RolesInternal.TryRemove(roleId, out var role)) this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild})."); var ea = new GuildRoleDeleteEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Invite /// /// Handles the invite create event. /// /// The channel id. /// The guild id. /// The invite. internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); invite.Discord = this; if (invite.Inviter is not null) { invite.Inviter.Discord = this; this.UserCache.AddOrUpdate(invite.Inviter.Id, invite.Inviter, (old, @new) => @new); } guild.Invites[invite.Code] = invite; var ea = new InviteCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the invite delete event. /// /// The channel id. /// The guild id. /// The raw invite. internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); if (!guild.Invites.TryRemove(dat["code"].ToString(), out var invite)) { invite = dat.ToObject(); invite.Discord = this; } invite.IsRevoked = true; var ea = new InviteDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message /// /// Handles the message acknowledge event. /// /// The channel. /// The message id. internal async Task OnMessageAckEventAsync(DiscordChannel chn, ulong messageId) { if (this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == chn.Id, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = chn.Id, Discord = this, }; } await this._messageAcknowledged.InvokeAsync(this, new MessageAcknowledgeEventArgs(this.ServiceProvider) { Message = msg }).ConfigureAwait(false); } /// /// Handles the message create event. /// /// The message. /// The transport user (author). /// The transport member. /// The reference transport user (author). /// The reference transport member. internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { message.Discord = this; this.PopulateMessageReactionsAndCache(message, author, member); message.PopulateMentions(); if (message.Channel == null && message.ChannelId == default) this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!"); if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } foreach (var sticker in message.Stickers) sticker.Discord = this; var ea = new MessageCreateEventArgs(this.ServiceProvider) { Message = message, MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal), MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null, MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null }; await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message update event. /// /// The message. /// The transport user (author). /// The transport member. /// The reference transport user (author). /// The reference transport member. internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { DiscordGuild guild; message.Discord = this; var eventMessage = message; DiscordMessage oldmsg = null; if (this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == eventMessage.Id && xm.ChannelId == eventMessage.ChannelId, out message)) { message = eventMessage; this.PopulateMessageReactionsAndCache(message, author, member); guild = message.Channel?.Guild; if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } } else { oldmsg = new DiscordMessage(message); guild = message.Channel?.Guild; message.EditedTimestampRaw = eventMessage.EditedTimestampRaw; if (eventMessage.Content != null) message.Content = eventMessage.Content; message.EmbedsInternal.Clear(); message.EmbedsInternal.AddRange(eventMessage.EmbedsInternal); message.Pinned = eventMessage.Pinned; message.IsTts = eventMessage.IsTts; } message.PopulateMentions(); var ea = new MessageUpdateEventArgs(this.ServiceProvider) { Message = message, MessageBefore = oldmsg, MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal), MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null, MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null }; await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message delete event. /// /// The message id. /// The channel id. /// The optional guild id. internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); var ea = new MessageDeleteEventArgs(this.ServiceProvider) { Channel = channel, Message = msg, Guild = guild }; await this._messageDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message bulk delete event. /// /// The message ids. /// The channel id. /// The optional guild id. internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var msgs = new List(messageIds.Length); foreach (var messageId in messageIds) { if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); msgs.Add(msg); } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageBulkDeleteEventArgs(this.ServiceProvider) { Channel = channel, Messages = new ReadOnlyCollection(msgs), Guild = guild }; await this._messagesBulkDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message Reaction /// /// Handles the message reaction add event. /// /// The user id. /// The message id. /// The channel id. /// The optional guild id. /// The transport member. /// The emoji. internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); emoji.Discord = this; var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, ReactionsInternal = new List() }; } var react = msg.ReactionsInternal.FirstOrDefault(xr => xr.Emoji == emoji); if (react == null) { msg.ReactionsInternal.Add(react = new DiscordReaction { Count = 1, Emoji = emoji, IsMe = this.CurrentUser.Id == userId }); } else { react.Count++; react.IsMe |= this.CurrentUser.Id == userId; } var ea = new MessageReactionAddEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove event. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The emoji. internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); emoji.Discord = this; if (!this.UserCache.TryGetValue(userId, out var usr)) usr = new DiscordUser { Id = userId, Discord = this }; if (channel?.Guild != null) usr = channel.Guild.Members.TryGetValue(userId, out var member) ? member : new DiscordMember(usr) { Discord = this, GuildId = channel.GuildId.Value }; if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var react = msg.ReactionsInternal?.FirstOrDefault(xr => xr.Emoji == emoji); if (react != null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; if (msg.ReactionsInternal != null && react.Count <= 0) // shit happens msg.ReactionsInternal.RemoveFirst(x => x.Emoji == emoji); } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionRemoveEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove event. /// Fired when all message reactions were removed. /// /// The message id. /// The channel id. /// The optional guild id. internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } msg.ReactionsInternal?.Clear(); var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionsClearEventArgs(this.ServiceProvider) { Message = msg }; await this._messageReactionsCleared.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove event. /// Fired when a emoji got removed. /// /// The message id. /// The channel id. /// The guild id. /// The raw discord emoji. internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var partialEmoji = dat.ToObject(); if (!guild.EmojisInternal.TryGetValue(partialEmoji.Id, out var emoji)) { emoji = partialEmoji; emoji.Discord = this; } msg.ReactionsInternal?.RemoveAll(r => r.Emoji.Equals(emoji)); var ea = new MessageReactionRemoveEmojiEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Message = msg, Emoji = emoji }; await this._messageReactionRemovedEmoji.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Stage Instance /// /// Handles the stage instance create event. /// /// The created stage instance. internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Handles the stage instance update event. /// /// The updated stage instance. internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Handles the stage instance delete event. /// /// The deleted stage instance. internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } #endregion #region Thread /// /// Handles the thread create event. /// /// The created thread. internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) { thread.Discord = this; this.InternalGetCachedGuild(thread.GuildId).ThreadsInternal.AddOrUpdate(thread.Id, thread, (oldThread, newThread) => newThread); await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }).ConfigureAwait(false); } /// /// Handles the thread update event. /// /// The updated thread. internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var guild = thread.Guild; var threadNew = this.InternalGetCachedThread(thread.Id); DiscordThreadChannel threadOld = null; ThreadUpdateEventArgs updateEvent; if (threadNew != null) { threadOld = new DiscordThreadChannel { Discord = this, Type = threadNew.Type, ThreadMetadata = thread.ThreadMetadata, ThreadMembersInternal = threadNew.ThreadMembersInternal, ParentId = thread.ParentId, OwnerId = thread.OwnerId, Name = thread.Name, LastMessageId = threadNew.LastMessageId, MessageCount = thread.MessageCount, MemberCount = thread.MemberCount, GuildId = thread.GuildId, LastPinTimestampRaw = threadNew.LastPinTimestampRaw, PerUserRateLimit = threadNew.PerUserRateLimit, CurrentMember = threadNew.CurrentMember }; threadNew.ThreadMetadata = thread.ThreadMetadata; threadNew.ParentId = thread.ParentId; threadNew.OwnerId = thread.OwnerId; threadNew.Name = thread.Name; threadNew.LastMessageId = thread.LastMessageId.HasValue ? thread.LastMessageId : threadOld.LastMessageId; threadNew.MessageCount = thread.MessageCount; threadNew.MemberCount = thread.MemberCount; threadNew.GuildId = thread.GuildId; threadNew.Discord = this; updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, ThreadBefore = threadOld, Guild = thread.Guild, Parent = thread.Parent }; } else { updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, Guild = thread.Guild, Parent = thread.Parent }; guild.ThreadsInternal[thread.Id] = thread; } await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false); } /// /// Handles the thread delete event. /// /// The deleted thread. internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var gld = thread.Guild; if (gld.ThreadsInternal.TryRemove(thread.Id, out var cachedThread)) thread = cachedThread; await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent, Type = thread.Type }).ConfigureAwait(false); } /// /// Handles the thread list sync event. /// /// The synced guild. /// The synced channel ids. /// The synced threads. /// The synced thread members. internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channelIds, IReadOnlyList threads, IReadOnlyList members) { guild.Discord = this; var channels = channelIds.Select(x => guild.GetChannel(x.Value)); //getting channel objects foreach (var chan in channels) { chan.Discord = this; } _ = threads.Select(x => x.Discord = this); await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs(this.ServiceProvider) { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, Members = members.ToList().AsReadOnly() }).ConfigureAwait(false); } /// /// Handles the thread member update event. /// /// The updated member. internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member) { member.Discord = this; var thread = this.InternalGetCachedThread(member.Id); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(member.Id); thread = this.GuildsInternal[member.GuildId].ThreadsInternal.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread); } thread.CurrentMember = member; thread.Guild.ThreadsInternal.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread); await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false); } /// /// Handles the thread members update event. /// /// The target guild. /// The thread id of the target thread this update belongs to. /// The added members. /// The ids of the removed members. /// The new member count. internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong threadId, JArray membersAdded, JArray membersRemoved, int memberCount) { var thread = this.InternalGetCachedThread(threadId); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(threadId); thread = guild.ThreadsInternal.AddOrUpdate(threadId, tempThread, (old, newThread) => newThread); } thread.Discord = this; guild.Discord = this; List addedMembers = new(); List removedMemberIds = new(); if (membersAdded != null) { foreach (var xj in membersAdded) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; xtm.GuildId = guild.Id; if (xtm != null) addedMembers.Add(xtm); if (xtm.Id == this.CurrentUser.Id) thread.CurrentMember = xtm; } } var removedMembers = new List(); if (membersRemoved != null) { foreach (var removedId in membersRemoved) { removedMembers.Add(guild.MembersInternal.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, GuildId = guild.Id, Discord = this }); } } if (removedMemberIds.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread thread.CurrentMember = null; thread.MemberCount = memberCount; var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider) { Guild = guild, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, MemberCount = memberCount }; await this._threadMembersUpdated.InvokeAsync(this, threadMembersUpdateArg).ConfigureAwait(false); } #endregion #region Activities /// /// Dispatches the event. /// /// The transport activity. /// The guild. /// The channel id. /// The users in the activity. /// The application id. internal async Task OnEmbeddedActivityUpdateAsync(JObject trActivity, DiscordGuild guild, ulong channelId, JArray jUsers, ulong appId) => await Task.Delay(20); /*{ try { var users = j_users?.ToObject>(); DiscordActivity old = null; var uid = $"{guild.Id}_{channel_id}_{app_id}"; if (this._embeddedActivities.TryGetValue(uid, out var activity)) { old = new DiscordActivity(activity); DiscordJson.PopulateObject(tr_activity, activity); } else { activity = tr_activity.ToObject(); this._embeddedActivities[uid] = activity; } var activity_users = new List(); var channel = this.InternalGetCachedChannel(channel_id) ?? await this.ApiClient.GetChannelAsync(channel_id); if (users != null) { foreach (var user in users) { var activity_user = guild._members.TryGetValue(user, out var member) ? member : new DiscordMember { Id = user, _guild_id = guild.Id, Discord = this }; activity_users.Add(activity_user); } } else activity_users = null; var ea = new EmbeddedActivityUpdateEventArgs(this.ServiceProvider) { Guild = guild, Users = activity_users, Channel = channel }; await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(ex, ex.Message); } }*/ #endregion #region User/Presence Update /// /// Handles the presence update event. /// /// The raw presence. /// The raw user. internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) { var uid = (ulong)rawUser["id"]; DiscordPresence old = null; if (this.PresencesInternal.TryGetValue(uid, out var presence)) { old = new DiscordPresence(presence); DiscordJson.PopulateObject(rawPresence, presence); } else { presence = rawPresence.ToObject(); presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); this.PresencesInternal[presence.InternalUser.Id] = presence; } // reuse arrays / avoid linq (this is a hot zone) if (presence.Activities == null || rawPresence["activities"] == null) { presence.InternalActivities = Array.Empty(); } else { if (presence.InternalActivities.Length != presence.RawActivities.Length) presence.InternalActivities = new DiscordActivity[presence.RawActivities.Length]; for (var i = 0; i < presence.InternalActivities.Length; i++) presence.InternalActivities[i] = new DiscordActivity(presence.RawActivities[i]); if (presence.InternalActivities.Length > 0) { presence.RawActivity = presence.RawActivities[0]; if (presence.Activity != null) presence.Activity.UpdateWith(presence.RawActivity); else presence.Activity = new DiscordActivity(presence.RawActivity); } } if (this.UserCache.TryGetValue(uid, out var usr)) { if (old != null) { old.InternalUser.Username = usr.Username; old.InternalUser.Discriminator = usr.Discriminator; old.InternalUser.AvatarHash = usr.AvatarHash; } if (rawUser["username"] is object) usr.Username = (string)rawUser["username"]; if (rawUser["discriminator"] is object) usr.Discriminator = (string)rawUser["discriminator"]; if (rawUser["avatar"] is object) usr.AvatarHash = (string)rawUser["avatar"]; presence.InternalUser.Username = usr.Username; presence.InternalUser.Discriminator = usr.Discriminator; presence.InternalUser.AvatarHash = usr.AvatarHash; } var usrafter = usr ?? new DiscordUser(presence.InternalUser); var ea = new PresenceUpdateEventArgs(this.ServiceProvider) { Status = presence.Status, Activity = presence.Activity, User = usr, PresenceBefore = old, PresenceAfter = presence, UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter, UserAfter = usrafter }; await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user settings update event. /// /// The transport user. internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) { var usr = new DiscordUser(user) { Discord = this }; var ea = new UserSettingsUpdateEventArgs(this.ServiceProvider) { User = usr }; await this._userSettingsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user update event. /// /// The transport user. internal async Task OnUserUpdateEventAsync(TransportUser user) { var usrOld = new DiscordUser { AvatarHash = this.CurrentUser.AvatarHash, Discord = this, Discriminator = this.CurrentUser.Discriminator, Email = this.CurrentUser.Email, Id = this.CurrentUser.Id, IsBot = this.CurrentUser.IsBot, MfaEnabled = this.CurrentUser.MfaEnabled, Username = this.CurrentUser.Username, Verified = this.CurrentUser.Verified }; this.CurrentUser.AvatarHash = user.AvatarHash; this.CurrentUser.Discriminator = user.Discriminator; this.CurrentUser.Email = user.Email; this.CurrentUser.Id = user.Id; this.CurrentUser.IsBot = user.IsBot; this.CurrentUser.MfaEnabled = user.MfaEnabled; this.CurrentUser.Username = user.Username; this.CurrentUser.Verified = user.Verified; var ea = new UserUpdateEventArgs(this.ServiceProvider) { UserAfter = this.CurrentUser, UserBefore = usrOld }; await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Voice /// /// Handles the voice state update event. /// /// The raw voice state update object. internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { var gid = (ulong)raw["guild_id"]; var uid = (ulong)raw["user_id"]; var gld = this.GuildsInternal[gid]; var vstateNew = raw.ToObject(); vstateNew.Discord = this; gld.VoiceStatesInternal.TryRemove(uid, out var vstateOld); if (vstateNew.Channel != null) { gld.VoiceStatesInternal[vstateNew.UserId] = vstateNew; } if (gld.MembersInternal.TryGetValue(uid, out var mbr)) { mbr.IsMuted = vstateNew.IsServerMuted; mbr.IsDeafened = vstateNew.IsServerDeafened; } else { var transportMbr = vstateNew.TransportMember; this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); } var ea = new VoiceStateUpdateEventArgs(this.ServiceProvider) { Guild = vstateNew.Guild, Channel = vstateNew.Channel, User = vstateNew.User, SessionId = vstateNew.SessionId, Before = vstateOld, After = vstateNew }; await this._voiceStateUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the voice server update event. /// /// The new endpoint. /// The new token. /// The guild. internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) { var ea = new VoiceServerUpdateEventArgs(this.ServiceProvider) { Endpoint = endpoint, VoiceToken = token, Guild = guild }; await this._voiceServerUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Commands /// /// Handles the application command create event. /// /// The application command. /// The optional guild id. internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command update event. /// /// The application command. /// The optional guild id. internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command delete event. /// /// The application command. /// The optional guild id. internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild application command counts update event. /// /// The count. /// The count. /// The count. /// The guild id. /// Count of application commands. internal async Task OnGuildApplicationCommandCountsUpdateAsync(int chatInputCommandCount, int userContextMenuCommandCount, int messageContextMenuCount, ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); if (guild == null) { guild = new DiscordGuild { Id = guildId, Discord = this }; } var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider) { SlashCommands = chatInputCommandCount, UserContextMenuCommands = userContextMenuCommandCount, MessageContextMenuCommands = messageContextMenuCount, Guild = guild }; await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command permissions update event. /// /// The new permissions. /// The command id. /// The guild id. /// The application id. internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong channelId, ulong guildId, ulong applicationId) { if (applicationId != this.CurrentApplication.Id) return; var guild = this.InternalGetCachedGuild(guildId); DiscordApplicationCommand cmd; try { cmd = await this.GetGuildApplicationCommandAsync(guildId, channelId); } catch (NotFoundException) { cmd = await this.GetGlobalApplicationCommandAsync(channelId); } if (guild == null) { guild = new DiscordGuild { Id = guildId, Discord = this }; } var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider) { Permissions = perms.ToList(), Command = cmd, ApplicationId = applicationId, Guild = guild }; await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Interaction /// /// Handles the interaction create event. /// /// The guild id. /// The channel id. /// The transport user. /// The transport member. /// The interaction. internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction, string rawInteraction) { - this.Logger.LogInformation("Interaction from {guild} on shard {shard}", guildId.HasValue ? guildId.Value : "dm", this.ShardId); - this.Logger.LogInformation("Interaction: {interaction}", rawInteraction); + this.Logger.LogTrace("Interaction from {guild} on shard {shard}", guildId.HasValue ? guildId.Value : "dm", this.ShardId); + this.Logger.LogTrace("Interaction: {interaction}", rawInteraction); var usr = new DiscordUser(user) { Discord = this }; interaction.ChannelId = channelId; interaction.GuildId = guildId; interaction.Discord = this; interaction.Data.Discord = this; if (member != null) { usr = new DiscordMember(member) { GuildId = guildId.Value, Discord = this }; this.UpdateUser(usr, guildId, interaction.Guild, member); } else { this.UserCache.AddOrUpdate(usr.Id, usr, (old, @new) => @new); } interaction.User = usr; var resolved = interaction.Data.Resolved; if (resolved != null) { if (resolved.Users != null) { foreach (var c in resolved.Users) { c.Value.Discord = this; this.UserCache.AddOrUpdate(c.Value.Id, c.Value, (old, @new) => @new); } } if (resolved.Members != null) { foreach (var c in resolved.Members) { c.Value.Discord = this; c.Value.Id = c.Key; c.Value.GuildId = guildId.Value; c.Value.User.Discord = this; this.UserCache.AddOrUpdate(c.Value.User.Id, c.Value.User, (old, @new) => @new); } } if (resolved.Channels != null) { foreach (var c in resolved.Channels) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Roles != null) { foreach (var c in resolved.Roles) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Messages != null) { foreach (var m in resolved.Messages) { m.Value.Discord = this; if (guildId.HasValue) m.Value.GuildId = guildId.Value; } } if (resolved.Attachments != null) foreach (var a in resolved.Attachments) a.Value.Discord = this; } if (interaction.Type is InteractionType.Component || interaction.Type is InteractionType.ModalSubmit) { if (interaction.Message != null) { interaction.Message.Discord = this; interaction.Message.ChannelId = interaction.ChannelId; } var cea = new ComponentInteractionCreateEventArgs(this.ServiceProvider) { Message = interaction.Message, Interaction = interaction }; await this._componentInteractionCreated.InvokeAsync(this, cea).ConfigureAwait(false); } else { if (interaction.Data.Target.HasValue) // Context-Menu. // { var targetId = interaction.Data.Target.Value; DiscordUser targetUser = null; DiscordMember targetMember = null; DiscordMessage targetMessage = null; interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage); interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); var ea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction, TargetUser = targetMember ?? targetUser, TargetMessage = targetMessage, Type = interaction.Data.Type }; await this._contextMenuInteractionCreated.InvokeAsync(this, ea); } else { var ea = new InteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction }; await this._interactionCreated.InvokeAsync(this, ea); } } } #endregion #region Misc /// /// Handles the typing start event. /// /// The user id. /// The channel id. /// The channel. /// The optional guild id. /// The time when the user started typing. /// The transport member. internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) { if (channel == null) { channel = new DiscordChannel { Discord = this, Id = channelId, GuildId = guildId ?? default, }; } var guild = this.InternalGetCachedGuild(guildId); var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); var ea = new TypingStartEventArgs(this.ServiceProvider) { Channel = channel, User = usr, Guild = guild, StartedAt = started }; await this._typingStarted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the webhooks update. /// /// The channel. /// The guild. internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) { var ea = new WebhooksUpdateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild }; await this._webhooksUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles all unknown events. /// /// The payload. internal async Task OnUnknownEventAsync(GatewayPayload payload) { var ea = new UnknownEventArgs(this.ServiceProvider) { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; await this._unknownEvent.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #endregion }