diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 2678899ef..64cd8fbff 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,2091 +1,2091 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.Context; using DisCatSharp.ApplicationCommands.Entities; using DisCatSharp.ApplicationCommands.Enums; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.ApplicationCommands.Exceptions; using DisCatSharp.ApplicationCommands.Workers; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands; /// /// A class that handles slash commands for a client. /// public sealed class ApplicationCommandsExtension : BaseExtension { /// /// A list of methods for top level commands. /// private static List s_commandMethods { get; set; } = new(); /// /// List of groups. /// private static List s_groupCommands { get; set; } = new(); /// /// List of groups with subgroups. /// private static List s_subGroupCommands { get; set; } = new(); /// /// List of context menus. /// private static List s_contextMenuCommands { get; set; } = new(); /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = new(); /// /// List of modules to register. /// private readonly List> _updateList = new(); /// /// Configuration for Discord. /// internal static ApplicationCommandsConfiguration Configuration; /// /// Set to true if anything fails when registering. /// private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => s_registeredCommands; private static List>> s_registeredCommands = new(); /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => GlobalCommandsInternal; internal static List GlobalCommandsInternal = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; internal static Dictionary> GuildCommandsInternal = new(); /// /// Gets the registration count. /// private static int s_registrationCount { get; set; } /// /// Gets the expected count. /// private static int s_expectedCount { get; set; } /// /// Gets the guild ids where the applications.commands scope is missing. /// private List _missingScopeGuildIds; /// /// Gets whether debug is enabled. /// internal static bool DebugEnabled { get; set; } internal static LogLevel ApplicationCommandsLogLevel => DebugEnabled ? LogLevel.Debug : LogLevel.Trace; /// /// Gets whether check through all guilds is enabled. /// internal static bool CheckAllGuilds { get; set; } /// /// Gets whether the registration check should be manually overridden. /// internal static bool ManOr { get; set; } /// /// Gets whether interactions should be automatically deffered. /// internal static bool AutoDeferEnabled { get; set; } /// /// Whether this module finished the startup. /// internal bool StartupFinished { get; set; } = false; /// /// Gets the service provider this module was configured with. /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] public IServiceProvider Services => Configuration.ServiceProvider; /// /// Gets a list of handled interactions. Fix for double interaction execution bug. /// internal static List HandledInteractions = new(); /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration = null) { Configuration = configuration; DebugEnabled = configuration?.DebugStartup ?? false; CheckAllGuilds = configuration?.CheckAllGuilds ?? false; ManOr = configuration?.ManualOverride ?? false; AutoDeferEnabled = configuration?.AutoDefer ?? false; } /// /// Runs setup. /// DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. /// /// The client to setup on. protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null); this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null); this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null); this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null); this._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null); this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null); this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null); this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null); this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync(); this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e) { if (!this.StartupFinished) await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")); else await Task.Delay(1); } private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) { if (!this.StartupFinished) await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")); else await Task.Delay(1); } private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.StartupFinished = true; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Cleans the module for a new start of the bot. /// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES. /// public void CleanModule() { this._updateList.Clear(); s_singletonModules.Clear(); s_errored = false; s_expectedCount = 0; s_registrationCount = 0; s_commandMethods.Clear(); s_groupCommands.Clear(); s_contextMenuCommands.Clear(); s_subGroupCommands.Clear(); s_singletonModules.Clear(); s_registeredCommands.Clear(); GlobalCommandsInternal.Clear(); GuildCommandsInternal.Clear(); } /// /// Cleans all guild application commands. /// You normally don't need to execute it. /// internal async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } /// /// Cleans the global application commands. /// You normally don't need to execute it. /// internal async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with optional translation setup for a guild. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup for a guild. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Registers a command class with optional translation setup globally. /// /// The command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule => this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); /// /// Registers a command class with optional translation setup globally. /// /// The of the command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Type type, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add => this._applicationCommandsModuleReady.Register(value); remove => this._applicationCommandsModuleReady.Unregister(value); } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add => this._applicationCommandsModuleStartupFinished.Register(value); remove => this._applicationCommandsModuleStartupFinished.Unregister(value); } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add => this._guildApplicationCommandsRegistered.Register(value); remove => this._guildApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add => this._globalApplicationCommandsRegistered.Register(value); remove => this._globalApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Request to register commands on shard {shard}", this.Client.ShardId); if (this.StartupFinished) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} already setup, skipping", this.Client.ShardId); this.FinishedRegistration(); return; } GlobalDiscordCommands = new(); GuildDiscordCommands = new(); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {count}", s_expectedCount); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Shard {shard} has {guilds} guilds.", this.Client.ShardId, this.Client.Guilds?.Count); List failedGuilds = new(); List globalCommands = null; globalCommands = (await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false)).ToList() ?? null; var updateList = this._updateList; var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys.ToList() : updateList.Where(x => x.Key != null)?.Select(x => x.Key.Value).Distinct().ToList(); var wrongShards = guilds.Where(x => !this.Client.Guilds.ContainsKey(x)).ToList(); if (wrongShards.Any()) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Some guilds are not on the same shard as the client. Removing them from the update list."); foreach (var guild in wrongShards) { updateList.RemoveAll(x => x.Key == guild); guilds.Remove(guild); } } var commandsPending = updateList.Select(x => x.Key).Distinct().ToList(); s_expectedCount = commandsPending.Count; foreach (var guild in guilds) { List commands = null; var unauthorized = false; try { commands = (await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false)).ToList() ?? null; } catch (UnauthorizedException) { unauthorized = true; } finally { if (!unauthorized && commands != null && commands.Any()) GuildDiscordCommands.Add(guild, commands.ToList()); else if (unauthorized) failedGuilds.Add(guild); } } //Default should be to add the help and slash commands can be added without setting any configuration //so this should still add the default help if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp)) { updateList.Add(new KeyValuePair (null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule)))); commandsPending = updateList.Select(x => x.Key).Distinct().ToList(); } if (globalCommands != null && globalCommands.Any()) GlobalDiscordCommands.AddRange(globalCommands); foreach (var key in commandsPending) { this.Client.Logger.Log(ApplicationCommandsLogLevel, key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); if (key.HasValue) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Found guild {guild} in shard {shard}!", key.Value, this.Client.ShardId); this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering"); } await this.RegisterCommands(updateList.Where(x => x.Key == key).Select(x => x.Value).ToList(), key); } this._missingScopeGuildIds = new(failedGuilds); await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider) { GuildsWithoutScope = failedGuilds }); this.Client.GuildDownloadCompleted -= async (c, e) => await this.UpdateAsync(); } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private async Task RegisterCommands(List types, ulong? guildId) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Registering commands on shard {shard}", this.Client.ShardId); //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); var groupTranslation = new List(); var translation = new List(); //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); //Add module to classes list if it's a group var extremeNestedGroup = false; if (module.GetCustomAttribute() != null) { classes.Add(module); } else if (module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).Any(x => x.IsDefined(typeof(SlashCommandGroupAttribute)))) { //Otherwise add the extreme nested groups classes = module.GetMembers(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) .Where(x => x.IsDefined(typeof(SlashCommandGroupAttribute))) .Select(x => module.GetNestedType(x.Name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static).GetTypeInfo()).ToList(); extremeNestedGroup = true; } else { //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); } if (module.GetCustomAttribute() != null || extremeNestedGroup) { List groupTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } var slashGroupsTuple = await NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations); if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any()) { updateList.AddRange(slashGroupsTuple.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cgwsgs = new List(); var cgs2 = new List(); foreach (var cmd in slashGroupsTuple.applicationCommands) { if (cmd.Type == ApplicationCommandType.ChatInput) { if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommandGroup) { var cgs = new List(); foreach (var scg in cmd.Options) { var cs = new List(); foreach (var sc in scg.Options) { if (sc.Options == null || !sc.Options.Any()) cs.Add(new Command(sc.Name, sc.Description, null, null)); else cs.Add(new Command(sc.Name, sc.Description, sc.Options.ToList(), null)); } cgs.Add(new CommandGroup(scg.Name, scg.Description, cs, null)); } cgwsgs.Add(new CommandGroupWithSubGroups(cmd.Name, cmd.Description, cgs, ApplicationCommandType.ChatInput)); } else if (cmd.Options.First().Type == ApplicationCommandOptionType.SubCommand) { var cs2 = new List(); foreach (var sc2 in cmd.Options) { if (sc2.Options == null || !sc2.Options.Any()) cs2.Add(new Command(sc2.Name, sc2.Description, null, null)); else cs2.Add(new Command(sc2.Name, sc2.Description, sc2.Options.ToList(), null)); } cgs2.Add(new CommandGroup(cmd.Name, cmd.Description, cs2, ApplicationCommandType.ChatInput)); } } } if (cgwsgs.Any()) foreach (var cgwsg in cgwsgs) groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cgwsg))); if (cgs2.Any()) foreach (var cg2 in cgs2) groupTranslation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(cg2))); } } if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any()) commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources); if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any()) s_singletonModules.AddRange(slashGroupsTuple.singletonModules); if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any()) groupCommands.AddRange(slashGroupsTuple.groupCommands); if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any()) subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands); } //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { List commandTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } //Slash commands var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var slashCommands = await CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations); if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any()) { updateList.AddRange(slashCommands.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cs = new List(); foreach (var cmd in slashCommands.applicationCommands) if (cmd.Type == ApplicationCommandType.ChatInput && (cmd.Options == null || !cmd.Options.Any(x => x.Type == ApplicationCommandOptionType.SubCommand || x.Type == ApplicationCommandOptionType.SubCommandGroup))) { if (cmd.Options == null || !cmd.Options.Any()) cs.Add(new Command(cmd.Name, cmd.Description, null, ApplicationCommandType.ChatInput)); else cs.Add(new Command(cmd.Name, cmd.Description, cmd.Options.ToList(), ApplicationCommandType.ChatInput)); } if (cs.Any()) foreach (var c in cs) translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))); } } if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any()) commandTypeSources.AddRange(slashCommands.commandTypeSources); if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any()) commandMethods.AddRange(slashCommands.commandMethods); //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var contextCommands = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations); if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any()) { updateList.AddRange(contextCommands.applicationCommands); if (Configuration.GenerateTranslationFilesOnly) { var cs = new List(); foreach (var cmd in contextCommands.applicationCommands) if (cmd.Type == ApplicationCommandType.Message || cmd.Type == ApplicationCommandType.User) cs.Add(new Command(cmd.Name, null, null, cmd.Type)); if (cs.Any()) foreach (var c in cs) translation.Add(JsonConvert.DeserializeObject(JsonConvert.SerializeObject(c))); } } if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any()) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any()) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response); else this.Client.Logger.LogCritical(ex, @"There was an error parsing the application commands"); } s_errored = true; } } if (!s_errored) { updateList = updateList.DistinctBy(x => x.Name).ToList(); if (Configuration.GenerateTranslationFilesOnly) { s_registrationCount++; this.CheckRegistrationStartup(ManOr, translation, groupTranslation); } else { try { List commands = new(); try { if (guildId == null) { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(this.Client, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } else { foreach (var cmd in GlobalDiscordCommands) { try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete global command {cmdId}. Please clean up manually", cmd.Id); } } } } else { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGuildCommandsAsync(this.Client, guildId.Value, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildId.Value, actualCommands); if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild)) guild.InternalRegisteredApplicationCommands.AddRange(actualCommands); } else { foreach (var cmd in GuildDiscordCommands.First(x => x.Key == guildId.Value).Value) { try { await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Could not delete guild command {cmdId} in guild {guildId}. Please clean up manually", cmd.Id, guildId.Value); } } } } } catch (UnauthorizedException ex) { this.Client.Logger.LogError("Could not register application commands for guild {guildId}.\nError: {exc}", guildId, ex.JsonMessage); return; } //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in commands) { if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com)) com.CommandId = command.Id; else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) groupCom.CommandId = command.Id; else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom)) subCom.CommandId = command.Id; else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) cmCom.CommandId = command.Id; } //Adds to the global lists finally s_commandMethods.AddRange(commandMethods.DistinctBy(x => x.Name)); s_groupCommands.AddRange(groupCommands.DistinctBy(x => x.Name)); s_subGroupCommands.AddRange(subGroupCommands.DistinctBy(x => x.Name)); s_contextMenuCommands.AddRange(contextMenuCommands.DistinctBy(x => x.Name)); s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } this.Client.Logger.Log(ApplicationCommandsLogLevel, "Expected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, s_registrationCount); if (guildId.HasValue) { await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null }); } else { await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }); } s_registrationCount++; this.CheckRegistrationStartup(ManOr); } catch (Exception ex) { if (ex is BadRequestException brex) { this.Client.Logger.LogCritical(brex, @"There was an error registering application commands: {res}", brex.WebResponse.Response); } else { if (ex.InnerException is not null && ex.InnerException is BadRequestException brex1) this.Client.Logger.LogCritical(brex1, @"There was an error registering application commands: {res}", brex1.WebResponse.Response); else this.Client.Logger.LogCritical(ex, @"There was an general error registering application commands"); } s_errored = true; } } } } private async void CheckRegistrationStartup(bool man = false, List translation = null, List groupTranslation = null) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Checking counts...\n\nExpected Count: {exp}\nCurrent Count: {cur}", s_expectedCount, s_registrationCount); if ((s_registrationCount == s_expectedCount) || man) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredGlobalCommands = GlobalCommandsInternal, RegisteredGuildCommands = GuildCommandsInternal, GuildsWithoutScope = this._missingScopeGuildIds }); if (Configuration.GenerateTranslationFilesOnly) { try { if (translation != null && translation.Any()) { var file_name = $"translation_generator_export-shard{this.Client.ShardId}-SINGLE-{s_registrationCount}_of_{s_expectedCount}.json"; var fs = File.Create(file_name); var ms = new MemoryStream(); var writer = new StreamWriter(ms); await writer.WriteAsync(JsonConvert.SerializeObject(translation.DistinctBy(x => x.Name), Formatting.Indented)); await writer.FlushAsync(); ms.Position = 0; await ms.CopyToAsync(fs); await fs.FlushAsync(); fs.Close(); await fs.DisposeAsync(); ms.Close(); await ms.DisposeAsync(); this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name); } if (groupTranslation != null && groupTranslation.Any()) { var file_name = $"translation_generator_export-shard{this.Client.ShardId}-GROUP-{s_registrationCount}_of_{s_expectedCount}.json"; var fs = File.Create(file_name); var ms = new MemoryStream(); var writer = new StreamWriter(ms); await writer.WriteAsync(JsonConvert.SerializeObject(groupTranslation.DistinctBy(x => x.Name), Formatting.Indented)); await writer.FlushAsync(); ms.Position = 0; await ms.CopyToAsync(fs); await fs.FlushAsync(); fs.Close(); await fs.DisposeAsync(); ms.Close(); await ms.DisposeAsync(); this.Client.Logger.LogInformation("Exported base translation to {exppath}", file_name); } } catch (Exception ex) { this.Client.Logger.LogError(@"{msg}", ex.Message); this.Client.Logger.LogError(@"{stack}", ex.StackTrace); } this.FinishedRegistration(); await this.Client.DisconnectAsync(); } else { this.FinishedRegistration(); } } } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got slash interaction on shard {shard}", this.Client.ShardId); if (HandledInteractions.Contains(e.Interaction.Id)) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received"); return Task.FromResult(true); } else HandledInteractions.Add(e.Interaction.Id); _ = Task.Run(async () => { if (e.Interaction.Type == InteractionType.ApplicationCommand) { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = Configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(), Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup.")); throw new InvalidOperationException("Application commands failed to register properly on startup."); } var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A application command was executed, but no command was registered for it.")); throw new InvalidOperationException("A application command was executed, but no command was registered for it."); } if (methods.Any()) { var method = methods.First().Method; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); await this.RunCommandAsync(context, method, args); } else if (groups.Any()) { var command = e.Interaction.Data.Options[0]; var method = groups.First().Methods.First(x => x.Key == command.Name).Value; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options); await this.RunCommandAsync(context, method, args); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options[0]; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); - var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; + var method = group.Methods.First(x => x.Key == command.Options[0].Name).Value; this.Client.Logger.LogDebug("Executing {cmd}", method.Name); - var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options.First().Options); + var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options[0].Options[0].Options); await this.RunCommandAsync(context, method, args); } await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Application commands failed to register properly on startup.")); throw new InvalidOperationException("Application commands failed to register properly on startup."); } var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("An autocomplete interaction was created, but no command was registered for it")); throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it"); } try { if (methods.Any()) { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = client, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (groups.Any()) { var command = e.Interaction.Data.Options[0]; var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options[0]; - var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value; + var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options[0].Name).Value; - var focusedOption = command.Options.First().Options.First(o => o.Focused); + var focusedOption = command.Options[0].Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Client = client, Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, - Options = command.Options.First().Options.ToList(), + Options = command.Options[0].Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Got context menu interaction on shard {shard}", this.Client.ShardId); if (HandledInteractions.Contains(e.Interaction.Id)) { this.Client.Logger.Log(ApplicationCommandsLogLevel, "Ignoring, already received"); return Task.FromResult(true); } else HandledInteractions.Add(e.Interaction.Id); _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = Configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale, AppPermissions = e.Interaction.AppPermissions }; try { if (s_errored) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("Context menus failed to register properly on startup.")); throw new InvalidOperationException("Context menus failed to register properly on startup."); } //Gets the method for the command var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) { await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().WithContent("A context menu command was executed, but no command was registered for it.")); throw new InvalidOperationException("A context menu command was executed, but no command was registered for it."); } await this.RunCommandAsync(context, method.Method, new[] { context }); await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { object classInstance; this.Client.Logger.Log(ApplicationCommandsLogLevel, "Executing {cmd}", method.Name); //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; switch (moduleLifespan) { case ApplicationCommandModuleLifespan.Scoped: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); break; default: throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); } ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; // Slash commands if (context is InteractionContext slashContext) { await this.RunPreexecutionChecksAsync(method, slashContext); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection /// /// The type. /// The services. internal static object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1); foreach (var parameter in parameters) { //Accounts for optional arguments without values given if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(ulong) || parameter.ParameterType == typeof(ulong?)) args.Add((ulong?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) if (option.Value == null) args.Add(null); else args.Add(Convert.ToInt64(option.Value)); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the pre-execution checks. /// /// The method info. /// The base context. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext cMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(cMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null) { var choices = new List(); foreach (var choiceProviderAttribute in customAttributes) { var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); if (method == null) throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); else { var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); // Abstract class offers more properties that can be set if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) { choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) ?.SetValue(instance, guildId); choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) ?.SetValue(instance, Configuration.ServiceProvider); } //Gets the choices from the method var result = await (Task>)method.Invoke(instance, null); if (result.Any()) { choices.AddRange(result); } } } return choices; } /// /// Gets the choice attributes from enum parameter. /// /// The enum parameter. private static List GetChoiceAttributesFromEnumParameter(Type enumParam) { var choices = new List(); foreach (Enum enumValue in Enum.GetValues(enumParam)) { choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); } return choices; } /// /// Gets the parameter type. /// /// The type. private static ApplicationCommandOptionType GetParameterType(Type type) { var parameterType = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parameterType; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) => !choiceAttributes.Any() ? null : choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); /// /// Parses the parameters. /// /// The parameters. /// The optional guild id. internal static async Task> ParseParametersAsync(IEnumerable parameters, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionAttribute = parameter.GetCustomAttribute(); if (optionAttribute == null) throw new ArgumentException("Arguments must have the Option attribute!"); var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; var minimumLength = parameter.GetCustomAttribute()?.Value ?? null; var maximumLength = parameter.GetCustomAttribute()?.Value ?? null; var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; var autocompleteAttribute = parameter.GetCustomAttribute(); if (optionAttribute.Autocomplete && autocompleteAttribute == null) throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!"); if (!optionAttribute.Autocomplete && autocompleteAttribute != null) throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!"); //Sets the type var type = parameter.ParameterType; var parameterType = GetParameterType(type); if (parameterType == ApplicationCommandOptionType.String) { minimumValue = null; maximumValue = null; } else if (parameterType == ApplicationCommandOptionType.Integer || parameterType == ApplicationCommandOptionType.Number) { minimumLength = null; maximumLength = null; } if (parameterType != ApplicationCommandOptionType.Channel) channelTypes = null; //Handles choices //From attributes var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); //From enums if (parameter.ParameterType.IsEnum) { choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); } //From choice provider var choiceProviders = parameter.GetCustomAttributes(); if (choiceProviders.Any()) { choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId); } options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue, minimumLength: minimumLength, maximumLength: maximumLength)); } return options; } /* /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. /// public async Task RefreshCommandsAsync() { s_commandMethods.Clear(); s_groupCommands.Clear(); s_subGroupCommands.Clear(); s_registeredCommands.Clear(); s_contextMenuCommands.Clear(); GlobalDiscordCommands.Clear(); GuildDiscordCommands.Clear(); GuildCommandsInternal.Clear(); GlobalCommandsInternal.Clear(); GlobalDiscordCommands = null; GuildDiscordCommands = null; s_errored = false; /*if (Configuration != null && Configuration.EnableDefaultHelp) { this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule)); }*/ /* await this.UpdateAsync(); }*/ /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { add => this._slashError.Register(value); remove => this._slashError.Unregister(value); } private AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add => this._slashExecuted.Register(value); remove => this._slashExecuted.Unregister(value); } private AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add => this._contextMenuErrored.Register(value); remove => this._contextMenuErrored.Unregister(value); } private AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add => this._contextMenuExecuted.Register(value); remove => this._contextMenuExecuted.Unregister(value); } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The translation setup. /// public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action translations = null) { this.Type = type; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = new(); } /// /// The context menu command. /// internal class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } #region Default Help /// /// Represents the default help module. /// internal class DefaultHelpModule : ApplicationCommandsModule { public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } foreach (var sc in slashCommands.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())); } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase)); if (command is null || command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand && c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); return options.AsEnumerable(); } var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (foundCommand is null || foundCommand.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand && x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } [SlashCommand("help", "Displays command help")] internal async Task DefaultHelpAsync(InteractionContext ctx, [Autocomplete(typeof(DefaultHelpAutoCompleteProvider))] [Option("option_one", "top level command to provide help for", true)] string commandName, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))] [Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))] [Option("option_three", "command to provide help for", true)] string commandTwoName = null) { List applicationCommands = null; var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); if (ctx.Guild != null) { var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } else { await Task.WhenAll(globalCommandsTask); applicationCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); } if (applicationCommands.Count < 1) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"There are no slash commands")); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands").AsEphemeral(true)); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var subCommandParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options .FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {cmdParent.Name} {cmd.Name}")}: {cmd.Description ?? "No description provided."}" }; if (cmd.Options is not null) { var commandOptions = cmd.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .AddEmbed(discordEmbed)); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{subCommandParent.Mention.Replace(subCommandParent.Name, $"{subCommandParent.Name} {subCommand.Name}")}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}")); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true)); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{command.Mention}: {command.Description ?? "No description provided."}" }.AddField(new DiscordEmbedField("Command is NSFW", command.IsNsfw.ToString())); if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append("`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } if (ApplicationCommandsExtension.Configuration.AutoDefer) await ctx.EditResponseAsync(new DiscordWebhookBuilder().AddEmbed(discordEmbed)); else await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } } } #endregion diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs index ace9d2686..d6b890a47 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs @@ -1,2103 +1,2110 @@ // 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.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord guild. /// public partial class DiscordGuild : SnowflakeObject, IEquatable { /// /// Gets the guild's name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the guild icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the guild icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null; /// /// Gets the guild splash's hash. /// [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] public string SplashHash { get; internal set; } /// /// Gets the guild splash's url. /// [JsonIgnore] public string SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null; /// /// Gets the guild discovery splash's hash. /// [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] public string DiscoverySplashHash { get; internal set; } /// /// Gets the guild discovery splash's url. /// [JsonIgnore] public string DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null; /// /// Gets the preferred locale of this guild. /// This is used for server discovery, interactions and notices from Discord. Defaults to en-US. /// [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] public string PreferredLocale { get; internal set; } /// /// Gets the ID of the guild's owner. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the guild's owner. /// [JsonIgnore] public DiscordMember Owner => this.Members.TryGetValue(this.OwnerId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).ConfigureAwait(false).GetAwaiter().GetResult(); /// /// Gets permissions for the user in the guild (does not include channel overrides) /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? Permissions { get; set; } /// /// Gets the guild's voice region ID. /// [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] internal string VoiceRegionId { get; set; } /// /// Gets the guild's voice region. /// [JsonIgnore] public DiscordVoiceRegion VoiceRegion => this.Discord.VoiceRegions[this.VoiceRegionId]; /// /// Gets the guild's AFK voice channel ID. /// [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong AfkChannelId { get; set; } /// /// Gets the guild's AFK voice channel. /// [JsonIgnore] public DiscordChannel AfkChannel => this.GetChannel(this.AfkChannelId); /// /// List of . /// Null if DisCatSharp.ApplicationCommands is not used or no guild commands are registered. /// [JsonIgnore] public ReadOnlyCollection RegisteredApplicationCommands => new(this.InternalRegisteredApplicationCommands); [JsonIgnore] internal List InternalRegisteredApplicationCommands { get; set; } = new(); /// /// Gets the guild's AFK timeout. /// [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] public int AfkTimeout { get; internal set; } /// /// Gets the guild's verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public VerificationLevel VerificationLevel { get; internal set; } /// /// Gets the guild's default notification settings. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public DefaultMessageNotifications DefaultMessageNotifications { get; internal set; } /// /// Gets the guild's explicit content filter settings. /// [JsonProperty("explicit_content_filter")] public ExplicitContentFilter ExplicitContentFilter { get; internal set; } /// /// Gets the guild's nsfw level. /// [JsonProperty("nsfw_level")] public NsfwLevel NsfwLevel { get; internal set; } /// /// Gets the system channel id. /// [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? SystemChannelId { get; set; } /// /// Gets the channel where system messages (such as boost and welcome messages) are sent. /// [JsonIgnore] public DiscordChannel SystemChannel => this.SystemChannelId.HasValue ? this.GetChannel(this.SystemChannelId.Value) : null; /// /// Gets the settings for this guild's system channel. /// [JsonProperty("system_channel_flags")] public SystemChannelFlags SystemChannelFlags { get; internal set; } /// /// Gets whether this guild's widget is enabled. /// [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? WidgetEnabled { get; internal set; } /// /// Gets the widget channel id. /// [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? WidgetChannelId { get; set; } /// /// Gets the widget channel for this guild. /// [JsonIgnore] public DiscordChannel WidgetChannel => this.WidgetChannelId.HasValue ? this.GetChannel(this.WidgetChannelId.Value) : null; /// /// Gets the rules channel id. /// [JsonProperty("rules_channel_id")] internal ulong? RulesChannelId { get; set; } /// /// Gets the rules channel for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel RulesChannel => this.RulesChannelId.HasValue ? this.GetChannel(this.RulesChannelId.Value) : null; /// /// Gets the public updates channel id. /// [JsonProperty("public_updates_channel_id")] internal ulong? PublicUpdatesChannelId { get; set; } /// /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel PublicUpdatesChannel => this.PublicUpdatesChannelId.HasValue ? this.GetChannel(this.PublicUpdatesChannelId.Value) : null; /// /// Gets the application id of this guild if it is bot created. /// [JsonProperty("application_id")] public ulong? ApplicationId { get; internal set; } /// /// Gets a collection of this guild's roles. /// [JsonIgnore] public IReadOnlyDictionary Roles => new ReadOnlyConcurrentDictionary(this.RolesInternal); [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary RolesInternal; /// /// Gets a collection of this guild's stickers. /// [JsonIgnore] public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this.StickersInternal); [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StickersInternal; /// /// Gets a collection of this guild's emojis. /// [JsonIgnore] public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.EmojisInternal); [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary EmojisInternal; /// /// Gets a collection of this guild's features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList RawFeatures { get; internal set; } /// /// Gets the guild's features. /// [JsonIgnore] public GuildFeatures Features => new(this); /// /// Gets the required multi-factor authentication level for this guild. /// [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] public MfaLevel MfaLevel { get; internal set; } /// /// Gets this guild's join date. /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Gets whether this guild is considered to be a large guild. /// [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] public bool IsLarge { get; internal set; } /// /// Gets whether this guild is unavailable. /// [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] public bool IsUnavailable { get; internal set; } /// /// Gets the total number of members in this guild. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int MemberCount { get; internal set; } /// /// Gets the maximum amount of members allowed for this guild. /// [JsonProperty("max_members")] public int? MaxMembers { get; internal set; } /// /// Gets the maximum amount of presences allowed for this guild. /// [JsonProperty("max_presences")] public int? MaxPresences { get; internal set; } /// - /// Gets the approximate number of members in this guild, when using and having withCounts set to true. + /// Gets the approximate number of members in this guild, when using and having withCounts set to true. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// - /// Gets the approximate number of presences in this guild, when using and having withCounts set to true. + /// Gets the approximate number of presences in this guild, when using and having withCounts set to true. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } /// /// Gets the maximum amount of users allowed per video channel. /// [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxVideoChannelUsers { get; internal set; } /// /// Gets the maximum amount of users allowed per video stage channel. /// [JsonProperty("max_stage_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxStageVideoChannelUsers { get; internal set; } /// /// Gets a dictionary of all the voice states for this guilds. The key for this dictionary is the ID of the user /// the voice state corresponds to. /// [JsonIgnore] public IReadOnlyDictionary VoiceStates => new ReadOnlyConcurrentDictionary(this.VoiceStatesInternal); [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary VoiceStatesInternal; /// /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. /// [JsonIgnore] public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this.MembersInternal); [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary MembersInternal; /// /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Channels => new ReadOnlyConcurrentDictionary(this.ChannelsInternal); [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ChannelsInternal; internal ConcurrentDictionary Invites; /// /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Threads { get; internal set; } [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadsInternal = new(); /// /// Gets a dictionary of all active stage instances. The dictionary's key is the stage ID. /// [JsonIgnore] public IReadOnlyDictionary StageInstances { get; internal set; } [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StageInstancesInternal = new(); /// /// Gets a dictionary of all scheduled events. /// [JsonIgnore] public IReadOnlyDictionary ScheduledEvents { get; internal set; } [JsonProperty("guild_scheduled_events", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ScheduledEventsInternal = new(); /// /// Gets the guild member for current user. /// [JsonIgnore] public DiscordMember CurrentMember => this._currentMemberLazy.Value; [JsonIgnore] private readonly Lazy _currentMemberLazy; /// /// Gets the @everyone role for this guild. /// [JsonIgnore] public DiscordRole EveryoneRole => this.GetRole(this.Id); [JsonIgnore] internal bool IsOwnerInternal; /// /// Gets whether the current user is the guild's owner. /// [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] public bool IsOwner { get => this.IsOwnerInternal || this.OwnerId == this.Discord.CurrentUser.Id; internal set => this.IsOwnerInternal = value; } /// /// Gets the vanity URL code for this guild, when applicable. /// [JsonProperty("vanity_url_code")] public string VanityUrlCode { get; internal set; } /// /// Gets the guild description, when applicable. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Gets this guild's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this guild's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Whether this guild has the community feature enabled. /// [JsonIgnore] public bool IsCommunity => this.Features.HasFeature(GuildFeaturesEnum.HasCommunityEnabled); /// /// Whether this guild has enabled the welcome screen. /// [JsonIgnore] public bool HasWelcomeScreen => this.Features.HasFeature(GuildFeaturesEnum.HasWelcomeScreenEnabled); /// /// Whether this guild has enabled membership screening. /// [JsonIgnore] public bool HasMemberVerificationGate => this.Features.HasFeature(GuildFeaturesEnum.HasMembershipScreeningEnabled); /// /// Gets this guild's premium tier (Nitro boosting). /// [JsonProperty("premium_tier")] public PremiumTier PremiumTier { get; internal set; } /// /// Gets the amount of members that boosted this guild. /// [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] public int? PremiumSubscriptionCount { get; internal set; } /// /// Whether the premium progress bar is enabled. /// [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool PremiumProgressBarEnabled { get; internal set; } /// /// Gets whether this guild is designated as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool IsNsfw { get; internal set; } /// /// Gets this guild's hub type, if applicable. /// [JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)] public HubType HubType { get; internal set; } /// /// Gets a dictionary of all by position ordered channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary OrderedChannels => new ReadOnlyDictionary(this.InternalSortChannels()); /// /// Sorts the channels. /// private Dictionary InternalSortChannels() { Dictionary keyValuePairs = new(); var ochannels = this.GetOrderedChannels(); foreach (var ochan in ochannels) { if (ochan.Key != 0) keyValuePairs.Add(ochan.Key, this.GetChannel(ochan.Key)); foreach (var chan in ochan.Value) keyValuePairs.Add(chan.Id, chan); } return keyValuePairs; } /// /// Gets an ordered list out of the channel cache. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public Dictionary> GetOrderedChannels() { IReadOnlyList rawChannels = this.ChannelsInternal.Values.ToList(); Dictionary> orderedChannels = new() { { 0, new List() } }; foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Gets an ordered list. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public async Task>> GetOrderedChannelsAsync() { var rawChannels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); Dictionary> orderedChannels = new() { { 0, new List() } }; foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News || c.Type == ChannelType.Forum)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Whether it is synced. /// [JsonIgnore] internal bool IsSynced { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordGuild() { this._currentMemberLazy = new Lazy(() => this.MembersInternal != null && this.MembersInternal.TryGetValue(this.Discord.CurrentUser.Id, out var member) ? member : null); this.Invites = new ConcurrentDictionary(); this.Threads = new ReadOnlyConcurrentDictionary(this.ThreadsInternal); this.StageInstances = new ReadOnlyConcurrentDictionary(this.StageInstancesInternal); this.ScheduledEvents = new ReadOnlyConcurrentDictionary(this.ScheduledEventsInternal); } #region Guild Methods /// /// Searches the current guild for members who's display name start with the specified name. /// /// The name to search for. /// The maximum amount of members to return. Max 1000. Defaults to 1. /// The members found, if any. public Task> SearchMembersAsync(string name, int? limit = 1) => this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); /// /// Adds a new member to this guild /// /// User to add /// User's access token (OAuth2) /// new nickname /// new roles /// whether this user has to be muted /// whether this user has to be deafened /// Thrown when the client does not have the permission. /// Thrown when the or is not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordUser user, string accessToken, string nickname = null, IEnumerable roles = null, bool muted = false, bool deaf = false) => this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, nickname, roles, muted, deaf); /// /// Deletes this guild. Requires the caller to be the owner of the guild. /// /// Thrown when the client is not the owner of the guild. /// Thrown when Discord is unable to process the request. public Task DeleteAsync() => this.Discord.ApiClient.DeleteGuildAsync(this.Id); /// /// Enables the mfa requirement for this guild. /// /// The audit log reason. /// Thrown when the current user is not the guilds owner. /// Thrown when the guild does not exist. /// Thrown when Discord is unable to process the request. public Task EnableMfaAsync(string reason = null) => this.IsOwner ? this.Discord.ApiClient.EnableGuildMfaAsync(this.Id, reason) : throw new Exception("The current user does not own the guild."); /// /// Disables the mfa requirement for this guild. /// /// The audit log reason. /// Thrown when the current user is not the guilds owner. /// Thrown when the guild does not exist. /// Thrown when Discord is unable to process the request. public Task DisableMfaAsync(string reason = null) => this.IsOwner ? this.Discord.ApiClient.DisableGuildMfaAsync(this.Id, reason) : throw new Exception("The current user does not own the guild."); /// /// Modifies this guild. /// /// Action to perform on this guild. /// The modified guild object. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new GuildEditModel(); action(mdl); var afkChannelId = mdl.PublicUpdatesChannel .MapOrNull(c => c.Type != ChannelType.Voice ? throw new ArgumentException("AFK channel needs to be a text channel.") : c.Id); static Optional ChannelToId(Optional ch, string name) => ch.MapOrNull(c => c.Type != ChannelType.Text && c.Type != ChannelType.News ? throw new ArgumentException($"{name} channel needs to be a text channel.") : c.Id); var rulesChannelId = ChannelToId(mdl.RulesChannel, "Rules"); var publicUpdatesChannelId = ChannelToId(mdl.PublicUpdatesChannel, "Public updates"); var systemChannelId = ChannelToId(mdl.SystemChannel, "System"); var iconb64 = ImageTool.Base64FromStream(mdl.Icon); var splashb64 = ImageTool.Base64FromStream(mdl.Splash); var bannerb64 = ImageTool.Base64FromStream(mdl.Banner); var discoverySplash64 = ImageTool.Base64FromStream(mdl.DiscoverySplash); return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, afkChannelId, mdl.AfkTimeout, iconb64, mdl.Owner.Map(e => e.Id), splashb64, systemChannelId, mdl.SystemChannelFlags, publicUpdatesChannelId, rulesChannelId, mdl.Description, bannerb64, discoverySplash64, mdl.PreferredLocale, mdl.PremiumProgressBarEnabled, mdl.AuditLogReason).ConfigureAwait(false); } /// /// Modifies the community settings async. /// This sets if not highest and . /// - /// If true, enable . + /// If true, enables . /// The rules channel. /// The public updates channel. /// The preferred locale. Defaults to en-US. /// The description. /// The default message notifications. Defaults to /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyCommunitySettingsAsync(bool enabled, DiscordChannel rulesChannel, DiscordChannel publicUpdatesChannel, string preferredLocale = "en-US", string description = null, DefaultMessageNotifications defaultMessageNotifications = DefaultMessageNotifications.MentionsOnly, string reason = null) { var verificationLevel = this.VerificationLevel; if (this.VerificationLevel != VerificationLevel.Highest) { verificationLevel = VerificationLevel.High; } var explicitContentFilter = ExplicitContentFilter.AllMembers; static Optional ChannelToId(DiscordChannel ch, string name) => ch == null ? null : ch.Type != ChannelType.Text && ch.Type != ChannelType.News ? throw new ArgumentException($"{name} channel needs to be a text channel.") : ch.Id; var rulesChannelId = ChannelToId(rulesChannel, "Rules"); var publicUpdatesChannelId = ChannelToId(publicUpdatesChannel, "Public updates"); List features = new(); var rfeatures = this.RawFeatures.ToList(); if (!this.RawFeatures.Contains("COMMUNITY") && enabled) { rfeatures.Add("COMMUNITY"); } else if (this.RawFeatures.Contains("COMMUNITY") && !enabled) { rfeatures.Remove("COMMUNITY"); } features = rfeatures; return await this.Discord.ApiClient.ModifyGuildCommunitySettingsAsync(this.Id, features, rulesChannelId, publicUpdatesChannelId, preferredLocale, description, defaultMessageNotifications, explicitContentFilter, verificationLevel, reason).ConfigureAwait(false); } /// /// Enables invites for the guild. /// /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task EnableInvitesAsync(string reason = null) { List features = new(); var rfeatures = this.RawFeatures.ToList(); if (this.Features.HasFeature(GuildFeaturesEnum.InvitesDisabled)) rfeatures.Remove("INVITES_DISABLED"); features = rfeatures; return await this.Discord.ApiClient.ModifyGuildFeaturesAsync(this.Id, features, reason); } /// /// Disables invites for the guild. /// /// /// /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task DisableInvitesAsync(string reason = null) { List features = new(); var rfeatures = this.RawFeatures.ToList(); if (!this.Features.HasFeature(GuildFeaturesEnum.InvitesDisabled)) rfeatures.Add("INVITES_DISABLED"); features = rfeatures; return await this.Discord.ApiClient.ModifyGuildFeaturesAsync(this.Id, features, reason); } /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime offset to time out the user. Up to 28 days. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, DateTimeOffset until, string reason = null) => until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The timespan to time out the user. Up to 28 days. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, TimeSpan until, string reason = null) => this.TimeoutAsync(memberId, DateTimeOffset.UtcNow + until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime to time out the user. Up to 28 days. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, DateTime until, string reason = null) => this.TimeoutAsync(memberId, until.ToUniversalTime() - DateTime.UtcNow, reason); /// /// Removes the timeout from a specified member in this guild. /// /// Member to remove the timeout from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveTimeoutAsync(ulong memberId, string reason = null) => this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, null, reason); /// /// Bans a specified member from this guild. /// /// Member to ban. /// How many days to remove messages from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task BanMemberAsync(DiscordMember member, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, deleteMessageDays, reason); /// /// Bans a specified user by ID. This doesn't require the user to be in this guild. /// /// ID of the user to ban. /// How many days to remove messages from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task BanMemberAsync(ulong userId, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, deleteMessageDays, reason); /// /// Unbans a user from this guild. /// /// User to unban. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(DiscordUser user, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); /// /// Unbans a user by ID. /// /// ID of the user to unban. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(ulong userId, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); /// /// Leaves this guild. /// /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveGuildAsync(this.Id); /// /// Gets the bans for this guild, allowing for pagination. /// /// Maximum number of bans to fetch. Max 1000. Defaults to 1000. /// The Id of the user before which to fetch the bans. Overrides if both are present. /// The Id of the user after which to fetch the bans. /// Collection of bans in this guild in ascending order by user id. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) => this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); /// /// Gets a ban for a specific user. /// /// The Id of the user to get the ban for. /// The requested ban object. /// Thrown when the specified user is not banned. public Task GetBanAsync(ulong userId) => this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); /// /// Tries to get a ban for a specific user. /// /// The Id of the user to get the ban for. /// The requested ban object or null if not found. public async Task TryGetBanAsync(ulong userId) { try { return await this.GetBanAsync(userId).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a ban for a specific user. /// /// The user to get the ban for. /// The requested ban object. /// Thrown when the specified user is not banned. public Task GetBanAsync(DiscordUser user) => this.GetBanAsync(user.Id); /// /// Tries to get a ban for a specific user. /// /// The user to get the ban for. /// The requested ban object or null if not found. public async Task TryGetBanAsync(DiscordUser user) { try { return await this.GetBanAsync(user).ConfigureAwait(false); } catch (NotFoundException) { return null; } } #region Scheduled Events /// /// Creates a scheduled event. /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The channel. /// The metadata. /// The description. /// The type. /// The cover image. /// The reason. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, Optional coverImage = default, string reason = null) { var coverb64 = ImageTool.Base64FromStream(coverImage); return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type == ScheduledEventEntityType.External ? null : channel?.Id, type == ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type == ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, coverb64, reason); } /// /// Creates a scheduled event with type . /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The location of the external event. /// The description. /// The cover image. /// The reason. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, Optional coverImage = default, string reason = null) { var coverb64 = ImageTool.Base64FromStream(coverImage); return await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new DiscordScheduledEventEntityMetadata(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, coverb64, reason); } /// /// Gets a specific scheduled events. /// /// The Id of the event to get. /// Whether to include user count. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) => this.ScheduledEventsInternal.TryGetValue(scheduledEventId, out var ev) ? ev : await this.Discord.ApiClient.GetGuildScheduledEventAsync(this.Id, scheduledEventId, withUserCount); /// /// Tries to get a specific scheduled events. /// /// The Id of the event to get. /// Whether to include user count. /// A scheduled event or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task TryGetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) { try { return await this.GetScheduledEventAsync(scheduledEventId, withUserCount).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets a specific scheduled events. /// /// The event to get. /// Whether to include user count. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) => await this.GetScheduledEventAsync(scheduledEvent.Id, withUserCount); /// /// Tries to get a specific scheduled events. /// /// The event to get. /// Whether to include user count. /// A scheduled event or null if not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task TryGetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) { try { return await this.GetScheduledEventAsync(scheduledEvent, withUserCount).ConfigureAwait(false); } catch (NotFoundException) { return null; } } /// /// Gets the guilds scheduled events. /// /// Whether to include user count. /// A list of the guilds scheduled events. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task> GetScheduledEventsAsync(bool? withUserCount = null) => await this.Discord.ApiClient.ListGuildScheduledEventsAsync(this.Id, withUserCount); #endregion /// /// Creates a new text channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// Slow mode timeout for users. /// The default auto archive duration for new threads. + /// The flags of the new channel. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateTextChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, Optional flags = default, string reason = null) => this.CreateChannelAsync(name, ChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, defaultAutoArchiveDuration, flags, reason); /// /// Creates a new forum channel in this guild. /// The field template is not yet released, so it won't applied. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// The default reaction emoji for posts. /// Slow mode timeout for users. /// Slow mode timeout for user post creations. /// The default auto archive duration for new threads. + /// The default sort order for posts in the new channel. + /// The flags of the new channel. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission or the guild does not have the forum channel feature. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateForumChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional defaultReactionEmoji = default, Optional perUserRateLimit = default, Optional postCreateUserRateLimit = default, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, Optional defaultSortOrder = default, Optional flags = default, string reason = null) => this.Discord.ApiClient.CreateForumChannelAsync(this.Id, name, parent?.Id, topic, null, nsfw, defaultReactionEmoji, perUserRateLimit, postCreateUserRateLimit, defaultSortOrder, defaultAutoArchiveDuration, overwrites, flags, reason); /// /// Creates a new channel category in this guild. /// /// Name of the new category. /// Permission overwrites for this category. /// Reason for audit logs. /// The newly-created channel category. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateChannelCategoryAsync(string name, IEnumerable overwrites = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Category, null, Optional.None, null, null, overwrites, null, Optional.None, null, null, Optional.None, reason); /// /// Creates a new stage channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this stage channel. /// Reason for audit logs. /// The newly-created stage channel. /// Thrown when the client does not have the . /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the guilds has not enabled community. public Task CreateStageChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasFeature(GuildFeaturesEnum.HasCommunityEnabled) ? this.CreateChannelAsync(name, ChannelType.Stage, null, Optional.None, null, null, overwrites, null, Optional.None, null, null, Optional.None, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a stage channel."); /// /// Creates a new news channel in this guild. /// /// Name of the new news channel. /// Permission overwrites for this news channel. /// The default auto archive duration for new threads. + /// The flags of the new channel. /// Reason for audit logs. /// The newly-created news channel. /// Thrown when the client does not have the . /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the guilds has not enabled community. public Task CreateNewsChannelAsync(string name, IEnumerable overwrites = null, string reason = null, ThreadAutoArchiveDuration defaultAutoArchiveDuration = ThreadAutoArchiveDuration.OneDay, Optional flags = default) => this.Features.HasFeature(GuildFeaturesEnum.HasCommunityEnabled) ? this.CreateChannelAsync(name, ChannelType.News, null, Optional.None, null, null, overwrites, null, Optional.None, null, defaultAutoArchiveDuration, flags, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a news channel."); /// /// Creates a new voice channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Bitrate of the channel. /// Maximum number of users in the channel. /// Permission overwrites for this channel. + /// The flags of the new channel. /// Video quality mode of the channel. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateVoiceChannelAsync(string name, DiscordChannel parent = null, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, VideoQualityMode? qualityMode = null, Optional flags = default, string reason = null) => this.CreateChannelAsync(name, ChannelType.Voice, parent, Optional.None, bitrate, userLimit, overwrites, null, Optional.None, qualityMode, null, flags, reason); /// /// Creates a new channel in this guild. /// /// Name of the new channel. /// Type of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Bitrate of the channel. Applies to voice only. /// Maximum number of users in the channel. Applies to voice only. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. Applies to text only. /// Slow mode timeout for users. /// Video quality mode of the channel. Applies to voice only. /// The default auto archive duration for new threads. + /// The flags of the new channel. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateChannelAsync(string name, ChannelType type, DiscordChannel parent = null, Optional topic = default, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, VideoQualityMode? qualityMode = null, ThreadAutoArchiveDuration? defaultAutoArchiveDuration = null, Optional flags = default, string reason = null) => // technically you can create news/store channels but not always type != ChannelType.Text && type != ChannelType.Voice && type != ChannelType.Category && type != ChannelType.News && type != ChannelType.Store && type != ChannelType.Stage ? throw new ArgumentException("Channel type must be text, voice, stage, or category.", nameof(type)) : type == ChannelType.Category && parent != null ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) : this.Discord.ApiClient.CreateGuildChannelAsync(this.Id, name, type, parent?.Id, topic, bitrate, userLimit, overwrites, nsfw, perUserRateLimit, qualityMode, defaultAutoArchiveDuration, flags, reason); /// /// Gets active threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetActiveThreadsAsync() => this.Discord.ApiClient.GetActiveThreadsAsync(this.Id); /// /// Deletes all channels in this guild. /// Note that this is irreversible. Use carefully! /// /// public Task DeleteAllChannelsAsync() { var tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); return Task.WhenAll(tasks); } /// /// Estimates the number of users to be pruned. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// The roles to be included in the prune. /// Number of users that will be pruned. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var rawRoleIds = includedRoles .Where(x => this.RolesInternal.ContainsKey(x.Id)) .Select(x => x.Id); return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); } /// /// Prunes inactive users from this guild. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. /// The roles to be included in the prune. /// Reason for audit logs. /// Number of users pruned. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var rawRoleIds = includedRoles .Where(x => this.RolesInternal.ContainsKey(x.Id)) .Select(x => x.Id); return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); } /// /// Gets integrations attached to this guild. /// /// Collection of integrations attached to this guild. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetIntegrationsAsync() => this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); /// /// Attaches an integration from current user to this guild. /// /// Integration to attach. /// The integration after being attached to the guild. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AttachUserIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); /// /// Modifies an integration in this guild. /// /// Integration to modify. /// Number of days after which the integration expires. /// Length of grace period which allows for renewing the integration. /// Whether emotes should be synced from this integration. /// The modified integration. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) => this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); /// /// Removes an integration from this guild. /// /// Integration to remove. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration); /// /// Forces re-synchronization of an integration for this guild. /// /// Integration to synchronize. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SyncIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); /// /// Gets the voice regions for this guild. /// /// Voice regions available for this guild. /// Thrown when Discord is unable to process the request. public async Task> ListVoiceRegionsAsync() { var vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id).ConfigureAwait(false); foreach (var xvr in vrs) this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); return vrs; } /// /// Gets an invite from this guild from an invite code. /// /// The invite code /// An invite, or null if not in cache. public DiscordInvite GetInvite(string code) => this.Invites.TryGetValue(code, out var invite) ? invite : null; /// /// Gets all the invites created for all the channels in this guild. /// /// A collection of invites. /// Thrown when Discord is unable to process the request. public async Task> GetInvitesAsync() { var res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (!intents.HasIntent(DiscordIntents.GuildInvites)) { foreach (var r in res) this.Invites[r.Code] = r; } return res; } /// /// Gets the vanity invite for this guild. /// /// A partial vanity invite. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task GetVanityInviteAsync() => this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); /// /// Gets all the webhooks created for all the channels in this guild. /// /// A collection of webhooks this guild has. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); /// /// Gets this guild's widget image. /// /// The format of the widget. /// The URL of the widget image. public string GetWidgetImage(WidgetType bannerType = WidgetType.Shield) { var param = bannerType switch { WidgetType.Banner1 => "banner1", WidgetType.Banner2 => "banner2", WidgetType.Banner3 => "banner3", WidgetType.Banner4 => "banner4", _ => "shield", }; return $"{Endpoints.BASE_URI}{Endpoints.GUILDS}/{this.Id}{Endpoints.WIDGET_PNG}?style={param}"; } /// /// Gets a member of this guild by their user ID. /// /// ID of the member to get. + /// Whether to fetch the member from the api prior to cache. /// The requested member. /// Thrown when Discord is unable to process the request. public async Task GetMemberAsync(ulong userId, bool fetch = false) { if (!fetch && this.MembersInternal != null && this.MembersInternal.TryGetValue(userId, out var mbr)) return mbr; mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (intents.HasIntent(DiscordIntents.GuildMembers)) { if (this.MembersInternal != null) { this.MembersInternal[userId] = mbr; } } return mbr; } /// /// Retrieves a full list of members from Discord. This method will bypass cache. /// /// A collection of all members in this guild. /// Thrown when Discord is unable to process the request. public async Task> GetAllMembersAsync() { var recmbr = new HashSet(); var recd = 1000; var last = 0ul; while (recd > 0) { var tms = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : last).ConfigureAwait(false); recd = tms.Count; foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); recmbr.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = this.Id }); } var tm = tms.LastOrDefault(); last = tm?.User.Id ?? 0; } return new ReadOnlySet(recmbr); } /// /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the event. /// If no arguments aside from and are specified, this will request all guild members. /// /// Filters the returned members based on what the username starts with. Either this or must not be null. /// The must also be greater than 0 if this is specified. /// Total number of members to request. This must be greater than 0 if is specified. /// Whether to include the associated with the fetched members. /// Whether to limit the request to the specified user ids. Either this or must not be null. /// The unique string to identify the response. public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable userIds = null, string nonce = null) { if (this.Discord is not DiscordClient client) throw new InvalidOperationException("This operation is only valid for regular Discord clients."); if (query == null && userIds == null) throw new ArgumentException("The query and user IDs cannot both be null."); if (query != null && userIds != null) query = null; var grgm = new GatewayRequestGuildMembers(this) { Query = query, Limit = limit >= 0 ? limit : 0, Presences = presences, UserIds = userIds, Nonce = nonce }; var payload = new GatewayPayload { OpCode = GatewayOpCode.RequestGuildMembers, Data = grgm }; var payloadStr = JsonConvert.SerializeObject(payload, Formatting.None); await client.WsSendAsync(payloadStr).ConfigureAwait(false); } /// /// Gets all the channels this guild has. /// /// A collection of this guild's channels. /// Thrown when Discord is unable to process the request. public Task> GetChannelsAsync() => this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); /// /// Creates a new role in this guild. /// /// Name of the role. /// Permissions for the role. /// Color for the role. /// Whether the role is to be hoisted. /// Whether the role is to be mentionable. /// Reason for audit logs. /// The newly-created role. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, reason); /// /// Gets a role from this guild by its ID. /// /// ID of the role to get. /// Requested role. /// Thrown when Discord is unable to process the request. public DiscordRole GetRole(ulong id) => this.RolesInternal.TryGetValue(id, out var role) ? role : null; /// /// Gets a channel from this guild by its ID. /// /// ID of the channel to get. /// Requested channel. /// Thrown when Discord is unable to process the request. public DiscordChannel GetChannel(ulong id) => this.ChannelsInternal != null && this.ChannelsInternal.TryGetValue(id, out var channel) ? channel : null; /// /// Gets a thread from this guild by its ID. /// /// ID of the thread to get. /// Requested thread. /// Thrown when Discord is unable to process the request. public DiscordThreadChannel GetThread(ulong id) => this.ThreadsInternal != null && this.ThreadsInternal.TryGetValue(id, out var thread) ? thread : null; /// /// Gets all of this guild's custom emojis. /// /// All of this guild's custom emojis. /// Thrown when Discord is unable to process the request. public Task> GetEmojisAsync() => this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); /// /// Gets this guild's specified custom emoji. /// /// ID of the emoji to get. /// The requested custom emoji. /// Thrown when Discord is unable to process the request. public Task GetEmojiAsync(ulong id) => this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); /// /// Creates a new custom emoji for this guild. /// /// Name of the new emoji. /// Image to use as the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The newly-created emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); if (name.Length < 2 || name.Length > 50) throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); if (image == null) throw new ArgumentNullException(nameof(image)); var image64 = ImageTool.Base64FromStream(image); return this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); } /// /// Modifies a this guild's custom emoji. /// /// Emoji to modify. /// New name for the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The modified emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) { if (emoji == null) throw new ArgumentNullException(nameof(emoji)); if (emoji.Guild.Id != this.Id) throw new ArgumentException("This emoji does not belong to this guild."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); return name.Length < 2 || name.Length > 50 ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") : this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); } /// /// Deletes this guild's custom emoji. /// /// Emoji to delete. /// Reason for audit log. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) => emoji == null ? throw new ArgumentNullException(nameof(emoji)) : emoji.Guild.Id != this.Id ? throw new ArgumentException("This emoji does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); /// /// Gets all of this guild's custom stickers. /// /// All of this guild's custom stickers. /// Thrown when Discord is unable to process the request. public async Task> GetStickersAsync() { var stickers = await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); foreach (var xstr in stickers) { this.StickersInternal.AddOrUpdate(xstr.Id, xstr, (id, old) => { old.Name = xstr.Name; old.Description = xstr.Description; old.InternalTags = xstr.InternalTags; return old; }); } return stickers; } /// /// Gets a sticker /// /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task GetStickerAsync(ulong stickerId) => this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); /// /// Creates a sticker /// /// The name of the sticker. /// The optional description of the sticker. /// The emoji to associate the sticker with. /// The file format the sticker is written in. /// The sticker. /// Audit log reason /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateStickerAsync(string name, string description, DiscordEmoji emoji, Stream file, StickerFormat format, string reason = null) { var fileExt = format switch { StickerFormat.Png => "png", StickerFormat.Apng => "png", StickerFormat.Lottie => "json", _ => throw new InvalidOperationException("This format is not supported.") }; var contentType = format switch { StickerFormat.Png => "image/png", StickerFormat.Apng => "image/png", StickerFormat.Lottie => "application/json", _ => throw new InvalidOperationException("This format is not supported.") }; return emoji.Id is not 0 ? throw new InvalidOperationException("Only unicode emoji can be used for stickers.") : name.Length < 2 || name.Length > 30 ? throw new ArgumentOutOfRangeException(nameof(name), "Sticker name needs to be between 2 and 30 characters long.") : description.Length < 1 || description.Length > 100 ? throw new ArgumentOutOfRangeException(nameof(description), "Sticker description needs to be between 1 and 100 characters long.") : this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description, emoji.GetDiscordName().Replace(":", ""), new DiscordMessageFile("sticker", file, null, fileExt, contentType), reason); } /// /// Modifies a sticker /// /// The id of the sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public async Task ModifyStickerAsync(ulong sticker, Optional name, Optional description, Optional emoji, string reason = null) { if (!this.StickersInternal.TryGetValue(sticker, out var stickerobj) || stickerobj.Guild.Id != this.Id) throw new ArgumentException("This sticker does not belong to this guild."); if (name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30)) throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long."); if (description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100)) throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long."); if (emoji.HasValue && emoji.Value.Id > 0) throw new ArgumentException("Only unicode emojis can be used with stickers."); string uemoji = null; if (emoji.HasValue) uemoji = emoji.Value.GetDiscordName().Replace(":", ""); var usticker = await this.Discord.ApiClient.ModifyGuildStickerAsync(this.Id, sticker, name, description, uemoji, reason).ConfigureAwait(false); if (this.StickersInternal.TryGetValue(usticker.Id, out var old)) this.StickersInternal.TryUpdate(usticker.Id, usticker, old); return usticker; } /// /// Modifies a sticker /// /// The sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task ModifyStickerAsync(DiscordSticker sticker, Optional name, Optional description, Optional emoji, string reason = null) => this.ModifyStickerAsync(sticker.Id, name, description, emoji, reason); /// /// Deletes a sticker /// /// Id of sticker to delete /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task DeleteStickerAsync(ulong sticker, string reason = null) => !this.StickersInternal.TryGetValue(sticker, out var stickerobj) ? throw new ArgumentNullException(nameof(sticker)) : stickerobj.Guild.Id != this.Id ? throw new ArgumentException("This sticker does not belong to this guild.") : this.Discord.ApiClient.DeleteGuildStickerAsync(this.Id, sticker, reason); /// /// Deletes a sticker /// /// Sticker to delete /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task DeleteStickerAsync(DiscordSticker sticker, string reason = null) => this.DeleteStickerAsync(sticker.Id, reason); /// /// Gets the default channel for this guild. /// Default channel is the first channel current member can see. /// /// This member's default guild. /// Thrown when Discord is unable to process the request. public DiscordChannel GetDefaultChannel() => this.ChannelsInternal?.Values.Where(xc => xc.Type == ChannelType.Text) .OrderBy(xc => xc.Position) .FirstOrDefault(xc => (xc.PermissionsFor(this.CurrentMember) & DisCatSharp.Enums.Permissions.AccessChannels) == DisCatSharp.Enums.Permissions.AccessChannels); /// /// Gets the guild's widget /// /// The guild's widget public Task GetWidgetAsync() => this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); /// /// Gets the guild's widget settings /// /// The guild's widget settings public Task GetWidgetSettingsAsync() => this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); /// /// Modifies the guild's widget settings /// /// If the widget is enabled or not /// Widget channel /// Reason the widget settings were modified /// The newly modified widget settings public Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) => this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); /// /// Gets all of this guild's templates. /// /// All of the guild's templates. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetTemplatesAsync() => this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); /// /// Creates a guild template. /// /// Name of the template. /// Description of the template. /// The template created. /// Throws when a template already exists for the guild or a null parameter is provided for the name. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateTemplateAsync(string name, string description = null) => this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); /// /// Syncs the template to the current guild's state. /// /// The code of the template to sync. /// The template synced. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task SyncTemplateAsync(string code) => this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); /// /// Modifies the template's metadata. /// /// The template's code. /// Name of the template. /// Description of the template. /// The template modified. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task ModifyTemplateAsync(string code, string name = null, string description = null) => this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); /// /// Deletes the template. /// /// The code of the template to delete. /// The deleted template. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task DeleteTemplateAsync(string code) => this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); /// /// Gets this guild's membership screening form. /// /// This guild's membership screening form. /// Thrown when Discord is unable to process the request. public Task GetMembershipScreeningFormAsync() => this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); /// /// Modifies this guild's membership screening form. /// /// Action to perform /// The modified screening form. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. public async Task ModifyMembershipScreeningFormAsync(Action action) { var mdl = new MembershipScreeningEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, mdl.Enabled, mdl.Fields, mdl.Description); } /// /// Gets all the application commands in this guild. /// /// A list of application commands in this guild. public Task> GetApplicationCommandsAsync() => this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete /// /// The list of commands to overwrite with. /// The list of guild commands public Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); /// /// Creates or overwrites a application command in this guild. /// /// The command to create. /// The created command. public Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); /// /// Edits a application command in this guild. /// /// The id of the command to edit. /// Action to perform. /// The edit command. public async Task EditApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission, mdl.IsNsfw).ConfigureAwait(false); } /// /// Gets this guild's welcome screen. /// /// This guild's welcome screen object. /// Thrown when Discord is unable to process the request. public Task GetWelcomeScreenAsync() => this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); /// /// Modifies this guild's welcome screen. /// /// Action to perform. /// The modified welcome screen. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. public async Task ModifyWelcomeScreenAsync(Action action) { var mdl = new WelcomeScreenEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, mdl.Enabled, mdl.WelcomeChannels, mdl.Description).ConfigureAwait(false); } #endregion /// /// Returns a string representation of this guild. /// /// String representation of this guild. public override string ToString() => $"Guild {this.Id}; {this.Name}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordGuild); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordGuild e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are equal. public static bool operator ==(DiscordGuild e1, DiscordGuild e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are not equal. public static bool operator !=(DiscordGuild e1, DiscordGuild e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Guild/DiscordRole.cs b/DisCatSharp/Entities/Guild/DiscordRole.cs index 2561a7960..c9931c605 100644 --- a/DisCatSharp/Entities/Guild/DiscordRole.cs +++ b/DisCatSharp/Entities/Guild/DiscordRole.cs @@ -1,292 +1,299 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord role, to which users can be assigned. /// public class DiscordRole : SnowflakeObject, IEquatable { /// /// Gets the name of this role. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } + + /// + /// Gets the description of this role. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + /// /// Gets the color of this role. /// [JsonIgnore] public DiscordColor Color => new(this.ColorInternal); [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] internal int ColorInternal; /// /// Gets whether this role is hoisted. /// [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] public bool IsHoisted { get; internal set; } /// /// Gets the position of this role in the role hierarchy. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; internal set; } /// /// Gets the permissions set for this role. /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions Permissions { get; internal set; } /// /// Gets whether this role is managed by an integration. /// [JsonProperty("managed", NullValueHandling = NullValueHandling.Ignore)] public bool IsManaged { get; internal set; } /// /// Gets whether this role is mentionable. /// [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] public bool IsMentionable { get; internal set; } /// /// Gets the tags this role has. /// [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] public DiscordRoleTags Tags { get; internal set; } /// /// Gets the role icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the role icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ROLE_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=64" : null; /// /// Gets the role unicode_emoji. /// [JsonProperty("unicode_emoji", NullValueHandling = NullValueHandling.Ignore)] internal string UnicodeEmojiString; /// /// Gets the unicode emoji. /// public DiscordEmoji UnicodeEmoji => this.UnicodeEmojiString != null ? DiscordEmoji.FromName(this.Discord, $":{this.UnicodeEmojiString}:", false) : null; [JsonIgnore] internal ulong GuildId = 0; /// /// Gets the role flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public RoleFlags Flags { get; internal set; } /// /// Gets a mention string for this role. If the role is mentionable, this string will mention all the users that belong to this role. /// public string Mention => Formatter.Mention(this); #region Methods /// /// Modifies this role's position. /// /// New position /// Reason why we moved it /// /// Thrown when the client does not have the permission. /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyPositionAsync(int position, string reason = null) { var roles = this.Discord.Guilds[this.GuildId].Roles.Values.OrderByDescending(xr => xr.Position) .Select(x => new RestGuildRoleReorderPayload { RoleId = x.Id, Position = x.Id == this.Id ? position : x.Position <= position ? x.Position - 1 : x.Position }); return this.Discord.ApiClient.ModifyGuildRolePositionAsync(this.GuildId, roles, reason); } /// /// Updates this role. /// /// New role name. /// New role permissions. /// New role color. /// New role hoist. /// Whether this role is mentionable. /// Audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.ModifyGuildRoleAsync(this.GuildId, this.Id, name, permissions, color?.Value, hoist, mentionable, null, null, reason); /// /// Updates this role. /// /// The action. /// Thrown when the client does not have the permission. /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Action action) { var mdl = new RoleEditModel(); action(mdl); var canContinue = true; if ((mdl.Icon.HasValue && mdl.Icon.Value != null) || (mdl.UnicodeEmoji.HasValue && mdl.UnicodeEmoji.Value != null)) canContinue = this.Discord.Guilds[this.GuildId].Features.HasFeature(GuildFeaturesEnum.CanSetRoleIcons); var iconb64 = Optional.FromNullable(null); if (mdl.Icon.HasValue && mdl.Icon.Value != null) iconb64 = ImageTool.Base64FromStream(mdl.Icon); else if (mdl.Icon.HasValue) iconb64 = Optional.Some(null); var emoji = Optional.FromNullable(null); if (mdl.UnicodeEmoji.HasValue && mdl.UnicodeEmoji.Value != null) emoji = mdl.UnicodeEmoji .MapOrNull(e => e.Id == 0 ? e.Name : throw new ArgumentException("Emoji must be unicode")); else if (mdl.UnicodeEmoji.HasValue) emoji = Optional.Some(null); return canContinue ? this.Discord.ApiClient.ModifyGuildRoleAsync(this.GuildId, this.Id, mdl.Name, mdl.Permissions, mdl.Color?.Value, mdl.Hoist, mdl.Mentionable, iconb64, emoji, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify role icon. Guild needs boost tier two."); } /// /// Deletes this role. /// /// Reason as to why this role has been deleted. /// /// Thrown when the client does not have the permission. /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteRoleAsync(this.GuildId, this.Id, reason); #endregion /// /// Initializes a new instance of the class. /// internal DiscordRole() { } /// /// Checks whether this role has specific permissions. /// /// Permissions to check for. /// Whether the permissions are allowed or not. public PermissionLevel CheckPermission(Permissions permission) => (this.Permissions & permission) != 0 ? PermissionLevel.Allowed : PermissionLevel.Unset; /// /// Returns a string representation of this role. /// /// String representation of this role. public override string ToString() => $"Role {this.Id}; {this.Name}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordRole); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordRole e) => e switch { null => false, _ => ReferenceEquals(this, e) || this.Id == e.Id }; /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First role to compare. /// Second role to compare. /// Whether the two roles are equal. public static bool operator ==(DiscordRole e1, DiscordRole e2) => e1 is null == e2 is null && ((e1 is null && e2 is null) || e1.Id == e2.Id); /// /// Gets whether the two objects are not equal. /// /// First role to compare. /// Second role to compare. /// Whether the two roles are not equal. public static bool operator !=(DiscordRole e1, DiscordRole e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index 016ce164e..ebac8b74f 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,493 +1,495 @@ // 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.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord user. /// public class DiscordUser : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordUser() { } /// /// Initializes a new instance of the class. /// /// The transport user. internal DiscordUser(TransportUser transport) { this.Id = transport.Id; this.Username = transport.Username; this.Discriminator = transport.Discriminator; this.AvatarHash = transport.AvatarHash; this.AvatarDecorationHash = transport.AvatarDecorationHash; this.BannerHash = transport.BannerHash; this.BannerColorInternal = transport.BannerColor; this.ThemeColorsInternal = (transport.ThemeColors ?? Array.Empty()).ToList(); this.IsBot = transport.IsBot; this.MfaEnabled = transport.MfaEnabled; this.Verified = transport.Verified; this.Email = transport.Email; this.PremiumType = transport.PremiumType; this.Locale = transport.Locale; this.Flags = transport.Flags; this.OAuthFlags = transport.OAuthFlags; this.Bio = transport.Bio; this.Pronouns = transport.Pronouns; } /// /// Gets this user's username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public virtual string Username { get; internal set; } /// /// Gets this user's username with the discriminator. /// Example: Discord#0000 /// [JsonIgnore] public virtual string UsernameWithDiscriminator => $"{this.Username}#{this.Discriminator}"; /// /// Gets the user's 4-digit discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] public virtual string Discriminator { get; internal set; } /// /// Gets the discriminator integer. /// [JsonIgnore] internal int DiscriminatorInt => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); /// /// Gets the user's banner color, if set. Mutually exclusive with . /// + [JsonIgnore] public virtual DiscordColor? BannerColor => !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value); /// /// Gets the user's theme colors, if set. /// + [JsonIgnore] public virtual IReadOnlyList? ThemeColors => !(this.ThemeColorsInternal is not null && this.ThemeColorsInternal.Count != 0) ? null : this.ThemeColorsInternal.Select(x => new DiscordColor(x)).ToList(); /// /// Gets the user's banner color integer. /// [JsonProperty("accent_color")] internal int? BannerColorInternal; /// /// Gets the user's theme color integers. /// [JsonProperty("theme_colors", NullValueHandling = NullValueHandling.Ignore)] internal List? ThemeColorsInternal; /// /// Gets the user's banner url /// [JsonIgnore] public string BannerUrl => string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}?size=4096"; /// /// Gets the user's profile banner hash. Mutually exclusive with . /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string BannerHash { get; internal set; } /// /// Gets the users bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public virtual string Bio { get; internal set; } /// /// Gets the user's avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarHash { get; internal set; } /// /// Gets the user's avatar decoration hash. /// [JsonProperty("avatar_decoration", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarDecorationHash { get; internal set; } /// /// Returns a uri to this users profile. /// [JsonIgnore] public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}"); /// /// Returns a string representing the direct URL to this users profile. /// /// The URL of this users profile. [JsonIgnore] public string ProfileUrl => this.ProfileUri.AbsoluteUri; /// /// Gets the user's avatar url. /// [JsonIgnore] public string AvatarUrl => string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the user's avatar decoration url. /// [JsonIgnore] public string? AvatarDecorationUrl => string.IsNullOrWhiteSpace(this.AvatarDecorationHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS_DECORATIONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarDecorationHash}.{(this.AvatarDecorationHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the URL of default avatar for this user. /// [JsonIgnore] public string DefaultAvatarUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; /// /// Gets whether the user is a bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public virtual bool IsBot { get; internal set; } /// /// Gets whether the user has multi-factor authentication enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? MfaEnabled { get; internal set; } /// /// Gets whether the user is an official Discord system user. /// [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] public bool? IsSystem { get; internal set; } /// /// Gets whether the user is verified. /// This is only present in OAuth. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? Verified { get; internal set; } /// /// Gets the user's email address. /// This is only present in OAuth. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public virtual string Email { get; internal set; } /// /// Gets the user's premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public virtual PremiumType? PremiumType { get; internal set; } /// /// Gets the user's chosen language /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public virtual string Locale { get; internal set; } /// /// Gets the user's flags for OAuth. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? OAuthFlags { get; internal set; } /// /// Gets the user's flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? Flags { get; internal set; } /// /// Gets the user's pronouns. /// [JsonProperty("pronouns", NullValueHandling = NullValueHandling.Ignore)] public virtual string Pronouns { get; internal set; } /// /// Gets the user's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this, this is DiscordMember); /// /// Gets whether this user is the Client which created this object. /// [JsonIgnore] public bool IsCurrent => this.Id == this.Discord.CurrentUser.Id; #region Extension of DiscordUser /// /// Whether this member is a /// /// [JsonIgnore] public bool IsMod => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsPartner => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsVerifiedBot => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsBotDev => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsStaff => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff); #endregion /// /// Fetches the user from the API. /// /// The user with fresh data from the API. public async Task GetFromApiAsync() => await this.Discord.ApiClient.GetUserAsync(this.Id); /// /// Whether this user is in a /// /// /// /// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806); /// DiscordUser user = await Client.GetUserAsync(469957180968271873); /// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}"); /// /// results to J_M_Lutra is a member of Project Nyaw~. /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public async Task IsInGuild(DiscordGuild guild) { try { var member = await guild.GetMemberAsync(this.Id); return member is not null; } catch (NotFoundException) { return false; } } /// /// Whether this user is not in a /// /// /// public async Task IsNotInGuild(DiscordGuild guild) => !await this.IsInGuild(guild); /// /// Returns the DiscordMember in the specified /// /// The to get this user on. /// The . /// Thrown when the user is not part of the guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ConvertToMember(DiscordGuild guild) => await guild.GetMemberAsync(this.Id); /// /// Unbans this user from a guild. /// /// Guild to unban this user from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanAsync(DiscordGuild guild, string reason = null) => guild.UnbanMemberAsync(this, reason); /// /// Gets this user's presence. /// [JsonIgnore] public DiscordPresence Presence => this.Discord is DiscordClient dc && dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null; /// /// Gets the user's avatar URL, in requested format and size. /// /// Format of the avatar to get. /// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048. /// URL of the user's avatar. public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024) { if (fmt == ImageFormat.Unknown) throw new ArgumentException("You must specify valid image format.", nameof(fmt)); if (size < 16 || size > 2048) throw new ArgumentOutOfRangeException(nameof(size)); var log = Math.Log(size, 2); if (log < 4 || log > 11 || log % 1 != 0) throw new ArgumentOutOfRangeException(nameof(size)); var sfmt = ""; sfmt = fmt switch { ImageFormat.Gif => "gif", ImageFormat.Jpeg => "jpg", ImageFormat.Png => "png", ImageFormat.WebP => "webp", ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? "gif" : "png" : "png", _ => throw new ArgumentOutOfRangeException(nameof(fmt)), }; var ssize = size.ToString(CultureInfo.InvariantCulture); if (!string.IsNullOrWhiteSpace(this.AvatarHash)) { var id = this.Id.ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}"; } else { var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}"; } } /// /// Returns a string representation of this user. /// /// String representation of this user. public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordUser); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordUser e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are equal. public static bool operator ==(DiscordUser e1, DiscordUser e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are not equal. public static bool operator !=(DiscordUser e1, DiscordUser e2) => !(e1 == e2); } /// /// Represents a user comparer. /// internal class DiscordUserComparer : IEqualityComparer { /// /// Whether the users are equal. /// /// The first user /// The second user. public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); /// /// Gets the hash code. /// /// The user. public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); }